🌳
pt0/deployF/gitF/transitivePkgBoundaryAI.mts
9import { type absFileDirPath } from '../../ptDirF.mts'
10import ts from 'typescript'
11import fs from 'fs'
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)
22 }
23 return true
26export const getRuntimePkgImports = async (ptPath: string): Promise<string[]> => {
27 if (isPtExtNotCode(ptPath as absFileDirPath)) return []
28 const absPath = pathDownJoin(ptDir, ptPath as absFileDirPath)
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
32 try {
33 ({ contents, tsExt } = await getTsExtContents({ ptPath: ptPath as absFileDirPath }))
34 } catch {
35 return []
36 }
37 if (isShellScript(contents)) return []
38 const runtimePkgs: string[] = []
39 if (tsExt) {
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)
47 }
48 }
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)
52 }
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)
56 }
57 ts.forEachChild(node, visit)
58 }
59 visit(sourceFile)
60 } else {
61 const ast = acornParse({ contents, ptPath: ptPath as absFileDirPath })
62 walkJsImportNodes(ast, (node) => {
63 if (!node.source.value.startsWith('.')) runtimePkgs.push(node.source.value)
64 })
65 }
66 return [...new Set(runtimePkgs)]
67 })
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)) {
79 visited.add(dep)
80 queue.push({ node: dep, path: [...path, dep] })
81 }
82 }
83 }
84 return null
87export const checkTransitivePackageImports = async (rootPtPath: string, webpackUnsafeRulesA: WebpackUnsafeRule[]): Promise<string[]> => {
88 if (webpackUnsafeRulesA.length === 0 || !isPtCodeFileExt(rootPtPath)) return []
90 return await betDurMs(`checkTransitivePackageImports:${rootPtPath}`, async () => {
91 let madgeH: Record<string, string[]>
92 try {
93 madgeH = await madgeDepFilterCtx.run({ dependencyFilter: () => true }, () => ptMadge([rootPtPath]))
94 } catch {
95 return []
96 }
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
105 closure.add(current)
106 for (const dep of (madgeH[current] || [])) bfsQueue.push(dep)
107 }
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}`)
118 }
119 }
120 }
121 return violations
122 })