chore(deps): see imports through comment trivia; name vitest.shared.mts in the README
Both suppressed comments from Copilot's fourth pass of #1962. TypeScript allows a comment anywhere whitespace is legal, so `import(/* webpackIgnore: true */ "pkg")` and `from /* why */ "pkg"` are valid imports the patterns stepped over. That is the dangerous direction: a missed specifier never enters the candidate set, so the package's skew passes the guard silently, against the over-approximation the module claims. A `TRIVIA` fragment now stands in for `\s*` at every inter-token position, with regression cases for the commented static, dynamic, side-effect, and `require` forms. I did not move to the TypeScript parser. This is a root `scripts/` `.mjs` tool with no bundler and no TS dependency; pulling in a compiler API to read import specifiers is a lot of machinery for a scan that only needs to over-approximate, and each missed form is a one-line pattern with a test beside it. The derived set is unchanged at 18 real names, so nothing here widens what is checked today. The README's command row also still described the candidate scan as `core/` and `test-servers/src` only, omitting the `vitest.shared.mts` the guard now includes; it also never mentioned that an allowlisted package is tolerated only within a major. Both stated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
This commit is contained in:
parent
35317fd9ed
commit
2dcf1450b2
3 changed files with 39 additions and 5 deletions
|
|
@ -270,7 +270,7 @@ Each client self-validates from its own folder; the root scripts chain them. The
|
|||
| `npm run verify:format-coverage` | Parses the `format:check` globs out of every `package.json` (only those reachable from `validate`), enumerates all tracked source files, and **fails** listing any not covered by a glob — the durable guard for the "every first-party source file is format-gated" invariant (#1792). Runs first in `validate`. |
|
||||
| `npm run test:scripts` | Table-driven unit tests (`node --test`) for the guard's own pure parsers (`scripts/lib/npm-scripts.mjs` + the exported helpers of `verify-typecheck-coverage.mjs` and `verify-dep-lockstep.mjs`), one case per rule they encode. Runs in `validate` — and `verify:typecheck-coverage` guards *this* gate in turn (reachable from `validate`, non-empty test set, every test file matched by the `test:scripts` glob), since `node --test` silently skips a file its glob misses and still exits 0. |
|
||||
| `npm run verify:typecheck-coverage` | The typecheck-coverage analog of the above (#1791): for each Node client (auto-discovered from disk — enrolled via its `typecheck` script's projects, or for a `tsc -b` client like `clients/web` via its `tsconfig.json` `references`) it runs those projects with `tsc --listFilesOnly`, unions them, and **fails** listing any tracked `.ts`/`.tsx`/`.mts`/`.cts` under the client that lands in no project (so a new top-level config/helper can't silently go untypechecked). It also requires, deny-by-default, the first-party TS no client owns (`test-servers/src`, the root `vitest.shared.mts`, all of `core/`, and any new top-level location) to land in some client project's tsc pass — so a `core` `*.tsx` web's projects don't reach is caught too. Also asserts the gate is wired (each client's typecheck pass — its `typecheck` script, or web's `tsc -b` — is reachable from its `validate`, and the root chain runs each client's `validate`). Runs in `validate`. |
|
||||
| `npm run verify:dep-lockstep` | Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles `core/` and `test-servers/src` — which resolve from the **root** install — alongside the client's own sources, putting the same package in one `tsc` program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod `4.3.6` vs `4.4.3` exhausted the 4GB tsc heap in `clients/web`). Derives its candidate set from what `core/` and `test-servers/src` import, compares the committed lockfiles' top-level entries, and **fails deny-by-default** on any skew not in the annotated `TOLERATED_SKEW` allowlist. Runs in `validate`. |
|
||||
| `npm run verify:dep-lockstep` | Guards the "one version per install-crossing dependency" invariant (#1896). v2 is not a workspace, so a client's test project compiles the shared first-party TypeScript — `core/`, `test-servers/src`, and the root-owned `vitest.shared.mts`, all of which resolve their dependencies from the **root** install — alongside the client's own sources, putting the same package in one `tsc` program twice. At the same version that's harmless; skewed, TypeScript must relate two structurally-distinct copies of every type, which for a recursive-generic surface is exponential (zod `4.3.6` vs `4.4.3` exhausted the 4GB tsc heap in `clients/web`). Derives its candidate set from what those shared sources import, compares the committed lockfiles' top-level entries, and **fails deny-by-default** on any skew not in the annotated `TOLERATED_SKEW` allowlist — and an allowlisted package is tolerated only *within a major version*. Runs in `validate`. |
|
||||
| `npm run ci` | **Mandatory pre-push command.** `validate` → `coverage` → `verify:build-gate` → `smoke` → Storybook. A true superset of GitHub CI. |
|
||||
| `npm run pack:verify` | Publish smoke — see [Publishing](#publishing). |
|
||||
|
||||
|
|
|
|||
|
|
@ -130,11 +130,27 @@ export function packageNameOf(specifier) {
|
|||
// `tools`…" would otherwise enter the candidate set as a package named
|
||||
// `tools`. Inert today, but a prose word that collides with a real package
|
||||
// name would silently widen the set.
|
||||
//
|
||||
// Whitespace between tokens is really *trivia*: TypeScript allows a comment
|
||||
// anywhere whitespace is legal, so `import(/* webpackIgnore: true */ "pkg")`
|
||||
// and `from /* why */ "pkg"` are valid and must still be seen (Copilot,
|
||||
// #1962). `TRIVIA` stands in for `\s*` at every such position.
|
||||
const TRIVIA = String.raw`(?:\s|/\*[\s\S]*?\*/)*`;
|
||||
const SPECIFIER_FORMS = [
|
||||
/\bfrom\s*["']([^"']+)["']/g, // import … from "x" / export … from "x"
|
||||
/\bimport\s+["']([^"']+)["']/g, // side-effect import "x"
|
||||
/\bimport\s*\(\s*["'`]([^"'`]+)["'`]/g, // dynamic import("x"[, opts])
|
||||
/\brequire\s*\(\s*["'`]([^"'`]+)["'`]/g, // import x = require("x"), require("x")
|
||||
// import … from "x" / export … from "x"
|
||||
new RegExp(String.raw`\bfrom${TRIVIA}["']([^"']+)["']`, "g"),
|
||||
// side-effect import "x"
|
||||
new RegExp(String.raw`\bimport${TRIVIA}["']([^"']+)["']`, "g"),
|
||||
// dynamic import("x"[, opts])
|
||||
new RegExp(
|
||||
String.raw`\bimport${TRIVIA}\(${TRIVIA}["'\`]([^"'\`]+)["'\`]`,
|
||||
"g",
|
||||
),
|
||||
// import x = require("x"), require("x")
|
||||
new RegExp(
|
||||
String.raw`\brequire${TRIVIA}\(${TRIVIA}["'\`]([^"'\`]+)["'\`]`,
|
||||
"g",
|
||||
),
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -100,6 +100,24 @@ test("importedPackageNames: CommonJS and awkward dynamic-import forms (Copilot,
|
|||
]);
|
||||
});
|
||||
|
||||
test("importedPackageNames: comment trivia between tokens (Copilot, #1962)", () => {
|
||||
// TypeScript allows a comment anywhere whitespace is legal, so all of these
|
||||
// are valid imports. Missing one is the dangerous direction: the package
|
||||
// never enters the candidate set and its skew passes the guard silently.
|
||||
const source = `
|
||||
import { a } from /* explanation */ "express";
|
||||
const b = await import(/* webpackIgnore: true */ "undici");
|
||||
const c = require(/* lazy */ "yaml");
|
||||
import /* side effect */ "pino";
|
||||
`;
|
||||
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
||||
"express",
|
||||
"pino",
|
||||
"undici",
|
||||
"yaml",
|
||||
]);
|
||||
});
|
||||
|
||||
test("importedPackageNames: the three specifier forms that introduce types", () => {
|
||||
const source = `
|
||||
import { z } from "zod/v4";
|
||||
|
|
|
|||
Loading…
Reference in a new issue