Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<template>
<div>{{ message }}</div>
</template>

<script>
export default {
data() {
return { message: 'Hello' };
}
}
</script>
`;
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 <script> block', () => {
const code = `<template>
<button @click="handleClick">Click</button>
</template>

<script>
function handleClick() {
console.log('clicked');
}

const count = 0;
</script>
`;
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 <script setup lang="ts"> block', () => {
const code = `<template>
<div>{{ count }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);

function increment(): void {
count.value++;
}
</script>
`;
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 <script> and <script setup> blocks', () => {
const code = `<template>
<div>{{ msg }}</div>
</template>

<script>
export default {
name: 'DualScript'
}
</script>

<script setup>
const msg = 'hello';

function greet() {
return msg;
}
</script>
`;
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 = `<template>
<div>Static content</div>
</template>
`;
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 = `<template>
<div>{{ value }}</div>
</template>

<script setup lang="ts">
const value = 42;
</script>
`;
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);
});
});
9 changes: 6 additions & 3 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Language, 'svelte' | 'liquid' | 'unknown'>;
export type GrammarLanguage = Exclude<Language, 'svelte' | 'vue' | 'liquid' | 'unknown'>;

/**
* WASM filename map — maps each language to its .wasm grammar file
Expand Down Expand Up @@ -68,6 +68,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.dart': 'dart',
'.liquid': 'liquid',
'.svelte': 'svelte',
'.vue': 'vue',
'.pas': 'pascal',
'.dpr': 'pascal',
'.dpk': 'pascal',
Expand Down Expand Up @@ -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;
Expand All @@ -194,15 +196,15 @@ 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);
}

/**
* 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'];
}

/**
Expand Down Expand Up @@ -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',
Expand Down
Loading