UNPKG

unimported

Version:

Scans your nodejs project folder and shows obsolete files and modules

351 lines (350 loc) 14.2 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.traverse = exports.getResultObject = exports.resolveImport = void 0; const path_1 = require("path"); const typescript_estree_1 = require("@typescript-eslint/typescript-estree"); const fs = __importStar(require("./fs")); const resolve_1 = __importDefault(require("resolve")); const flow_remove_types_1 = __importDefault(require("flow-remove-types")); const cache_1 = require("./cache"); const log_1 = require("./log"); function getDependencyName(path, config) { if (config.preset === 'meteor' && path.startsWith('meteor/')) { return path; } const [namespace, module] = path.split('/'); const name = path[0] === '@' ? `${namespace}/${module}` : namespace; if (config.dependencies[name]) { return name; } if (config.dependencies[`@types/${name}`]) { return `@types/${name}`; } return null; } function transformPath(rawPath, config) { let path = rawPath; if (config.pathTransforms) { for (const [search, replace] of Object.entries(config.pathTransforms)) { path = path.replace(new RegExp(search, 'g'), replace); } } return path; } function resolveImport(rawPath, cwd, config) { let path = transformPath(rawPath, config); const dependencyName = getDependencyName(path, config); if (dependencyName) { return { type: 'node_module', name: dependencyName, path, }; } try { return { type: 'source_file', path: resolve_1.default .sync(path, { basedir: cwd, extensions: config.extensions, moduleDirectory: config.moduleDirectory, }) .replace(/\\/g, '/'), }; } catch (e) { } // import { random } from '@helpers' if (config.aliases[`${path}/`]) { // append a slash to the path so that the resolve logic below recognizes this as an /index import path = `${path}/`; } // import random from '@helpers/random' > '@helpers/random'.startsWith('@helpers/') const aliases = Object.keys(config.aliases).filter((alias) => path.startsWith(alias)); for (const alias of aliases) { for (const alt of config.aliases[alias]) { try { return { type: 'source_file', path: resolve_1.default .sync(path.replace(alias, alt), { basedir: cwd, extensions: config.extensions, moduleDirectory: config.moduleDirectory, }) .replace(/\\/g, '/'), }; } catch (e) { } } } // last attempt, try prefix the path with ./, `import 'index' to `import './index'` // can be useful for the entry files try { return { type: 'source_file', path: resolve_1.default .sync(`./${path}`, { basedir: cwd, extensions: config.extensions, moduleDirectory: config.moduleDirectory, }) .replace(/\\/g, '/'), }; } catch (e) { } // if nothing else works out :( return { type: 'unresolved', path: path, }; } exports.resolveImport = resolveImport; const VueScriptRegExp = new RegExp('<script(?:(\\s?(?<key>lang|setup|src))(?:=[\'"](?<value>.+)[\'"])?)*\\s?\\/?>', 'i'); function extractFromScriptTag(code) { var _a, _b; const lines = code.split('\n'); const start = []; const end = []; // walk the code from start to end to find the first <script> tag on it's own line for (let idx = 0; idx < lines.length; idx++) { const matches = lines[idx].match(VueScriptRegExp); if (!matches) { continue; } if (((_a = matches.groups) === null || _a === void 0 ? void 0 : _a.key) === 'src') { return `import '${(_b = matches.groups) === null || _b === void 0 ? void 0 : _b.value.trim()}';`; } start.push(idx); if (start.length === 2) { break; } } // walk the code in reverse to find the last </script> tag on it's own line for (let idx = lines.length - 1; idx >= 0; idx--) { if (lines[idx].trim() === '</script>') { end.push(idx); } if (end.length === 2) { break; } } let str = ''; if (start.length > 0 && end.length > 0) { const endReversed = end.reverse(); start.forEach((value, index) => { str += lines.slice(value + 1, endReversed[index]).join('\n'); }); } return str; } // removeFlowTypes checks for pragmas, we use app arguments to override and // strip flow annotations from all files, regardless if it contains the pragma. function handleFlowType(code, config) { // note that we've patched `flow-remove-types` to strip import kinds, // but not the statements return (0, flow_remove_types_1.default)(code, { all: config.flow }).toString(); } function parse(path, config) { return __awaiter(this, void 0, void 0, function* () { log_1.log.info('parse %s', path); const stats = { path, extname: (0, path_1.extname)(path), dirname: (0, path_1.dirname)(path), imports: [], }; let code = yield fs.readText(path); // Let's just assume that nobody is going to write flow in .ts files. if (stats.extname !== '.ts' && stats.extname !== '.tsx') { code = handleFlowType(code, config); } if (stats.extname === '.vue') { code = extractFromScriptTag(code); } // this jsx check isn't bullet proof, but I have no idea how we can deal with // this better. The parser will fail on generics like <T> in jsx files, if we // don't specify those as being jsx. const ast = (0, typescript_estree_1.parse)(code, { comment: false, jsx: stats.extname !== '.ts', }); (0, typescript_estree_1.simpleTraverse)(ast, { enter(node) { var _a, _b; let target; switch (node.type) { // import x from './x'; case typescript_estree_1.AST_NODE_TYPES.ImportDeclaration: if (!node.source.value) { break; } target = node.source.value; break; // export { x } from './x'; case typescript_estree_1.AST_NODE_TYPES.ExportNamedDeclaration: if (!((_a = node.source) === null || _a === void 0 ? void 0 : _a.value)) { break; } target = node.source.value; break; // export * from './x'; case typescript_estree_1.AST_NODE_TYPES.ExportAllDeclaration: if (!node.source) { break; } target = node.source.value; break; // import('.x') || await import('.x') case typescript_estree_1.AST_NODE_TYPES.ImportExpression: const { source } = node; if (!source) { break; } if (source.type === 'TemplateLiteral') { // Allow for constant template literals, import(`.x`) if (source.expressions.length === 0 && source.quasis.length === 1) { target = source.quasis[0].value.cooked; } } else { target = source.value; } break; // require('./x') || await require('./x') case typescript_estree_1.AST_NODE_TYPES.CallExpression: { if (((_b = node.callee) === null || _b === void 0 ? void 0 : _b.name) !== 'require') { break; } const [argument] = node.arguments; if (argument.type === 'TemplateLiteral') { // Allow for constant template literals, require(`.x`) if (argument.expressions.length === 0 && argument.quasis.length === 1) { target = argument.quasis[0].value.cooked; } } else { target = argument.value; } break; } } if (target) { const resolved = resolveImport(target, stats.dirname, config); stats.imports.push(resolved); } }, }); return stats; }); } const getResultObject = () => ({ unresolved: new Map(), modules: new Set(), files: new Map(), }); exports.getResultObject = getResultObject; function traverse(path, config, result = (0, exports.getResultObject)()) { return __awaiter(this, void 0, void 0, function* () { if (Array.isArray(path)) { yield Promise.all(path.map((x) => traverse(x, config, result))); return result; } path = path.replace(/\\/g, '/'); // be sure to only process each file once, and not end up in recursion troubles if (result.files.has(path)) { return result; } // only process code files, no json or css const ext = (0, path_1.extname)(path); if (!config.extensions.includes(ext)) { if (config.assetExtensions.includes(ext)) { result.files.set(path, { path, extname: (0, path_1.extname)(path), dirname: (0, path_1.dirname)(path), imports: [], }); } return result; } let parseResult; try { const generator = () => parse(String(path), config); parseResult = config.cacheId ? yield (0, cache_1.resolveEntry)(path, generator, config.cacheId) : yield parse(path, config); result.files.set(path, parseResult); for (const file of parseResult.imports) { switch (file.type) { case 'node_module': result.modules.add(file.name); break; case 'unresolved': const current = result.unresolved.get(file.path) || []; const path = (0, path_1.relative)(config.root, parseResult.path); const next = current.includes(path) ? current : [...current, path]; result.unresolved.set(file.path, next); break; case 'source_file': if (result.files.has(file.path)) { break; } yield traverse(file.path, config, result); break; } } } catch (error) { if (config.cacheId) { (0, cache_1.invalidateEntry)(path); (0, cache_1.invalidateEntries)((meta) => { // Invalidate anyone referencing this file return !!meta.imports.find((x) => x.path === path); }); } if (error instanceof Error && !(error instanceof cache_1.InvalidCacheError)) { throw cache_1.InvalidCacheError.wrap(error, path); } throw error; } return result; }); } exports.traverse = traverse;