🌳
pt0/deployF/jsImportsF/ptMadgeF.mts
1import * as _ from 'lodash-es'
3import { dependencyFilter as defaultDepFilter } from "../dockerF/filterDevOnlyPathsF.mts"
10import { tsAbsPath } from '../../ptDirF.mts'
11import fs from 'fs'
15export const jsShebangA = ['#!/usr/bin/env ptnode', '#!/usr/bin/env node']
16export const pathBinDirname = 'path_bin'
18export const isPathBinJsScript = (filePath: string) => {
19 if (!filePath.includes(`/${pathBinDirname}/`) && !filePath.startsWith(`${pathBinDirname}/`)) return false
20 const absPath = filePath.startsWith('/') ? filePath : ptDir + '/' + filePath
21 try {
22 const firstLine = fs.readFileSync(absPath, 'utf8').split('\n')[0]
23 return jsShebangA.includes(firstLine)
24 } catch {
25 return false
26 }
29export const madgeDepFilterCtx = genContext()
30export const contentTransformCtx = genContext()
32const acornH = async (pathsA: string[]) => {
33 const store = madgeDepFilterCtx.getStore() as {dependencyFilter?: (p: string) => boolean} | undefined
34 let dependencyFilter = store?.dependencyFilter ?? defaultDepFilter
35 dependencyFilter ||= () => true
37 const seedPathSet = new Set(pathsA)
38 const remainPathsA = _.clone(_.castArray(pathsA))
40 const transformFn = contentTransformCtx.getStore() as ((s: string) => string) | undefined
41 const retH: Record<string, string[]> = {}
42 let visited = 0
43 while (true) {
44 const filePath = remainPathsA.shift()
45 if (!filePath) break
46 if (retH[filePath]) continue
47 // ptMadge is the sanctioned bulk-walk tool (the acorn budget breaker's message directs callers here).
48 // Reset periodically so legitimately large closures (pages/ reachability ~1900 files) don't trip the
49 // OOM breaker, while raw acornSingleFile loops (the actual bug class) still do.
50 if (++visited % 500 === 0) resetAcornParseCount()
51 if (!seedPathSet.has(filePath) && !dependencyFilter(filePath)) continue
52 if (isProdPatched(filePath)) continue
53 let acornOpts: {contents: string} | undefined
54 if (transformFn) {
55 try {
56 acornOpts = {contents: transformFn(fs.readFileSync(ptDir + '/' + filePath, 'utf8'))}
57 } catch (err: any) {
58 console.warn(`ptMadge: skipping unreadable stale-path ${filePath}: ${err.message}`)
59 continue
60 }
61 }
62 const importsA = _.chain(await acornSingleFile(tsAbsPath(filePath), acornOpts)).keys().flatten().value()
63 if (importsA.length == 0) continue
64 retH[filePath] = importsA
65 importsA.forEach((ptPath) => remainPathsA.push(ptPath))
66 }
67 return retH
70export const ptMadge = async (paths: string[]) => {
71 resetAcornParseCount() // budget is per-operation; devserver/stats run many walks in one process
72 if (paths.length == 0) return {}
73 const origPaths = paths
74 assertArray(paths, {origPaths})
76 paths = _.filter(paths, (p) => isPtCodeFileExt(p) || isPathBinJsScript(p))
78 throwIf(() => paths.length == 0, {paths, origPaths})
80 return await betDurMs(`ptMadge:${paths.length}seeds`, () => acornH(paths))