chore(deps): extract specifiers with ts.preProcessFile; document the transitive boundary

From Copilot's sixth pass of #1962.

Take the TypeScript-parser suggestion, which was raised across three rounds
and was right. The regex scan got both directions wrong and every widening
traded one failure for the other: it *missed* valid syntax (`import x =
require(…)`, an import-attributes argument, a comment between tokens — each a
silent miss, so the package never entered the candidate set and its skew
passed), and it *invented* names from prose, since `// adapted from "react"`
is indistinguishable from an import to a pattern that cannot tell code from a
comment. The earlier prose test only passed because `cwd omitted` isn't a
valid package name.

`ts.preProcessFile` is the cheap answer I'd missed while arguing against the
full AST: TypeScript's lightweight pre-parse scanner, no parse tree and no
type checking, purpose-built to return module specifiers. It handles every
import form, trivia, strings, and regex literals, and never reads a comment as
code. typescript resolves from `clients/web`, which already carries it, via
the `createRequire` base `smoke-web-browser.mjs` established — a bare import
would resolve relative to `scripts/`, not the cwd.

The derived set drops 18 → 17 real names: every genuine package is retained
and the one prose artifact is gone. Regression tests now use *real* package
names in comments and strings, the case the old test couldn't cover.

Also records the KNOWN BOUNDARY the review's other finding identified.
Candidates cover what the shared sources name directly, so a package reaching
the program only through another package's `.d.ts` is invisible —
`@modelcontextprotocol/sdk` is the live example, skewed 1.29.0 vs 1.30.0 and
present in web's program from both installs, never written in first-party
code. Closing it properly changes what the guard measures, so it is tracked
as #1965, with both candidate derivations measured there: a lockfile closure
is unusable (155 packages, 25 skewed, nearly all irrelevant tooling, and it
misses the SDK anyway), while `tsc --listFilesOnly` is correct and small
(15 for clients/web).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
This commit is contained in:
cliffhall 2026-08-10 21:24:55 -04:00
commit fcd45037c9

View file

@ -155,26 +155,26 @@ test("importedPackageNames: the three specifier forms that introduce types", ()
]);
});
test("importedPackageNames: prose after `from` is not an import", () => {
// The scan over-approximates on purpose, but must not invent package names
// out of comment text — a bogus name absent from every lockfile is inert,
// yet a *plausible* one would silently widen the candidate set.
test("importedPackageNames: prose in comments never becomes a package (Copilot, #1962)", () => {
// The regex scan this replaced could not tell code from a comment, so
// `// adapted from "react"` added `react` to the candidate set — and if that
// installed package were skewed, an unrelated comment would fail `validate`.
// These use REAL package names, which is the case the old prose test missed:
// it only passed because `cwd omitted` isn't a valid package name.
const source = `
// Resolved relative to the runner dir, not from "cwd omitted" by the caller.
/** Reads the manifest from "file:" URLs. */
// adapted from "react"
/** Mirrors the behavior of "express", see require("yaml") below. */
/** The excluded set derived from \\\`hono\\\`-style paths. */
// const disabled = await import("undici");
import { z } from "zod";
`;
assert.deepEqual([...importedPackageNames(source)], ["zod"]);
});
test("importedPackageNames: a backticked `from` in prose is not an import", () => {
// Backticks are legal in the *call* forms (a static template literal) but
// never after `from`, which requires a string literal. Accepting them there
// would sweep in inline code from comments — this codebase writes it with
// backticks throughout — and a prose word colliding with a real package name
// would silently widen the candidate set.
test("importedPackageNames: a specifier inside a string literal is not an import", () => {
const source = `
/** The excluded set derived from \\\`tools\\\`, keyed off \\\`express\\\`-style paths. */
const msg = 'run require("chokidar") to load it';
const re = /"jose"/;
import { z } from "zod";
`;
assert.deepEqual([...importedPackageNames(source)], ["zod"]);