Skip to content

fix(resolution): component and scoped-alias imports link the module file - #2528

Merged
colbymchenry merged 1 commit into
mainfrom
claude/link-sfc-alias-imports
Oct 12, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/link-sfc-alias-imports

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

An import statement's own module reference (the imports ref named by its specifier, which normally resolves to a file-to-file imports edge) linked nothing in two cases. The statement's bindings still resolved through that file:

  • A Vue, Svelte or Astro component's import. isJsPathImportRef only covered the script languages (JS_MODULE_LANGUAGES: JS, JSX, TS, TSX, ArkTS), so import { DropdownMenuGroup } from '../../ui' in a .vue file failed (357 such failures in vben on main 041db10).
  • A bare scoped package that a tsconfig path or a workspace package maps to a project file. isJsPathSpecifier excluded @scope/pkg, so import { debounce } from '@excalidraw/common' failed. Its binding debounce resolved through the very file the alias maps to (packages/common/src/index.ts); excalidraw has 381 such failures.

Two things followed:

Fix

  • src/resolution/import-resolver.ts:

    • isJsPathImportRef covers ESM_IMPORT_LANGUAGES, adding Vue, Svelte and Astro.
    • isJsPathSpecifier accepts a bare scoped name, which no binding can be named.
    • Both resolve through resolveImportPath, as a script's path import already does since fix(js): a CommonJS require and a module path import the file they name #2263. An outside scoped package (@testing-library/react) still names no file.
    • The framework resolvers (Vue's @/, Svelte's $lib) run first and are unchanged.
  • Sync's failed-import retry (src/index.ts, src/db/queries.ts, ReferenceResolver.resolveModuleFile). When a sync adds files, it also retries the failed imports that now resolve to one of them but wait under no key the file's path gives:

    Without these, the module edges a fresh index now has stayed failed after a barrel came back: excalidraw went from 44 differing lines to 6,078 in the first A/B round, before this part.

No schema change and no extraction change (so no kernel change). Fresh indexes change for projects with component imports or mapped scoped aliases; the CHANGELOG asks users to re-index.

Validation

Tests

  • __tests__/sfc-alias-module-imports.test.ts:
    • a Vue, a Svelte and an Astro component each link ../lib/util as a module edge;
    • a tsconfig-mapped @acme/common links its file, while @testing-library/react links nothing.
  • __tests__/sync-module-importers.test.ts, ES-module fixture:
    • a Vue importer of the barrel, and a scoped alias @app/widgets named unlike its folder (as @excalidraw/common is);
    • a .vue file beside the barrel importing it as ..
  • On main, 4 of these 8 fail.
  • Each part has teeth, checked by mutation:
    • scripts only (no component languages): 3 fail;
    • scoped names excluded again: 3 fail;
    • no scoped-name retry: 2 fail (restored and added late);
    • no folder-import retry: 2 fail.
  • They pass with the kernel on and off.
  • Full suite on Windows: 6,942 passed, 82 skipped.

Fresh-index A/B (main and this PR). Every change is one of two kinds; no edge is retargeted or lost:

repo new module edges (each replacing a failed ref) same edge, now import@0.9 instead of file-path@0.7
excalidraw 1,019 (scoped) –
vben 358 (Vue) 96
shadcn-svelte 500 (Svelte) 1,027
vue3-element-admin 59 (Vue) 66
astro-paper 59 (Astro) 64
starlight 47 (Astro) 50
takenote 41 (scoped) –
svelte-realworld – 10
  • The second kind is a component import written with an extension (./Nav.svelte), which a basename match linked before and the resolver links now.
  • zod, requests, gin and serilog are byte-identical.

Sync A/B. Each arm's synced graph is compared with its own fresh index. Cells are differing lines, main → this PR. Barrels are every 2nd index.{ts,tsx,js,jsx}; the rest are every 7th source file.

repo barrels deleted every 7th deleted barrels restored every 7th restored barrels added late every 7th added late
excalidraw 5,399 → 3 3,693 → 215 44 → 20 24 → 0 5,438 → 20 3,502 → 0
vben 560 → 82 273 → 151 0 → 0 96 → 24 478 → 0 210 → 24
starlight 3 → 3 46 → 42 8 → 8 8 → 8 8 → 8 8 → 8
  • vue3-element-admin, zod, astro-paper and svelte-realworld: unchanged in every scenario.
  • Controls (requests in Python, gin in Go, serilog in C#; every 7th deleted, restored and added late): synced graphs byte-identical to main's in all 9 runs.
  • No scenario rises. Line for line, this PR's residue is a subset of main's everywhere except 4 lines each in vben's every-7th restore and added-late runs (see Residues), against 76 and 190 removed.

Cost

  • Sync wall time, main and this PR, two interleaved rounds:
    • excalidraw, 17 barrels deleted: 2.23 / 2.26 s → 3.03 / 3.01 s. That's re-resolving the ~2,700 edges main left stale.
    • excalidraw, 17 barrels added late: 3.41 / 3.50 s → 3.97 / 3.82 s.
    • zod, every 7th added late: 3.32 / 3.37 s → 3.31 / 3.43 s.
    • vben, every 7th added late: 7.49 / 7.60 s → 7.71 / 7.52 s.
  • Fresh-index time is unchanged within noise: excalidraw 12.4 → 11.3 s, vben 11.9 → 11.0 s, shadcn-svelte 16.3 → 15.4 s.

Residues

Overlap

🤖 Generated with Claude Code

An import statement's own module reference linked nothing in two cases,
even when its bindings resolved through the file: a Vue, Svelte or Astro
component's import (`isJsPathImportRef` covered only the script languages),
and a bare scoped package that a `tsconfig` path or a workspace package maps
to a project file (`isJsPathSpecifier` excluded `@scope/pkg`). So a barrel or
a side-effect module showed no dependency from those files, and sync's
importer discovery, which follows the module edge into a changed file,
never found them when a barrel they import through was deleted or added.

Both now resolve through `resolveImportPath`, as a script's path import
does; an outside scoped package still names no file. Component imports
written with an extension, which a basename match linked before, now link
through the resolver too. When a sync adds a file, it also retries the
failed imports that now resolve to it but wait under no key its path gives:
a scoped package parked under its last segment (`common` for
`@excalidraw/common`, mapped to `packages/common/src/index.ts`, whose keys
are `src` and `index`), under the module ceiling, and an import of the
importer's own folder or the one above (`.`, `..`), parked under no tail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant