diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts
index 05ac094..b631e6a 100644
--- a/__tests__/extraction.test.ts
+++ b/__tests__/extraction.test.ts
@@ -2764,3 +2764,153 @@ describe('Directory Exclusion', () => {
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
});
});
+
+describe('Vue Extraction', () => {
+ it('should detect Vue files', () => {
+ expect(detectLanguage('App.vue')).toBe('vue');
+ expect(detectLanguage('components/Button.vue')).toBe('vue');
+ expect(isLanguageSupported('vue')).toBe(true);
+ });
+
+ it('should extract component node from a Vue SFC', () => {
+ const code = `
+ {{ message }}
+
+
+
+`;
+ const result = extractFromSource('HelloWorld.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+ expect(componentNode?.name).toBe('HelloWorld');
+ expect(componentNode?.language).toBe('vue');
+ expect(componentNode?.isExported).toBe(true);
+ });
+
+ it('should extract functions from
+`;
+ const result = extractFromSource('Button.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+ expect(componentNode?.name).toBe('Button');
+
+ const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'handleClick');
+ expect(funcNode).toBeDefined();
+ expect(funcNode?.language).toBe('vue');
+ });
+
+ it('should extract from
+`;
+ const result = extractFromSource('Counter.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+ expect(componentNode?.name).toBe('Counter');
+
+ const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'increment');
+ expect(funcNode).toBeDefined();
+ expect(funcNode?.language).toBe('vue');
+
+ // All nodes should be marked as vue language
+ for (const node of result.nodes) {
+ expect(node.language).toBe('vue');
+ }
+ });
+
+ it('should extract from both
+
+
+`;
+ const result = extractFromSource('DualScript.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+
+ const greetFunc = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet');
+ expect(greetFunc).toBeDefined();
+ });
+
+ it('should create component node for template-only Vue file', () => {
+ const code = `
+ Static content
+
+`;
+ const result = extractFromSource('Static.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+ expect(componentNode?.name).toBe('Static');
+ expect(componentNode?.language).toBe('vue');
+
+ // Only the component node should exist (no script nodes)
+ expect(result.nodes.length).toBe(1);
+ });
+
+ it('should create containment edges from component to script nodes', () => {
+ const code = `
+ {{ value }}
+
+
+
+`;
+ const result = extractFromSource('Contained.vue', code);
+
+ const componentNode = result.nodes.find((n) => n.kind === 'component');
+ expect(componentNode).toBeDefined();
+
+ // Should have containment edges from component to child nodes
+ const containEdges = result.edges.filter(
+ (e) => e.source === componentNode!.id && e.kind === 'contains'
+ );
+ expect(containEdges.length).toBeGreaterThan(0);
+ });
+});
diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts
index e6c3d0f..4f1a5a9 100644
--- a/src/extraction/grammars.ts
+++ b/src/extraction/grammars.ts
@@ -10,7 +10,7 @@ import * as path from 'path';
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
import { Language } from '../types';
-export type GrammarLanguage = Exclude;
+export type GrammarLanguage = Exclude;
/**
* WASM filename map — maps each language to its .wasm grammar file
@@ -68,6 +68,7 @@ export const EXTENSION_MAP: Record = {
'.dart': 'dart',
'.liquid': 'liquid',
'.svelte': 'svelte',
+ '.vue': 'vue',
'.pas': 'pascal',
'.dpr': 'pascal',
'.dpk': 'pascal',
@@ -185,6 +186,7 @@ export function detectLanguage(filePath: string): Language {
*/
export function isLanguageSupported(language: Language): boolean {
if (language === 'svelte') return true; // custom extractor (script block delegation)
+ if (language === 'vue') return true; // custom extractor (script block delegation)
if (language === 'liquid') return true; // custom regex extractor
if (language === 'unknown') return false;
return language in WASM_GRAMMAR_FILES;
@@ -194,7 +196,7 @@ export function isLanguageSupported(language: Language): boolean {
* Check if a grammar has been loaded and is ready for parsing.
*/
export function isGrammarLoaded(language: Language): boolean {
- if (language === 'svelte' || language === 'liquid') return true;
+ if (language === 'svelte' || language === 'vue' || language === 'liquid') return true;
return languageCache.has(language);
}
@@ -202,7 +204,7 @@ export function isGrammarLoaded(language: Language): boolean {
* Get all supported languages (those with grammar definitions).
*/
export function getSupportedLanguages(): Language[] {
- return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'liquid'];
+ return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'vue', 'liquid'];
}
/**
@@ -248,6 +250,7 @@ export function getLanguageDisplayName(language: Language): string {
kotlin: 'Kotlin',
dart: 'Dart',
svelte: 'Svelte',
+ vue: 'Vue',
liquid: 'Liquid',
pascal: 'Pascal / Delphi',
unknown: 'Unknown',
diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts
index 183fae8..d311710 100644
--- a/src/extraction/tree-sitter.ts
+++ b/src/extraction/tree-sitter.ts
@@ -3012,6 +3012,201 @@ export class SvelteExtractor {
}
}
+/**
+ * VueExtractor - Extracts code relationships from Vue Single-File Component files
+ *
+ * Vue SFCs are multi-language (script + template + style). Rather than
+ * parsing the full Vue grammar, we extract the