UNPKG

dpdm-fast

Version:

Analyze circular dependencies in your JavaScript/TypeScript projects with Rust.

194 lines (185 loc) 5.34 kB
/*! * Copyright 2019 acrazing <joking.young@gmail.com>. All rights reserved. * @since 2019-07-17 18:45:32 */ import fs from 'fs-extra'; import * as G from 'glob'; import path from 'path'; import ts from 'typescript'; import { DependencyKind } from './consts'; import { Dependency, DependencyTree, ParseOptions } from './types'; import { normalizeOptions, Resolver, shortenTree, simpleResolver, } from './utils'; const typescriptTransformOptions: ts.CompilerOptions = { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve, isolatedModules: true, }; async function parseTreeRecursive( context: string, request: string, options: ParseOptions, output: DependencyTree, resolve: Resolver, ): Promise<string | null> { const id = await resolve(context, request, options.extensions); if (!id || output[id]) { return id; } if (!options.include.test(id) || options.exclude.test(id)) { output[id] = null; return id; } if (options.js.indexOf(path.extname(id)) === -1) { output[id] = []; return id; } options.onProgress('start', id); const dependencies: Dependency[] = (output[id] = []); const jobs: Promise<string | null>[] = []; const newContext = path.dirname(id); function nodeVisitor(node: ts.Node) { let newRequest: string; let kind: DependencyKind; if (ts.isImportDeclaration(node)) { newRequest = (node.moduleSpecifier as ts.StringLiteral).text; kind = DependencyKind.StaticImport; } else if ( ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0]) && !options.skipDynamicImports ) { newRequest = (node.arguments[0] as ts.StringLiteral).text; kind = DependencyKind.DynamicImport; } else if ( ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.escapedText === 'require' && node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0]) ) { newRequest = (node.arguments[0] as ts.StringLiteral).text; kind = DependencyKind.CommonJS; } else if ( ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ) { newRequest = (node.moduleSpecifier as ts.StringLiteral).text; kind = DependencyKind.StaticExport; } else { ts.forEachChild(node, nodeVisitor); return; } dependencies.push({ issuer: id!, request: newRequest, kind: kind, id: null, }); jobs.push( parseTreeRecursive(newContext, newRequest, options, output, resolve), ); } const code = await fs.readFile(id, 'utf8'); const ext = path.extname(id); let source: ts.SourceFile | undefined; if ( options.transform && (ext === ts.Extension.Ts || ext === ts.Extension.Tsx) ) { ts.transpileModule(code, { compilerOptions: typescriptTransformOptions, transformers: { after: [() => (node) => (source = node)], }, }); } else { source = ts.createSourceFile( id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX, ); } ts.forEachChild(source!, nodeVisitor); options.onProgress('end', id); return Promise.all(jobs).then((deps) => { deps.forEach((id, index) => (dependencies[index].id = id)); return id; }); } /** * @param entries - the entry glob list * @param options */ export async function parseDependencyTree( entries: string[] | string, options: Partial<ParseOptions>, ): Promise<DependencyTree> { if (!Array.isArray(entries)) { entries = [entries]; } const currentDirectory = process.cwd(); const output: DependencyTree = {}; const fullOptions = normalizeOptions(options); let resolve = simpleResolver; if (options.tsconfig) { const compilerOptions = ts.parseJsonConfigFileContent( ts.readConfigFile(options.tsconfig, ts.sys.readFile).config, ts.sys, path.dirname(options.tsconfig), ).options; const host = ts.createCompilerHost(compilerOptions); resolve = async (context, request, extensions) => { const module = ts.resolveModuleName( request, path.join(context, 'index.ts'), compilerOptions, host, ).resolvedModule; if (module && module.extension !== ts.Extension.Dts) { return module.resolvedFileName; } else { const filename = await simpleResolver(context, request, extensions); if (filename === null && module) { return simpleResolver( context, module.resolvedFileName.slice(0, -ts.Extension.Dts.length), extensions, ); } return filename; } }; } await Promise.all( entries.map((entry) => G.glob(entry).then((matches) => Promise.all( matches.map((filename) => parseTreeRecursive( currentDirectory, path.join(currentDirectory, filename), fullOptions, output, resolve, ), ), ), ), ), ); if (fullOptions.context) { return shortenTree(fullOptions.context, output); } return output; }