UNPKG

unimported

Version:

Scans your nodejs project folder and shows obsolete files and modules

328 lines (327 loc) 14.7 kB
"use strict"; 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; }