unimported
Version:
Scans your nodejs project folder and shows obsolete files and modules
328 lines (327 loc) • 14.7 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.main = void 0;
const simple_git_1 = __importDefault(require("simple-git"));
const fs = __importStar(require("./fs"));
const read_pkg_up_1 = __importDefault(require("read-pkg-up"));
const path_1 = __importStar(require("path"));
const ora_1 = __importDefault(require("ora"));
const print_1 = require("./print");
const meta = __importStar(require("./meta"));
const traverse_1 = require("./traverse");
const chalk_1 = __importDefault(require("chalk"));
const yargs_1 = __importDefault(require("yargs"));
const process_1 = require("./process");
const config_1 = require("./config");
const cache_1 = require("./cache");
const log_1 = require("./log");
const presets_1 = require("./presets");
const delete_1 = require("./delete");
const oraStub = {
set text(msg) {
log_1.log.info(msg);
},
stop(msg = '') {
log_1.log.info(msg);
},
};
function main(args) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
const projectPkg = yield (0, read_pkg_up_1.default)({ cwd: args.cwd });
const unimportedPkg = yield (0, read_pkg_up_1.default)({ cwd: __dirname });
// equality check to prevent tests from walking up and running on unimported itself
if (!projectPkg || !unimportedPkg || unimportedPkg.path === projectPkg.path) {
console.error(chalk_1.default.redBright(`could not resolve package.json, are you in a node project?`));
process.exit(1);
return;
}
// change the work dir for the process to the project root, this enables us
// to run unimported from nested paths within the project
process.chdir(path_1.default.dirname(projectPkg.path));
const cwd = process.cwd();
// clear cache and return
if (args.clearCache) {
return (0, cache_1.purgeCache)();
}
const spinner = log_1.log.enabled() || process.env.NODE_ENV === 'test'
? oraStub
: (0, ora_1.default)('initializing').start();
try {
const config = yield (0, config_1.getConfig)(args);
if (args.showConfig) {
spinner.stop();
console.dir(config, { depth: 5 });
process.exit(0);
}
if (typeof args.showPreset === 'string') {
spinner.stop();
if (args.showPreset) {
console.dir(yield (0, config_1.getPreset)(args.showPreset), { depth: 5 });
}
else {
const available = presets_1.presets
.map((x) => x.name)
.sort()
.map((x) => ` - ${x}`)
.join('\n');
console.log(`you didn't provide a preset name, please choose one of the following: \n\n${available}`);
}
process.exit(0);
}
const [dependencies, peerDependencies] = yield Promise.all([
meta.getDependencies(cwd),
meta.getPeerDependencies(cwd),
]);
const moduleDirectory = (_a = config.moduleDirectory) !== null && _a !== void 0 ? _a : ['node_modules'];
const context = Object.assign(Object.assign({ dependencies,
peerDependencies,
moduleDirectory }, args), { config, cwd: cwd.replace(/\\/g, '/') });
if (args.init) {
yield (0, config_1.writeConfig)({
ignorePatterns: config.ignorePatterns,
ignoreUnimported: config.ignoreUnimported,
ignoreUnused: config.ignoreUnused,
ignoreUnresolved: config.ignoreUnresolved,
respectGitignore: config.respectGitignore,
});
spinner.stop();
process.exit(0);
}
// Filter untracked files from git repositories
if (args.ignoreUntracked) {
const git = (0, simple_git_1.default)({ baseDir: context.cwd });
const status = yield git.status();
config.ignorePatterns.push(...status.not_added.map((file) => path_1.default.resolve(file)));
}
spinner.text = `resolving imports`;
const traverseResult = (0, traverse_1.getResultObject)();
for (const entry of config.entryFiles) {
log_1.log.info('start traversal at %s', entry);
const traverseConfig = {
extensions: entry.extensions,
assetExtensions: config.assetExtensions,
// resolve full path of aliases
aliases: yield meta.getAliases(entry),
cacheId: args.cache ? (0, cache_1.getCacheIdentity)(entry) : undefined,
flow: config.flow,
moduleDirectory,
preset: config.preset,
dependencies,
pathTransforms: config.pathTransforms,
root: context.cwd,
};
// we can't use the third argument here, to keep feeding to traverseResult
// as that would break the import alias overrides. A client-entry file
// can resolve `create-api` as `create-api-client.js` while server-entry
// would resolve `create-api` to `create-api-server`. Sharing the subresult
// between the initial and retry attempt, would make it fail cache recovery
const subResult = yield (0, traverse_1.traverse)(path_1.default.resolve(entry.file), traverseConfig).catch((err) => {
if (err instanceof cache_1.InvalidCacheError) {
(0, cache_1.purgeCache)();
// Retry once after invalid cache case.
return (0, traverse_1.traverse)(path_1.default.resolve(entry.file), traverseConfig);
}
else {
throw err;
}
});
subResult.files = new Map([...subResult.files].sort());
// and that's why we need to merge manually
subResult.modules.forEach((module) => {
traverseResult.modules.add(module);
});
subResult.unresolved.forEach((unresolved, key) => {
traverseResult.unresolved.set(key, unresolved);
});
for (const [key, stat] of subResult.files) {
const prev = traverseResult.files.get(key);
if (!prev) {
traverseResult.files.set(key, stat);
continue;
}
const added = new Set(prev.imports.map((x) => x.path));
for (const file of stat.imports) {
if (!added.has(file.path)) {
prev.imports.push(file);
added.add(file.path);
}
}
}
}
// traverse the file system and get system data
spinner.text = 'traverse the file system';
const scannedDirs = Array.from(new Set(['./src', ...((_b = config.scannedDirs) !== null && _b !== void 0 ? _b : [])]));
const scanningPromises = scannedDirs.map((dir) => __awaiter(this, void 0, void 0, function* () {
const baseUrl = (yield fs.exists(dir, cwd)) ? (0, path_1.join)(cwd, dir) : cwd;
return yield fs.list('**/*', baseUrl, {
extensions: [...config.extensions, ...config.assetExtensions],
ignore: config.ignorePatterns,
});
}));
let files = [];
yield Promise.all(scanningPromises).then((ret) => {
ret.map((value) => {
if (Array.isArray(value)) {
files = files.concat(...value);
}
});
});
const normalizedFiles = files.map((path) => path.replace(/\\/g, '/'));
spinner.text = 'process results';
spinner.stop();
const result = yield (0, process_1.processResults)(normalizedFiles, traverseResult, context);
if (args.cache) {
(0, cache_1.storeCache)();
}
if (args.fix) {
const deleteResult = yield (0, delete_1.removeUnused)(result, context);
if (deleteResult.error) {
console.log(chalk_1.default.redBright(`✕`) + ` ${deleteResult.error}`);
process.exit(1);
}
(0, print_1.printDeleteResult)(deleteResult);
process.exit(0);
}
if (args.update) {
yield (0, config_1.updateAllowLists)(result, context);
// doesn't make sense here to return a error code
process.exit(0);
}
else {
(0, print_1.printResults)(result, context);
}
// return non-zero exit code in case the result wasn't clean, to support
// running in CI environments.
if (!result.clean) {
process.exit(1);
}
}
catch (error) {
spinner.stop();
// console.log is intercepted for output comparison, this helps debugging
if (process.env.NODE_ENV === 'test' && error instanceof Error) {
console.log(error.message);
}
if (error instanceof cache_1.InvalidCacheError) {
console.error(chalk_1.default.redBright(`\nFailed parsing ${error['path']}`));
}
else if (error instanceof Error) {
console.error(chalk_1.default.redBright(error.message));
}
else {
// Who knows what this is, hopefully the .toString() is meaningful
console.error(`Unexpected value thrown: ${error}`);
}
process.exit(1);
}
});
}
exports.main = main;
if (process.env.NODE_ENV !== 'test') {
/* istanbul ignore next */
yargs_1.default
.scriptName('unimported')
.usage('$0 <cmd> [args]')
.command('* [cwd]', 'scan your project for dead files', (yargs) => {
yargs.positional('cwd', {
type: 'string',
describe: 'The root directory that unimported should run from.',
});
yargs.option('cache', {
type: 'boolean',
describe: 'Whether to use the cache. Disable the cache using --no-cache.',
default: true,
});
yargs.option('fix', {
type: 'boolean',
describe: 'Removes unused files and dependencies. This is a destructive operation, use with caution.',
default: false,
});
yargs.option('clear-cache', {
type: 'boolean',
describe: 'Clears the cache file and then exits.',
});
yargs.option('flow', {
alias: 'f',
type: 'boolean',
describe: 'Whether to strip flow types, regardless of @flow pragma.',
});
yargs.option('ignore-untracked', {
type: 'boolean',
describe: 'Ignore files that are not currently tracked by git.',
});
yargs.option('init', {
alias: 'i',
type: 'boolean',
describe: 'Dump default settings to .unimportedrc.json.',
});
yargs.option('show-config', {
type: 'boolean',
describe: 'Show config and then exists.',
});
yargs.option('show-preset', {
type: 'string',
describe: 'Show preset and then exists.',
});
yargs.option('update', {
alias: 'u',
type: 'boolean',
describe: 'Update the ignore-lists stored in .unimportedrc.json.',
});
yargs.option('config', {
type: 'string',
describe: 'The path to the config file.',
});
yargs.option('show-unused-files', {
type: 'boolean',
describe: 'formats and only prints unimported files',
});
yargs.option('show-unused-deps', {
type: 'boolean',
describe: 'formats and only prints unused dependencies',
});
yargs.option('show-unresolved-imports', {
type: 'boolean',
describe: 'formats and only prints unresolved imports',
});
}, function (argv) {
return main(argv);
})
.help().argv;
}
;