UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

212 lines (211 loc) 8.91 kB
/** * Vue Enhancement Pack * 条件: { languages: ['typescript', 'javascript'], frameworks: ['vue', 'nuxt'] } * * 覆盖 Vue 3.5+ Composition API 生态: * - Composable 函数 (useXxx) * - Pinia Store 模式 * - SFC <script setup> 预处理 * - defineProps / defineEmits / defineModel 宏 * - VueRouter 导航守卫 * - provide/inject 依赖注入 */ import { EnhancementPack } from './EnhancementPack.js'; class VueEnhancement extends EnhancementPack { get id() { return 'vue'; } get displayName() { return 'Vue 3.5 / Nuxt Enhancement'; } get conditions() { return { languages: ['typescript', 'javascript'], frameworks: ['vue', 'nuxt'] }; } getExtraDimensions() { return [ { id: 'composable-scan', label: 'Composable 函数分析', guide: 'Composable 函数(useXxx)+ 内部 ref/computed/watch 调用、Composable 组合链、provide/inject 依赖注入模式', tierHint: 2, knowledgeTypes: ['code-pattern'], skillWorthy: true, dualOutput: true, skillMeta: { name: 'project-vue-composables', description: 'Vue Composable patterns and composition API usage (auto-generated by enhancement)', }, }, { id: 'pinia-store-scan', label: 'Pinia Store 分析', guide: 'Pinia Store 拓扑分析 — defineStore 定义、state/getters/actions 结构、Store 间依赖、$subscribe/$onAction 订阅模式、setup vs option 语法', tierHint: 2, knowledgeTypes: ['architecture', 'code-pattern'], skillWorthy: true, dualOutput: true, skillMeta: { name: 'project-vue-stores', description: 'Pinia store definitions, dependencies and subscription patterns (auto-generated by enhancement)', }, }, { id: 'vue-component-api-scan', label: '组件 API 分析', guide: 'Vue 组件 API 分析 — defineProps/defineEmits/defineModel/defineExpose 使用分布、prop 类型定义规范 (runtime vs type-only)、slot 模式、v-model 双向绑定', tierHint: 2, knowledgeTypes: ['code-pattern', 'code-standard'], skillWorthy: true, dualOutput: true, skillMeta: { name: 'project-vue-component-api', description: 'Vue component API conventions — defineProps/defineEmits/defineModel patterns (auto-generated by enhancement)', }, }, ]; } getGuardRules() { return [ { ruleId: 'vue-no-v-html', category: 'safety', dimension: 'file', severity: 'warning', languages: ['typescript', 'javascript'], pattern: /v-html/, message: '避免使用 v-html,存在 XSS 风险', }, { ruleId: 'vue-composable-naming', category: 'style', dimension: 'file', severity: 'info', languages: ['typescript', 'javascript'], pattern: /export\s+function\s+(?!use)[a-z]/, message: 'Composable 函数建议以 use 前缀命名', }, { ruleId: 'vue-reactive-destructure', category: 'correctness', dimension: 'file', severity: 'warning', languages: ['typescript', 'javascript'], pattern: /(?:const|let)\s*\{[^}]+\}\s*=\s*(?:reactive|toRefs)\s*\(/, message: '解构 reactive() 对象会失去响应性,使用 toRefs() 或保持引用访问。Vue 3.5 的 Reactive Props Destructure 除外', }, { ruleId: 'vue-computed-side-effect', category: 'correctness', dimension: 'file', severity: 'warning', languages: ['typescript', 'javascript'], pattern: /computed\s*\(\s*(?:\(\)|function\s*\(\))\s*(?:=>)?\s*\{[^}]*(?:fetch|axios|console\.log|\.value\s*=)/, message: 'computed 中不应有副作用 (网络请求、修改 ref、console.log) — 使用 watch/watchEffect 代替', }, { ruleId: 'vue-v-for-with-v-if', category: 'performance', dimension: 'file', severity: 'warning', languages: ['typescript', 'javascript'], pattern: /v-for=[\s\S]*?v-if=/, message: '避免在同一元素上同时使用 v-for 和 v-if — v-if 优先级高于 v-for(Vue 3),用 computed 过滤或包裹 <template>', }, { ruleId: 'vue-no-ref-in-reactive', category: 'correctness', dimension: 'file', severity: 'info', languages: ['typescript', 'javascript'], pattern: /reactive\s*\(\s*\{[^}]*ref\s*\(/, message: '避免在 reactive() 内嵌套 ref() — reactive 会自动解包 ref,但手动嵌套会造成混淆', }, ]; } /** .vue SFC 预处理 → 提取 <script> / <script setup> 块 */ preprocessFile(content, ext) { if (ext !== '.vue') { return null; } // 优先提取 <script setup> const setupMatch = content.match(/<script\s+setup(?:\s+lang="(ts|typescript)")?\s*>([\s\S]*?)<\/script>/i); if (setupMatch) { return { content: setupMatch[2], lang: setupMatch[1] ? 'typescript' : 'javascript' }; } // 退 fallback 到普通 <script> const scriptMatch = content.match(/<script(?:\s+lang="(ts|typescript)")?\s*>([\s\S]*?)<\/script>/i); if (scriptMatch) { return { content: scriptMatch[2], lang: scriptMatch[1] ? 'typescript' : 'javascript' }; } return null; } detectPatterns(astSummary) { const patterns = []; // ── Composable functions ── for (const m of astSummary.methods || []) { if (m.name && /^use[A-Z]/.test(m.name) && !m.className) { patterns.push({ type: 'vue-composable', methodName: m.name, line: m.line, confidence: 0.9, }); } } // ── Pinia stores (defineStore) ── for (const m of astSummary.methods || []) { if (m.name && /^use\w+Store$/.test(m.name) && !m.className) { patterns.push({ type: 'vue-pinia-store', methodName: m.name, line: m.line, confidence: 0.9, }); } } // ── Vue Router guard patterns ── for (const m of astSummary.methods || []) { if (!m.className && m.name) { const nameLower = m.name.toLowerCase(); if (nameLower.includes('beforeeach') || nameLower.includes('beforeenter') || nameLower.includes('beforerouteleave') || nameLower === 'beforeRouteEnter' || nameLower === 'beforeRouteUpdate') { patterns.push({ type: 'vue-router-guard', methodName: m.name, line: m.line, confidence: 0.85, }); } } } // ── Plugin install functions ── for (const m of astSummary.methods || []) { if (m.isExported && m.name === 'install' && !m.className) { patterns.push({ type: 'vue-plugin', methodName: m.name, line: m.line, confidence: 0.85, }); } } // ── Vue ecosystem imports ── const vueImports = (astSummary.imports || []).filter((imp) => imp.includes('vue') || imp.includes('pinia') || imp.includes('vue-router') || imp.includes('vueuse') || imp.includes('@vueuse')); if (vueImports.length > 0) { patterns.push({ type: 'vue-ecosystem-usage', importCount: vueImports.length, confidence: 0.85, }); } return patterns; } } export const pack = new VueEnhancement();