10import ts from 'typescript' 14type WebpackUnsafeRule = [RegExp, string[], boolean] 16const isRuntimeImportClause = (clause: ts.ImportClause): boolean => { 17 if (clause.isTypeOnly) return false 18 if (clause.name) return true 19 if (clause.namedBindings) { 20 if (ts.isNamespaceImport(clause.namedBindings)) return true 21 if (ts.isNamedImports(clause.namedBindings)) return clause.namedBindings.elements.some(el => !el.isTypeOnly) 26export const getRuntimePkgImports = async (ptPath: string): Promise<string[]> => { 29 const mtime = fs.statSync(absPath, { throwIfNoEntry: false })?.mtimeMs || 0 30 return runMemoTempfile({ cacheKeyA: ['runtimePkgImports-v1', ptPath, String(mtime)] }, async () => { 31 let contents: string, tsExt: string | undefined 38 const runtimePkgs: string[] = [] 40 const sourceFile = tsSrcFile({ ptPath: ptPath as absFileDirPath, contents }) 41 const visit = (node: ts.Node) => { 42 if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { 43 const spec = node.moduleSpecifier.text 44 if (!spec.startsWith('.')) { 45 const clause = node.importClause 46 if (!clause || isRuntimeImportClause(clause)) runtimePkgs.push(spec) 49 if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && !node.isTypeOnly) { 50 const spec = node.moduleSpecifier.text 51 if (!spec.startsWith('.')) runtimePkgs.push(spec) 53 if (ts.isCallExpression(node) && node.expression?.kind === ts.SyntaxKind.ImportKeyword) { 54 const arg = node.arguments?.[0] 55 if (arg && ts.isStringLiteral(arg) && !arg.text.startsWith('.')) runtimePkgs.push(arg.text) 57 ts.forEachChild(node, visit) 61 const ast = acornParse({ contents, ptPath: ptPath as absFileDirPath }) 63 if (!node.source.value.startsWith('.')) runtimePkgs.push(node.source.value) 66 return [...new Set(runtimePkgs)] 70const findShortestPath = (madgeH: Record<string, string[]>, from: string, to: string): string[] | null => { 71 if (from === to) return [from] 72 const queue: { node: string, path: string[] }[] = [{ node: from, path: [from] }] 73 const visited = new Set([from]) 74 while (queue.length) { 75 const { node, path } = queue.shift()! 76 for (const dep of (madgeH[node] || [])) { 77 if (dep === to) return [...path, dep] 78 if (!visited.has(dep)) { 80 queue.push({ node: dep, path: [...path, dep] }) 87export const checkTransitivePackageImports = async (rootPtPath: string, webpackUnsafeRulesA: WebpackUnsafeRule[]): Promise<string[]> => { 90 return await betDurMs(`checkTransitivePackageImports:${rootPtPath}`, async () => { 91 let madgeH: Record<string, string[]> 93 madgeH = await madgeDepFilterCtx.run({ dependencyFilter: () => true }, () => ptMadge([rootPtPath])) 98 if (!madgeH || Object.keys(madgeH).length === 0) return [] 100 const closure = new Set<string>() 101 const bfsQueue = [rootPtPath] 102 while (bfsQueue.length) { 103 const current = bfsQueue.shift()! 104 if (closure.has(current)) continue 106 for (const dep of (madgeH[current] || [])) bfsQueue.push(dep) 109 const violations: string[] = [] 110 for (const filePtPath of closure) { 111 const pkgImports = await getRuntimePkgImports(filePtPath) 112 for (const pkgSpec of pkgImports) { 113 for (const [pkgRe] of webpackUnsafeRulesA) { 114 if (!pkgRe.test(pkgSpec)) continue 115 const chain = findShortestPath(madgeH, rootPtPath, filePtPath) 116 const chainStr = chain && chain.length > 1 ? ' via ' + chain.slice(1).join(' → ') : '' 117 violations.push(`${rootPtPath} transitively imports ${pkgSpec}${chainStr}`)