UNPKG

check-package-usage

Version:

A CLI that checks how a package is used in your source; shows unused packages; which packages are used in CommonJS/ESM; and other insights about your packages.

469 lines (444 loc) 17.7 kB
#!/usr/bin/env node /*! * check-package-usage v0.2.0 * (c) Vitor Luiz Cavalcanti * Released under the MIT License. */ 'use strict'; var cac = require('cac'); var path = require('path'); var fs = require('fs'); var readline = require('readline'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n['default'] = e; return Object.freeze(n); } var cac__default = /*#__PURE__*/_interopDefaultLegacy(cac); var path__default = /*#__PURE__*/_interopDefaultLegacy(path); var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs); var readline__default = /*#__PURE__*/_interopDefaultLegacy(readline); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(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()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function __spreadArray(to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; } /** * Resolve path with received segments. * @param {...string} segments * @returns {string} */ function resolvePath() { var segments = []; for (var _i = 0; _i < arguments.length; _i++) { segments[_i] = arguments[_i]; } return path__default['default'].resolve.apply(path__default['default'], __spreadArray([process.cwd()], segments)); } /** * Get a list of packages from package.json's dependencies and devDependencies. * @returns {Promise<string[]>} */ function getPackages() { return __awaiter(this, void 0, void 0, function () { var path, _a, _b, dependencies, _c, devDependencies; return __generator(this, function (_e) { switch (_e.label) { case 0: _e.trys.push([0, 2, , 3]); path = resolvePath('package.json'); return [4 /*yield*/, Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require(path)); })]; case 1: _a = _e.sent(), _b = _a.dependencies, dependencies = _b === void 0 ? {} : _b, _c = _a.devDependencies, devDependencies = _c === void 0 ? {} : _c; return [2 /*return*/, __spreadArray(__spreadArray([], Object.keys(dependencies)), Object.keys(devDependencies))]; case 2: _e.sent(); // TODO: Use original error as reason. process.stdout.write("Error: Couldn't import 'package.json'."); process.exit(1); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); } /** * The representation of options object. * @typedef {Object} Options * @property {BufferEncoding} encoding */ /** * The default options object. * @type {Readonly<Options>} */ var DEFAULT_OPTIONS = Object.freeze({ encoding: 'utf-8', exclude: ['./.git', './node_modules'], packagesNames: [], }); /** * A mutable binding with options object. * @type {Options} */ var options = __assign({}, DEFAULT_OPTIONS); /** * A function that mutates options object. * @param {(options: Options) => Options} fn * @returns {Options} */ function setOptions(fn) { // Updates exported binding with options object. options = fn(options); return options; } /** * Get a list of dirents from the received path. * @param {string} path * @returns {Promise<import('fs').Dirent[]>} */ function getDirents(path) { var settings = { encoding: options.encoding, withFileTypes: true, }; return new Promise(function (resolve, reject) { // TODO: Add option to verbosely display read directories. fs__default['default'].readdir(path, settings, function (error, dirents) { if (error) { // TODO: Display errors as warnings. reject(error); return; } resolve(dirents); }); }); } /** * Check if package was used in import declaration as module specifier. * @see {@link https://tc39.es/ecma262/#prod-ImportDeclaration} for more details. * @param {string} packageName * @param {string} line * @returns {boolean} */ function packageWasUsedInImportDeclaration(packageName, line) { var matcher = new RegExp("import\\s+['\"]" + packageName + "['\"/]"); return matcher.test(line); } /** * Check if package was used in from clause as module specifier. * @see {@link https://tc39.es/ecma262/#prod-FromClause} for more details. * @param {string} packageName * @param {string} line * @returns {boolean} */ function packageWasUsedInFromClause(packageName, line) { var matcher = new RegExp("from\\s+['\"]" + packageName + "['\"/]"); return matcher.test(line); } /** * Check if package was used in require function as id (module name). * @see {@link https://nodejs.org/dist/latest-v15.x/docs/api/modules.html#modules_require_id} for more details. * @param {string} packageName * @param {string} line * @returns {boolean} */ function packageWasUsedInRequireFunction(packageName, line) { var matcher = new RegExp("require\\([`'\"]" + packageName + "[`'\"/]"); return matcher.test(line); } /** * Check if package was used in import call as assignment expression. * @see {@link https://tc39.es/ecma262/#sec-import-calls} for more details. * @param {string} packageName * @param {string} line * @returns {boolean} */ function packageWasUsedInImportCall(packageName, line) { var matcher = new RegExp("import\\([`'\"]" + packageName + "[`'\"/]"); return matcher.test(line); } /** * Check if package was used. * @param {string} packageName * @param {string} line * @returns {boolean} */ function packageWasUsed(packageName, line) { var checkers = [ packageWasUsedInFromClause, packageWasUsedInImportDeclaration, packageWasUsedInRequireFunction, packageWasUsedInImportCall, ]; return checkers.some(function (check) { return check(packageName, line); }); } /** * Reads the file line-by-line and calls 'onLine' callback for each one of them. * @param {string} path * @param {(line: string) => void} onLine * @returns {Promise<void>} */ function readFileLineByLine(path, onLine) { var readStreamOptions = { encoding: options.encoding, flags: 'r', // read-only }; return new Promise(function (resolve, reject) { var input = fs__default['default'].createReadStream(resolvePath(path), readStreamOptions); var file = readline__default['default'].createInterface({ input: input, terminal: false, }); file.on('line', onLine); file.on('close', function () { resolve(); }); file.on('SIGINT', function () { reject(new Error('SIGINT')); }); }); } var store = new Map(); function checkPackagesUsageInFile(file) { process.stdout.write("Searching into " + file + "\n"); // The line number. var number = 0; return readFileLineByLine(file, function (line) { number++; options.packagesNames.forEach(function (packageName) { var _a; if (!packageWasUsed(packageName, line)) return; var usage = { file: file, line: number, }; var usages = (_a = store.get(packageName)) !== null && _a !== void 0 ? _a : []; store.set(packageName, __spreadArray(__spreadArray([], usages), [usage])); }); }); } function checkPackagesUsageInDirectory(folder) { return __awaiter(this, void 0, void 0, function () { var dirents, _i, dirents_1, dirent, path, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, getDirents(folder)]; case 1: dirents = _a.sent(); _i = 0, dirents_1 = dirents; _a.label = 2; case 2: if (!(_i < dirents_1.length)) return [3 /*break*/, 11]; dirent = dirents_1[_i]; path = folder + "/" + dirent.name; _a.label = 3; case 3: _a.trys.push([3, 9, , 10]); if (!options.exclude.includes(path)) return [3 /*break*/, 4]; return [3 /*break*/, 10]; case 4: if (!dirent.isFile()) return [3 /*break*/, 6]; return [4 /*yield*/, checkPackagesUsageInFile(path)]; case 5: _a.sent(); return [3 /*break*/, 8]; case 6: if (!dirent.isDirectory()) return [3 /*break*/, 8]; return [4 /*yield*/, checkPackagesUsageInDirectory(path)]; case 7: _a.sent(); _a.label = 8; case 8: return [3 /*break*/, 10]; case 9: error_1 = _a.sent(); process.stdout.write("Error: " + (error_1 || error_1.message) + "\n"); return [3 /*break*/, 10]; case 10: _i++; return [3 /*break*/, 2]; case 11: return [2 /*return*/]; } }); }); } function checkPackageUsage(path) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: options.packagesNames.forEach(function (packageName) { // Initialize store with all the package names. store.set(packageName, []); }); return [4 /*yield*/, checkPackagesUsageInDirectory(resolvePath(path))]; case 1: _a.sent(); return [2 /*return*/, store]; } }); }); } /** * Resolves the value to array. If value is already an array just return it, * otherwise creates a new array with the value as first/single element. * @param {T | Array.<T>} value * @returns {Array.<T>} * @template T */ function resolveToArray(value) { if (Array.isArray(value)) { return value; } return [value]; } /** * The comma character. */ var COMMA = ','; /** * Resolve argument to a list of paths. * @param {unknown} value * @returns {string[]} */ function resolveToPaths(value) { return resolveToArray(value).flatMap(function (paths) { return String(paths) .split(COMMA) .map(function (path) { return resolvePath(path); }); }); } var name = "check-package-usage"; var version = "0.2.0"; var cli = cac__default['default'](name); cli.version(version); cli .command('[...packages]', 'Check how packages are used.') .option('--exclude <A comma separated list of globs>', 'Specifies a list of globs to be excluded from compilation.', { type: [], default: options.exclude, }) .option('--encoding <A buffer enconding>', 'Specifies the buffer encondig to be used.', { default: options.encoding }) .action(function (_, args) { return __awaiter(void 0, void 0, void 0, function () { var packagesNames, packagesUsages, entries, packagesNotImportedAtAll, packagesOnlyImportedOnce; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, getPackages()]; case 1: packagesNames = _a.sent(); // Update options. setOptions(function (options) { return (__assign(__assign({}, options), { packagesNames: packagesNames, encoding: args.encoding, exclude: resolveToPaths(args.exclude) })); }); return [4 /*yield*/, checkPackageUsage('.')]; case 2: _a.sent(); return [4 /*yield*/, checkPackageUsage('.')]; case 3: packagesUsages = _a.sent(); entries = Array.from(packagesUsages.entries()); packagesNotImportedAtAll = entries.filter(function (_a) { var imports = _a[1]; return imports.length === 0; }); if (packagesNotImportedAtAll.length) { process.stdout.write("Packages not imported at all:\n"); packagesNotImportedAtAll.forEach(function (_a) { var packageName = _a[0]; var message = " - '" + packageName + "' wasn't imported in any source.\n"; process.stdout.write(message); }); } packagesOnlyImportedOnce = entries.filter(function (_a) { var imports = _a[1]; return imports.length === 1; }); if (packagesOnlyImportedOnce.length) { process.stdout.write("Packages only imported once:\n"); packagesOnlyImportedOnce.forEach(function (_a) { var packageName = _a[0], imports = _a[1]; return imports.forEach(function (_a) { var line = _a.line, file = _a.file; process.stdout.write("- '" + packageName + "' was just imported here:\n"); process.stdout.write(" " + file + ":" + line + "\n\n"); }); }); } return [2 /*return*/]; } }); }); }); cli.help(); cli.parse(); //# sourceMappingURL=main.js.map