fix(svelte,vue): a component's instance script declarations are its own (#2227)

isVisibleAcrossFiles rejects a declaration in a .svelte instance <script> or a
Vue <script setup> from another file; a Svelte <script module>, a plain Vue
<script>, and a type a <script setup> exports stay reachable.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 01:25:35 +00:00
committed by GitHub
parent 83dac15cff
commit 3e6252f38b
3 changed files with 140 additions and 0 deletions
+45
View File
@@ -1300,6 +1300,47 @@ function rustModuleDir(filePath: string): string {
return path.posix.join(dir, base.replace(/\.rs$/, ''));
}
const SFC_SCRIPT_RANGES = new WeakMap<ResolutionContext, Map<string, Array<{ start: number; end: number; exported: boolean }>>>();
/**
* Whether a declaration in a `.svelte` / `.vue` file is the component's own:
* anything but the component itself, unless it sits in a block that can export
* — Svelte's `<script module>` (`context="module"`), or a Vue `<script>` that
* is not `setup` — or is a type a Vue `<script setup>` exports.
*/
function isSfcPrivate(n: Node, context: ResolutionContext): boolean {
const svelte = n.filePath.endsWith('.svelte');
if ((!svelte && !n.filePath.endsWith('.vue')) || n.kind === 'component' || n.kind === 'file') return false;
let memo = SFC_SCRIPT_RANGES.get(context);
if (!memo) SFC_SCRIPT_RANGES.set(context, (memo = new Map()));
let ranges = memo.get(n.filePath);
if (!ranges) {
ranges = [];
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
let open: { start: number; exported: boolean } | null = null;
lines.forEach((text, i) => {
const tag = /<script\b([^>]*)>/i.exec(text);
if (tag && !open) {
const attrs = tag[1] ?? '';
open = { start: i + 1, exported: svelte ? /\bmodule\b|context\s*=\s*["']module["']/.test(attrs) : !/\bsetup\b/.test(attrs) };
}
if (open && /<\/script\s*>/i.test(text)) {
ranges!.push({ ...open, end: i + 1 });
open = null;
}
});
memo.set(n.filePath, ranges);
}
const block = ranges.find((r) => n.startLine >= r.start && n.startLine <= r.end);
if (block?.exported) return false;
// Vue hoists the types `<script setup>` exports: mealie imports CrudTable.vue's `TableConfig`.
if (!svelte && block) {
const line = context.getFileLines?.(n.filePath)?.[n.startLine - 1] ?? context.readFile(n.filePath)?.split(/\r?\n/)[n.startLine - 1] ?? '';
if (/^\s*export\s+(?:declare\s+)?(?:interface|type|enum)\b/.test(line)) return false;
}
return true;
}
/**
* Whether `candidate` can be NAMED from a reference in `ref`'s file at all,
* given what its language says about the definition's visibility. A
@@ -1344,6 +1385,10 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
// A test suite is not linked into the program: typeorm's `Record<K, V>` is
// not a test entity `Record`, tokio's `Output` not a `runtime/tests` type.
if (isTestSuitePath(candidate.filePath) && !isTestPath(ref.filePath)) return false;
// A Svelte component's instance script, or a Vue SFC's `<script setup>`, is
// private to the component: shadcn-svelte's 838 `<Item.Root>` (a namespace
// import) went to a `type Item` one example component declares for itself.
if (isSfcPrivate(candidate, context)) return false;
if (candidate.language === 'csharp' && ref.language === 'csharp' && CSHARP_TYPE_KINDS.has(candidate.kind) &&
/^[A-Za-z_]\w*$/.test(ref.referenceName) &&
(!isCsharpTypeVisible(candidate, ref, context) || !isCsharpNestedTypeInScope(candidate, ref, context))) return false;