Files
codegraph/__tests__/vue-self-closing-script.test.ts
T
Colby MchenryandClaude Opus 5.5 de0a4eb362 fix(vue): a self-closing <script /> no longer swallows the next script (#3070)
The Vue extractor read a self-closing `<script src="./legacy.js" />` block
as an opening tag running on to the next `</script>`, as the Astro and
Svelte extractors did until PR #3063 (PRs #2995 and #2997) landed. The
`<script setup>` after it was parsed with whatever lay in between and
contributed nothing, and a `<template>` in between lost its calls and its
components too. One copy of the same pattern sat in each of 9 places in 5
files, and that fix reached the 6 copies in Astro, Svelte and the viewer.

sfc-script.ts, which the Vue, Svelte and Astro extractors already share,
now exports the script pattern and the script-or-style pattern, built from
@maxmilian's regexes, plus sfcBlockLineRanges, the line ranges a template
scan skips. A self-closing tag has no body, so it covers nothing and isn't
blanked, and calls in its attributes are read. All 9 sites use them: each
extractor's script blocks, Astro's covered ranges, both Svelte template
scans, Vue's template-components scan, the Vue template-calls blanking
and the viewer's highlight regions. The pair also accepts a closing tag
written with a space, `</script >`, as the template-calls blanking always
did; the viewer finds a block's body from its opening tag for that reason.

Validation on main 61776927 (Windows 11):
- Red/green: 4 Vue tests (a `<script src ... />` block before
  `<script setup>`, one before `<template>`, an attribute call on a
  self-closing script in the template, and `</script >`) fail with the
  previous sources and pass; the 6 Astro and Svelte tests still pass.
- A/B: 5 Vue repos (vuejs/core, vuejs/docs, NekoBox, justgage,
  element-plus-playground; none has a self-closing script) are
  byte-identical to main. 7 Astro repos, 2 Svelte subtrees (shadcn-svelte
  docs routes, SvelteKit's basics test app) and the flask and express
  controls are byte-identical to the Astro and Svelte fix before this one.

Fixes #2989

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:02:53 +00:00

103 lines
3.8 KiB
TypeScript

/**
* A self-closing `<script ... />` in a Vue single-file component, such as a
* `<script src="./legacy.js" />` block, has no body. Vue, Svelte and Astro
* shared one script pattern that read it as an opening tag running on to the
* next `</script>`: the `<script setup>` after it was parsed with whatever lay
* in between, failed, and contributed no symbols, and a template in between
* lost its calls and components too (#2989, #2995 and #2997 for Astro and
* Svelte). The three extractors now share one pattern pair, and a
* self-closing tag covers nothing a template scan should skip.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
});
const calls = (result: ReturnType<typeof extractFromSource>, name: string, line: number) =>
result.unresolvedReferences.some((ref) => ref.referenceKind === 'calls' && ref.referenceName === name && ref.line === line);
const renders = (result: ReturnType<typeof extractFromSource>, name: string, line: number) =>
result.unresolvedReferences.some((ref) => ref.referenceKind === 'references' && ref.referenceName === name && ref.line === line);
describe('Vue self-closing <script />', () => {
it('does not swallow the <script setup> that follows it', () => {
const code = `<template>
<p>{{ formatDate(date) }}</p>
<Header />
</template>
<script src="./legacy.js" />
<script setup>
const ready = true;
function go() { return helper(); }
</script>
`;
const result = extractFromSource('src/components/Page.vue', code);
expect(result.errors).toEqual([]);
expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(7);
expect(result.nodes.find((n) => n.name === 'go')?.startLine).toBe(8);
expect(calls(result, 'helper', 8)).toBe(true);
expect(calls(result, 'formatDate', 2)).toBe(true);
expect(renders(result, 'Header', 3)).toBe(true);
});
it('keeps the template it comes before', () => {
const code = `<script src="./legacy.js" />
<template>
<p>{{ formatDate(date) }}</p>
<Header />
</template>
<script setup>
const ready = true;
</script>
`;
const result = extractFromSource('src/components/First.vue', code);
expect(result.errors).toEqual([]);
expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(7);
expect(calls(result, 'formatDate', 3)).toBe(true);
expect(renders(result, 'Header', 4)).toBe(true);
});
it("still reads a call in a self-closing <script />'s attributes", () => {
const code = `<template>
<article>
<script type="application/ld+json" :innerHTML="toJsonLd(post)" />
<p>{{ formatDate(post.date) }}</p>
<Header />
</article>
</template>
<script setup>
const ready = true;
</script>
`;
const result = extractFromSource('src/components/Post.vue', code);
expect(result.errors).toEqual([]);
expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(9);
expect(calls(result, 'toJsonLd', 3)).toBe(true);
expect(calls(result, 'formatDate', 4)).toBe(true);
expect(renders(result, 'Header', 5)).toBe(true);
});
// The template-call scan always took `</script >` as a closing tag; with one
// pattern for every site, the script reader does too.
it('ends a block at a closing tag written with a space, `</script >`', () => {
const code = `<template>
<p>{{ formatDate(date) }}</p>
</template>
<script setup>
const ready = true;
</script >
`;
const result = extractFromSource('src/components/Spaced.vue', code);
expect(result.errors).toEqual([]);
expect(result.nodes.find((n) => n.name === 'ready')?.startLine).toBe(5);
expect(calls(result, 'formatDate', 2)).toBe(true);
});
});