@ticket-gate/engine 0.0.0 → 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +82 -0
- package/bin/ticket-gate.mjs +261 -0
- package/checks/assertions-unguarded.mjs +31 -0
- package/checks/assertions.mjs +319 -0
- package/checks/cache-report.mjs +313 -0
- package/checks/docs-stale.mjs +69 -0
- package/checks/duplication.mjs +258 -0
- package/checks/ratchet-options.mjs +51 -0
- package/checks/surface.mjs +397 -0
- package/cli.mjs +379 -0
- package/commands-config.mjs +55 -0
- package/config.mjs +597 -0
- package/digest.mjs +195 -0
- package/env-name.mjs +24 -0
- package/failures.mjs +256 -0
- package/fu.mjs +458 -0
- package/graph/js.mjs +517 -0
- package/graph/load-time.mjs +309 -0
- package/graph/typescript.mjs +28 -0
- package/hooks/pre-tool-use.mjs +213 -0
- package/hooks/stop.mjs +495 -0
- package/is-main.mjs +15 -0
- package/kill-group.mjs +13 -0
- package/ledger.mjs +186 -0
- package/lock.d.mts +72 -0
- package/lock.mjs +329 -0
- package/move-proof.mjs +206 -0
- package/mutations/check.mjs +506 -0
- package/mutations/cli.mjs +361 -0
- package/mutations/exec.mjs +120 -0
- package/mutations/gate.mjs +148 -0
- package/mutations/harness.mjs +240 -0
- package/mutations/kinds.mjs +148 -0
- package/mutations/locate.mjs +71 -0
- package/mutations/project.mjs +23 -0
- package/mutations/registry.mjs +173 -0
- package/mutations/rotate.mjs +419 -0
- package/mutations/rotation-status.mjs +93 -0
- package/mutations/run.mjs +252 -0
- package/mutations/select.mjs +333 -0
- package/mutations/served.mjs +216 -0
- package/mutations/validate.mjs +86 -0
- package/mutations/verdict.mjs +159 -0
- package/nightly/cli.mjs +30 -0
- package/nightly/define.mjs +111 -0
- package/nightly/e2e-runs.mjs +73 -0
- package/nightly/gate.mjs +25 -0
- package/nightly/io.mjs +109 -0
- package/nightly/launchd.mjs +42 -0
- package/nightly/record.mjs +231 -0
- package/nightly/run.mjs +470 -0
- package/nightly/schedule.mjs +76 -0
- package/nightly/seed.mjs +78 -0
- package/nightly/status.mjs +189 -0
- package/nightly/suites.mjs +43 -0
- package/nightly-config.mjs +38 -0
- package/nightly-window.mjs +6 -0
- package/pack-hooks-all.mjs +15 -0
- package/pack-hooks.mjs +26 -0
- package/package.json +21 -1
- package/plan.mjs +272 -0
- package/precheck.mjs +90 -0
- package/project.mjs +21 -0
- package/ratchet.mjs +431 -0
- package/receipt.mjs +34 -0
- package/review-map.mjs +432 -0
- package/root.mjs +33 -0
- package/run.mjs +291 -0
- package/runners/playwright.mjs +132 -0
- package/score/adapters/stryker.mjs +111 -0
- package/score/base.mjs +82 -0
- package/score/cli.mjs +182 -0
- package/score/judge.mjs +107 -0
- package/score/lines.mjs +40 -0
- package/score/schema.mjs +68 -0
- package/score/survivors.mjs +103 -0
- package/score/targets.mjs +34 -0
- package/score-config.mjs +120 -0
- package/selection/check.mjs +45 -0
- package/selection/cli.mjs +267 -0
- package/selection/define.mjs +55 -0
- package/selection/for.mjs +235 -0
- package/selection/graphs.mjs +38 -0
- package/selection/project.mjs +11 -0
- package/selection/select.mjs +159 -0
- package/selection/units.mjs +646 -0
- package/selection/value-rules.mjs +142 -0
- package/selection-config.mjs +76 -0
- package/serve.mjs +142 -0
- package/summary.mjs +34 -0
- package/tree.mjs +211 -0
package/graph/js.mjs
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { evaluationSpecs, hasLoadTimeCode } from './load-time.mjs';
|
|
5
|
+
import { projectSelection } from '../selection/project.mjs';
|
|
6
|
+
import { unionGraph } from '../selection/graphs.mjs';
|
|
7
|
+
import { lazyTs } from './typescript.mjs';
|
|
8
|
+
|
|
9
|
+
const ts = lazyTs();
|
|
10
|
+
|
|
11
|
+
let tsOptions = null;
|
|
12
|
+
export const tsOptionsOf = () =>
|
|
13
|
+
(tsOptions ??= {
|
|
14
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
15
|
+
module: ts.ModuleKind.ESNext,
|
|
16
|
+
target: ts.ScriptTarget.ESNext,
|
|
17
|
+
allowImportingTsExtensions: true,
|
|
18
|
+
resolveJsonModule: true,
|
|
19
|
+
jsx: ts.JsxEmit.Preserve,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const posix = (p) => p.split('\\').join('/');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------- file helpers
|
|
25
|
+
|
|
26
|
+
export function listFiles(root, rel, accept) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const visit = (dir) => {
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(join(root, dir), { withFileTypes: true });
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const e of entries) {
|
|
36
|
+
if (e.name === 'node_modules' || e.name === '.next') continue;
|
|
37
|
+
const p = `${dir}/${e.name}`;
|
|
38
|
+
if (e.isDirectory()) visit(p);
|
|
39
|
+
else if (accept(p)) out.push(p);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
visit(rel);
|
|
43
|
+
return out.sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const readText = (root, rel) => {
|
|
47
|
+
try {
|
|
48
|
+
return readFileSync(join(root, rel), 'utf8');
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------- import graph
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse one file into its import uses and re-exports.
|
|
58
|
+
* uses: [{ spec, names: string[] | null }] (null = the whole module)
|
|
59
|
+
* exports: pure-barrel re-exports. isBarrel: only import/export-from/type statements.
|
|
60
|
+
*/
|
|
61
|
+
function parseModule(text, rel) {
|
|
62
|
+
const kind = rel.endsWith('.tsx')
|
|
63
|
+
? ts.ScriptKind.TSX
|
|
64
|
+
: rel.endsWith('.ts')
|
|
65
|
+
? ts.ScriptKind.TS
|
|
66
|
+
: ts.ScriptKind.JS;
|
|
67
|
+
const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true, kind);
|
|
68
|
+
const uses = new Map();
|
|
69
|
+
const addUse = (spec, names) => {
|
|
70
|
+
const prev = uses.get(spec);
|
|
71
|
+
if (prev === undefined) uses.set(spec, names === null ? null : [...names]);
|
|
72
|
+
else if (prev !== null)
|
|
73
|
+
uses.set(spec, names === null ? null : [...prev, ...names]);
|
|
74
|
+
};
|
|
75
|
+
const exports = [];
|
|
76
|
+
let isBarrel = true;
|
|
77
|
+
for (const st of sf.statements) {
|
|
78
|
+
if (ts.isImportDeclaration(st)) {
|
|
79
|
+
const spec = st.moduleSpecifier.text;
|
|
80
|
+
const c = st.importClause;
|
|
81
|
+
const nb = c?.namedBindings;
|
|
82
|
+
if (!c || c.name || (nb && ts.isNamespaceImport(nb))) addUse(spec, null);
|
|
83
|
+
else if (nb && ts.isNamedImports(nb)) {
|
|
84
|
+
const names = nb.elements.map((e) => (e.propertyName ?? e.name).text);
|
|
85
|
+
addUse(spec, names.includes('default') ? null : names);
|
|
86
|
+
}
|
|
87
|
+
} else if (ts.isExportDeclaration(st) && st.moduleSpecifier) {
|
|
88
|
+
const spec = st.moduleSpecifier.text;
|
|
89
|
+
const c = st.exportClause;
|
|
90
|
+
if (!c) {
|
|
91
|
+
exports.push({ kind: 'star', spec });
|
|
92
|
+
addUse(spec, null);
|
|
93
|
+
} else if (ts.isNamespaceExport(c)) {
|
|
94
|
+
exports.push({ kind: 'ns', spec, exported: c.name.text });
|
|
95
|
+
addUse(spec, null);
|
|
96
|
+
} else {
|
|
97
|
+
const locals = [];
|
|
98
|
+
for (const e of c.elements) {
|
|
99
|
+
const local = (e.propertyName ?? e.name).text;
|
|
100
|
+
exports.push({ kind: 'named', spec, exported: e.name.text, local });
|
|
101
|
+
locals.push(local);
|
|
102
|
+
}
|
|
103
|
+
addUse(spec, locals.includes('default') ? null : locals);
|
|
104
|
+
}
|
|
105
|
+
} else if (
|
|
106
|
+
!ts.isInterfaceDeclaration(st) &&
|
|
107
|
+
!ts.isTypeAliasDeclaration(st)
|
|
108
|
+
) {
|
|
109
|
+
isBarrel = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const spec of ts
|
|
113
|
+
.preProcessFile(text, true, true)
|
|
114
|
+
.importedFiles.map((f) => f.fileName))
|
|
115
|
+
if (!uses.has(spec)) uses.set(spec, null);
|
|
116
|
+
return { uses, exports, isBarrel, sf };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Names a (non-barrel or barrel) module exports, following `export *`. */
|
|
120
|
+
function exportedNamesOf(sf, loadExports, seen = new Set()) {
|
|
121
|
+
const names = new Set();
|
|
122
|
+
for (const st of sf.statements) {
|
|
123
|
+
const mods = ts.canHaveModifiers(st) ? ts.getModifiers(st) : undefined;
|
|
124
|
+
const exported = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
125
|
+
const isDefault = mods?.some(
|
|
126
|
+
(m) => m.kind === ts.SyntaxKind.DefaultKeyword,
|
|
127
|
+
);
|
|
128
|
+
if (exported && isDefault) names.add('default');
|
|
129
|
+
else if (exported && ts.isVariableStatement(st))
|
|
130
|
+
for (const d of st.declarationList.declarations) {
|
|
131
|
+
if (ts.isIdentifier(d.name)) names.add(d.name.text);
|
|
132
|
+
else
|
|
133
|
+
for (const el of d.name.elements)
|
|
134
|
+
if (ts.isBindingElement(el) && ts.isIdentifier(el.name))
|
|
135
|
+
names.add(el.name.text);
|
|
136
|
+
}
|
|
137
|
+
else if (exported && st.name && ts.isIdentifier(st.name))
|
|
138
|
+
names.add(st.name.text);
|
|
139
|
+
else if (ts.isExportDeclaration(st)) {
|
|
140
|
+
const c = st.exportClause;
|
|
141
|
+
if (c && ts.isNamedExports(c))
|
|
142
|
+
for (const e of c.elements) names.add(e.name.text);
|
|
143
|
+
else if (c && ts.isNamespaceExport(c)) names.add(c.name.text);
|
|
144
|
+
else if (!c && st.moduleSpecifier)
|
|
145
|
+
loadExports(st.moduleSpecifier.text, names, seen);
|
|
146
|
+
} else if (ts.isExportAssignment(st)) names.add('default');
|
|
147
|
+
}
|
|
148
|
+
return names;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Roots: the files whose closure carries an area. Existing files only. */
|
|
152
|
+
export function fileRoots(files, area) {
|
|
153
|
+
return {
|
|
154
|
+
name: 'files',
|
|
155
|
+
entries: (root) =>
|
|
156
|
+
files
|
|
157
|
+
.filter((f) => existsSync(join(root, f)))
|
|
158
|
+
.map((file) => ({ file, area })),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `prefix` + rest resolved through the project's TypeScript from `${dir}/_.ts`; a miss is unresolved. */
|
|
163
|
+
export function alias(prefix, dir) {
|
|
164
|
+
return {
|
|
165
|
+
name: `alias ${prefix}`,
|
|
166
|
+
resolve(spec, _from, ctx) {
|
|
167
|
+
if (!spec.startsWith(prefix)) return null;
|
|
168
|
+
return (
|
|
169
|
+
ctx.viaTs(`./${spec.slice(prefix.length)}`, `${dir}/_.ts`) ?? {
|
|
170
|
+
unresolved: true,
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The import graph source over a checkout. Lazy per file, cached for this graph only.
|
|
179
|
+
* @param {{ roots: object[], resolvers?: object[], skipEdges?: string[][], tables?: string[], loadTime?: boolean }} options
|
|
180
|
+
*/
|
|
181
|
+
export function jsGraph({
|
|
182
|
+
roots: rootSources,
|
|
183
|
+
resolvers = [],
|
|
184
|
+
skipEdges = [],
|
|
185
|
+
tables = [],
|
|
186
|
+
loadTime = false,
|
|
187
|
+
}) {
|
|
188
|
+
return {
|
|
189
|
+
name: 'js',
|
|
190
|
+
graph: (root, cfg) =>
|
|
191
|
+
graphOf(root, cfg, {
|
|
192
|
+
rootSources,
|
|
193
|
+
resolvers,
|
|
194
|
+
skipEdges,
|
|
195
|
+
tables,
|
|
196
|
+
loadTime,
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildGraph(root, cfg = projectSelection()) {
|
|
202
|
+
return unionGraph(cfg.graphs, root, cfg);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function graphOf(root, cfg, o) {
|
|
206
|
+
const { rootSources, resolvers, skipEdges, tables, loadTime } = o;
|
|
207
|
+
const mods = new Map();
|
|
208
|
+
const resolved = new Map();
|
|
209
|
+
const unresolved = [];
|
|
210
|
+
const leaves = new Set();
|
|
211
|
+
const host = ts.sys;
|
|
212
|
+
const abs = (rel) => join(root, rel);
|
|
213
|
+
const rel = (a) => posix(relative(root, a));
|
|
214
|
+
|
|
215
|
+
const load = (file) => {
|
|
216
|
+
if (leaves.has(file)) return null;
|
|
217
|
+
if (mods.has(file)) return mods.get(file);
|
|
218
|
+
const text = readText(root, file);
|
|
219
|
+
const m = text === null ? null : parseModule(text, file);
|
|
220
|
+
mods.set(file, m);
|
|
221
|
+
return m;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const viaTs = (name, containing) => {
|
|
225
|
+
const r = ts.resolveModuleName(
|
|
226
|
+
name,
|
|
227
|
+
abs(containing),
|
|
228
|
+
tsOptionsOf(),
|
|
229
|
+
host,
|
|
230
|
+
).resolvedModule;
|
|
231
|
+
if (r && !r.isExternalLibraryImport)
|
|
232
|
+
return { file: rel(r.resolvedFileName) };
|
|
233
|
+
return null;
|
|
234
|
+
};
|
|
235
|
+
const ctx = {
|
|
236
|
+
root,
|
|
237
|
+
viaTs,
|
|
238
|
+
exists: (r) => existsSync(abs(r)),
|
|
239
|
+
readText: (r) => readText(root, r),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** -> { file } | { external: true } | { unresolved: true } */
|
|
243
|
+
const resolveSpec = (from, spec) => {
|
|
244
|
+
const key = `${from}\0${spec}`;
|
|
245
|
+
if (resolved.has(key)) return resolved.get(key);
|
|
246
|
+
let out = null;
|
|
247
|
+
for (const r of resolvers) {
|
|
248
|
+
out = r.resolve(spec, from, ctx);
|
|
249
|
+
if (out) break;
|
|
250
|
+
}
|
|
251
|
+
if (out?.leaf) leaves.add(out.file);
|
|
252
|
+
if (!out && spec.startsWith('.')) {
|
|
253
|
+
out = viaTs(spec, from);
|
|
254
|
+
if (!out) {
|
|
255
|
+
const plain = posix(join(dirname(from), spec));
|
|
256
|
+
if (
|
|
257
|
+
/\.[a-z]+$/i.test(spec) &&
|
|
258
|
+
existsSync(abs(plain)) &&
|
|
259
|
+
statSync(abs(plain)).isFile()
|
|
260
|
+
)
|
|
261
|
+
out = { file: plain };
|
|
262
|
+
}
|
|
263
|
+
} else if (!out && !spec.startsWith('.')) {
|
|
264
|
+
out = { external: true };
|
|
265
|
+
}
|
|
266
|
+
out ??= { unresolved: true };
|
|
267
|
+
if (out.unresolved) unresolved.push({ importer: from, spec });
|
|
268
|
+
resolved.set(key, out);
|
|
269
|
+
return out;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const barrelOf = (file) => {
|
|
273
|
+
const m = load(file);
|
|
274
|
+
return m?.isBarrel ? m : null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const exportNamesOfFile = (file, seen = new Set()) => {
|
|
278
|
+
if (seen.has(file)) return new Set();
|
|
279
|
+
seen.add(file);
|
|
280
|
+
const m = load(file);
|
|
281
|
+
if (!m) return new Set();
|
|
282
|
+
return exportedNamesOf(
|
|
283
|
+
m.sf,
|
|
284
|
+
(spec, names, s) => {
|
|
285
|
+
const t = resolveSpec(file, spec);
|
|
286
|
+
if (t.file) for (const n of exportNamesOfFile(t.file, s)) names.add(n);
|
|
287
|
+
},
|
|
288
|
+
seen,
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** Follow `name` through barrel `b`; null = could not resolve (caller expands b whole). */
|
|
293
|
+
const followName = (b, name, marks, seen) => {
|
|
294
|
+
const key = `${b}\0${name}`;
|
|
295
|
+
if (seen.has(key)) return [];
|
|
296
|
+
seen.add(key);
|
|
297
|
+
marks.add(b);
|
|
298
|
+
const m = load(b);
|
|
299
|
+
for (const e of m.exports) {
|
|
300
|
+
if ((e.kind === 'named' || e.kind === 'ns') && e.exported === name) {
|
|
301
|
+
const t = resolveSpec(b, e.spec);
|
|
302
|
+
if (!t.file) return null;
|
|
303
|
+
if (e.kind === 'ns') return [t.file];
|
|
304
|
+
if (barrelOf(t.file)) return followName(t.file, e.local, marks, seen);
|
|
305
|
+
return [t.file];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const e of m.exports) {
|
|
309
|
+
if (e.kind !== 'star') continue;
|
|
310
|
+
const t = resolveSpec(b, e.spec);
|
|
311
|
+
if (!t.file) continue;
|
|
312
|
+
if (barrelOf(t.file)) {
|
|
313
|
+
const r = followName(t.file, name, marks, seen);
|
|
314
|
+
if (r && r.length) return r;
|
|
315
|
+
} else if (exportNamesOfFile(t.file).has(name)) {
|
|
316
|
+
return [t.file];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const edgeCache = new Map();
|
|
323
|
+
/** -> { expand: string[], mark: string[] } */
|
|
324
|
+
const edgesOf = (file) => {
|
|
325
|
+
if (edgeCache.has(file)) return edgeCache.get(file);
|
|
326
|
+
const expand = new Set();
|
|
327
|
+
const mark = new Set();
|
|
328
|
+
const m = load(file);
|
|
329
|
+
if (m) {
|
|
330
|
+
for (const [spec, rawNames] of m.uses) {
|
|
331
|
+
const t = resolveSpec(file, spec);
|
|
332
|
+
if (!t.file || tables.includes(t.file)) continue;
|
|
333
|
+
const names = m.isBarrel ? null : rawNames;
|
|
334
|
+
if (names !== null && barrelOf(t.file)) {
|
|
335
|
+
for (const n of names) {
|
|
336
|
+
const marks = new Set();
|
|
337
|
+
const r = followName(t.file, n, marks, new Set());
|
|
338
|
+
if (r === null) expand.add(t.file);
|
|
339
|
+
else {
|
|
340
|
+
for (const x of marks) mark.add(x);
|
|
341
|
+
for (const x of r) expand.add(x);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} else expand.add(t.file);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const out = { expand: [...expand], mark: [...mark] };
|
|
348
|
+
edgeCache.set(file, out);
|
|
349
|
+
return out;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/** Entry closures skip the `skipEdges` edges; spec closures do not. */
|
|
353
|
+
const skipped = new Set(skipEdges.map(([a, b]) => `${a}\0${b}`));
|
|
354
|
+
const closureOf = (roots, skipEdges = false) => {
|
|
355
|
+
const seen = new Set();
|
|
356
|
+
const expanded = new Set();
|
|
357
|
+
const queue = [...roots];
|
|
358
|
+
for (const r of roots) seen.add(r);
|
|
359
|
+
while (queue.length) {
|
|
360
|
+
const f = queue.pop();
|
|
361
|
+
if (expanded.has(f)) continue;
|
|
362
|
+
expanded.add(f);
|
|
363
|
+
const { expand, mark } = edgesOf(f);
|
|
364
|
+
for (const x of mark) seen.add(x);
|
|
365
|
+
for (const x of expand) {
|
|
366
|
+
if (skipEdges && skipped.has(`${f}\0${x}`)) continue;
|
|
367
|
+
seen.add(x);
|
|
368
|
+
if (!expanded.has(x)) queue.push(x);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return seen;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const entries = rootSources.flatMap((r) => r.entries(root));
|
|
375
|
+
const specDir = cfg.specs.dir;
|
|
376
|
+
const specFiles = listFiles(root, specDir, (p) => cfg.specs.match.test(p));
|
|
377
|
+
|
|
378
|
+
let entryClosures = null;
|
|
379
|
+
let specClosures = null;
|
|
380
|
+
const prepare = () => {
|
|
381
|
+
entryClosures ??= [
|
|
382
|
+
...entries.map((e) => ({
|
|
383
|
+
area: e.area,
|
|
384
|
+
set: closureOf([e.file], true),
|
|
385
|
+
})),
|
|
386
|
+
...skipEdges
|
|
387
|
+
.filter(([, to]) => existsSync(abs(to)))
|
|
388
|
+
.map(([, to, area]) => ({ area, set: closureOf([to], true) })),
|
|
389
|
+
];
|
|
390
|
+
specClosures ??= specFiles.map((f) => ({
|
|
391
|
+
spec: f.slice(specDir.length + 1),
|
|
392
|
+
set: closureOf([f]),
|
|
393
|
+
}));
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Everything evaluated when a shell page loads. Module-level edges only
|
|
397
|
+
// (evaluationSpecs), barrels taken whole, skipEdges NOT skipped.
|
|
398
|
+
const evalEdgeCache = new Map();
|
|
399
|
+
// quiet: base text may name a file HEAD no longer has; that is not an unresolved import of HEAD.
|
|
400
|
+
const evalTargets = (file, text, quiet = false) => {
|
|
401
|
+
const out = new Set();
|
|
402
|
+
for (const spec of evaluationSpecs(text, file)) {
|
|
403
|
+
const before = unresolved.length;
|
|
404
|
+
const t = resolveSpec(file, spec);
|
|
405
|
+
if (quiet && t.unresolved) {
|
|
406
|
+
unresolved.length = before;
|
|
407
|
+
resolved.delete(`${file}\0${spec}`);
|
|
408
|
+
}
|
|
409
|
+
if (t.file && !tables.includes(t.file)) out.add(t.file);
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
};
|
|
413
|
+
const evalEdgesOf = (file) => {
|
|
414
|
+
if (!evalEdgeCache.has(file)) {
|
|
415
|
+
const text = readText(root, file);
|
|
416
|
+
evalEdgeCache.set(
|
|
417
|
+
file,
|
|
418
|
+
text === null ? new Set() : evalTargets(file, text),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return evalEdgeCache.get(file);
|
|
422
|
+
};
|
|
423
|
+
const shellRoots = () =>
|
|
424
|
+
entries.filter((e) => e.area === '*').map((e) => e.file);
|
|
425
|
+
const evalClosure = (skip = new Set()) => {
|
|
426
|
+
const seen = new Set(shellRoots());
|
|
427
|
+
const queue = [...seen];
|
|
428
|
+
while (queue.length) {
|
|
429
|
+
const f = queue.pop();
|
|
430
|
+
for (const x of evalEdgesOf(f)) {
|
|
431
|
+
if (skip.has(`${f}\0${x}`) || seen.has(x)) continue;
|
|
432
|
+
seen.add(x);
|
|
433
|
+
queue.push(x);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return seen;
|
|
437
|
+
};
|
|
438
|
+
let evalSet = null;
|
|
439
|
+
const getEvalSet = () => (evalSet ??= evalClosure());
|
|
440
|
+
let newlyMemo = null;
|
|
441
|
+
|
|
442
|
+
const loadTimeMembers = () => ({
|
|
443
|
+
/** Is `path` evaluated when any shell page loads? */
|
|
444
|
+
evaluates(path) {
|
|
445
|
+
prepare();
|
|
446
|
+
return getEvalSet().has(path);
|
|
447
|
+
},
|
|
448
|
+
/** The evaluation set, sorted. */
|
|
449
|
+
evaluationSet() {
|
|
450
|
+
prepare();
|
|
451
|
+
return [...getEvalSet()].sort();
|
|
452
|
+
},
|
|
453
|
+
/**
|
|
454
|
+
* Changed paths whose added evaluation edge newly evaluates load-time code.
|
|
455
|
+
* E' skips every changed file's added edges at once; N = E minus E'.
|
|
456
|
+
*/
|
|
457
|
+
newlyEvaluatedLoadTime(changed, readBase) {
|
|
458
|
+
prepare();
|
|
459
|
+
if (
|
|
460
|
+
newlyMemo?.readBase === readBase &&
|
|
461
|
+
newlyMemo.key === changed.join('\n')
|
|
462
|
+
)
|
|
463
|
+
return newlyMemo.result;
|
|
464
|
+
const E = getEvalSet();
|
|
465
|
+
const skip = new Set();
|
|
466
|
+
const withAdded = new Set();
|
|
467
|
+
for (const p of changed) {
|
|
468
|
+
if (!E.has(p)) continue;
|
|
469
|
+
const headEdges = evalEdgesOf(p);
|
|
470
|
+
const baseText = readBase(p);
|
|
471
|
+
const baseEdges =
|
|
472
|
+
baseText === null ? new Set() : evalTargets(p, baseText, true);
|
|
473
|
+
for (const t of headEdges)
|
|
474
|
+
if (!baseEdges.has(t)) {
|
|
475
|
+
skip.add(`${p}\0${t}`);
|
|
476
|
+
withAdded.add(p);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
let result = new Set();
|
|
480
|
+
if (withAdded.size > 0) {
|
|
481
|
+
const kept = evalClosure(skip);
|
|
482
|
+
const newly = [...E].filter((f) => !kept.has(f));
|
|
483
|
+
if (
|
|
484
|
+
newly.some((f) => {
|
|
485
|
+
const text = readText(root, f);
|
|
486
|
+
return text !== null && hasLoadTimeCode(text, f);
|
|
487
|
+
})
|
|
488
|
+
)
|
|
489
|
+
result = withAdded;
|
|
490
|
+
}
|
|
491
|
+
newlyMemo = { readBase, key: changed.join('\n'), result };
|
|
492
|
+
return result;
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
/** First unresolved import met by any walk, or null. */
|
|
498
|
+
get unresolved() {
|
|
499
|
+
prepare();
|
|
500
|
+
return unresolved[0] ?? null;
|
|
501
|
+
},
|
|
502
|
+
reach(path) {
|
|
503
|
+
prepare();
|
|
504
|
+
const areas = new Set();
|
|
505
|
+
const specs = new Set();
|
|
506
|
+
for (const c of entryClosures) if (c.set.has(path)) areas.add(c.area);
|
|
507
|
+
for (const c of specClosures) if (c.set.has(path)) specs.add(c.spec);
|
|
508
|
+
return { areas, specs };
|
|
509
|
+
},
|
|
510
|
+
...(loadTime ? loadTimeMembers() : {}),
|
|
511
|
+
/** Specs whose e2e closure holds `file`. */
|
|
512
|
+
importersOf(file) {
|
|
513
|
+
prepare();
|
|
514
|
+
return specClosures.filter((c) => c.set.has(file)).map((c) => c.spec);
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
}
|