diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa50ec778..4821d8e2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Go methods declared on generic types (e.g. `func (s *Stack[T]) Push(...)`) are now correctly attached to their type, so callers, callees, and impact include them. (#583)
- Asking what a symbol impacts no longer drags in every unrelated sibling method of its class — impact now follows real dependencies instead of the structural "contains" relationship, keeping the result focused on what actually depends on the symbol. (#536)
- CodeGraph's MCP server now answers an agent's `resources/list` and `prompts/list` probes with an empty list instead of an error, clearing the `-32601` messages some clients (opencode, Codex) logged on connect. (#621)
+- Svelte and Vue components used through a barrel file — `export { default as Button } from './Button.svelte'` re-exported from an `index.ts` and imported elsewhere — are no longer falsely reported as having **0 callers**. CodeGraph now follows the default re-export all the way to the component and resolves the imports that `.svelte` / `.vue` files themselves use, so `codegraph_callers` and `codegraph_impact` see every place a component is used. This also covers components imported from another package in a workspace/monorepo (`@scope/ui/widgets`) and bare directory imports (`import { x } from './'`). Previously a live component consumed only through a barrel looked like dead code. Thanks @nakisen. (#629)
## [0.9.9] - 2026-06-02
diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts
index 03b8ea6ab..66932c97d 100644
--- a/__tests__/resolution.test.ts
+++ b/__tests__/resolution.test.ts
@@ -1248,6 +1248,147 @@ func main() {
const callers = cg.getCallers(signInNode!.id);
expect(callers.some((c) => c.node.filePath === 'src/main.ts')).toBe(true);
});
+
+ it('follows a default re-export of a .svelte component (export { default as Foo } from ./RealButton.svelte) (#629)', async () => {
+ // The ubiquitous Svelte/React component-barrel form. The leaf is a
+ // .svelte component (extracted as kind 'component', the default
+ // export). The re-export ALIAS (`Foo`) deliberately differs from the
+ // component's real name (`RealButton`) so the name-matcher fallback
+ // can't coincidentally connect them — the only path to the edge is
+ // the import-chase, which must match a `component` (not just
+ // function/class) for the default export. Otherwise the
+ // consumer↔component edge is never created and `callers` returns a
+ // false 0.
+ fs.mkdirSync(path.join(tempDir, 'src/lib'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tempDir, 'src/lib/RealButton.svelte'),
+ `\n\n\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'src/lib/index.ts'),
+ `export { default as Foo } from './RealButton.svelte';\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'src/Bar.svelte'),
+ `\n\n\n`
+ );
+
+ cg = await CodeGraph.init(tempDir, { index: true });
+ cg.resolveReferences();
+
+ const fooNode = cg
+ .getNodesByKind('component')
+ .find((n) => n.name === 'RealButton' && n.filePath === 'src/lib/RealButton.svelte');
+ expect(fooNode).toBeDefined();
+ const callers = cg.getCallers(fooNode!.id);
+ expect(callers.some((c) => c.node.filePath === 'src/Bar.svelte')).toBe(true);
+ });
+
+ it('resolves a bare directory import (import { x } from "." / "./") to index.ts (#629)', async () => {
+ // `import { helper } from '.'` (or './') must map to the
+ // directory's index.ts before the re-export chase can run. The
+ // barrel renames `realHelper` → `helper` so the name-matcher can't
+ // mask a path-resolution failure: only the bare-dir resolution +
+ // rename chase can connect the edge.
+ fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tempDir, 'src/util.ts'),
+ `export function realHelper(): void {}\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'src/index.ts'),
+ `export { realHelper as helper } from './util';\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'src/main.ts'),
+ `import { helper } from '.';\nexport function go(): void { helper(); }\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'src/main2.ts'),
+ `import { helper } from './';\nexport function go2(): void { helper(); }\n`
+ );
+
+ cg = await CodeGraph.init(tempDir, { index: true });
+ cg.resolveReferences();
+
+ const helperNode = cg
+ .getNodesByKind('function')
+ .find((n) => n.name === 'realHelper' && n.filePath === 'src/util.ts');
+ expect(helperNode).toBeDefined();
+ const callers = cg.getCallers(helperNode!.id);
+ expect(callers.some((c) => c.node.filePath === 'src/main.ts')).toBe(true);
+ expect(callers.some((c) => c.node.filePath === 'src/main2.ts')).toBe(true);
+ });
+
+ it('resolves a workspace package-subpath barrel (@scope/pkg/sub) to its index (#629)', async () => {
+ // bun/npm/pnpm workspace: `@scope/ui/widgets` → the `ui` package's
+ // `widgets/` subdir index, which re-exports a .svelte component.
+ // Alias `Thing` ≠ component `Widget` defeats the name-matcher, so
+ // only workspace-package resolution can connect the edge.
+ fs.mkdirSync(path.join(tempDir, 'packages/ui/widgets'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tempDir, 'package.json'),
+ JSON.stringify({ name: 'root', private: true, workspaces: ['packages/*'] }, null, 2)
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'packages/ui/package.json'),
+ JSON.stringify({ name: '@scope/ui', version: '1.0.0' }, null, 2)
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'packages/ui/widgets/Widget.svelte'),
+ `\n\n\n`
+ );
+ fs.writeFileSync(
+ path.join(tempDir, 'packages/ui/widgets/index.ts'),
+ `export { default as Thing } from './Widget.svelte';\n`
+ );
+ fs.mkdirSync(path.join(tempDir, 'app'), { recursive: true });
+ fs.writeFileSync(
+ path.join(tempDir, 'app/App.svelte'),
+ `\n\n\n`
+ );
+
+ cg = await CodeGraph.init(tempDir, { index: true });
+ cg.resolveReferences();
+
+ const buttonNode = cg
+ .getNodesByKind('component')
+ .find((n) => n.name === 'Widget' && n.filePath === 'packages/ui/widgets/Widget.svelte');
+ expect(buttonNode).toBeDefined();
+ const callers = cg.getCallers(buttonNode!.id);
+ expect(callers.some((c) => c.node.filePath === 'app/App.svelte')).toBe(true);
+ });
+
+ it('resolves a barrel import from a Vue SFC \n\n`
+ );
+
+ cg = await CodeGraph.init(tempDir, { index: true });
+ cg.resolveReferences();
+
+ const runNode = cg
+ .getNodesByKind('function')
+ .find((n) => n.name === 'realRun' && n.filePath === 'src/util.ts');
+ expect(runNode).toBeDefined();
+ const callers = cg.getCallers(runNode!.id);
+ expect(callers.some((c) => c.node.filePath === 'src/App.vue')).toBe(true);
+ });
});
describe('C/C++ Import Resolution', () => {
diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts
index bc493704d..5d263cf82 100644
--- a/src/resolution/import-resolver.ts
+++ b/src/resolution/import-resolver.ts
@@ -9,6 +9,7 @@ import * as path from 'path';
import { Language, Node } from '../types';
import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, ReExport } from './types';
import { applyAliases } from './path-aliases';
+import { resolveWorkspaceImport } from './workspace-packages';
/**
* Extension resolution order by language
@@ -18,6 +19,11 @@ const EXTENSION_RESOLUTION: Record = {
javascript: ['.js', '.jsx', '.mjs', '.cjs', '/index.js', '/index.jsx'],
tsx: ['.tsx', '.ts', '.d.ts', '.js', '.jsx', '/index.tsx', '/index.ts', '/index.js'],
jsx: ['.jsx', '.js', '/index.jsx', '/index.js'],
+ // SFC consumers import plain TS/JS, sibling components, and barrels
+ // (`./lib` → `./lib/index.ts`). Without a list, relative imports from a
+ // `.svelte`/`.vue` file resolve to nothing, so barrel callers vanish (#629).
+ svelte: ['.ts', '.js', '.svelte', '.tsx', '.jsx', '/index.ts', '/index.js', '/index.svelte'],
+ vue: ['.ts', '.js', '.vue', '.tsx', '.jsx', '/index.ts', '/index.js', '/index.vue'],
python: ['.py', '/__init__.py'],
go: ['.go'],
rust: ['.rs', '/mod.rs'],
@@ -124,6 +130,15 @@ function isExternalImport(
return false;
}
+ // Workspace-member imports (`@scope/ui`, `@scope/ui/widgets`) are LOCAL to
+ // a monorepo even though they look like bare npm specifiers. Consult the
+ // workspace map first so they aren't misclassified as external (#629). The
+ // map is null for single-package repos, so this is a no-op there.
+ const workspaces = context?.getWorkspacePackages?.();
+ if (workspaces && resolveWorkspaceImport(importPath, workspaces)) {
+ return false;
+ }
+
// Common external patterns
if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
// Node built-ins
@@ -255,6 +270,18 @@ function resolveAliasedImport(
}
}
+ // 1.5 Workspace packages (`@scope/ui/widgets` → `packages/ui/widgets`).
+ // Resolves a monorepo member import to the member's directory; the
+ // extension/index permutations below then find its barrel (#629).
+ const workspaces = context.getWorkspacePackages?.();
+ if (workspaces) {
+ const base = resolveWorkspaceImport(importPath, workspaces);
+ if (base) {
+ const hit = tryWithExt(base);
+ if (hit) return hit;
+ }
+ }
+
// 2. Hard-coded fallback list. Kept for projects that use these
// conventional aliases without declaring them in tsconfig.
const fallbackAliases: Record = {
@@ -496,6 +523,16 @@ export function extractImportMappings(
if (language === 'typescript' || language === 'javascript' || language === 'tsx' || language === 'jsx') {
mappings.push(...extractJSImports(content));
+ } else if (language === 'svelte' || language === 'vue') {
+ // Svelte/Vue single-file components import via plain ES6 inside their
+ // `