diff --git a/CHANGELOG.md b/CHANGELOG.md
index 204eb734..48d0ce55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -65,6 +65,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Indexing large Python projects is much faster again and needs less memory: since 1.6.2, resolving Python references re-read source files over and over, so a project the size of CPython took several times as long to index. The graph it builds is unchanged. Thanks @bompus for the report. (#2332)
- In VB.NET, reading or setting a `Shared` field or property through its class or module name, like `AppSession.SessionId`, `AppSession.CurrentUser = "demo"` or `AppSession.Items(0)`, now counts as a use of that member and of the class, and so do reading an `Enum` value like `Mode.Fast` and calling a `Shared` function without parentheses. Before, only calls written with parentheses were linked, so `codegraph callers` on such a field, property or class came back empty and impact missed most of the code that depends on it. A local, parameter or field that only shares a class's name is not mistaken for the class. Thanks @serkanince for the report and @ChrisPrapas. (#2305)
- In Vue and Nuxt components, calls written in the template (`{{ formatDate(post) }}`, `:to="getAccountRoute(account)"`, `@click="save(item)"`) and calls in a destructuring declaration at the top of `
+`,
+ // Types and JSX together, which only the TSX grammar reads.
+ 'src/Card.vue': `
+`,
+ // Controls: TypeScript stays TypeScript, no lang stays JavaScript.
+ 'src/Plain.vue': `{{ n }}
+
+`,
+ 'src/Legacy.vue': `{{ total }}
+
+`,
+};
+
+let roots: string[] = [];
+let graphs: CodeGraph[] = [];
+
+afterEach(() => {
+ for (const graph of graphs) graph.close();
+ graphs = [];
+ for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
+ roots = [];
+});
+
+async function indexProject(): Promise {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vue-tsx-'));
+ roots.push(root);
+ for (const [file, text] of Object.entries(PROJECT)) {
+ fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
+ fs.writeFileSync(path.join(root, file), text);
+ }
+ const graph = await CodeGraph.init(root, { index: true });
+ graphs.push(graph);
+ return graph;
+}
+
+const named = (nodes: Node[], kind: string, name: string) => nodes.find((n) => n.kind === kind && n.name === name);
+
+/**
+ * The file's extraction errors. The file must be stored: one whose grammar
+ * was not loaded is never parsed, and nothing at all is stored for it.
+ */
+function errorsOf(graph: CodeGraph, file: string) {
+ const record = graph.getFile(file);
+ expect(record, `${file} is stored`).not.toBeNull();
+ return record!.errors ?? [];
+}
+
+describe('Vue \n', 'svelte')).toEqual(['javascript']);
+ });
+});
diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts
index e405adcc..66c69687 100644
--- a/src/extraction/grammars.ts
+++ b/src/extraction/grammars.ts
@@ -463,7 +463,9 @@ function resolveWasmPath(lang: GrammarLanguage): string {
* SFC languages (svelte/vue/astro) have no grammar of their own — their
* extractors delegate