diff --git a/packages/core/src/plugin/registry.ts b/packages/core/src/plugin/registry.ts index f81e257..e4cabee 100644 --- a/packages/core/src/plugin/registry.ts +++ b/packages/core/src/plugin/registry.ts @@ -1,6 +1,7 @@ import { genericPlugin } from '../plugins/generic.js'; import { reactPlugin } from '../plugins/react.js'; import { typescriptPlugin } from '../plugins/typescript.js'; +import { vuePlugin } from '../plugins/vue.js'; import type { Error2FixPlugin, PluginRegistry } from '../types/plugin.js'; export function createPluginRegistry( @@ -17,5 +18,10 @@ export function registerPlugin( } export function getDefaultPluginRegistry(): PluginRegistry { - return createPluginRegistry([typescriptPlugin, reactPlugin, genericPlugin]); + return createPluginRegistry([ + typescriptPlugin, + reactPlugin, + vuePlugin, + genericPlugin, + ]); } diff --git a/packages/core/src/plugins/vue.ts b/packages/core/src/plugins/vue.ts new file mode 100644 index 0000000..b4d5481 --- /dev/null +++ b/packages/core/src/plugins/vue.ts @@ -0,0 +1,300 @@ +import type { CoreAnalysisInput } from '../types/core.js'; +import type { Error2FixPlugin } from '../types/plugin.js'; +import { + normalizeDiagnosticMessage, + readAnalysisLogText, + unique, +} from '../utils/text.js'; + +type VueFailureKind = + | 'sfc_compile' + | 'template_compile' + | 'vue_runtime' + | 'vue_router' + | 'pinia_runtime' + | 'nuxt_runtime' + | 'hydration_mismatch' + | 'vite_plugin_vue'; + +interface VueDiagnostic { + kind: VueFailureKind; + message: string; + file?: string; +} + +interface VueContext { + framework: 'vue' | 'nuxt' | 'unknown'; + configFiles: string[]; + componentFiles: string[]; +} + +interface VueData { + diagnostics: VueDiagnostic[]; + failureKinds: VueFailureKind[]; +} + +const VUE_FILE_PATTERN = /\.vue$/; +const FRONTEND_SOURCE_PATTERN = /\.(vue|ts|tsx|js|jsx|mjs|cjs)$/; +const VUE_CONFIG_PATTERN = + /^(vite|nuxt|vue|vitest|jest|tailwind|postcss)\.config\.(ts|mts|cts|js|mjs|cjs)$/; + +function isProjectSourceFile(file: string): boolean { + return ( + FRONTEND_SOURCE_PATTERN.test(file) && + !/node_modules|\.nuxt|dist|build/.test(file) + ); +} + +function hasVueSignal(input: CoreAnalysisInput): boolean { + const text = readAnalysisLogText(input); + return ( + /\b(?:Vue|Nuxt|Pinia|Vue Router|vue-router|vue-tsc)\b/i.test(text) || + /@vue\/(?:compiler-sfc|compiler-dom|runtime-core)|@vitejs\/plugin-vue|\[plugin:vite:vue\]/i.test( + text, + ) || + /\[Vue warn\]|\[Vue Router warn\]|getActivePinia|Hydration completed but contains mismatches/i.test( + text, + ) || + (input.signals.relatedFiles.some((file) => VUE_FILE_PATTERN.test(file)) && + /\b(?:component|template|script setup|compiler|render|hydration)\b/i.test( + text, + )) || + input.workspace.files.some( + (file) => + file.startsWith('nuxt.config.') || file.startsWith('vue.config.'), + ) + ); +} + +function detectFramework(input: CoreAnalysisInput): VueContext['framework'] { + const text = readAnalysisLogText(input); + if ( + /\b(?:Nuxt|Nitro|\.nuxt|nuxt\/|#app|#imports)\b/i.test(text) || + input.workspace.files.some((file) => file.startsWith('nuxt.config.')) + ) { + return 'nuxt'; + } + if ( + /\b(?:Vue|Pinia|Vue Router|vue-router|@vue\/|@vitejs\/plugin-vue)\b/i.test( + text, + ) || + input.signals.relatedFiles.some((file) => VUE_FILE_PATTERN.test(file)) + ) { + return 'vue'; + } + return 'unknown'; +} + +function firstRelatedSourceFile(input: CoreAnalysisInput): string | undefined { + return ( + input.signals.relatedFiles.find((file) => VUE_FILE_PATTERN.test(file)) ?? + input.signals.relatedFiles.find(isProjectSourceFile) + ); +} + +function addDiagnostic( + diagnostics: VueDiagnostic[], + input: CoreAnalysisInput, + kind: VueFailureKind, + pattern: RegExp, +): void { + const text = readAnalysisLogText(input); + const match = text.match(pattern); + if (!match) { + return; + } + + diagnostics.push({ + kind, + message: normalizeDiagnosticMessage(match[0]), + file: firstRelatedSourceFile(input), + }); +} + +function extractDiagnostics(input: CoreAnalysisInput): VueDiagnostic[] { + const diagnostics: VueDiagnostic[] = []; + + addDiagnostic( + diagnostics, + input, + 'sfc_compile', + /(?:\[plugin:vite:vue\][^\n]*|@vue\/compiler-sfc[^\n]*|VueCompilerError:[^\n]*|