dpdm
Version:
Analyze circular dependencies in your JavaScript/TypeScript projects.
367 lines • 12.9 kB
JavaScript
;
/*!
* Copyright 2019 acrazing <joking.young@gmail.com>. All rights reserved.
* @since 2019-07-17 18:45:32
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.simpleResolver = exports.defaultOptions = void 0;
exports.normalizeOptions = normalizeOptions;
exports.appendSuffix = appendSuffix;
exports.shortenTree = shortenTree;
exports.getPackageName = getPackageName;
exports.groupDependencyTreeByPackage = groupDependencyTreeByPackage;
exports.groupEntriesByPackage = groupEntriesByPackage;
exports.parseCircular = parseCircular;
exports.parseDependents = parseDependents;
exports.parseWarnings = parseWarnings;
exports.prettyTree = prettyTree;
exports.prettyCircular = prettyCircular;
exports.prettyWarning = prettyWarning;
exports.isEmpty = isEmpty;
const tslib_1 = require("tslib");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const module_1 = require("module");
const path_1 = tslib_1.__importDefault(require("path"));
const consts_1 = require("./consts");
const allBuiltins = new Set(module_1.builtinModules);
function createSkippedImportsRegExp(skipImports) {
if (skipImports.length === 0) {
return /$./;
}
return new RegExp(`^(?:${skipImports.map((item) => item.join(':')).join('|')})$`);
}
exports.defaultOptions = {
cwd: process.cwd(),
context: process.cwd(),
extensions: ['', '.ts', '.tsx', '.mjs', '.js', '.jsx', '.json'],
js: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
include: /.*/,
exclude: /node_modules/,
tsconfig: void 0,
transform: false,
skipDynamicImports: false,
onProgress: () => void 0,
};
function normalizeOptions(options) {
const newOptions = Object.assign(Object.assign({}, exports.defaultOptions), options);
newOptions.cwd = path_1.default.resolve(options.cwd || process.cwd());
newOptions.context = path_1.default.resolve(newOptions.cwd, options.context || '.');
if (newOptions.extensions.indexOf('') < 0) {
newOptions.extensions.unshift('');
}
if (options.tsconfig === void 0) {
try {
const tsconfig = path_1.default.join(newOptions.context, 'tsconfig.json');
const stat = fs_extra_1.default.statSync(tsconfig);
if (stat.isFile()) {
newOptions.tsconfig = tsconfig;
}
}
catch (_a) { }
}
else {
const tsconfig = path_1.default.resolve(newOptions.cwd, options.tsconfig);
let stat;
try {
stat = fs_extra_1.default.statSync(tsconfig);
}
catch (_b) { }
if (!stat || !stat.isFile()) {
throw new Error(`specified tsconfig "${options.tsconfig}" is not a file`);
}
newOptions.tsconfig = tsconfig;
}
return newOptions;
}
function appendSuffix(request, extensions) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
for (const ext of extensions) {
try {
const stat = yield fs_extra_1.default.stat(request + ext);
if (stat.isFile()) {
return request + ext;
}
}
catch (_a) { }
}
try {
const stat = yield fs_extra_1.default.stat(request);
if (stat.isDirectory()) {
return appendSuffix(path_1.default.join(request, 'index'), extensions);
}
}
catch (_b) { }
return null;
});
}
const simpleResolver = (context, request, extensions) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
if (path_1.default.isAbsolute(request)) {
return appendSuffix(request, extensions);
}
if (request.charAt(0) === '.') {
return appendSuffix(path_1.default.join(context, request), extensions);
}
// is package
const nodePath = { paths: [context] };
try {
const pkgPath = require.resolve(path_1.default.join(request, 'package.json'), nodePath);
const pkgJson = yield fs_extra_1.default.readJSON(pkgPath);
const id = path_1.default.join(path_1.default.dirname(pkgPath), pkgJson.module || pkgJson.main);
return appendSuffix(id, extensions);
}
catch (_a) { }
try {
return require.resolve(request, nodePath);
}
catch (_b) { }
return null;
});
exports.simpleResolver = simpleResolver;
function shortenTree(context, tree) {
const output = {};
for (const key in tree) {
const shortKey = path_1.default.relative(context, key);
output[shortKey] = tree[key]
? tree[key].map((item) => (Object.assign(Object.assign({}, item), { issuer: shortKey, id: item.id === null ? null : path_1.default.relative(context, item.id) })))
: null;
}
return output;
}
function getPackageNameFromRequest(request) {
if (request.startsWith('.') || path_1.default.isAbsolute(request)) {
return null;
}
const parts = request.split('/');
if (request.startsWith('@')) {
return parts.length > 1 ? parts.slice(0, 2).join('/') : request;
}
return parts[0] || null;
}
function getPackageNameFromPath(context, id, cache) {
if (allBuiltins.has(id)) {
return id;
}
const fullPath = path_1.default.isAbsolute(id) ? id : path_1.default.resolve(context, id);
let current = path_1.default.extname(fullPath) ? path_1.default.dirname(fullPath) : fullPath;
const root = path_1.default.parse(current).root;
while (true) {
const cached = cache.get(current);
if (cached !== void 0) {
return cached;
}
try {
const pkg = fs_extra_1.default.readJSONSync(path_1.default.join(current, 'package.json'));
const name = typeof pkg.name === 'string' && pkg.name
? pkg.name
: path_1.default.relative(context, current) || path_1.default.basename(current);
cache.set(current, name);
return name;
}
catch (_a) { }
if (current === root) {
cache.set(current, null);
return null;
}
current = path_1.default.dirname(current);
}
}
function getPackageName(context, id) {
return getPackageNameFromPath(context, id, new Map());
}
function groupDependencyTreeByPackage(tree, context) {
var _a;
const packages = {};
const edges = {};
const cache = new Map();
function ensurePackage(id, ignored = false) {
if (!(id in packages)) {
packages[id] = ignored ? null : [];
}
else if (packages[id] === null && !ignored) {
packages[id] = [];
}
}
for (const id in tree) {
const issuerPackage = getPackageNameFromPath(context, id, cache) || id;
const deps = tree[id];
ensurePackage(issuerPackage, deps === null);
if (!deps) {
continue;
}
for (const dep of deps) {
const dependencyPackage = dep.id
? getPackageNameFromPath(context, dep.id, cache)
: getPackageNameFromRequest(dep.request);
if (!dependencyPackage || dependencyPackage === issuerPackage) {
continue;
}
ensurePackage(dependencyPackage, dep.id ? tree[dep.id] === null : false);
const edgeSet = (edges[issuerPackage] =
edges[issuerPackage] || new Set());
if (edgeSet.has(dependencyPackage)) {
continue;
}
edgeSet.add(dependencyPackage);
packages[issuerPackage].push({
issuer: issuerPackage,
request: dep.request,
kind: dep.kind,
id: dependencyPackage,
});
}
}
for (const id in packages) {
(_a = packages[id]) === null || _a === void 0 ? void 0 : _a.sort((a, b) => a.id.localeCompare(b.id));
}
return packages;
}
function groupEntriesByPackage(entries, context) {
const output = [];
const seen = new Set();
const cache = new Map();
for (const entry of entries) {
const id = getPackageNameFromPath(context, entry, cache) || entry;
if (!seen.has(id)) {
output.push(id);
seen.add(id);
}
}
return output;
}
function parseCircular(tree, skipDynamicImports = false, skipImports = []) {
const circulars = [];
const skippedImports = createSkippedImportsRegExp(skipImports);
tree = Object.assign({}, tree);
function visit(id, used) {
const index = used.indexOf(id);
if (index > -1) {
circulars.push(used.slice(index));
}
else if (tree[id]) {
used.push(id);
const deps = tree[id];
delete tree[id];
deps &&
deps.forEach((dep) => {
if (dep.id &&
(!skipDynamicImports ||
dep.kind !== consts_1.DependencyKind.DynamicImport) &&
!skippedImports.test(`${dep.issuer}:${dep.id}`)) {
visit(dep.id, used.slice());
}
});
}
}
for (const id in tree) {
visit(id, []);
}
return circulars;
}
function parseDependents(tree) {
const output = {};
for (const key in tree) {
const deps = tree[key];
if (deps) {
deps.forEach((dep) => {
if (dep.id) {
(output[dep.id] = output[dep.id] || []).push(key);
}
});
}
}
for (const key in output) {
output[key].sort();
}
return output;
}
function parseWarnings(tree, dependents = parseDependents(tree)) {
const warnings = [];
const builtin = new Set();
for (const key in tree) {
const deps = tree[key];
if (!builtin.has(key) && allBuiltins.has(key)) {
builtin.add(key);
}
if (!deps) {
const parents = dependents[key] || [];
const total = parents.length;
warnings.push(`skip ${JSON.stringify(key)}, issuers: ${parents
.slice(0, 2)
.map((id) => JSON.stringify(id))
.join(', ')}${total > 2 ? ` (${total - 2} more...)` : ''}`);
}
else {
for (const dep of deps) {
if (!dep.id) {
warnings.push(`miss ${JSON.stringify(dep.request)} in ${JSON.stringify(dep.issuer)}`);
}
}
}
}
if (builtin.size > 0) {
warnings.push('node ' + Array.from(builtin, (item) => JSON.stringify(item)).join(', '));
}
return warnings.sort();
}
function prettyTree(tree, entries, prefix = ' ') {
const lines = [];
let id = 0;
const idMap = {};
const digits = Math.ceil(Math.log10(Object.keys(tree).length));
function visit(item, prefix, hasMore) {
const isNew = idMap[item] === void 0;
const iid = (idMap[item] = idMap[item] || id++);
let line = chalk_1.default.gray(prefix + '- ' + iid.toString().padStart(digits, '0') + ') ');
const deps = tree[item];
if (allBuiltins.has(item)) {
lines.push(line + chalk_1.default.blue(item));
return;
}
else if (!isNew) {
lines.push(line + chalk_1.default.gray(item));
return;
}
else if (!deps) {
lines.push(line + chalk_1.default.yellow(item));
return;
}
lines.push(line + item);
prefix += hasMore ? '· ' : ' ';
for (let i = 0; i < deps.length; i++) {
visit(deps[i].id || deps[i].request, prefix, i < deps.length - 1);
}
}
for (let i = 0; i < entries.length; i++) {
visit(entries[i], prefix, i < entries.length - 1);
}
return lines.join('\n');
}
function prettyCircular(circulars, prefix = ' ') {
const digits = Math.ceil(Math.log10(circulars.length));
return circulars
.map((line, index) => {
return (chalk_1.default.gray(`${prefix}${(index + 1).toString().padStart(digits, '0')}) `) + line.map((item) => chalk_1.default.red(item)).join(chalk_1.default.gray(' -> ')));
})
.join('\n');
}
function prettyWarning(warnings, prefix = ' ') {
const digits = Math.ceil(Math.log10(warnings.length));
return warnings
.map((line, index) => {
return (chalk_1.default.gray(`${prefix}${(index + 1).toString().padStart(digits, '0')}) `) + chalk_1.default.yellow(line));
})
.join('\n');
}
function isEmpty(v) {
if (v == null) {
return true;
}
for (const k in v) {
if (v.hasOwnProperty(k)) {
return false;
}
}
return true;
}
//# sourceMappingURL=utils.js.map