Skip to content

Commit 3b8b328

Browse files
committed
fix(devx): seven gates that import typescript refuse an uninstalled tree with a named prerequisite, not a raw stack
`check:system-context-census` and six sibling sites still carried the bare top-level `import ts from 'typescript'` (and, at one site, a bare dynamic `import('@typescript-eslint/parser')`) that PR #11824 converted everywhere else. node resolves those before any module body runs, so the gate cannot preflight its own missing dependency: on a fresh worktree — the checkout shape CLAUDE.md mandates — it died with a node-internals `ERR_MODULE_NOT_FOUND` stack and exit **1**, the same code a real finding uses. Each site now loads the dependency through `scripts/import-prerequisite.mjs`, in the shape the 27 already-converted sites use, so the answer is `PREREQUISITE NOT MET`, exit 3, and an explicit "nothing was measured". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
1 parent d701e65 commit 3b8b328

7 files changed

Lines changed: 46 additions & 16 deletions

scripts/audits/14744-before-update-per-row-value-census.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
import { readFileSync, readdirSync, statSync } from 'node:fs';
6969
import { join, relative } from 'node:path';
7070
import { fileURLToPath } from 'node:url';
71-
import ts from 'typescript';
71+
import { requireDefaultExport } from '../import-prerequisite.mjs';
72+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
7273
// ⛔ Never `ts.createSourceFile` directly. It does not throw on a source it
7374
// cannot read — the errors are parked on `parseDiagnostics` and the recovered
7475
// tree walks like any other, so a file this census could not parse would be

scripts/check-comment-mask-corpus.mjs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ import { tmpdir } from 'node:os';
130130
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
131131
import { fileURLToPath, pathToFileURL } from 'node:url';
132132
import { isEntrypoint } from './invoked-as.mjs';
133+
import { requireDependency } from './import-prerequisite.mjs';
133134

134135
const HERE = dirname(fileURLToPath(import.meta.url));
135136
const REPO_ROOT = resolve(HERE, '..');
@@ -301,9 +302,25 @@ export async function loadMasker(maskerPath) {
301302
return module.scanSource;
302303
}
303304

304-
/** The parser is loaded lazily so importing this module stays cheap. */
305+
/**
306+
* The parser is loaded lazily so importing this module stays cheap — and through
307+
* the prerequisite thunk, so an uninstalled tree gets a NAMED prerequisite and
308+
* exit 3 instead of a raw `ERR_MODULE_NOT_FOUND` stack and exit 1. A dynamic
309+
* import defers the resolution failure past linking, but it does not change what
310+
* the failure LOOKS like: the rejection reaches the top level unhandled and node
311+
* prints the same node-internals stack with the same exit 1 a finding uses.
312+
*
313+
* ⛔ `requireDependency`, not `requireDefaultExport`: this module wants the
314+
* NAMESPACE (`parser.parse`). `@typescript-eslint/parser` has no default export
315+
* worth reading, and the default-export helper reads `.default` strictly.
316+
*/
305317
async function loadParser() {
306-
const parser = await import('@typescript-eslint/parser');
318+
const parser = await requireDependency(
319+
'@typescript-eslint/parser',
320+
() => import('@typescript-eslint/parser'),
321+
import.meta.url,
322+
{ measures: "`js-comment-mask.mjs` and an independent parser agree on every comment range in the tree" },
323+
);
307324
return (source, options) => parser.parse(source, options);
308325
}
309326

scripts/check-exported-any-returns.mts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@
118118
// them to the rest of the program — which is why landing this file lowered the
119119
// ledger entry by 54 errors it did not author. See the PR body.
120120

121-
import ts from 'typescript';
121+
// `ts` is the RUNTIME namespace, loaded through the prerequisite thunk below; `TS`
122+
// is the same namespace as TYPES ONLY. `import type` is erased before the module
123+
// graph is linked, so it cannot bring back the `ERR_MODULE_NOT_FOUND` this closes.
124+
import type TS from 'typescript';
122125
import fs from 'node:fs';
123126
import path from 'node:path';
124127
import url from 'node:url';
@@ -127,6 +130,11 @@ import os from 'node:os';
127130
import { distIsStale } from './check-regen-pending.mjs';
128131
import { isEntrypoint } from './invoked-as.mjs';
129132

133+
import { requireDefaultExport } from './import-prerequisite.mjs';
134+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url, {
135+
measures: 'any exported callable of an SDK package resolves to `any`',
136+
});
137+
130138
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
131139
const ROOT = path.resolve(HERE, '..');
132140
const SELF_TEST = process.argv.includes('--self-test');
@@ -200,7 +208,7 @@ export type ScanResult = {
200208
anyReturns: Map<string, string>;
201209
};
202210

