detective-typescript
Version:
Get the dependencies of a TypeScript module
170 lines (135 loc) • 4.11 kB
JavaScript
;
const parser = require('@typescript-eslint/typescript-estree');
const types = require('ast-module-types');
const Walker = require('node-source-walk');
/**
* Extracts the dependencies of the supplied TypeScript module
*
* @param {String|Object} src - File's content or AST
* @return {String[]}
*/
module.exports = (src, options = {}) => {
if (src === undefined) throw new Error('src not given');
if (src === '') return [];
const walkerOptions = { ...options, parser };
// Determine whether to skip "type-only" imports
// 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
const skipTypeImports = Boolean(options.skipTypeImports);
// Remove skipTypeImports option, as this option may not be recognized by the walker/parser
delete walkerOptions.skipTypeImports;
const mixedImports = Boolean(options.mixedImports);
delete walkerOptions.mixedImports;
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 (options.onFile) {
options.onFile({
options,
src,
ast,
walker
});
}
walker.walk(src, node => {
switch (node.type) {
case 'ImportExpression': {
if (!options.skipAsyncImports && node.source?.value) {
dependencies.push(node.source.value);
}
break;
}
case 'ImportDeclaration': {
if (skipTypeImports && isTypeImports(node)) {
break;
}
if (node.source?.value) {
dependencies.push(node.source.value);
}
break;
}
case 'ExportNamedDeclaration':
case 'ExportAllDeclaration': {
if (skipTypeImports && isTypeExports(node)) {
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 && node.argument.type === 'TSLiteralType') {
dependencies.push(node.argument.literal.value);
}
break;
}
case 'CallExpression': {
if (!mixedImports || !types.isRequire(node) ||
!node.arguments ||
node.arguments.length === 0) {
break;
}
if (types.isPlainRequire(node)) {
const result = extractDependencyFromRequire(node);
if (result) {
dependencies.push(result);
}
} else if (types.isMainScopedRequire(node)) {
dependencies.push(extractDependencyFromMainRequire(node));
}
break;
}
default:
// nothing
}
});
if (options.onAfterFile) {
options.onAfterFile({
options,
src,
ast,
walker,
dependencies
});
}
return dependencies;
};
module.exports.tsx = (src, options = {}) => {
return module.exports(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 isTypeImports(node) {
if (node.importKind === 'type') {
return true;
}
if (node.specifiers?.length && node.specifiers?.every(n => n.importKind === 'type')) {
return true;
}
}
function isTypeExports(node) {
if (node.exportKind === 'type') {
return true;
}
if (node.specifiers?.length && node.specifiers?.every(n => n.exportKind === 'type')) {
return true;
}
}