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

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>
This commit is contained in:
Colby Mchenry
2026-06-02 21:37:56 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7b62356f53
commit bdfd55e69c
6 changed files with 396 additions and 4 deletions
+19 -1
View File
@@ -22,6 +22,7 @@ import { detectFrameworks } from './frameworks';
import { synthesizeCallbackEdges } from './callback-synthesizer';
import { loadProjectAliases, type AliasMap } from './path-aliases';
import { loadGoModule, type GoModule } from './go-module';
import { loadWorkspacePackages, type WorkspacePackages } from './workspace-packages';
import { logDebug } from '../errors';
import type { ReExport } from './types';
import { LRUCache } from './lru-cache';
@@ -203,6 +204,8 @@ export class ReferenceResolver {
private projectAliases: AliasMap | null | undefined = undefined;
// go.mod module path. Same lazy/immutable convention as projectAliases.
private goModule: GoModule | null | undefined = undefined;
// Monorepo workspace member packages. Same lazy/immutable convention.
private workspacePackages: WorkspacePackages | null | undefined = undefined;
constructor(projectRoot: string, queries: QueryBuilder) {
this.projectRoot = projectRoot;
@@ -423,6 +426,13 @@ export class ReferenceResolver {
return this.goModule;
},
getWorkspacePackages: () => {
if (this.workspacePackages === undefined) {
this.workspacePackages = loadWorkspacePackages(this.projectRoot);
}
return this.workspacePackages;
},
getReExports: (filePath: string, language) => {
const cached = this.reExportCache.get(filePath);
if (cached) return cached;
@@ -431,7 +441,15 @@ export class ReferenceResolver {
this.reExportCache.set(filePath, []);
return [];
}
const reExports = extractReExports(content, language);
// Re-exports are a JS/TS-only construct, and what matters is the
// BARREL file's own language — not the consuming reference's. A
// `.svelte`/`.vue` consumer threads its own language down the
// re-export chase, which would make extractReExports() bail on a
// `.ts` index barrel and silently break the chain (#629). Re-key
// the parse on the barrel's extension so the chase works no matter
// what kind of file imports through it.
const isJsFamily = /\.(?:d\.ts|[cm]?tsx?|[cm]?jsx?)$/i.test(filePath);
const reExports = extractReExports(content, isJsFamily ? 'typescript' : language);
this.reExportCache.set(filePath, reExports);
return reExports;
},