Skip to content

Commit e47d5ef

Browse files
os-steveclaude
andauthored
fix(scripts): report an unmet prerequisite instead of dying with a module stack (#11824)
A fresh per-task worktree has no `node_modules` until `pnpm install` runs. The gates in `scripts/` that import `typescript`, `yaml`, `semver`, `eslint` or `github-slugger` used to answer that with a node-internals `ERR_MODULE_NOT_FOUND` stack trace, which reads exactly like a real finding: same exit 1, no statement that nothing was measured. Twenty-nine root gates did this, measured on one tree. The failure is thrown while node LINKS the module graph, which completes before any module body runs, so a preflight imported at the top of a gate never executes. The failing imports are therefore deferred behind a thunk the gate hands to a shared helper, which is the one shape that puts a catchable boundary around them. `scripts/import-prerequisite.mjs` classifies four failures that arrive wearing the same error code and keeps them apart, because they have different remedies: a package with no directory (`pnpm install`), a `@objectstack/*` package present but never built (build it — including via node's self-reference resolution, which a node_modules-only walk misses and would misreport as uninstalled), a partial install, and a package that resolved and then threw — the last rethrown untouched so its stack survives. Wording follows `check-i18n-coverage.mjs`'s `reportPrerequisiteNotMet`: what is unmet, why, the command that clears it, and that nothing was measured. Following `cli-build-prerequisite.mjs`, the frame is shared and the claim about what went unmeasured stays with the gate. No gate's measurement changes; only how an unmet precondition is reported. Every affected gate still produces its normal verdict on a built tree. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0392a12 commit e47d5ef

40 files changed

Lines changed: 695 additions & 50 deletions

packages/lint/scripts/check-doc-formula-expressions.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,19 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs';
151151
import { join, relative, resolve, sep, dirname } from 'node:path';
152152
import { fileURLToPath } from 'node:url';
153153

154-
import ts from 'typescript';
155-
import {
154+
import { requireDefaultExport, requireDependency } from '../../../scripts/import-prerequisite.mjs';
155+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
156+
const {
156157
validateExpression,
157158
isSupportedRlsExpression,
158159
sqlPredicateToCel,
159160
isPushdownableCel,
160-
} from '@objectstack/formula';
161+
} = await requireDependency('@objectstack/formula', () => import('@objectstack/formula'), import.meta.url);
161162
// The field-level `*When` root verdict AND its message, imported from the one
162163
// place that owns them (#11407). Same discipline as `validateExpression` above:
163164
// this gate is the SECOND consumer of that rule, and a second consumer that
164165
// re-derives the rule owns a dialect of it instead. See surface 3 below.
165-
import { fieldRuleRootIssue, FIELD_RULE_BOUND_ROOTS } from '@objectstack/lint';
166+
const { fieldRuleRootIssue, FIELD_RULE_BOUND_ROOTS } = await requireDependency('@objectstack/lint', () => import('@objectstack/lint'), import.meta.url);
166167

167168
const HERE = dirname(fileURLToPath(import.meta.url));
168169
const REPO_ROOT = resolve(HERE, '../../..');

packages/lint/scripts/check-doc-security-posture.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ import { join, relative, resolve, dirname } from 'node:path';
123123
import { fileURLToPath } from 'node:url';
124124
import { tmpdir } from 'node:os';
125125

126-
import ts from 'typescript';
127-
import { validateSecurityPosture, SECURITY_CBP_NO_RELATION } from '@objectstack/lint';
126+
import { requireDefaultExport, requireDependency } from '../../../scripts/import-prerequisite.mjs';
127+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
128+
const { validateSecurityPosture, SECURITY_CBP_NO_RELATION } = await requireDependency('@objectstack/lint', () => import('@objectstack/lint'), import.meta.url);
128129

129130
const HERE = dirname(fileURLToPath(import.meta.url));
130131
const REPO_ROOT = resolve(HERE, '../../..');

scripts/check-aggregator-roster.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
* naming what could not be read (#4690). The self-test pins each refusal.
8181
*/
8282

83+
import { requireDependency } from './import-prerequisite.mjs';
8384
import { readFileSync } from 'node:fs';
8485
import { dirname, join, resolve } from 'node:path';
8586
import { fileURLToPath } from 'node:url';
@@ -324,7 +325,7 @@ export function judge({ workflows }) {
324325

325326
/** Read and parse both workflows into the shape `judge` consumes. */
326327
export async function readWorkflows(root) {
327-
const { parse } = await import('yaml');
328+
const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
328329
const workflows = new Map();
329330
for (const file of WORKFLOW_FILES) {
330331
const path = join(root, '.github', 'workflows', file);
@@ -381,7 +382,7 @@ async function selfTest() {
381382
};
382383

383384
const root = scriptRepoRoot();
384-
const { parse } = await import('yaml');
385+
const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
385386
const sources = Object.fromEntries(
386387
WORKFLOW_FILES.map((file) => [file, readFileSync(join(root, '.github', 'workflows', file), 'utf8')]),
387388
);

scripts/check-ci-filter-parity.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ import { join, resolve } from 'node:path';
120120
import { fileURLToPath } from 'node:url';
121121
import process from 'node:process';
122122

123-
import { parse } from 'yaml';
123+
import { requireDependency } from './import-prerequisite.mjs';
124+
const { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
124125

125126
import { CROSS_PACKAGE_TEST_INPUTS } from './check-cross-package-test-inputs.mjs';
126127
import { isEntrypoint } from './invoked-as.mjs';

scripts/check-cli-test-child-env.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ import { tmpdir } from 'node:os';
160160
import { dirname, join, relative, resolve, sep } from 'node:path';
161161
import { fileURLToPath } from 'node:url';
162162

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

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

scripts/check-closing-keyword-parity.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
import { execFileSync } from 'node:child_process';
9999
import { readFileSync, statSync } from 'node:fs';
100100
import { join } from 'node:path';
101-
import { parseDocument } from 'yaml';
101+
import { requireDependency } from './import-prerequisite.mjs';
102+
const { parseDocument } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
102103
import { isEntrypoint } from './invoked-as.mjs';
103104

104105
const SELF = 'scripts/check-closing-keyword-parity.mjs';

scripts/check-cross-repo-closer-outcome.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ import { execFileSync } from 'node:child_process';
120120
import { createRequire } from 'node:module';
121121
import { existsSync, readFileSync } from 'node:fs';
122122
import { join } from 'node:path';
123-
import { isMap, isSeq, parseDocument } from 'yaml';
123+
import { requireDependency } from './import-prerequisite.mjs';
124+
const { isMap, isSeq, parseDocument } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
124125
import { isEntrypoint } from './invoked-as.mjs';
125126

126127
const WORKFLOW = '.github/workflows/cross-repo-issue-closer.yml';

scripts/check-doc-anchors.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@
119119
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
120120
import { tmpdir } from 'node:os';
121121
import { dirname, join, relative, resolve } from 'node:path';
122-
import Slugger from 'github-slugger';
122+
import { requireDefaultExport } from './import-prerequisite.mjs';
123+
const Slugger = await requireDefaultExport('github-slugger', () => import('github-slugger'), import.meta.url);
123124

124125
import { stripCodeSpans, stripFencedBlocks } from './check-adr-links.mjs';
125126
import { isEntrypoint } from './invoked-as.mjs';

scripts/check-doc-frontmatter.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ import { createRequire } from 'node:module';
147147
import { join, relative, resolve } from 'node:path';
148148
import { fileURLToPath } from 'node:url';
149149

150-
import { parse, parseDocument } from 'yaml';
150+
import { requireDependency } from './import-prerequisite.mjs';
151+
const { parse, parseDocument } = await requireDependency('yaml', () => import('yaml'), import.meta.url);
151152

152153
import { isEntrypoint } from './invoked-as.mjs';
153154

scripts/check-driver-memory-census.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs';
107107
import { join, dirname, relative, sep } from 'node:path';
108108
import { fileURLToPath } from 'node:url';
109109
import { execFileSync } from 'node:child_process';
110-
import ts from 'typescript';
110+
import { requireDefaultExport } from './import-prerequisite.mjs';
111+
const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url);
111112
import { parseSourceFile } from './ts-parse.mjs';
112113
import { isEntrypoint } from './invoked-as.mjs';
113114

0 commit comments

Comments
 (0)