Repository navigation
fix(frontend): Vue/Svelte indexing and five-repo smoke matrix #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,10 @@ import { | |
| categorizeDependency | ||
| } from '../../utils/dependency-detection.js'; | ||
| import type { WorkspacePackageJson } from '../../utils/workspace-detection.js'; | ||
| import { | ||
| detectFrontendFrameworkFromDeps, | ||
| extractSingleFileComponentScript | ||
| } from '../../utils/frontend-framework-metadata.js'; | ||
|
|
||
| export class GenericAnalyzer implements FrameworkAnalyzer { | ||
| readonly name = 'generic'; | ||
|
|
@@ -91,7 +95,9 @@ export class GenericAnalyzer implements FrameworkAnalyzer { | |
| '.css', | ||
| '.scss', | ||
| '.sass', | ||
| '.less' | ||
| '.less', | ||
| '.vue', | ||
| '.svelte' | ||
| ]; | ||
| readonly priority = 10; // Low priority - fallback analyzer | ||
|
|
||
|
|
@@ -126,8 +132,23 @@ export class GenericAnalyzer implements FrameworkAnalyzer { | |
| usesTreeSitterSymbols = true; | ||
| } | ||
|
|
||
| if (language === 'typescript' || language === 'javascript') { | ||
| const parsed = await this.parseJSTSFile(filePath, content, language); | ||
| let scriptLanguage: 'typescript' | 'javascript' | null = null; | ||
| let scriptBody: string | null = null; | ||
| if (language === 'vue' || language === 'svelte') { | ||
| const extracted = extractSingleFileComponentScript(content, language); | ||
| if (extracted) { | ||
| scriptBody = extracted.body; | ||
| scriptLanguage = extracted.language; | ||
| } | ||
| } | ||
|
|
||
| const jstsLanguage = | ||
| scriptLanguage ?? | ||
| (language === 'typescript' || language === 'javascript' ? language : null); | ||
| const jstsContent = scriptBody ?? content; | ||
|
|
||
| if (jstsLanguage) { | ||
| const parsed = await this.parseJSTSFile(filePath, jstsContent, jstsLanguage); | ||
|
Comment on lines
+148
to
+151
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsing the trimmed script resets source line numbers, but Preserve the original line positions when extracting scripts, or map parsed positions back before using them. |
||
| imports = parsed.imports; | ||
| exports = parsed.exports; | ||
|
|
||
|
|
@@ -247,6 +268,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { | |
|
|
||
| let workspaceType: string = 'single'; | ||
| let workspacePackages: WorkspacePackageJson[] = []; | ||
| let frameworkClaim: CodebaseMetadata['framework']; | ||
|
|
||
| try { | ||
| workspaceType = await detectWorkspaceType(rootPath); | ||
|
|
@@ -276,6 +298,11 @@ export class GenericAnalyzer implements FrameworkAnalyzer { | |
| version: version as string, | ||
| category: categorizeDependency(name) | ||
| })); | ||
|
|
||
| const framework = detectFrontendFrameworkFromDeps(rawDeps); | ||
| if (framework) { | ||
| frameworkClaim = framework; | ||
| } | ||
| } catch (_error) { | ||
| // skip | ||
| } | ||
|
|
@@ -285,6 +312,7 @@ export class GenericAnalyzer implements FrameworkAnalyzer { | |
| rootPath, | ||
| languages: [], | ||
| dependencies, | ||
| framework: frameworkClaim, | ||
| architecture: { | ||
| type: 'mixed', | ||
| layers: { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,7 +71,19 @@ export class NextJsAnalyzer implements FrameworkAnalyzer { | |
| return false; | ||
| } | ||
|
|
||
| if (isInAppRouter(filePath) || isInPagesRouter(filePath) || isProxyFile(filePath)) { | ||
| if (isSvelteKitRouteFile(filePath)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (isProxyFile(filePath)) { | ||
| return true; | ||
| } | ||
|
|
||
| if (matchesNextAppRouterConvention(filePath)) { | ||
| return true; | ||
| } | ||
|
|
||
| if (isInPagesRouter(filePath)) { | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -342,6 +354,18 @@ function analyzeRouting(filePath: string, content: string): NextRoutingInfo { | |
| }; | ||
| } | ||
|
|
||
| function isSvelteKitRouteFile(filePath: string): boolean { | ||
| const baseName = path.basename(filePath, path.extname(filePath)); | ||
| return baseName.startsWith('+'); | ||
| } | ||
|
|
||
| function matchesNextAppRouterConvention(filePath: string): boolean { | ||
| if (!isInAppRouter(filePath)) { | ||
| return false; | ||
| } | ||
| return getAppRouteFileKind(path.basename(filePath)) !== null; | ||
| } | ||
|
Comment on lines
+362
to
+367
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The narrower filename check excludes real Next.js files such as Include Next.js metadata filenames in the convention check while keeping the SvelteKit exclusion. |
||
|
|
||
| function isInAppRouter(filePath: string): boolean { | ||
| const normalizedPath = filePath.replace(/\\/g, '/'); | ||
| return normalizedPath.includes('/app/') || normalizedPath.includes('/src/app/'); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import type { FrameworkInfo } from '../types/index.js'; | ||
| import { normalizeAnalyzerVersion } from '../analyzers/shared/metadata.js'; | ||
|
|
||
| function hasDep(deps: Record<string, string>, name: string): boolean { | ||
| return Object.prototype.hasOwnProperty.call(deps, name); | ||
| } | ||
|
|
||
| /** | ||
| * Detect Vue / Nuxt / Svelte / SvelteKit from merged package dependencies. | ||
| * Uses the same MIN_INDICATORS=3 gate as indexer.selectFramework. | ||
| */ | ||
| export function detectFrontendFrameworkFromDeps( | ||
| deps: Record<string, string> | ||
| ): FrameworkInfo | undefined { | ||
| const indicators: string[] = []; | ||
|
|
||
| if (hasDep(deps, 'nuxt')) indicators.push('dep:nuxt'); | ||
| if (hasDep(deps, '@nuxt/kit')) indicators.push('dep:@nuxt/kit'); | ||
| if (hasDep(deps, '@nuxt/schema')) indicators.push('dep:@nuxt/schema'); | ||
| if (hasDep(deps, 'vue')) indicators.push('dep:vue'); | ||
| if (hasDep(deps, '@sveltejs/kit')) indicators.push('dep:@sveltejs/kit'); | ||
| if (hasDep(deps, '@sveltejs/vite-plugin-svelte')) { | ||
| indicators.push('dep:@sveltejs/vite-plugin-svelte'); | ||
| } | ||
| if (hasDep(deps, 'svelte')) indicators.push('dep:svelte'); | ||
| if (hasDep(deps, 'vite')) indicators.push('dep:vite'); | ||
|
|
||
| if (indicators.length < 3) { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (indicators.includes('dep:nuxt') || indicators.includes('dep:@nuxt/kit')) { | ||
| return { | ||
| name: 'Nuxt', | ||
| version: normalizeAnalyzerVersion(deps.nuxt ?? deps['@nuxt/kit'] ?? 'unknown'), | ||
| type: 'vue', | ||
| variant: 'nuxt', | ||
| uiLibraries: hasDep(deps, 'tailwindcss') ? ['Tailwind'] : undefined, | ||
| indicators | ||
| }; | ||
| } | ||
|
|
||
| if (indicators.includes('dep:@sveltejs/kit')) { | ||
| return { | ||
| name: 'SvelteKit', | ||
| version: normalizeAnalyzerVersion(deps['@sveltejs/kit']), | ||
| type: 'svelte', | ||
| indicators | ||
| }; | ||
| } | ||
|
|
||
| if (indicators.includes('dep:vue')) { | ||
| return { | ||
| name: 'Vue', | ||
| version: normalizeAnalyzerVersion(deps.vue), | ||
| type: 'vue', | ||
| indicators | ||
| }; | ||
| } | ||
|
|
||
| if (indicators.includes('dep:svelte')) { | ||
| return { | ||
| name: 'Svelte', | ||
| version: normalizeAnalyzerVersion(deps.svelte), | ||
| type: 'svelte', | ||
| indicators | ||
| }; | ||
| } | ||
|
|
||
| return undefined; | ||
| } | ||
|
|
||
| export function extractSingleFileComponentScript( | ||
| content: string, | ||
| kind: 'vue' | 'svelte' | ||
| ): { body: string; language: 'typescript' | 'javascript' } | null { | ||
| const tag = 'script'; | ||
| const match = content.match(new RegExp(`<${tag}([^>]*)>([\\s\\S]*?)<\\/${tag}>`, 'i')); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For components containing two script blocks, Useful? React with 👍 / 👎. |
||
| if (!match?.[2]) { | ||
| return null; | ||
|
Comment on lines
+78
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The single regex match reads only the first script block. Vue files can contain both Extract and analyze every script block, keeping each block's language and original positions. |
||
| } | ||
|
|
||
| const attrs = match[1] ?? ''; | ||
| const body = match[2].trim(); | ||
| if (!body) { | ||
| return null; | ||
| } | ||
|
|
||
| const isTypeScript = | ||
| /\blang\s*=\s*['"]ts['"]/i.test(attrs) || | ||
| /\blang\s*=\s*['"]typescript['"]/i.test(attrs) || | ||
| (kind === 'svelte' && attrs.includes('lang="ts"')); | ||
|
|
||
| return { | ||
| body, | ||
| language: isTypeScript ? 'typescript' : 'javascript' | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <script setup lang="ts"> | ||
| export interface ButtonProps { | ||
| label: string; | ||
| variant?: 'primary' | 'ghost'; | ||
| } | ||
|
|
||
| const props = defineProps<ButtonProps>(); | ||
| </script> | ||
|
|
||
| <template> | ||
| <button type="button" :data-variant="props.variant">{{ props.label }}</button> | ||
| </template> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a Vue or Svelte script follows a template or other leading content, parsing the extracted, trimmed body makes all component and import locations relative to the script rather than the original file. Those components are then passed to
createChunksFromCodealongside the full source, so its component-boundary slicing selects unrelated lines. For example, a function on source line 24 becomes a component on line 1, producing a chunk of template lines that omits the function entirely. Preserve the original line positions during extraction or translate the parsed locations before using them for chunking.Useful? React with 👍 / 👎.