detective-typescript
Version:
Get the dependencies of a TypeScript module
159 lines (127 loc) • 3.87 kB
JavaScript
import parser from '@typescript-eslint/typescript-estree';
import { isRequire, isPlainRequire, isMainScopedRequire } from 'ast-module-types';
import Walker from 'node-source-walk';
/**
* Extracts the dependencies of the supplied TypeScript module
*
* @param {String|Object} src - File's content or AST
* @return {String[]}
*/
export default function detective(src, options = {}) {
if (src === undefined) throw new Error('src not given');
if (src === '') return [];
// Destructure detective-specific options; the rest are forwarded to the walker/parser.
const {
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#import-types
// https://www.typescriptlang.org/v2/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
skipTypeImports: skipTypeImportsRaw,
mixedImports: mixedImportsRaw,
skipAsyncImports,
onFile,
onAfterFile,
...walkerOptions
} = options;
const skipTypeImports = Boolean(skipTypeImportsRaw);
const mixedImports = Boolean(mixedImportsRaw);
walkerOptions.parser = parser;
const walker = new Walker(walkerOptions);
const dependencies = [];
// Pre-parse the source to get the AST to pass to `onFile`,
// then reuse that AST below in our walker walk.
const ast = typeof src === 'string' ? walker.parse(src) : src;
if (onFile) {
onFile({
options,
src,
ast,
walker
});
}
walker.walk(ast, node => {
switch (node.type) {
case 'ImportExpression': {
if (!skipAsyncImports && node.source?.value) {
dependencies.push(node.source.value);
}
break;
}
case 'ImportDeclaration': {
if (skipTypeImports && isTypeNode(node, 'importKind')) {
break;
}
if (node.source?.value) {
dependencies.push(node.source.value);
}
break;
}
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration': {
if (skipTypeImports && isTypeNode(node, 'exportKind')) {
break;
}
if (node.source?.value) {
dependencies.push(node.source.value);
}
break;
}
case 'TSExternalModuleReference': {
if (node.expression?.value) {
dependencies.push(node.expression.value);
}
break;
}
case 'TSImportType': {
if (skipTypeImports) break;
if (node.argument.type === 'TSLiteralType') {
dependencies.push(node.argument.literal.value);
}
break;
}
case 'CallExpression': {
const dep = handleCallExpression(node, mixedImports);
if (dep) dependencies.push(dep);
break;
}
default:
// nothing
}
});
if (onAfterFile) {
onAfterFile({
options,
src,
ast,
walker,
dependencies
});
}
return dependencies;
}
detective.tsx = function(src, options = {}) {
return detective(src, { ...options, jsx: true });
};
function extractDependencyFromRequire(node) {
if (['Literal', 'StringLiteral'].includes(node.arguments[0].type)) {
return node.arguments[0].value;
}
if (node.arguments[0].type === 'TemplateLiteral') {
return node.arguments[0].quasis[0].value.raw;
}
}
function extractDependencyFromMainRequire(node) {
return node.arguments[0].value;
}
function isTypeNode(node, kind) {
return node[kind] === 'type' || (node.specifiers?.length > 0 && node.specifiers.every(n => n[kind] === 'type'));
}
function handleCallExpression(node, mixedImports) {
if (!mixedImports || !isRequire(node) || !node.arguments || node.arguments.length === 0) {
return;
}
if (isPlainRequire(node)) {
return extractDependencyFromRequire(node);
}
if (isMainScopedRequire(node)) {
return extractDependencyFromMainRequire(node);
}
}