From dd0f5eed7dcb78421e3bc391984c9c5a479ce8ca Mon Sep 17 00:00:00 2001 From: Nguyen Hai Son Date: Fri, 3 Apr 2026 15:48:55 +0700 Subject: [PATCH] feat(vue): Vue SFC support + destructured call result tracking (#604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(vue): add Vue SFC (.vue) support for indexing Vue Single File Components are now fully supported in the indexing pipeline. The implementation extracts diff --git a/gitnexus/test/fixtures/lang-resolution/vue-basic/OldStyle.vue b/gitnexus/test/fixtures/lang-resolution/vue-basic/OldStyle.vue new file mode 100644 index 000000000..3bc6331f7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/vue-basic/OldStyle.vue @@ -0,0 +1,21 @@ + + + diff --git a/gitnexus/test/fixtures/lang-resolution/vue-basic/components/Button.vue b/gitnexus/test/fixtures/lang-resolution/vue-basic/components/Button.vue new file mode 100644 index 000000000..5755ff084 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/vue-basic/components/Button.vue @@ -0,0 +1,30 @@ + + + diff --git a/gitnexus/test/fixtures/lang-resolution/vue-basic/types.ts b/gitnexus/test/fixtures/lang-resolution/vue-basic/types.ts new file mode 100644 index 000000000..70fcc6260 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/vue-basic/types.ts @@ -0,0 +1,9 @@ +export interface User { + id: number; + name: string; + email: string; +} + +export function formatUser(user: User): string { + return `${user.name} <${user.email}>`; +} diff --git a/gitnexus/test/integration/resolvers/vue.test.ts b/gitnexus/test/integration/resolvers/vue.test.ts new file mode 100644 index 000000000..c4c487ab1 --- /dev/null +++ b/gitnexus/test/integration/resolvers/vue.test.ts @@ -0,0 +1,122 @@ +/** + * Vue SFC: script extraction, symbol parsing, import resolution, template component edges + */ +import { describe, it, expect, beforeAll } from 'vitest'; +import path from 'path'; +import { + FIXTURES, + getRelationships, + getNodesByLabel, + getNodesByLabelFull, + runPipelineFromRepo, + type PipelineResult, +} from './helpers.js'; + +describe('Vue SFC support', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'vue-basic'), () => {}); + }, 60000); + + // ------------------------------------------------------------------------- + // Symbol extraction from +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.isSetup).toBe(true); + expect(result!.scriptContent).toContain("import { ref } from 'vue'"); + expect(result!.scriptContent).toContain('const count = ref(0)'); + // Line 0-3 is template + blank, line 4 is +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.isSetup).toBe(false); + expect(result!.scriptContent).toContain('export default'); + }); + + it('prefers + + + + +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.isSetup).toBe(true); + expect(result!.scriptContent).toContain("const name = ref('test')"); + expect(result!.scriptContent).not.toContain('inheritAttrs'); + }); + + it('returns null for .vue files with no +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.scriptContent).toContain('NoLang'); + expect(result!.isSetup).toBe(false); + }); + + it('handles +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.isSetup).toBe(true); + expect(result!.scriptContent).toContain('const x = 1'); + }); + + it('computes correct lineOffset for script at top of file', () => { + const vue = ` + + +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + // +`; + const result = extractVueScript(vue); + expect(result).not.toBeNull(); + expect(result!.isSetup).toBe(true); + expect(result!.scriptContent).toContain("import { ref } from 'vue'"); + }); +}); + +describe('extractTemplateComponents', () => { + it('finds PascalCase component tags', () => { + const vue = ` + + +`; + const components = extractTemplateComponents(vue); + expect(components).toContain('MyButton'); + expect(components).toContain('AppHeader'); + expect(components).not.toContain('div'); + expect(components).not.toContain('span'); + }); + + it('returns empty array when no template', () => { + const vue = ` +`; + expect(extractTemplateComponents(vue)).toEqual([]); + }); + + it('deduplicates repeated component usage', () => { + const vue = ` +`; + const components = extractTemplateComponents(vue); + expect(components.filter((c) => c === 'MyButton')).toHaveLength(1); + }); + + it('ignores HTML elements and lowercase tags', () => { + const vue = ` +`; + const components = extractTemplateComponents(vue); + expect(components).toEqual(['MyComponent']); + }); +});