angular-cn/dev-infra/ts-circular-dependencies/index.ts

144 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* @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 {existsSync, readFileSync, writeFileSync} from 'fs';
import {sync as globSync} from 'glob';
import {isAbsolute, relative, resolve} from 'path';
import * as ts from 'typescript';
import * as yargs from 'yargs';
import chalk from 'chalk';
import {Analyzer, ReferenceChain} from './analyzer';
import {compareGoldens, convertReferenceChainToGolden, Golden} from './golden';
import {convertPathToForwardSlash} from './file_system';
import {loadTestConfig, CircularDependenciesTestConfig} from './config';
export function tsCircularDependenciesBuilder(localYargs: yargs.Argv) {
return localYargs.help()
.strict()
.demandCommand()
.option(
'config',
{type: 'string', demandOption: true, description: 'Path to the configuration file.'})
.option('warnings', {type: 'boolean', description: 'Prints all warnings.'})
.command(
'check', 'Checks if the circular dependencies have changed.', {},
(argv: yargs.Arguments) => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(false, config, warnings));
})
.command(
'approve', 'Approves the current circular dependencies.', {}, (argv: yargs.Arguments) => {
const {config: configArg, warnings} = argv;
const configPath = isAbsolute(configArg) ? configArg : resolve(configArg);
const config = loadTestConfig(configPath);
process.exit(main(true, config, warnings));
});
}
/**
* Runs the ts-circular-dependencies tool.
* @param approve Whether the detected circular dependencies should be approved.
* @param config Configuration for the current circular dependencies test.
* @param printWarnings Whether warnings should be printed out.
* @returns Status code.
*/
export function main(
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>();
globSync(glob, {absolute: true}).forEach(filePath => {
const sourceFile = analyzer.getSourceFile(filePath);
cycles.push(...analyzer.findCycles(sourceFile, checkedNodes));
});
const actual = convertReferenceChainToGolden(cycles, baseDir);
console.info(
chalk.green(` Current number of cycles: ${chalk.yellow(cycles.length.toString())}`));
if (approve) {
writeFileSync(goldenFile, JSON.stringify(actual, null, 2));
console.info(chalk.green('✅ Updated golden file.'));
return 0;
} else if (!existsSync(goldenFile)) {
console.error(chalk.red(`❌ Could not find golden file: ${goldenFile}`));
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 && warningsCount !== 0) {
console.info(chalk.yellow('⚠ The following imports could not be resolved:'));
Array.from(analyzer.unresolvedModules)
.sort()
.forEach(specifier => console.info(`${specifier}`));
analyzer.unresolvedFiles.forEach((value, key) => {
console.info(`${getRelativePath(baseDir, key)}`);
value.sort().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'));
const {fixedCircularDeps, newCircularDeps} = compareGoldens(actual, expected);
const isMatching = fixedCircularDeps.length === 0 && newCircularDeps.length === 0;
if (isMatching) {
console.info(chalk.green('✅ Golden matches current circular dependencies.'));
return 0;
}
console.error(chalk.red('❌ Golden does not match current circular dependencies.'));
if (newCircularDeps.length !== 0) {
console.error(chalk.yellow(` New circular dependencies which are not allowed:`));
newCircularDeps.forEach(c => console.error(`${convertReferenceChainToString(c)}`));
console.error();
}
if (fixedCircularDeps.length !== 0) {
console.error(
chalk.yellow(` Fixed circular dependencies that need to be removed from the golden:`));
fixedCircularDeps.forEach(c => console.error(`${convertReferenceChainToString(c)}`));
console.error();
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(process.cwd(), goldenFile)}.`));
}
}
return 1;
}
/** Gets the specified path relative to the base directory. */
function getRelativePath(baseDir: string, path: string) {
return convertPathToForwardSlash(relative(baseDir, path));
}
/** Converts the given reference chain to its string representation. */
function convertReferenceChainToString(chain: ReferenceChain<string>) {
return chain.join(' → ');
}
if (require.main === module) {
tsCircularDependenciesBuilder(yargs).parse();
}