Skip to content

fix(resolution): resolve Svelte/Vue component barrels & workspace imports (#629)#657

Merged
colbymchenry merged 1 commit into
mainfrom
fix/svelte-component-barrel-resolution
Jun 3, 2026
Merged

fix(resolution): resolve Svelte/Vue component barrels & workspace imports (#629)#657
colbymchenry merged 1 commit into
mainfrom
fix/svelte-component-barrel-resolution

Conversation

@colbymchenry
Copy link
Copy Markdown
Owner

Closes #629.

Problem

Component barrels — export { default as Foo } from './Foo.svelte' re-exported from an index.ts and imported elsewhere — left the consumer↔component edge uncreated, so live Svelte/Vue components showed a false 0 callers. Because 0 callers is the canonical dead-code signal, this risks deleting live code (the reporter only caught it via an external grep). The issue listed three still-missed forms after #130.

What the reproduction revealed

The naive fixture passed on main because the name-matcher masked the bug. Using fixtures where the re-export alias ≠ the component's real name (which is what the reporter's real "0 callers" requires) exposed that the Svelte default-barrel case breaks at four layers — each alone leaves it unresolved:

  1. findExportedSymbol matched only function/class for a default export, never component (SFCs are kind: 'component').
  2. extractImportMappings had no svelte/vue branch → SFC consumers produced zero import mappings, so resolveViaImport never ran.
  3. EXTENSION_RESOLUTION had no svelte/vue entry → relative imports from an SFC (./lib/index.ts) resolved to nothing.
  4. getReExports parsed the barrel in the consumer's threaded language → a .svelte consumer made extractReExports bail on a .ts index. Now keyed on the barrel's own extension.

Coverage of the three forms

Form Status
export { default as X } from './X.svelte' ✅ fixed (both callers/impact and callees)
bare ./ / . directory import ✅ already worked — regression test added
workspace package-subpath barrel @scope/ui/widgets ✅ implemented

New src/resolution/workspace-packages.ts (mirrors go-module.ts/path-aliases.ts): reads package.json workspaces (npm/yarn/bun) + pnpm-workspace.yaml, maps member name→dir, resolves @scope/ui/widgetspackages/ui/widgets. Gated behind the workspaces field, so single-package repos get null and see zero behavior change.

Scope boundaries (deliberate)

  • Vue template usage is not end-to-end: the resolver side is generalized to .vue (script imports resolve — validated), but Vue's extractor doesn't capture <Tag/> template usages as references (Svelte's does). That's a separate pre-existing extraction gap, tracked as follow-up.
  • Workspace discovery expands one level of * globs (packages/*, apps/*) and resolves subpaths by directory; it does not yet read a member's exports map.

Validation

  • 4 new tests under re-export chain following: Svelte default barrel, bare ./, workspace subpath, Vue SFC <script> import.
  • Verified both directions empirically: callers/impact/callees all show the resolved edge.
  • Non-workspace repos: @scope/external stays external, no behavior change.
  • Full suite green: 1126 passed / 2 skipped, 59 files.

🤖 Generated with Claude Code

…orts (#629)

Component barrels (`export { default as X } from './X.svelte'`) and
monorepo workspace imports (`@scope/ui/widgets`) left the consumer↔component
edge uncreated, so live components showed a false `0 callers` — the canonical
dead-code signal — risking deletion of live code.

The Svelte default-barrel case broke at FOUR layers, each of which alone left
it unresolved:
- findExportedSymbol matched only function/class for a default export, never
  `component` (Svelte/Vue SFCs are kind 'component').
- extractImportMappings had no svelte/vue branch, so SFC consumers produced
  zero import mappings and resolveViaImport never ran.
- EXTENSION_RESOLUTION had no svelte/vue entry, so relative imports from an
  SFC (`./lib` -> `/index.ts`) resolved to nothing.
- getReExports parsed the barrel in the CONSUMER's threaded language, so a
  .svelte consumer made extractReExports bail on a .ts index barrel.

Workspace package-subpath barrels get a new workspace-packages module
(mirrors go-module/path-aliases): reads package.json `workspaces`
(npm/yarn/bun) + pnpm-workspace.yaml, maps member name->dir, resolves
`@scope/ui/widgets` -> `packages/ui/widgets`. Gated behind the workspaces
field so single-package repos are unaffected.

Bare `./`/`.` directory imports already resolved; covered with a regression
test. Verified both directions (callers/impact AND callees) for Svelte; Vue
script-level imports also resolve. 4 new tests; full suite green (1126).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@colbymchenry colbymchenry merged commit bdfd55e into main Jun 3, 2026
1 check passed
@colbymchenry colbymchenry deleted the fix/svelte-component-barrel-resolution branch June 3, 2026 02:37
colbymchenry added a commit that referenced this pull request Jun 3, 2026
…p) (#659)

Vue's extractor parsed only the <script> block, so a component used solely
in another component's <template> (`<MyButton />`) produced no reference —
and thus showed a false 0 callers, even after the barrel-resolution fix in
PR #657. This is the Vue analogue of Svelte's extractTemplateComponents.

extractTemplateComponents() now scans the template (everything outside the
<script>/<style> blocks, which also handles nested <template> tags for
v-if/slots) for component tags:
- PascalCase tags (`<MyButton/>`) — captured as-is.
- kebab-case tags (`<my-button/>`) — converted to PascalCase so they match
  the imported component's name. Safe: an unmatched name creates no edge
  during resolution, so native custom elements just don't resolve.
- Native HTML elements (lowercase, no hyphen) and Vue built-ins
  (Transition, KeepAlive, …) are skipped.

Adds no nodes — only `references` — so node counts stay stable. With this
plus #657, a Vue component re-exported through a barrel and used only in a
template now resolves end-to-end (callers/impact/callees).

Co-authored-by: Claude Opus 4.8 (1M context) <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

1 participant