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:
co-authored by
Claude Opus 4.8
parent
7b62356f53
commit
bdfd55e69c
@@ -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<string, string[]> = {
|
||||
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<string, string> = {
|
||||
@@ -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
|
||||
// `<script>` block. Without this, a `.svelte`/`.vue` consumer produces
|
||||
// zero import mappings, so `resolveViaImport` can't run and a barrel
|
||||
// import (`import { Foo } from './lib'`) falls back to name-matching —
|
||||
// which silently fails whenever the re-export alias differs from the
|
||||
// component's real name, yielding a false 0 callers (#629). The ES6
|
||||
// import regex only matches `import … from '…'`, so running it over the
|
||||
// whole SFC (markup + styles included) is safe.
|
||||
mappings.push(...extractJSImports(content));
|
||||
} else if (language === 'python') {
|
||||
mappings.push(...extractPythonImports(content));
|
||||
} else if (language === 'go') {
|
||||
@@ -1248,9 +1285,17 @@ function findExportedSymbol(
|
||||
|
||||
// 1. Direct hit: the symbol is declared in this file.
|
||||
if (want.isDefault) {
|
||||
const direct = nodesInFile.find(
|
||||
(n) => n.isExported && (n.kind === 'function' || n.kind === 'class')
|
||||
);
|
||||
// Svelte/Vue single-file components ARE the module's default export,
|
||||
// but are extracted as kind 'component' (not function/class). Prefer
|
||||
// the component node; fall back to an exported function/class for the
|
||||
// `.ts`/`.tsx` `export default fn`/`class` case. Without the component
|
||||
// branch, an `export { default as X } from './X.svelte'` barrel never
|
||||
// resolves and the component shows a false 0 callers (#629).
|
||||
const direct =
|
||||
nodesInFile.find((n) => n.isExported && n.kind === 'component') ??
|
||||
nodesInFile.find(
|
||||
(n) => n.isExported && (n.kind === 'function' || n.kind === 'class')
|
||||
);
|
||||
if (direct) return direct;
|
||||
} else if (want.isNamespace && want.memberName) {
|
||||
const direct = nodesInFile.find(
|
||||
|
||||
Reference in New Issue
Block a user