chore(deps): count triple-slash type references as candidates

From Copilot's twelfth pass of #1962. A `/// <reference types="x" />` pulls in
a package's declarations exactly like an import does, but TypeScript reports
it in `preProcessFile`'s `typeReferenceDirectives`, not `importedFiles` — and
only the latter was read. A shared source using one could therefore load the
root's copy of a package alongside a client's without it ever entering the
candidate set, so its skew would pass. Same silent-miss direction as the
earlier syntax gaps, through a channel the parser switch didn't cover because
I only read one of its two outputs.

`typeReferencePackageNames` maps a directive to both candidates, since the
name is the *type* name and not the package: `node` resolves to `@types/node`,
while a package shipping its own declarations resolves to itself. Returning
both over-approximates, the safe direction — whichever isn't installed drops
out downstream. Scoped names mangle `@scope/pkg` → `@types/scope__pkg`,
TypeScript's convention. `path` references name a file, not a package, and are
ignored.

The real candidate set is unchanged at 17: no shared source uses a triple-slash
reference today. As with the other syntax fixes, this closes the channel
against the one that arrives later.

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 22:09:42 -04:00
commit 87c1e05420

View file

@ -16,6 +16,7 @@ import {
partitionSkew,
sourcesWithNoFiles,
topLevelLockVersions,
typeReferencePackageNames,
} from "./verify-dep-lockstep.mjs";
test("isSharedSourceFile: all four TS extensions, not just .ts/.tsx", () => {
@ -196,6 +197,41 @@ test("importedPackageNames: static, side-effect, and dynamic forms; builtins and
]);
});
test("importedPackageNames: triple-slash type references count (Copilot, #1962)", () => {
// A `/// <reference types="x" />` pulls in declarations exactly like an
// import, but TypeScript reports it in `typeReferenceDirectives`, not
// `importedFiles` — so reading only the latter let a referenced package skew
// unseen. `path` references name a file, not a package, and are ignored.
const source = [
'/// <reference types="node" />',
'/// <reference types="express" />',
'/// <reference path="./local.d.ts" />',
'import { z } from "zod";',
].join("\n");
assert.deepEqual([...importedPackageNames(source)].sort(), [
"@types/express",
"@types/node",
"express",
"node",
"zod",
]);
});
test("typeReferencePackageNames: both the bare and the @types form (Copilot, #1962)", () => {
// The directive names a *type*, not a package: `node` resolves to
// `@types/node`, while a package shipping its own declarations resolves to
// itself. Returning both over-approximates, the safe direction — whichever
// isn't installed drops out downstream.
assert.deepEqual(typeReferencePackageNames("node"), ["node", "@types/node"]);
// Scoped names mangle with a double underscore, TypeScript's convention.
assert.deepEqual(typeReferencePackageNames("@scope/pkg"), [
"@scope/pkg",
"@types/scope__pkg",
]);
assert.deepEqual(typeReferencePackageNames("./relative"), []);
assert.deepEqual(typeReferencePackageNames(""), []);
});
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