From c43283cf4594c36ac687787e928b2a2f4d484b1e Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Wed, 25 Mar 2026 15:01:21 +0000 Subject: [PATCH] feat: add Vue support --- __tests__/extraction.test.ts | 150 +++++++++++++ src/extraction/grammars.ts | 9 +- src/extraction/tree-sitter.ts | 201 +++++++++++++++++ src/resolution/frameworks/index.ts | 3 + src/resolution/frameworks/vue.ts | 338 +++++++++++++++++++++++++++++ src/types.ts | 3 + 6 files changed, 701 insertions(+), 3 deletions(-) create mode 100644 src/resolution/frameworks/vue.ts 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 = ` + + +`; + 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 = ` +`; + 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 = ` + + +`; + 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