Files
orca/config/scripts/ci-unit-dependency-graph.mjs
T
Neil 9179b93ebf ci: reduce repeated runner work and validate affected-test selection (#23540)
* ci: stage heavy checks and measure affected-test selection

* fix(ci): exercise the real Git boundary in unit selection planning

* Harden review cancellation and CI demand reporting
2026-09-27 23:25:20 -07:00

85 lines
2.3 KiB
JavaScript

import { globSync, readFileSync } from 'node:fs'
import { posix, join } from 'node:path'
import ts from 'typescript-api'
const EXTENSIONS = [
'',
'.ts',
'.tsx',
'.mjs',
'.js',
'.cjs',
'.json',
'/index.ts',
'/index.tsx',
'/index.js'
]
const INDIRECT_INPUT =
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
function localPath(file, specifier) {
if (specifier.startsWith('.')) {
return posix.join(posix.dirname(file), specifier)
}
if (specifier.startsWith('@renderer/')) {
return `src/renderer/src/${specifier.slice(10)}`
}
if (specifier.startsWith('@/')) {
return `src/renderer/src/${specifier.slice(2)}`
}
return null
}
export function buildUnitDependencyGraph(sources) {
const reverse = new Map()
const opaque = new Set()
for (const [file, source] of sources) {
if (INDIRECT_INPUT.test(source) || file.startsWith('config/') || file.startsWith('tests/')) {
opaque.add(file)
}
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
const path = localPath(file, imported.fileName)
if (path === null) {
continue
}
const resolved = EXTENSIONS.map((extension) => path + extension).find((candidate) =>
sources.has(candidate)
)
if (!resolved) {
opaque.add(file)
continue
}
if (!reverse.has(resolved)) {
reverse.set(resolved, new Set())
}
reverse.get(resolved).add(file)
}
}
return { reverse, opaque }
}
export function collectUnitDependencyGraph(root = process.cwd()) {
const files = globSync(
[
'src/**/*.{ts,tsx,js,mjs,cjs,json}',
'config/**/*.{ts,tsx,js,mjs,cjs,json}',
'tests/**/*.{ts,tsx,js,mjs,cjs,json}'
],
{ cwd: root }
)
const sources = new Map(
files.map((file) => [file.replaceAll('\\', '/'), readFileSync(join(root, file), 'utf8')])
)
return { ...buildUnitDependencyGraph(sources), files: new Set(sources.keys()) }
}
export function unitConsumers(seeds, reverse) {
const result = new Set(seeds)
for (const file of result) {
for (const consumer of reverse.get(file) ?? []) {
result.add(consumer)
}
}
return result
}