Skip to content

Commit b5dbc59

Browse files
claude[bot]claude
andauthored
fix(scripts): guard check-cross-package-test-inputs dispatch, and stop mirroring globToRegExp (#10628)
* fix(scripts): guard check-cross-package-test-inputs dispatch with isEntrypoint Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt * test(scripts): pin the entry guard, and import globToRegExp instead of mirroring it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b5f562a commit b5dbc59

2 files changed

Lines changed: 75 additions & 51 deletions

File tree

scripts/check-cross-package-test-inputs.mjs

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,14 @@
148148
// node scripts/check-cross-package-test-inputs.mjs --self-test
149149

150150
import { readFileSync, readdirSync, statSync, existsSync, writeFileSync, mkdtempSync } from 'node:fs';
151+
import { spawnSync } from 'node:child_process';
151152
import { tmpdir } from 'node:os';
152153
import { join, resolve, relative, dirname, sep, isAbsolute } from 'node:path';
153-
import { fileURLToPath } from 'node:url';
154+
import { fileURLToPath, pathToFileURL } from 'node:url';
154155
import process from 'node:process';
155156

157+
import { isEntrypoint } from './invoked-as.mjs';
158+
156159
const HERE = dirname(fileURLToPath(import.meta.url));
157160
const REPO_ROOT = resolve(HERE, '..');
158161

@@ -1882,6 +1885,31 @@ function selfTest() {
18821885
unioned.length > 0 && unioned.every((i) => existsSync(resolve(REPO_ROOT, i.path))),
18831886
);
18841887

1888+
// ── the entry guard, driven for real ────────────────────────────────────
1889+
//
1890+
// This module EXPORTS helpers, and the dispatch below used to run on IMPORT:
1891+
// `await import(...)` printed this gate's verdict into the importer's stdout
1892+
// and, on an unhappy tree, called `process.exit(1)` -- handing a consumer that
1893+
// asked for `globToRegExp` this gate's verdict as its own exit status.
1894+
// `check-examples-live-imports.mjs` hand-copied the helper rather than pay it.
1895+
//
1896+
// A spawned child is the only honest witness: the guard's answer depends on
1897+
// what node puts in `process.argv[1]`, which cannot be modelled in-process.
1898+
// Without this case the guard can be deleted as quietly as it was missing.
1899+
const importProbe = spawnSync(
1900+
process.execPath,
1901+
['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(fileURLToPath(import.meta.url)).href)});\nconsole.log('ALIVE');`],
1902+
{ encoding: 'utf8' },
1903+
);
1904+
ok(
1905+
'importing this module prints NOTHING -- the dispatch is behind the entry guard',
1906+
(importProbe.stdout || '').trim() === 'ALIVE' && (importProbe.stderr || '').trim() === '',
1907+
);
1908+
ok(
1909+
'importing this module does not exit the importer -- it survives to run its own code',
1910+
importProbe.status === 0 && (importProbe.stdout || '').includes('ALIVE'),
1911+
);
1912+
18851913
const failed = cases.filter((c) => !c.cond);
18861914
for (const c of cases) console.log(`${c.cond ? 'ok ' : 'FAIL'} ${c.label}`);
18871915
if (failed.length) {
@@ -1891,19 +1919,37 @@ function selfTest() {
18911919
console.log(`\nAll ${cases.length} self-test cases passed.`);
18921920
}
18931921

1894-
const argv = process.argv.slice(2);
1895-
if (argv.includes('--self-test')) selfTest();
1896-
else if (argv.includes('--list-escapes')) {
1897-
for (const [name, info] of [...findEscapingPackages()].sort()) {
1898-
console.log(`${name} (${info.dir})`);
1899-
for (const t of info.tests) console.log(` ${t}`);
1900-
}
1901-
} else if (argv.includes('--union-into')) {
1902-
const listPath = argv[argv.indexOf('--union-into') + 1];
1903-
const changedPath = argv[argv.indexOf('--changed') + 1];
1904-
if (!listPath || !changedPath) {
1905-
console.error('usage: check-cross-package-test-inputs.mjs --union-into <turbo-ls.json> --changed <file>');
1906-
process.exit(2);
1907-
}
1908-
unionInto(listPath, changedPath);
1909-
} else verify();
1922+
// ---------------------------------------------------------------------------
1923+
// Entry guard -- this module EXPORTS helpers, so the dispatch must not run on
1924+
// import.
1925+
//
1926+
// Until the guard was added, the `else verify()` fallthrough below fired on
1927+
// `await import(...)` as well as on invocation. Importing the module for
1928+
// `globToRegExp` or `findEscapingPackages` printed this gate's verdict to the
1929+
// importer's stdout, and on an unhappy tree called `process.exit(1)` -- so a
1930+
// consumer inherited THIS gate's verdict as its own exit status, having asked
1931+
// only for a helper. `check-examples-live-imports.mjs` paid that cost: it
1932+
// hand-copied `globToRegExp` rather than import it, naming this load-time gate
1933+
// as the reason.
1934+
//
1935+
// `isEntrypoint` is the repo's one answer to "was I run?" -- see
1936+
// `scripts/invoked-as.mjs` for why the hand-typed spellings are wrong, and
1937+
// `check:entry-guard`, which fails any other spelling in `scripts/**`.
1938+
if (isEntrypoint(import.meta.url)) {
1939+
const argv = process.argv.slice(2);
1940+
if (argv.includes('--self-test')) selfTest();
1941+
else if (argv.includes('--list-escapes')) {
1942+
for (const [name, info] of [...findEscapingPackages()].sort()) {
1943+
console.log(`${name} (${info.dir})`);
1944+
for (const t of info.tests) console.log(` ${t}`);
1945+
}
1946+
} else if (argv.includes('--union-into')) {
1947+
const listPath = argv[argv.indexOf('--union-into') + 1];
1948+
const changedPath = argv[argv.indexOf('--changed') + 1];
1949+
if (!listPath || !changedPath) {
1950+
console.error('usage: check-cross-package-test-inputs.mjs --union-into <turbo-ls.json> --changed <file>');
1951+
process.exit(2);
1952+
}
1953+
unionInto(listPath, changedPath);
1954+
} else verify();
1955+
}

scripts/check-examples-live-imports.mjs

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@
107107

108108
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
109109
import { maskComments } from './js-comment-mask.mjs';
110+
// `globToRegExp` decides whether a declared input glob really covers a coupling,
111+
// so this gate and `check-cross-package-test-inputs` MUST agree about turbo's
112+
// glob semantics -- `**` spans whole segments, `*` stays inside one.
113+
//
114+
// It used to be hand-copied into this file, with the reason recorded in the
115+
// copy: that module "runs its gate at load time -- importing it would execute a
116+
// second gate as a side effect of classifying". That was true, and it is no
117+
// longer: the module's dispatch is behind `isEntrypoint(import.meta.url)`, so
118+
// importing it is silent and exit-neutral. The reason for the copy is gone, so
119+
// the copy is too -- one implementation, and no way for the two gates to drift
120+
// apart on the semantics that decide both their verdicts.
121+
import { globToRegExp } from './check-cross-package-test-inputs.mjs';
110122
import { join, resolve, relative, dirname, sep, posix } from 'node:path';
111123
import { fileURLToPath } from 'node:url';
112124
import process from 'node:process';
@@ -309,40 +321,6 @@ function packagesWithExampleInputs() {
309321
return declared;
310322
}
311323

312-
/**
313-
* Turbo input-glob semantics: `**` spans whole segments, `*` stays inside one.
314-
*
315-
* Mirrored from `globToRegExp` in `check-cross-package-test-inputs.mjs` rather
316-
* than imported, because that module runs its gate at load time -- importing it
317-
* would execute a second gate as a side effect of classifying. The duplication
318-
* is pinned by `--self-test` on both sides; the two must agree, since this is
319-
* the check that decides whether a declared radius really covers a coupling.
320-
*/
321-
function globToRegExp(glob) {
322-
let re = '';
323-
for (let i = 0; i < glob.length; i++) {
324-
const c = glob[i];
325-
if (c === '*') {
326-
if (glob[i + 1] === '*') {
327-
if (glob[i + 2] === '/') {
328-
re += '(?:[^/]+/)*';
329-
i += 2;
330-
} else {
331-
re += '.*';
332-
i += 1;
333-
}
334-
} else {
335-
re += '[^/]*';
336-
}
337-
} else if ('.+?^${}()|[]\\/'.includes(c)) {
338-
re += `\\${c}`;
339-
} else {
340-
re += c;
341-
}
342-
}
343-
return new RegExp(`^${re}$`);
344-
}
345-
346324
/**
347325
* Does one declared input glob cover this coupling target?
348326
*

0 commit comments

Comments
 (0)