unimported
Version:
Scans your nodejs project folder and shows obsolete files and modules
351 lines (350 loc) • 14.2 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.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;
;