#!/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(); 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) { return chain.join(' → '); } if (require.main === module) { tsCircularDependenciesBuilder(yargs).parse(); }