UNPKG

dependency-cruiser

Version:

Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.

268 lines (245 loc) 8.76 kB
/* eslint-disable no-inline-comments */ import tryImport from "semver-try-require"; import meta from "../../meta.js"; const typescript = await tryImport( "typescript", meta.supportedTranspilers.typescript ); function isTypeOnly(pStatement) { return ( (pStatement.importClause && pStatement.importClause.isTypeOnly) || // for some reason the isTypeOnly indicator is on _statement_ level // and not in exportClause as it is in the importClause ¯\_ (ツ)_/¯. // Also in the case of the omission of an alias the exportClause // is not there entirely. So regardless whether there is a // pStatement.exportClause or not, we can directly test for the // isTypeOnly attribute. pStatement.isTypeOnly ); } /* * Both extractImport* assume the imports/ exports can only occur at * top level. AFAIK this the only place they're allowed, so we should * be good. Otherwise we'll need to walk the tree. */ /** * Get all import and export statements from the top level AST node * * @param {import("typescript").Node} pAST - the (top-level in this case) AST node * @returns {{module: string; moduleSystem: string; exoticallyRequired: boolean; dependencyTypes?: string[];}[]} - * all import and export statements in the * (top level) AST node */ function extractImportsAndExports(pAST) { return pAST.statements .filter( (pStatement) => (typescript.SyntaxKind[pStatement.kind] === "ImportDeclaration" || typescript.SyntaxKind[pStatement.kind] === "ExportDeclaration") && Boolean(pStatement.moduleSpecifier) ) .map((pStatement) => ({ module: pStatement.moduleSpecifier.text, moduleSystem: "es6", exoticallyRequired: false, ...(isTypeOnly(pStatement) ? { dependencyTypes: ["type-only"] } : {}), })); } /** * Get all import equals statements from the top level AST node * * E.g. import thing = require('some-thing') * Ignores import equals of variables (e.g. import protocol = ts.server.protocol * which happens in typescript/lib/protocol.d.ts) * * @param {import("typescript").Node} pAST - the (top-level in this case) AST node * @returns {{module: string, moduleSystem: string;exoticallyRequired: boolean;}[]} - all import equals statements in the * (top level) AST node */ function extractImportEquals(pAST) { return pAST.statements .filter( (pStatement) => typescript.SyntaxKind[pStatement.kind] === "ImportEqualsDeclaration" && pStatement.moduleReference && pStatement.moduleReference.expression && pStatement.moduleReference.expression.text ) .map((pStatement) => ({ module: pStatement.moduleReference.expression.text, moduleSystem: "cjs", exoticallyRequired: false, })); } /** * might be wise to distinguish the three types of /// directive that * can come out of this as the resolution algorithm might differ * * @param {import("typescript").Node} pAST - typescript syntax tree * @returns {{module: string, moduleSystem: string}[]} - 'tripple slash' dependencies */ function extractTripleSlashDirectives(pAST) { return pAST.referencedFiles .map((pReference) => ({ module: pReference.fileName, moduleSystem: "tsd", exoticallyRequired: false, })) .concat( pAST.typeReferenceDirectives.map((pReference) => ({ module: pReference.fileName, moduleSystem: "tsd", exoticallyRequired: false, })) ) .concat( pAST.amdDependencies.map((pReference) => ({ module: pReference.path, moduleSystem: "tsd", exoticallyRequired: false, })) ); } function firstArgumentIsAString(pASTNode) { const lFirstArgument = pASTNode.arguments[0]; return ( lFirstArgument && // "thing" or 'thing' (typescript.SyntaxKind[lFirstArgument.kind] === "StringLiteral" || // `thing` typescript.SyntaxKind[lFirstArgument.kind] === "FirstTemplateToken") ); } function isRequireCallExpression(pASTNode) { return ( typescript.SyntaxKind[pASTNode.kind] === "CallExpression" && pASTNode.expression && typescript.SyntaxKind[pASTNode.expression.originalKeywordKind] === "RequireKeyword" && firstArgumentIsAString(pASTNode) ); } function isSingleExoticRequire(pASTNode, pString) { return ( typescript.SyntaxKind[pASTNode.kind] === "CallExpression" && pASTNode.expression && pASTNode.expression.text === pString && firstArgumentIsAString(pASTNode) ); } /* eslint complexity:0 */ function isCompositeExoticRequire(pASTNode, pObjectName, pPropertyName) { return ( typescript.SyntaxKind[pASTNode.kind] === "CallExpression" && pASTNode.expression && typescript.SyntaxKind[pASTNode.expression.kind] === "PropertyAccessExpression" && pASTNode.expression.expression && typescript.SyntaxKind[pASTNode.expression.expression.kind] === "Identifier" && pASTNode.expression.expression.escapedText === pObjectName && pASTNode.expression.name && typescript.SyntaxKind[pASTNode.expression.name.kind] === "Identifier" && pASTNode.expression.name.escapedText === pPropertyName && firstArgumentIsAString(pASTNode) ); } function isExoticRequire(pASTNode, pString) { const lRequireStringElements = pString.split("."); return lRequireStringElements.length > 1 ? isCompositeExoticRequire(pASTNode, ...lRequireStringElements) : isSingleExoticRequire(pASTNode, pString); } function isDynamicImportExpression(pASTNode) { return ( typescript.SyntaxKind[pASTNode.kind] === "CallExpression" && pASTNode.expression && typescript.SyntaxKind[pASTNode.expression.kind] === "ImportKeyword" && firstArgumentIsAString(pASTNode) ); } function isTypeImport(pASTNode) { return ( typescript.SyntaxKind[pASTNode.kind] === "LastTypeNode" && pASTNode.argument && typescript.SyntaxKind[pASTNode.argument.kind] === "LiteralType" && ((pASTNode.argument.literal && typescript.SyntaxKind[pASTNode.argument.literal.kind] === "StringLiteral") || typescript.SyntaxKind[pASTNode.argument.literal.kind] === "FirstTemplateToken") ); } function walk(pResult, pExoticRequireStrings) { return (pASTNode) => { // require('a-string'), require(`a-template-literal`) if (isRequireCallExpression(pASTNode)) { pResult.push({ module: pASTNode.arguments[0].text, moduleSystem: "cjs", exoticallyRequired: false, }); } // const want = require; {lalala} = want('yudelyo'), window.require('elektron') pExoticRequireStrings.forEach((pExoticRequireString) => { if (isExoticRequire(pASTNode, pExoticRequireString)) { pResult.push({ module: pASTNode.arguments[0].text, moduleSystem: "cjs", exoticallyRequired: true, exoticRequire: pExoticRequireString, }); } }); // import('a-string'), import(`a-template-literal`) if (isDynamicImportExpression(pASTNode)) { pResult.push({ module: pASTNode.arguments[0].text, moduleSystem: "es6", dynamic: true, exoticallyRequired: false, }); } // const atype: import('./types').T // const atype: import(`./types`).T if (isTypeImport(pASTNode)) { pResult.push({ module: pASTNode.argument.literal.text, moduleSystem: "es6", exoticallyRequired: false, }); } typescript.forEachChild(pASTNode, walk(pResult, pExoticRequireStrings)); }; } /** * returns an array of dependencies that come potentially nested within * a source file, like commonJS or dynamic imports * * @param {import("typescript").Node} pAST - typescript syntax tree * @returns {{module: string, moduleSystem: string}[]} - all commonJS dependencies */ function extractNestedDependencies(pAST, pExoticRequireStrings) { let lResult = []; walk(lResult, pExoticRequireStrings)(pAST); return lResult; } /** * returns all dependencies in the AST * * @type {(pTypeScriptAST: (import("typescript").Node), pExoticRequireStrings: string[]) => {module: string, moduleSystem: string, dynamic: boolean}[]} */ export default function extractTypeScriptDependencies( pTypeScriptAST, pExoticRequireStrings ) { return Boolean(typescript) ? extractImportsAndExports(pTypeScriptAST) .concat(extractImportEquals(pTypeScriptAST)) .concat(extractTripleSlashDirectives(pTypeScriptAST)) .concat( extractNestedDependencies(pTypeScriptAST, pExoticRequireStrings) ) .map((pModule) => ({ dynamic: false, ...pModule })) : /* c8 ignore next */ []; }