build: allow custom module resolution for ts-circular-deps tests (#36226)
Currently the `ts-circular-deps` tool uses a hard-coded module resolver that only works in the `angular/angular` repository. If the tool is consumed in other repositories through the shared dev-infra package, the module resolution won't work, and a few resolvable imports (usually cross-entry-points) are accidentally skipped. For each test, the resolution might differ, so tests can now configure their module resolution in a configuration file. Note that we intentionally don't rely on tsconfig's for module resolution as parsing their mappings rather complicates the circular dependency tool. Additionally, not every test has a corresponding tsconfig file. Also, hard-coding mappings to `@angular/*` while accepting a path to the packages folder would work, but it would mean that the circular deps tool is no longer self-contained. Rather, and also for better flexibility, a custom resolver should be specified. PR Close #36226
This commit is contained in:
parent
b07b6edc2a
commit
44acf6734b
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {dirname, isAbsolute, resolve} from 'path';
|
||||
|
||||
import {ModuleResolver} from './analyzer';
|
||||
|
||||
|
||||
/** Configuration for a circular dependencies test. */
|
||||
export interface CircularDependenciesTestConfig {
|
||||
/** Base directory used for shortening paths in the golden file. */
|
||||
baseDir: string;
|
||||
/** Path to the golden file that is used for checking and approving. */
|
||||
goldenFile: string;
|
||||
/** Glob that resolves source files which should be checked. */
|
||||
glob: string
|
||||
/**
|
||||
* Optional module resolver function that can be used to resolve modules
|
||||
* to absolute file paths.
|
||||
*/
|
||||
resolveModule?: ModuleResolver;
|
||||
/**
|
||||
* Optional command that will be displayed if the golden check failed. This can be used
|
||||
* to consistently use script aliases for checking/approving the golden.
|
||||
*/
|
||||
approveCommand?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the configuration for the circular dependencies test. If the config cannot be
|
||||
* loaded, an error will be printed and the process exists with a non-zero exit code.
|
||||
*/
|
||||
export function loadTestConfig(configPath: string): CircularDependenciesTestConfig {
|
||||
const configBaseDir = dirname(configPath);
|
||||
const resolveRelativePath = (relativePath: string) => resolve(configBaseDir, relativePath);
|
||||
|
||||
try {
|
||||
const config = require(configPath) as CircularDependenciesTestConfig;
|
||||
if (!isAbsolute(config.baseDir)) {
|
||||
config.baseDir = resolveRelativePath(config.baseDir);
|
||||
}
|
||||
if (!isAbsolute(config.goldenFile)) {
|
||||
config.goldenFile = resolveRelativePath(config.goldenFile);
|
||||
}
|
||||
if (!isAbsolute(config.glob)) {
|
||||
config.glob = resolveRelativePath(config.glob);
|
||||
}
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error('Could not load test configuration file at: ' + configPath);
|
||||
console.error(`Failed with: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import {sync as globSync} from 'glob';
|
||||
import {join, relative, resolve} from 'path';
|
||||
import {isAbsolute, relative, resolve} from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import * as yargs from 'yargs';
|
||||
import chalk from 'chalk';
|
||||
|
@ -17,56 +17,36 @@ import chalk from 'chalk';
|
|||
import {Analyzer, ReferenceChain} from './analyzer';
|
||||
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
|
||||
import {convertPathToForwardSlash} from './file_system';
|
||||
|
||||
import {getRepoBaseDir} from '../utils/config';
|
||||
|
||||
const projectDir = getRepoBaseDir();
|
||||
const packagesDir = join(projectDir, 'packages/');
|
||||
// The default glob does not capture deprecated packages such as http, or the webworker platform.
|
||||
const defaultGlob =
|
||||
join(packagesDir, '!(http|platform-webworker|platform-webworker-dynamic)/**/*.ts');
|
||||
import {loadTestConfig, CircularDependenciesTestConfig} from './config';
|
||||
|
||||
if (require.main === module) {
|
||||
const {_: command, goldenFile, glob, baseDir, warnings} =
|
||||
const {_: command, config: configArg, warnings} =
|
||||
yargs.help()
|
||||
.strict()
|
||||
.command('check <goldenFile>', 'Checks if the circular dependencies have changed.')
|
||||
.command('approve <goldenFile>', 'Approves the current circular dependencies.')
|
||||
.command('check', 'Checks if the circular dependencies have changed.')
|
||||
.command('approve', 'Approves the current circular dependencies.')
|
||||
.demandCommand()
|
||||
.option(
|
||||
'approve',
|
||||
{type: 'boolean', description: 'Approves the current circular dependencies.'})
|
||||
'config',
|
||||
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
|
||||
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
|
||||
.option('base-dir', {
|
||||
type: 'string',
|
||||
description: 'Base directory used for shortening paths in the golden file.',
|
||||
default: projectDir,
|
||||
defaultDescription: 'Project directory'
|
||||
})
|
||||
.option('glob', {
|
||||
type: 'string',
|
||||
description: 'Glob that matches source files which should be checked.',
|
||||
default: defaultGlob,
|
||||
defaultDescription: 'All release packages'
|
||||
})
|
||||
.argv;
|
||||
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
|
||||
const config = loadTestConfig(configPath);
|
||||
const isApprove = command.includes('approve');
|
||||
process.exit(main(baseDir, isApprove, goldenFile, glob, warnings));
|
||||
process.exit(main(isApprove, config, warnings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the ts-circular-dependencies tool.
|
||||
* @param baseDir Base directory which is used to build up relative file paths in goldens.
|
||||
* @param approve Whether the detected circular dependencies should be approved.
|
||||
* @param goldenFile Path to the golden file.
|
||||
* @param glob Glob that is used to collect all source files which should be checked/approved.
|
||||
* @param printWarnings Whether warnings should be printed. Warnings for unresolved modules/files
|
||||
* are not printed by default.
|
||||
* @param config Configuration for the current circular dependencies test.
|
||||
* @param printWarnings Whether warnings should be printed out.
|
||||
* @returns Status code.
|
||||
*/
|
||||
export function main(
|
||||
baseDir: string, approve: boolean, goldenFile: string, glob: string,
|
||||
printWarnings: boolean): number {
|
||||
approve: boolean, config: CircularDependenciesTestConfig, printWarnings: boolean): number {
|
||||
const {baseDir, goldenFile, glob, resolveModule, approveCommand} = config;
|
||||
const analyzer = new Analyzer(resolveModule);
|
||||
const cycles: ReferenceChain[] = [];
|
||||
const checkedNodes = new WeakSet<ts.SourceFile>();
|
||||
|
@ -90,17 +70,21 @@ export function main(
|
|||
return 1;
|
||||
}
|
||||
|
||||
const warningsCount = analyzer.unresolvedFiles.size + analyzer.unresolvedModules.size;
|
||||
|
||||
// By default, warnings for unresolved files or modules are not printed. This is because
|
||||
// it's common that third-party modules are not resolved/visited. Also generated files
|
||||
// from the View Engine compiler (i.e. factories, summaries) cannot be resolved.
|
||||
if (printWarnings &&
|
||||
(analyzer.unresolvedFiles.size !== 0 || analyzer.unresolvedModules.size !== 0)) {
|
||||
console.info(chalk.yellow('The following imports could not be resolved:'));
|
||||
if (printWarnings && warningsCount !== 0) {
|
||||
console.info(chalk.yellow('⚠ The following imports could not be resolved:'));
|
||||
analyzer.unresolvedModules.forEach(specifier => console.info(` • ${specifier}`));
|
||||
analyzer.unresolvedFiles.forEach((value, key) => {
|
||||
console.info(` • ${getRelativePath(baseDir, key)}`);
|
||||
value.forEach(specifier => console.info(` ${specifier}`));
|
||||
});
|
||||
} else {
|
||||
console.info(chalk.yellow(`⚠ ${warningsCount} imports could not be resolved.`));
|
||||
console.info(chalk.yellow(` Please rerun with "--warnings" to inspect unresolved imports.`));
|
||||
}
|
||||
|
||||
const expected: Golden = JSON.parse(readFileSync(goldenFile, 'utf8'));
|
||||
|
@ -122,16 +106,12 @@ export function main(
|
|||
chalk.yellow(` Fixed circular dependencies that need to be removed from the golden:`));
|
||||
fixedCircularDeps.forEach(c => console.error(` • ${convertReferenceChainToString(c)}`));
|
||||
console.info();
|
||||
// Print the command for updating the golden. Note that we hard-code the script name for
|
||||
// approving default packages golden in `goldens/`. We cannot infer the script name passed to
|
||||
// Yarn automatically since script are launched in a child process where `argv0` is different.
|
||||
if (resolve(goldenFile) === resolve(projectDir, 'goldens/packages-circular-deps.json')) {
|
||||
console.info(
|
||||
chalk.yellow(` Please approve the new golden with: yarn ts-circular-deps:approve`));
|
||||
if (approveCommand) {
|
||||
console.info(chalk.yellow(` Please approve the new golden with: ${approveCommand}`));
|
||||
} else {
|
||||
console.info(chalk.yellow(
|
||||
` Please update the golden. The following command can be ` +
|
||||
`run: yarn ts-circular-deps approve ${getRelativePath(baseDir, goldenFile)}.`));
|
||||
`run: yarn ts-circular-deps approve ${getRelativePath(process.cwd(), goldenFile)}.`));
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
|
@ -146,14 +126,3 @@ function getRelativePath(baseDir: string, path: string) {
|
|||
function convertReferenceChainToString(chain: ReferenceChain<string>) {
|
||||
return chain.join(' → ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom module resolver that maps specifiers starting with `@angular/` to the
|
||||
* local packages folder.
|
||||
*/
|
||||
function resolveModule(specifier: string) {
|
||||
if (specifier.startsWith('@angular/')) {
|
||||
return packagesDir + specifier.substr('@angular/'.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -35,9 +35,9 @@
|
|||
"tslint": "tsc -p tools/tsconfig.json && tslint -c tslint.json \"+(packages|modules|scripts|tools)/**/*.+(js|ts)\"",
|
||||
"public-api:check": "node goldens/public-api/manage.js test",
|
||||
"public-api:update": "node goldens/public-api/manage.js accept",
|
||||
"ts-circular-deps": "ts-node dev-infra/ts-circular-dependencies/index.ts",
|
||||
"ts-circular-deps:check": "yarn -s ts-circular-deps check ./goldens/packages-circular-deps.json",
|
||||
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve ./goldens/packages-circular-deps.json"
|
||||
"ts-circular-deps": "ts-node dev-infra/ts-circular-dependencies/index.ts --config ./packages/circular-deps-test.conf.js",
|
||||
"ts-circular-deps:check": "yarn -s ts-circular-deps check",
|
||||
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve"
|
||||
},
|
||||
"// 1": "dependencies are used locally and by bazel",
|
||||
"dependencies": {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
baseDir: '../',
|
||||
goldenFile: '../goldens/packages-circular-deps.json',
|
||||
// The test should not capture deprecated packages such as `http`, or the `webworker` platform.
|
||||
glob: `./!(http|platform-webworker|platform-webworker-dynamic)/**/*.ts`,
|
||||
// Command that will be displayed if the golden needs to be updated.
|
||||
approveCommand: 'yarn ts-circular-deps:approve',
|
||||
resolveModule: resolveModule
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom module resolver that maps specifiers starting with `@angular/` to the
|
||||
* local packages folder. This ensures that cross package/entry-point dependencies
|
||||
* can be detected.
|
||||
*/
|
||||
function resolveModule(specifier) {
|
||||
if (specifier.startsWith('@angular/')) {
|
||||
return path.join(__dirname, specifier.substr('@angular/'.length));
|
||||
}
|
||||
return null;
|
||||
}
|
Loading…
Reference in New Issue