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

@ -175,7 +175,30 @@ function typescript() {
}
/**
* Every third-party package name imported by a blob of TypeScript source.
* The package(s) a `/// <reference types="x" />` directive can resolve to
* (Copilot, #1962). Such a directive pulls in declarations exactly like an
* import does, but TypeScript reports it separately from `importedFiles`, so
* reading only the latter would let a referenced package skew unseen.
*
* Both candidates are returned because the directive name is the *type* name,
* not the package: `node` resolves to `@types/node`, while a package shipping
* its own declarations resolves to itself. Returning both over-approximates,
* which is the safe direction — whichever isn't installed drops out. A scoped
* name mangles as `@scope/pkg` → `@types/scope__pkg`, TypeScript's convention.
*/
export function typeReferencePackageNames(directive) {
const name = packageNameOf(directive);
if (!name) return [];
const scoped = /^@([^/]+)\/(.+)$/.exec(name);
const typesName = scoped
? `@types/${scoped[1]}__${scoped[2]}`
: `@types/${name}`;
return [name, typesName];
}
/**
* Every third-party package name whose declarations a blob of TypeScript source
* pulls in — via an import of any form, or a triple-slash type reference.
* Over-approximating is safe (a name absent from every lockfile contributes
* nothing downstream — `@inspector/core` is a build-time alias, not a package,
* and drops out that way); under-approximating is not, since a missed package
@ -185,11 +208,14 @@ export function importedPackageNames(source) {
const names = new Set();
// (source, readImportFiles, detectJavaScriptImports) — the latter two make it
// report `require(…)` and dynamic imports as well as static ones.
const { importedFiles } = typescript().preProcessFile(source, true, true);
const { importedFiles, typeReferenceDirectives } =
typescript().preProcessFile(source, true, true);
for (const { fileName } of importedFiles) {
const name = packageNameOf(fileName);
if (name) names.add(name);
}
for (const { fileName } of typeReferenceDirectives ?? [])
for (const name of typeReferencePackageNames(fileName)) names.add(name);
return names;
}