203-
function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Program {
211+
function makeProgram(files: string[], extra: TS.CompilerOptions = {}): TS.Program {
204212
return ts.createProgram(files, {
205213
module: ts.ModuleKind.NodeNext,
206214
moduleResolution: ts.ModuleResolutionKind.NodeNext,
@@ -224,7 +232,7 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra
224232
* refuses. A named class that IS part of the surface (`RealtimeAPI`,
225233
* `QueryBuilder`) is reached anyway, as its own module export.
226234
*/
227-
export function scan(program: ts.Program, entryFile: string): ScanResult {
235+
export function scan(program: TS.Program, entryFile: string): ScanResult {
228236
const checker = program.getTypeChecker();
229237
const result: ScanResult = { callables: 0, generics: 0, anyReturns: new Map() };
230238

@@ -236,19 +244,19 @@ export function scan(program: ts.Program, entryFile: string): ScanResult {
236244
);
237245
}
238246

239-
const isAny = (t: ts.Type | undefined): boolean => Boolean(t && t.flags & ts.TypeFlags.Any);
240-
const unalias = (s: ts.Symbol): ts.Symbol =>
247+
const isAny = (t: TS.Type | undefined): boolean => Boolean(t && t.flags & ts.TypeFlags.Any);
248+
const unalias = (s: TS.Symbol): TS.Symbol =>
241249
s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s;
242250

243251
/** A `{ … }` type literal — the namespacing shape, not a named data type. */
244-
const isAnonymousObject = (t: ts.Type): boolean => {
252+
const isAnonymousObject = (t: TS.Type): boolean => {
245253
if (!(t.flags & ts.TypeFlags.Object)) return false;
246-
if (!((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous)) return false;
254+
if (!((t as TS.ObjectType).objectFlags & ts.ObjectFlags.Anonymous)) return false;
247255
const decl = t.getSymbol()?.declarations?.[0];
248256
return Boolean(decl && ts.isTypeLiteralNode(decl));
249257
};
250258

251-
const record = (key: string, sig: ts.Signature): void => {
259+
const record = (key: string, sig: TS.Signature): void => {
252260
result.callables++;
253261
if ((sig.getTypeParameters() ?? []).length > 0) result.generics++;
254262
const ret = checker.getReturnTypeOfSignature(sig);
@@ -272,7 +280,7 @@ export function scan(program: ts.Program, entryFile: string): ScanResult {
272280
// walk happened to reach first, so the second path's sites are invisible to the
273281
// ratchet and the ledger keys silently depend on walk order. A per-branch set
274282
// still terminates (a cycle must revisit an ancestor) and reports every path.
275-
const walk = (type: ts.Type, path: string, depth: number, ancestors: ReadonlySet<ts.Type>): void => {
283+
const walk = (type: TS.Type, path: string, depth: number, ancestors: ReadonlySet<TS.Type>): void => {
276284
if (depth > 8 || ancestors.has(type)) return;
277285
const branch = new Set(ancestors).add(type);
278286
for (const prop of checker.getPropertiesOfType(type)) {

scripts/check-sdui-lockstep.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ import { fileURLToPath } from 'node:url';
109109

110110
import { isEntrypoint } from './invoked-as.mjs';
111111
import { parseSourceFile } from './ts-parse.mjs';
112-
import ts from 'typescript';
112+
import { requireDefaultExport } from './import-prerequisite.mjs';
113+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
113114

114115
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
115116

scripts/isystem-census.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ import { execFileSync } from 'node:child_process';
7575
import { dirname, join } from 'node:path';
7676
import { fileURLToPath } from 'node:url';
7777

78-
import ts from 'typescript';
78+
import { requireDefaultExport } from './import-prerequisite.mjs';
79+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
7980

8081
import { isEntrypoint } from './invoked-as.mjs';
8182
import { parseSourceFile } from './ts-parse.mjs';

scripts/measure-durability-swallow-family.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@
256256
import { readFileSync, readdirSync, statSync } from 'node:fs';
257257
import { join, relative, sep } from 'node:path';
258258
import { fileURLToPath } from 'node:url';
259-
import ts from 'typescript';
259+
import { requireDefaultExport } from './import-prerequisite.mjs';
260+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
260261
import { parseSourceFile } from './ts-parse.mjs';
261262

262263
const ROOT = fileURLToPath(new URL('..', import.meta.url));

scripts/tenant-audit-census.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ import { execFileSync } from 'node:child_process';
144144
import { join } from 'node:path';
145145
import { fileURLToPath } from 'node:url';
146146

147-
import ts from 'typescript';
147+
import { requireDefaultExport } from './import-prerequisite.mjs';
148+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
148149

149150
import { isEntrypoint } from './invoked-as.mjs';
150151
import { parseSourceFile } from './ts-parse.mjs';

0 commit comments

Comments
 (0)