dpdm-fast
Version:
Analyze circular dependencies in your JavaScript/TypeScript projects with Rust.
228 lines • 9.21 kB
JavaScript
#!/usr/bin/env node
/*!
* Copyright 2019 acrazing <joking.young@gmail.com>. All rights reserved.
* @since 2019-07-17 18:45:32
*/
import { __awaiter } from "tslib";
import chalk from 'chalk';
import fs from 'fs-extra';
import * as G from 'glob';
import ora from 'ora';
import path from 'path';
import yargs from 'yargs';
import { parseDependencyTree } from '../parser';
import { defaultOptions, isEmpty, parseCircular, parseWarnings, prettyCircular, prettyTree, prettyWarning, simpleResolver, } from '../utils';
function main() {
return __awaiter(this, void 0, void 0, function* () {
const argv = yield yargs
.strict()
.usage('$0 [options] <files...>', "Analyze the files' dependencies.", (yargs) => {
return yargs.positional('files', {
description: 'The file paths or globs',
demandOption: true,
type: 'string',
array: true,
});
})
.option('context', {
type: 'string',
desc: 'the context directory to shorten path, default is current directory',
})
.option('extensions', {
alias: 'ext',
type: 'string',
desc: 'comma separated extensions to resolve',
default: defaultOptions.extensions.filter(Boolean).join(','),
})
.option('js', {
type: 'string',
desc: 'comma separated extensions indicate the file is js like',
default: defaultOptions.js.join(','),
})
.option('include', {
type: 'string',
desc: 'included filenames regexp in string, default includes all files',
default: defaultOptions.include.source,
})
.option('exclude', {
type: 'string',
desc: 'excluded filenames regexp in string, set as empty string to include all files',
default: defaultOptions.exclude.source,
})
.option('output', {
alias: 'o',
type: 'string',
desc: 'output json to file',
})
.option('tree', {
type: 'boolean',
desc: 'print tree to stdout',
default: true,
})
.option('circular', {
type: 'boolean',
desc: 'print circular to stdout',
default: true,
})
.option('warning', {
type: 'boolean',
desc: 'print warning to stdout',
default: true,
})
.option('tsconfig', {
type: 'string',
desc: 'the tsconfig path, which is used for resolve path alias, default is tsconfig.json if it exists in context directory',
})
.option('transform', {
type: 'boolean',
desc: 'transform typescript modules to javascript before analyze, it allows you to omit types dependency in typescript',
default: defaultOptions.transform,
alias: 'T',
})
.option('exit-code', {
type: 'string',
desc: 'exit with specified code, the value format is CASE:CODE, `circular` is the only supported CASE, ' +
'CODE should be a integer between 0 and 128. ' +
'For example: `dpdm --exit-code circular:1` the program will exit with code 1 if circular dependency found.',
})
.option('progress', {
type: 'boolean',
desc: 'show progress bar',
default: process.stdout.isTTY && !process.env.CI,
})
.option('detect-unused-files-from', {
type: 'string',
desc: 'this file is a glob, used for finding unused files.',
})
.option('skip-dynamic-imports', {
type: 'string',
choices: ['tree', 'circular'],
desc: 'Skip parse import(...) statement.',
})
.alias('h', 'help')
.wrap(Math.min(yargs.terminalWidth(), 100)).argv;
const files = argv.files;
if (files.length === 0) {
yargs.showHelp();
console.log('\nMissing entry file');
process.exit(1);
}
const exitCases = new Set(['circular']);
const exitCodes = [];
if (argv['exit-code']) {
argv['exit-code'].split(',').forEach((c) => {
const [label, code] = c.split(':');
if (!code || !isFinite(+code)) {
throw new TypeError(`exit code should be a number`);
}
if (!exitCases.has(label)) {
throw new TypeError(`unsupported exit case "${label}"`);
}
exitCodes.push([label, +code]);
});
}
const o = ora('Start analyzing dependencies...').start();
let total = 0;
let ended = 0;
let current = '';
const context = argv.context || process.cwd();
function onProgress(event, target) {
switch (event) {
case 'start':
total += 1;
current = path.relative(context, target);
break;
case 'end':
ended += 1;
break;
}
if (argv.progress) {
o.text = `[${ended}/${total}] Analyzing ${current}...`;
o.render();
}
}
const options = {
context,
extensions: argv.extensions.split(','),
js: argv.js.split(','),
include: new RegExp(argv.include || '.*'),
exclude: new RegExp(argv.exclude || '$.'),
tsconfig: argv.tsconfig,
transform: argv.transform,
skipDynamicImports: argv.skipDynamicImports === 'tree',
onProgress,
};
parseDependencyTree(files, options)
.then((tree) => __awaiter(this, void 0, void 0, function* () {
if (isEmpty(tree)) {
throw new Error(`No entry files were matched.`);
}
o.succeed(`[${ended}/${total}] Analyze done!`);
const entriesDeep = yield Promise.all(files.map((g) => G.glob(g)));
const entries = yield Promise.all(Array()
.concat(...entriesDeep)
.map((name) => simpleResolver(options.context, path.join(options.context, name), options.extensions).then((id) => (id ? path.relative(options.context, id) : name))));
const circulars = parseCircular(tree, argv.skipDynamicImports === 'circular');
if (argv.output) {
yield fs.outputJSON(argv.output, { entries, tree, circulars }, { spaces: 2 });
}
if (argv.tree) {
console.log(chalk.bold('• Dependencies Tree'));
console.log(prettyTree(tree, entries));
console.log('');
}
if (argv.circular) {
console.log(chalk.bold[circulars.length === 0 ? 'green' : 'red']('• Circular Dependencies'));
if (circulars.length === 0) {
console.log(chalk.bold.green(' ✅ Congratulations, no circular dependency was found in your project.'));
}
else {
console.log(prettyCircular(circulars));
}
console.log('');
}
if (argv.warning) {
console.log(chalk.bold.yellow('• Warnings'));
console.log(prettyWarning(parseWarnings(tree)));
console.log('');
}
if (argv.detectUnusedFilesFrom) {
const allFiles = yield G.glob(argv.detectUnusedFilesFrom);
const shortAllFiles = allFiles.map((v) => path.relative(context, v));
const unusedFiles = shortAllFiles.filter((v) => !(v in tree)).sort();
console.log(chalk.bold.cyan('• Unused files'));
if (unusedFiles.length === 0) {
console.log(chalk.bold.green(' ✅ Congratulations, no unused file was found in your project. (total: ' +
allFiles.length +
', used: ' +
Object.keys(tree).length +
')'));
}
else {
const len = unusedFiles.length.toString().length;
unusedFiles.forEach((f, i) => {
console.log('%s) %s', i.toString().padStart(len, '0'), f);
});
}
}
for (const [label, code] of exitCodes) {
switch (label) {
case 'circular':
if (circulars.length > 0) {
process.exit(code);
}
}
}
}))
.catch((e) => {
o.fail();
console.error(e.stack || e);
process.exit(1);
});
});
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
//# sourceMappingURL=dpdm.js.map