autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
212 lines (211 loc) • 8.91 kB
JavaScript
/**
* 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();