diff --git a/dev-infra/ts-circular-dependencies/config.ts b/dev-infra/ts-circular-dependencies/config.ts new file mode 100644 index 0000000000..cb06ef8171 --- /dev/null +++ b/dev-infra/ts-circular-dependencies/config.ts @@ -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); + } +} diff --git a/dev-infra/ts-circular-dependencies/index.ts b/dev-infra/ts-circular-dependencies/index.ts index f55d8be5c0..f4a5cb3591 100644 --- a/dev-infra/ts-circular-dependencies/index.ts +++ b/dev-infra/ts-circular-dependencies/index.ts @@ -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 ', 'Checks if the circular dependencies have changed.') - .command('approve ', '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(); @@ -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) { 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; -} diff --git a/package.json b/package.json index 5a831ab9b6..13c597d34c 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/circular-deps-test.conf.js b/packages/circular-deps-test.conf.js new file mode 100644 index 0000000000..83f9a1d5f4 --- /dev/null +++ b/packages/circular-deps-test.conf.js @@ -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; +}