build: allow auto-discover all typings files in npm package by ts-api-guardian (#35691)
Adds a new feature to ts-api-guardian allowing for automatically discovering all entry point d.ts files from all package.json files in a provided directory. PR Close #35691
This commit is contained in:
parent
7b13977c3a
commit
b7519e5cfa
@ -74,3 +74,63 @@ def ts_api_guardian_test(
|
|||||||
templated_args = args + ["--out", golden, actual],
|
templated_args = args + ["--out", golden, actual],
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def ts_api_guardian_test_npm_package(
|
||||||
|
name,
|
||||||
|
goldenDir,
|
||||||
|
actualDir,
|
||||||
|
data = [],
|
||||||
|
strip_export_pattern = ["^__", "^ɵ[^ɵ]"],
|
||||||
|
allow_module_identifiers = COMMON_MODULE_IDENTIFIERS,
|
||||||
|
use_angular_tag_rules = True,
|
||||||
|
**kwargs):
|
||||||
|
"""Runs ts_api_guardian
|
||||||
|
"""
|
||||||
|
data += [
|
||||||
|
# Locally we need to add the TS build target
|
||||||
|
# But it will replaced to @npm//ts-api-guardian when publishing
|
||||||
|
"@angular//tools/ts-api-guardian:lib",
|
||||||
|
"@angular//tools/ts-api-guardian:bin",
|
||||||
|
# The below are required during runtime
|
||||||
|
"@npm//chalk",
|
||||||
|
"@npm//diff",
|
||||||
|
"@npm//minimist",
|
||||||
|
"@npm//typescript",
|
||||||
|
]
|
||||||
|
|
||||||
|
args = [
|
||||||
|
# Needed so that node doesn't walk back to the source directory.
|
||||||
|
# From there, the relative imports would point to .ts files.
|
||||||
|
"--node_options=--preserve-symlinks",
|
||||||
|
# We automatically discover the enpoints for our NPM package.
|
||||||
|
"--autoDiscoverEntrypoints",
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in strip_export_pattern:
|
||||||
|
# The below replacement is needed because under Windows '^' needs to be escaped twice
|
||||||
|
args += ["--stripExportPattern", i.replace("^", "^^^^")]
|
||||||
|
|
||||||
|
for i in allow_module_identifiers:
|
||||||
|
args += ["--allowModuleIdentifiers", i]
|
||||||
|
|
||||||
|
if use_angular_tag_rules:
|
||||||
|
args += ["--useAngularTagRules"]
|
||||||
|
|
||||||
|
nodejs_test(
|
||||||
|
name = name,
|
||||||
|
data = data,
|
||||||
|
entry_point = "@angular//tools/ts-api-guardian:bin/ts-api-guardian",
|
||||||
|
templated_args = args + ["--verifyDir", goldenDir, "--rootDir", actualDir],
|
||||||
|
tags = ["api_guard"],
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
nodejs_binary(
|
||||||
|
name = name + ".accept",
|
||||||
|
testonly = True,
|
||||||
|
data = data,
|
||||||
|
entry_point = "@angular//tools/ts-api-guardian:bin/ts-api-guardian",
|
||||||
|
templated_args = args + ["--outDir", goldenDir, "--rootDir", actualDir],
|
||||||
|
tags = ["api_guard"],
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
@ -14,7 +14,7 @@ const chalk = require('chalk');
|
|||||||
import * as minimist from 'minimist';
|
import * as minimist from 'minimist';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile} from './main';
|
import {SerializationOptions, generateGoldenFile, verifyAgainstGoldenFile, discoverAllEntrypoints} from './main';
|
||||||
|
|
||||||
const CMD = 'ts-api-guardian';
|
const CMD = 'ts-api-guardian';
|
||||||
|
|
||||||
@ -46,6 +46,15 @@ export function startCli() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In autoDiscoverEntrypoints mode we set the inputed files as the discovered entrypoints
|
||||||
|
// for the rootDir
|
||||||
|
let entrypoints: string[];
|
||||||
|
if (argv['autoDiscoverEntrypoints']) {
|
||||||
|
entrypoints = discoverAllEntrypoints(argv['rootDir']);
|
||||||
|
} else {
|
||||||
|
entrypoints = argv._.slice();
|
||||||
|
}
|
||||||
|
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
}
|
}
|
||||||
@ -53,7 +62,7 @@ export function startCli() {
|
|||||||
if (mode === 'help') {
|
if (mode === 'help') {
|
||||||
printUsageAndExit(!!errors.length);
|
printUsageAndExit(!!errors.length);
|
||||||
} else {
|
} else {
|
||||||
const targets = resolveFileNamePairs(argv, mode);
|
const targets = resolveFileNamePairs(argv, mode, entrypoints);
|
||||||
|
|
||||||
if (mode === 'out') {
|
if (mode === 'out') {
|
||||||
for (const {entrypoint, goldenFile} of targets) {
|
for (const {entrypoint, goldenFile} of targets) {
|
||||||
@ -110,7 +119,7 @@ export function parseArguments(input: string[]):
|
|||||||
'allowModuleIdentifiers'
|
'allowModuleIdentifiers'
|
||||||
],
|
],
|
||||||
boolean: [
|
boolean: [
|
||||||
'help', 'useAngularTagRules',
|
'help', 'useAngularTagRules', 'autoDiscoverEntrypoints',
|
||||||
// Options used by chalk automagically
|
// Options used by chalk automagically
|
||||||
'color', 'no-color'
|
'color', 'no-color'
|
||||||
],
|
],
|
||||||
@ -147,15 +156,26 @@ export function parseArguments(input: string[]):
|
|||||||
modes.push('verify');
|
modes.push('verify');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!argv._.length) {
|
if (argv['autoDiscoverEntrypoints']) {
|
||||||
errors.push('No input file specified.');
|
if (!argv['rootDir']) {
|
||||||
modes = ['help'];
|
errors.push(`--rootDir must be provided with --autoDiscoverEntrypoints.`);
|
||||||
} else if (modes.length !== 1) {
|
modes = ['help'];
|
||||||
errors.push('Specify either --out[Dir] or --verify[Dir]');
|
}
|
||||||
modes = ['help'];
|
if (!argv['outDir'] && !argv['verifyDir']) {
|
||||||
} else if (argv._.length > 1 && !argv['outDir'] && !argv['verifyDir']) {
|
errors.push(`--outDir or --verifyDir must be used with --autoDiscoverEntrypoints.`);
|
||||||
errors.push(`More than one input specified. Use --${modes[0]}Dir instead.`);
|
modes = ['help'];
|
||||||
modes = ['help'];
|
}
|
||||||
|
} else {
|
||||||
|
if (!argv._.length) {
|
||||||
|
errors.push('No input file specified.');
|
||||||
|
modes = ['help'];
|
||||||
|
} else if (modes.length !== 1) {
|
||||||
|
errors.push('Specify either --out[Dir] or --verify[Dir]');
|
||||||
|
modes = ['help'];
|
||||||
|
} else if (argv._.length > 1 && !argv['outDir'] && !argv['verifyDir']) {
|
||||||
|
errors.push(`More than one input specified. Use --${modes[0]}Dir instead.`);
|
||||||
|
modes = ['help'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {argv, mode: modes[0], errors};
|
return {argv, mode: modes[0], errors};
|
||||||
@ -184,7 +204,8 @@ Options:
|
|||||||
--useAngularTagRules <boolean> Whether the Angular specific tag rules should be used.
|
--useAngularTagRules <boolean> Whether the Angular specific tag rules should be used.
|
||||||
--stripExportPattern <regexp> Do not output exports matching the pattern
|
--stripExportPattern <regexp> Do not output exports matching the pattern
|
||||||
--allowModuleIdentifiers <identifier>
|
--allowModuleIdentifiers <identifier>
|
||||||
Allow identifier for "* as foo" imports`);
|
Allow identifier for "* as foo" imports
|
||||||
|
--autoDiscoverEntrypoints Automatically find all entrypoints .d.ts files in the rootDir`);
|
||||||
process.exit(error ? 1 : 0);
|
process.exit(error ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,24 +220,31 @@ function resolveBazelFilePath(fileName: string): string {
|
|||||||
// are not available in the working directory. In order to resolve the real path for the
|
// are not available in the working directory. In order to resolve the real path for the
|
||||||
// runfile, we need to use `require.resolve` which handles runfiles properly on Windows.
|
// runfile, we need to use `require.resolve` which handles runfiles properly on Windows.
|
||||||
if (process.env['BAZEL_TARGET']) {
|
if (process.env['BAZEL_TARGET']) {
|
||||||
return path.relative(process.cwd(), require.resolve(fileName));
|
// This try/catch block is necessary because if the path is to the source file directly
|
||||||
|
// rather than via symlinks in the bazel output directories, require is not able to
|
||||||
|
// resolve it.
|
||||||
|
try {
|
||||||
|
return path.relative(process.cwd(), require.resolve(fileName));
|
||||||
|
} catch (err) {
|
||||||
|
return path.relative(process.cwd(), fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileName;
|
return fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveFileNamePairs(
|
function resolveFileNamePairs(argv: minimist.ParsedArgs, mode: string, entrypoints: string[]):
|
||||||
argv: minimist.ParsedArgs, mode: string): {entrypoint: string, goldenFile: string}[] {
|
{entrypoint: string, goldenFile: string}[] {
|
||||||
if (argv[mode]) {
|
if (argv[mode]) {
|
||||||
return [{
|
return [{
|
||||||
entrypoint: resolveBazelFilePath(argv._[0]),
|
entrypoint: resolveBazelFilePath(entrypoints[0]),
|
||||||
goldenFile: resolveBazelFilePath(argv[mode]),
|
goldenFile: resolveBazelFilePath(argv[mode]),
|
||||||
}];
|
}];
|
||||||
} else { // argv[mode + 'Dir']
|
} else { // argv[mode + 'Dir']
|
||||||
let rootDir = argv['rootDir'] || '.';
|
let rootDir = argv['rootDir'] || '.';
|
||||||
const goldenDir = argv[mode + 'Dir'];
|
const goldenDir = argv[mode + 'Dir'];
|
||||||
|
|
||||||
return argv._.map((fileName: string) => {
|
return entrypoints.map((fileName: string) => {
|
||||||
return {
|
return {
|
||||||
entrypoint: resolveBazelFilePath(fileName),
|
entrypoint: resolveBazelFilePath(fileName),
|
||||||
goldenFile: resolveBazelFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
|
goldenFile: resolveBazelFilePath(path.join(goldenDir, path.relative(rootDir, fileName))),
|
||||||
|
@ -16,6 +16,14 @@ export {SerializationOptions, publicApi} from './serializer';
|
|||||||
export function generateGoldenFile(
|
export function generateGoldenFile(
|
||||||
entrypoint: string, outFile: string, options: SerializationOptions = {}): void {
|
entrypoint: string, outFile: string, options: SerializationOptions = {}): void {
|
||||||
const output = publicApi(entrypoint, options);
|
const output = publicApi(entrypoint, options);
|
||||||
|
|
||||||
|
// BUILD_WORKSPACE_DIRECTORY environment variable is only available during bazel
|
||||||
|
// run executions. This workspace directory allows us to generate golden files directly
|
||||||
|
// in the source file tree rather than via a symlink.
|
||||||
|
if (process.env['BUILD_WORKSPACE_DIRECTORY']) {
|
||||||
|
outFile = path.join(process.env['BUILD_WORKSPACE_DIRECTORY'], outFile);
|
||||||
|
}
|
||||||
|
|
||||||
ensureDirectory(path.dirname(outFile));
|
ensureDirectory(path.dirname(outFile));
|
||||||
fs.writeFileSync(outFile, output);
|
fs.writeFileSync(outFile, output);
|
||||||
}
|
}
|
||||||
@ -23,7 +31,7 @@ export function generateGoldenFile(
|
|||||||
export function verifyAgainstGoldenFile(
|
export function verifyAgainstGoldenFile(
|
||||||
entrypoint: string, goldenFile: string, options: SerializationOptions = {}): string {
|
entrypoint: string, goldenFile: string, options: SerializationOptions = {}): string {
|
||||||
const actual = publicApi(entrypoint, options);
|
const actual = publicApi(entrypoint, options);
|
||||||
const expected = fs.readFileSync(goldenFile).toString();
|
const expected = fs.existsSync(goldenFile) ? fs.readFileSync(goldenFile).toString() : '';
|
||||||
|
|
||||||
if (actual === expected) {
|
if (actual === expected) {
|
||||||
return '';
|
return '';
|
||||||
@ -43,3 +51,49 @@ function ensureDirectory(dir: string) {
|
|||||||
fs.mkdirSync(dir);
|
fs.mkdirSync(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the provided path is a directory.
|
||||||
|
*/
|
||||||
|
function isDirectory(dirPath: string) {
|
||||||
|
try {
|
||||||
|
fs.lstatSync(dirPath).isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an array of paths to the typings files for each of the recursively discovered
|
||||||
|
* package.json
|
||||||
|
* files from the directory provided.
|
||||||
|
*/
|
||||||
|
export function discoverAllEntrypoints(dirPath: string) {
|
||||||
|
// Determine all of the package.json files
|
||||||
|
const packageJsons: string[] = [];
|
||||||
|
const entryPoints: string[] = [];
|
||||||
|
const findPackageJsonsInDir = (nextPath: string) => {
|
||||||
|
for (const file of fs.readdirSync(nextPath)) {
|
||||||
|
const fullPath = path.join(nextPath, file);
|
||||||
|
if (isDirectory(fullPath)) {
|
||||||
|
findPackageJsonsInDir(fullPath);
|
||||||
|
} else {
|
||||||
|
if (file === 'package.json') {
|
||||||
|
packageJsons.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
findPackageJsonsInDir(dirPath);
|
||||||
|
|
||||||
|
// Get all typings file locations from package.json files
|
||||||
|
for (const packageJson of packageJsons) {
|
||||||
|
const packageJsonObj = JSON.parse(fs.readFileSync(packageJson, {encoding: 'utf8'}));
|
||||||
|
const typings = packageJsonObj.typings;
|
||||||
|
if (typings) {
|
||||||
|
entryPoints.push(path.join(path.dirname(packageJson), typings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
|
@ -62,4 +62,22 @@ describe('cli: parseArguments', () => {
|
|||||||
chai.assert.equal(mode, 'verify');
|
chai.assert.equal(mode, 'verify');
|
||||||
chai.assert.deepEqual(errors, []);
|
chai.assert.deepEqual(errors, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show usage with error when supplied with --autoDiscoverEntrypoints without --baseDir',
|
||||||
|
() => {
|
||||||
|
const {mode, errors} =
|
||||||
|
parseArguments(['--autoDiscoverEntrypoints', '--outDir', 'something']);
|
||||||
|
chai.assert.equal(mode, 'help');
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
errors, ['--rootDir must be provided with --autoDiscoverEntrypoints.']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show usage with error when supplied with --autoDiscoverEntrypoints without --outDir/verifyDir',
|
||||||
|
() => {
|
||||||
|
const {mode, errors} =
|
||||||
|
parseArguments(['--autoDiscoverEntrypoints', '--rootDir', 'something']);
|
||||||
|
chai.assert.equal(mode, 'help');
|
||||||
|
chai.assert.deepEqual(
|
||||||
|
errors, ['--outDir or --verifyDir must be used with --autoDiscoverEntrypoints.']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user