Repository navigation
fix(js,ts): a declare module answers its own package, behind a real module - #3157
Merged
Merged
Conversation
…odule A declare module stub in a .d.ts took imports away from the real file, even from another package, which main must not do against 1.6.2. On react, packages/eslint-plugin-react-hooks stubs shared/ReactFeatureFlags for its own tsc, and 94 imports across the reconciler, react-dom and the rest landed on that stub. Test-only stubs under packages/react/src/__tests__ took 478 imports from the whole repository. Regression from PR #2718. Root cause: every script declaration in the repository answered every bare specifier, ahead of any file, because TypeScript looks at ambient modules first. That holds inside one program, and a monorepo has one per package. Fix: - A block counts for a file in its own package, in a package nested under that one (vscode's extensions, the root package's src/vscode-dts), or in a package depending on it (astro:assets from astro/client.d.ts). A package.json that only says {"type":"module"} makes no package. A test's block counts only for tests in its own package. - A module of a package the repository holds, a workspace member or a path dependency, comes before a block, in the stub's own package too: the stub describes that module, and the module is what runs. A name only the stub declares still resolves to it. A paths target stays behind the block, as the checker has it (copilot's vscode shim), and so does a package whose module is only a declaration file. - A name bound to a module reaches a block only if the block counts for the file, and a block that doesn't is no candidate in the exact-name pool: its bodiless functions, which PR #2718 added and 1.6.2's crowd never held, took react's createRef in the ranking and were then dropped by the gate. - Sync revisits an import's module edge into a declaration file when a real module appears. Validation: A/B against main with both arms on the same commit. react: 711 edges moved, 49 lost, 0 added; REG's 203 are all back on 1.6.2's target, imports into the test stubs go 609 to 0 and into ReactFeatureFlags.d.ts 94 to 0, and none of the 49 lost is a name 1.6.2 linked. kit: 146 moved from the generated types/index.d.ts to the implementations. vue core: 27 moved to the package entry. astro: 5 moved, 390 links through dependents kept. Byte-identical: vscode's extensions (324,231 edges, 18,847 into src/vscode-dts), outline, dub, excalidraw, starlight, taxonomy, and the controls gin and flask. Red to green: 13 of 17 new tests fail on main's source; each of the 12 safeguards fails a test when it is disabled. On vue core a sync matches a fresh index edge for edge in both directions. Index cost: CPU -2.1% on core, -0.6% on excalidraw, median of 5, interleaved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Main must not regress from published 1.6.2, and #2718 did: a
declare modulestub in a.d.tstook imports away from the real file, even from another package. On react,packages/eslint-plugin-react-hooksstubsshared/ReactFeatureFlagsfor its owntsc, and 94 imports across the reconciler,react-domand the rest landed on that stub instead ofpackages/shared/ReactFeatureFlags.js. Test-only stubs inpackages/react/src/__tests__/testDefinitions/collected imports from the whole repository.Now a
declare modulecounts only where the checker can see it, and a module of one of the repository's own packages comes before it. What #2718 added stays: a project whosedeclare module 'x'is the only answer forxstill links. This never shipped, so the CHANGELOG change is a sentence on #2718's[Unreleased]bullet.Root cause
findAmbientModuleExportandfindAmbientModuleFileanswered every bare specifier from every script.d.tsin the repository, ahead of any file, because TypeScript looks at ambient modules first. That holds inside one program, and a monorepo has one per package. Going back to "file first" is no fix either: copilot mapsvscodeto a test shim throughpaths, and there the checker does take the declared module.Fix
reachesProgram,ambient-modules.ts). A block counts for a file in its own package, in a package nested under that one (vscode's extensions and the root package'ssrc/vscode-dts), or in a package that depends on it (astro:assetsin an Astro project, fromastro/client.d.ts). A package is a folder whosepackage.jsonnames one or lists dependencies; a{"type":"module"}marker is not. A test's or a fixture's block counts only for tests in its own package.findImportedExport,declaredModuleFile,isRepositoryModule,import-resolver.ts). A module of a package this repository holds, a workspace member or a path dependency, comes first, in the stub's own package too: the stub describes that module, and the module is what runs. A name only the stub declares still resolves to the stub. Apathstarget stays behind the block, as the checker has it, and so does a package whose module is only a declaration file.reachesAmbientModule). A name bound to the module reaches a block only if the block counts for the file.matchByExactName). A block that doesn't count for a file is no candidate. fix(js,ts): an import of a package the repository declares itself links to that declaration #2718 gave these blocks function nodes that 1.6.2's crowd never held: react's test stubcreateRefwon the ranking, the gate then dropped the reference, and an edge 1.6.2 had was lost. This restores that crowd rather than manufacturing a guess (fix(resolution): fuzzy matching skips a nested function the reference cannot reach #1709).outrankedImports). An import's module edge into a declaration file is revisited when a real module appears.One call to review: the brief said "only importers in its own package". I kept nested packages and dependents as well, because the numbers say a strict rule would cut true links: 390 on astro (every
examples/*project and integration readingastro/client.d.ts), 56 on SvelteKit, and vscode's 18,847 through the root package. React is identical under both rules.Validation
Tests.
__tests__/ambient-module-imports.test.ts: 17 new tests, 48 in all. 13 of the 17 fail on main's source; the other 4 guard against over-correcting and pass on both, and none of the 31 existing tests changes. Each of the 12 safeguards fails at least one test when disabled, most of them exactly one (a mutation of the source, restored after each run).A/B against main, both arms on
9d3abb84, kernel included, 13 repos. Nodes are identical everywhere, as this touches no extraction.c5ec5c6d04aac59b.gite3bec3d2extensions/,src/vscode-dts,src/typings69e9e92f6a041c95d3b094356b463b4b.git6c9ca431298a88570f09c3a9d086db85Unresolved references differ only on react: 44 more, the names of the lost edges below. Metadata-only changes: 9 on react, 2 on astro, confidence 0.7 to 0.9 on the same target, because a stub left the crowd.
react, against REG's PUB index. Every changed edge was checked against what 1.6.2 had for that name at that line.
shared/ReactFeatureFlags, 91react, 18react-dom/client.React.d.ts554,ReactDOMClient.d.ts24,ReactDOM.d.ts23,PropTypes.d.ts8). IntoReactFeatureFlags.d.ts: 94 to 0. 13 method calls from tests inpackages/reactstay onReact.d.ts, its own package.React.d.tstopackages/react/index.js; 94 the flags stub to the real file; 21 and 18ReactDOM.d.tsandReactDOMClient.d.tstopackages/react-dom; 19fixtures/flight-parcel/types.d.tsto the realreact-server-dom-parcelandreact-dommodules; 6createReftoReactCreateRef.js; 8 to a fixture namesake at 0.4, as on 1.6.2 (below).react,react-domandreact-dom/clientnames the real barrels don't hand over (below); 8prop-typesinfixtures/dom, an npm package; 2hermes-eslintfrom a sibling's stub; 2 synthesized constructor edges from the stub's class to fixture classes.flight-parcel: main'sexportshandling picksclient.node.jsoverclient.js, a choice between two real files. One import of a name that module lacks stays on the fixture's own stub.The others, read against the source.
types/index.d.tsto the implementations:sveltekittosrc/exports/vite/index.js,failtosrc/exports/index.js.@vue/runtime-corefromtypes/globalComponents.d.tsto the package entry.astro/jsx-runtimefrom the language server's stub tosrc/jsx-runtime/index.ts. 2 got worse, in one fixture (netlify/test/functions/fixtures/middleware): its manifest lists@astrojs/netlifybut notastro, soastro/client.d.tsno longer answers, and a name match takes another fixture'sImage.astroat 0.7. Only astro's own repository can do this: elsewhereastro:is declared outside the project.src/vscode-dtsare untouched, and about 9,400 references still take the declaration over copilot'spathsshim.Sync. On vue core, removing a file, indexing, restoring it and syncing matches a fresh index edge for edge in both directions. Restoring the stub is byte-identical. Restoring the real entry leaves 24
exact-matchvsimportmetadata differences on the same targets, which main's build shows too.Cost. Within noise. CPU across all threads, interleaved: vscode's extensions, the worst case with 162 declaration files, 122.5 s to 122.8 s (+0.2%, median of 3; wall 32.6 s to 32.4 s); core 23.7 s to 23.2 s and excalidraw 34.1 s to 33.9 s (median of 5). One wall-clock pass on react: 48.4 s to 47.8 s. Peak memory stays in the same range, with runs overlapping (vscode 3.75 to 3.93 GB against 3.79 to 3.87 GB). Each folder's
package.jsonis read once, and only for specifiers a block declares.Suite. On this commit, Vitest 4,
CODEGRAPH_KERNEL_EXPECT=1, the kernel built from this tree,--maxWorkers=4: 848 files passed, 6 skipped; 10,552 tests passed, 2 expected failures (it.fails, in unrelated files), 87 skipped, 0 failed, nothing rerun.tsc --noEmitis clean.One crash, not reproduced. The first bitwarden run on the fix arm died 8.7 s in, during extraction, with a libuv assertion (
!(handle->flags & UV_HANDLE_CLOSING), exit 0xC0000409). It's in worker teardown, before any code this PR touches runs; the re-run exited 0 and is the row above.Left alone
includeor/// <reference path>, with no dependency between the packages, no longer links. Nothing here reads tsconfiginclude, and none of the 13 repos needed it. For the same reason bitwarden'sbit-common/.../sdk-alias.d.ts, in the root package, keeps answeringlibs/*, as on main.import { Component } from 'react'stays unresolved, as on 1.6.2, becausefindExportedSymboldoesn't followimport { x }plusexport { x }throughReactClient.js. That's why 37 of the lost edges have no real target to land on, and why 8 go to a fixture namesake at 0.4 (class Chrome extends Componenttofixtures/stacks/Components.js), wrong on 1.6.2 too. It deserves its own PR.declare functions, likescripts/jest/typescript/jest.d.ts'sit, aren'tdeclare moduleblocks and are untouched.package.jsonedit that only changes dependencies doesn't re-open importers on a sync, the same limit the out-of-repo guard has.🤖 Generated with Claude Code