Skip to content

Commit 5ab8174

Browse files
abhijeetj100colbymchenryclaude
authored
feat: add Vue support (colbymchenry#66)
Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 804ab67 commit 5ab8174

7 files changed

Lines changed: 705 additions & 3 deletions

File tree

__tests__/extraction.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3080,6 +3080,156 @@ describe('Directory Exclusion', () => {
30803080
});
30813081
});
30823082

3083+
describe('Vue Extraction', () => {
3084+
it('should detect Vue files', () => {
3085+
expect(detectLanguage('App.vue')).toBe('vue');
3086+
expect(detectLanguage('components/Button.vue')).toBe('vue');
3087+
expect(isLanguageSupported('vue')).toBe(true);
3088+
});
3089+
3090+
it('should extract component node from a Vue SFC', () => {
3091+
const code = `<template>
3092+
<div>{{ message }}</div>
3093+
</template>
3094+
3095+
<script>
3096+
export default {
3097+
data() {
3098+
return { message: 'Hello' };
3099+
}
3100+
}
3101+
</script>
3102+
`;
3103+
const result = extractFromSource('HelloWorld.vue', code);
3104+
3105+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3106+
expect(componentNode).toBeDefined();
3107+
expect(componentNode?.name).toBe('HelloWorld');
3108+
expect(componentNode?.language).toBe('vue');
3109+
expect(componentNode?.isExported).toBe(true);
3110+
});
3111+
3112+
it('should extract functions from <script> block', () => {
3113+
const code = `<template>
3114+
<button @click="handleClick">Click</button>
3115+
</template>
3116+
3117+
<script>
3118+
function handleClick() {
3119+
console.log('clicked');
3120+
}
3121+
3122+
const count = 0;
3123+
</script>
3124+
`;
3125+
const result = extractFromSource('Button.vue', code);
3126+
3127+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3128+
expect(componentNode).toBeDefined();
3129+
expect(componentNode?.name).toBe('Button');
3130+
3131+
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'handleClick');
3132+
expect(funcNode).toBeDefined();
3133+
expect(funcNode?.language).toBe('vue');
3134+
});
3135+
3136+
it('should extract from <script setup lang="ts"> block', () => {
3137+
const code = `<template>
3138+
<div>{{ count }}</div>
3139+
</template>
3140+
3141+
<script setup lang="ts">
3142+
import { ref } from 'vue';
3143+
3144+
const count = ref(0);
3145+
3146+
function increment(): void {
3147+
count.value++;
3148+
}
3149+
</script>
3150+
`;
3151+
const result = extractFromSource('Counter.vue', code);
3152+
3153+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3154+
expect(componentNode).toBeDefined();
3155+
expect(componentNode?.name).toBe('Counter');
3156+
3157+
const funcNode = result.nodes.find((n) => n.kind === 'function' && n.name === 'increment');
3158+
expect(funcNode).toBeDefined();
3159+
expect(funcNode?.language).toBe('vue');
3160+
3161+
// All nodes should be marked as vue language
3162+
for (const node of result.nodes) {
3163+
expect(node.language).toBe('vue');
3164+
}
3165+
});
3166+
3167+
it('should extract from both <script> and <script setup> blocks', () => {
3168+
const code = `<template>
3169+
<div>{{ msg }}</div>
3170+
</template>
3171+
3172+
<script>
3173+
export default {
3174+
name: 'DualScript'
3175+
}
3176+
</script>
3177+
3178+
<script setup>
3179+
const msg = 'hello';
3180+
3181+
function greet() {
3182+
return msg;
3183+
}
3184+
</script>
3185+
`;
3186+
const result = extractFromSource('DualScript.vue', code);
3187+
3188+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3189+
expect(componentNode).toBeDefined();
3190+
3191+
const greetFunc = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet');
3192+
expect(greetFunc).toBeDefined();
3193+
});
3194+
3195+
it('should create component node for template-only Vue file', () => {
3196+
const code = `<template>
3197+
<div>Static content</div>
3198+
</template>
3199+
`;
3200+
const result = extractFromSource('Static.vue', code);
3201+
3202+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3203+
expect(componentNode).toBeDefined();
3204+
expect(componentNode?.name).toBe('Static');
3205+
expect(componentNode?.language).toBe('vue');
3206+
3207+
// Only the component node should exist (no script nodes)
3208+
expect(result.nodes.length).toBe(1);
3209+
});
3210+
3211+
it('should create containment edges from component to script nodes', () => {
3212+
const code = `<template>
3213+
<div>{{ value }}</div>
3214+
</template>
3215+
3216+
<script setup lang="ts">
3217+
const value = 42;
3218+
</script>
3219+
`;
3220+
const result = extractFromSource('Contained.vue', code);
3221+
3222+
const componentNode = result.nodes.find((n) => n.kind === 'component');
3223+
expect(componentNode).toBeDefined();
3224+
3225+
// Should have containment edges from component to child nodes
3226+
const containEdges = result.edges.filter(
3227+
(e) => e.source === componentNode!.id && e.kind === 'contains'
3228+
);
3229+
expect(containEdges.length).toBeGreaterThan(0);
3230+
});
3231+
});
3232+
30833233
describe('Instantiates + Decorates edge extraction', () => {
30843234
it('emits an instantiates ref for `new Foo()`', () => {
30853235
const code = `

src/extraction/grammars.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as path from 'path';
1010
import { Parser, Language as WasmLanguage } from 'web-tree-sitter';
1111
import { Language } from '../types';
1212

13-
export type GrammarLanguage = Exclude<Language, 'svelte' | 'liquid' | 'unknown'>;
13+
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'unknown'>;
1414

1515
/**
1616
* WASM filename map — maps each language to its .wasm grammar file
@@ -68,6 +68,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
6868
'.dart': 'dart',
6969
'.liquid': 'liquid',
7070
'.svelte': 'svelte',
71+
'.vue': 'vue',
7172
'.pas': 'pascal',
7273
'.dpr': 'pascal',
7374
'.dpk': 'pascal',
@@ -201,6 +202,7 @@ function looksLikeCpp(source: string): boolean {
201202
*/
202203
export function isLanguageSupported(language: Language): boolean {
203204
if (language === 'svelte') return true; // custom extractor (script block delegation)
205+
if (language === 'vue') return true; // custom extractor (script block delegation)
204206
if (language === 'liquid') return true; // custom regex extractor
205207
if (language === 'unknown') return false;
206208
return language in WASM_GRAMMAR_FILES;
@@ -210,15 +212,15 @@ export function isLanguageSupported(language: Language): boolean {
210212
* Check if a grammar has been loaded and is ready for parsing.
211213
*/
212214
export function isGrammarLoaded(language: Language): boolean {
213-
if (language === 'svelte' || language === 'liquid') return true;
215+
if (language === 'svelte' || language === 'vue' || language === 'liquid') return true;
214216
return languageCache.has(language);
215217
}
216218

217219
/**
218220
* Get all supported languages (those with grammar definitions).
219221
*/
220222
export function getSupportedLanguages(): Language[] {
221-
return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'liquid'];
223+
return [...(Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]), 'svelte', 'vue', 'liquid'];
222224
}
223225

224226
/**
@@ -282,6 +284,7 @@ export function getLanguageDisplayName(language: Language): string {
282284
kotlin: 'Kotlin',
283285
dart: 'Dart',
284286
svelte: 'Svelte',
287+
vue: 'Vue',
285288
liquid: 'Liquid',
286289
pascal: 'Pascal / Delphi',
287290
unknown: 'Unknown',

src/extraction/tree-sitter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { EXTRACTORS } from './languages';
2222
import { LiquidExtractor } from './liquid-extractor';
2323
import { SvelteExtractor } from './svelte-extractor';
2424
import { DfmExtractor } from './dfm-extractor';
25+
import { VueExtractor } from './vue-extractor';
2526

2627
// Re-export for backward compatibility
2728
export { generateNodeId } from './tree-sitter-helpers';
@@ -2489,6 +2490,12 @@ export function extractFromSource(
24892490
return extractor.extract();
24902491
}
24912492

2493+
// Use custom extractor for Vue
2494+
if (detectedLanguage === 'vue') {
2495+
const extractor = new VueExtractor(filePath, source);
2496+
return extractor.extract();
2497+
}
2498+
24922499
// Use custom extractor for Liquid
24932500
if (detectedLanguage === 'liquid') {
24942501
const extractor = new LiquidExtractor(filePath, source);

0 commit comments

Comments
 (0)