docs(vue-best-practices): 新增Vue 3最佳实践文档集
添加关于Vue 3 TypeScript开发、Volar工具链、性能优化和测试相关的13个最佳实践文档,涵盖模板类型检查、CSS模块、路由参数类型等核心场景 - 新增vue-tsc严格模板检查规则 - 添加CSS模块严格类型验证指南 - 补充Volar 3.0迁移问题解决方案 - 包含Pinia测试和SSR HMR调试实践 - 添加defineModel和withDefaults的TypeScript处理方案 - 补充模块解析和自动导入冲突解决建议
Showing
18 changed files
with
1542 additions
and
0 deletions
.trae/skills/vue-best-practices/SKILL.md
0 → 100644
| 1 | +--- | ||
| 2 | +name: vue-best-practices | ||
| 3 | +description: Vue 3 TypeScript, vue-tsc, Volar, Vite, component props, testing, composition API. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# Vue Best Practices | ||
| 7 | + | ||
| 8 | +## Capability Rules | ||
| 9 | + | ||
| 10 | +| Rule | Keywords | Description | | ||
| 11 | +|------|----------|-------------| | ||
| 12 | +| [vue-tsc-strict-templates](rules/vue-tsc-strict-templates.md) | undefined component, template error, strictTemplates | Catch undefined components in templates | | ||
| 13 | +| [fallthrough-attributes](rules/fallthrough-attributes.md) | fallthrough, $attrs, wrapper component | Type-check fallthrough attributes | | ||
| 14 | +| [strict-css-modules](rules/strict-css-modules.md) | css modules, $style, typo | Catch CSS module class typos | | ||
| 15 | +| [data-attributes-config](rules/data-attributes-config.md) | data-*, strictTemplates, attribute | Allow data-* attributes | | ||
| 16 | +| [volar-3-breaking-changes](rules/volar-3-breaking-changes.md) | volar, vue-language-server, editor | Fix Volar 3.0 upgrade issues | | ||
| 17 | +| [module-resolution-bundler](rules/module-resolution-bundler.md) | cannot find module, @vue/tsconfig, moduleResolution | Fix module resolution errors | | ||
| 18 | +| [unplugin-auto-import-conflicts](rules/unplugin-auto-import-conflicts.md) | unplugin, auto-import, types any | Fix unplugin type conflicts | | ||
| 19 | +| [codeactions-save-performance](rules/codeactions-save-performance.md) | slow save, vscode, performance | Fix slow save in large projects | | ||
| 20 | +| [duplicate-plugin-detection](rules/duplicate-plugin-detection.md) | duplicate plugin, vite, vue plugin | Detect duplicate plugins | | ||
| 21 | +| [define-model-update-event](rules/define-model-update-event.md) | defineModel, update event, undefined | Fix model update errors | | ||
| 22 | +| [with-defaults-union-types](rules/with-defaults-union-types.md) | withDefaults, union type, default | Fix union type defaults | | ||
| 23 | +| [deep-watch-numeric](rules/deep-watch-numeric.md) | watch, deep, array, Vue 3.5 | Efficient array watching | | ||
| 24 | +| [vue-directive-comments](rules/vue-directive-comments.md) | @vue-ignore, @vue-skip, template | Control template type checking | | ||
| 25 | +| [script-setup-jsdoc](rules/script-setup-jsdoc.md) | jsdoc, script setup, documentation | Add JSDoc to script setup | | ||
| 26 | +| [vue-router-typed-params](rules/vue-router-typed-params.md) | route params, typed router, unplugin | Fix route params typing | | ||
| 27 | + | ||
| 28 | +## Efficiency Rules | ||
| 29 | + | ||
| 30 | +| Rule | Keywords | Description | | ||
| 31 | +|------|----------|-------------| | ||
| 32 | +| [hmr-vue-ssr](rules/hmr-vue-ssr.md) | hmr, ssr, hot reload | Fix HMR in SSR apps | | ||
| 33 | +| [pinia-store-mocking](rules/pinia-store-mocking.md) | pinia, mock, vitest, store | Mock Pinia stores | | ||
| 34 | + | ||
| 35 | +## Reference | ||
| 36 | + | ||
| 37 | +- [Vue Language Tools](https://github.com/vuejs/language-tools) | ||
| 38 | +- [Vue 3 Documentation](https://vuejs.org/) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | +--- | ||
| 2 | +title: Fix Slow Save Times with Code Actions Setting | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: fixes 30-60 second save delays in large Vue projects | ||
| 5 | +type: capability | ||
| 6 | +tags: performance, save-time, vscode, code-actions, volar | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Fix Slow Save Times with Code Actions Setting | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - fixes 30-60 second save delays in large Vue projects | ||
| 12 | + | ||
| 13 | +In large Vue projects, saving files can take 30-60+ seconds due to VSCode's code actions triggering expensive TypeScript state synchronization. | ||
| 14 | + | ||
| 15 | +## Problem | ||
| 16 | + | ||
| 17 | +Symptoms: | ||
| 18 | +- Save operation takes 30+ seconds | ||
| 19 | +- Editor becomes unresponsive during save | ||
| 20 | +- CPU spikes when saving Vue files | ||
| 21 | +- Happens more in larger projects | ||
| 22 | + | ||
| 23 | +## Root Cause | ||
| 24 | + | ||
| 25 | +VSCode emits document change events multiple times during save cycles. Each event triggers Volar to synchronize with TypeScript, causing expensive re-computation. | ||
| 26 | + | ||
| 27 | +## Solution | ||
| 28 | + | ||
| 29 | +Disable code actions or limit their timeout: | ||
| 30 | + | ||
| 31 | +**Option 1: Disable code actions (fastest)** | ||
| 32 | +```json | ||
| 33 | +// .vscode/settings.json | ||
| 34 | +{ | ||
| 35 | + "vue.codeActions.enabled": false | ||
| 36 | +} | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +**Option 2: Limit code action time** | ||
| 40 | +```json | ||
| 41 | +// .vscode/settings.json | ||
| 42 | +{ | ||
| 43 | + "vue.codeActions.savingTimeLimit": 1000 | ||
| 44 | +} | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +**Option 3: Disable specific code actions** | ||
| 48 | +```json | ||
| 49 | +// .vscode/settings.json | ||
| 50 | +{ | ||
| 51 | + "vue.codeActions.enabled": true, | ||
| 52 | + "editor.codeActionsOnSave": { | ||
| 53 | + "source.organizeImports": "never" | ||
| 54 | + } | ||
| 55 | +} | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +## VSCode Version Requirement | ||
| 59 | + | ||
| 60 | +VSCode 1.81.0+ includes fixes that reduce save time issues. Upgrade if using an older version. | ||
| 61 | + | ||
| 62 | +## Additional Optimizations | ||
| 63 | + | ||
| 64 | +```json | ||
| 65 | +// .vscode/settings.json | ||
| 66 | +{ | ||
| 67 | + "vue.codeActions.enabled": false, | ||
| 68 | + "editor.formatOnSave": true, | ||
| 69 | + "editor.codeActionsOnSave": {}, | ||
| 70 | + "[vue]": { | ||
| 71 | + "editor.formatOnSave": true, | ||
| 72 | + "editor.defaultFormatter": "Vue.volar" | ||
| 73 | + } | ||
| 74 | +} | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +## Reference | ||
| 78 | + | ||
| 79 | +- [Vue Language Tools Discussion #2740](https://github.com/vuejs/language-tools/discussions/2740) |
| 1 | +--- | ||
| 2 | +title: Allow Data Attributes with Strict Templates | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes data-testid and data-* attribute errors in strict mode | ||
| 5 | +type: capability | ||
| 6 | +tags: dataAttributes, vueCompilerOptions, strictTemplates, data-testid, testing | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Allow Data Attributes with Strict Templates | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes data-testid and data-* attribute errors in strict mode | ||
| 12 | + | ||
| 13 | +With `strictTemplates` enabled, `data-*` attributes on components cause type errors. Use the `dataAttributes` option to allow specific patterns. | ||
| 14 | + | ||
| 15 | +## Problem | ||
| 16 | + | ||
| 17 | +```vue | ||
| 18 | +<template> | ||
| 19 | + <!-- Error: Property 'data-testid' does not exist on type... --> | ||
| 20 | + <MyComponent data-testid="submit-button" /> | ||
| 21 | + | ||
| 22 | + <!-- Error: Property 'data-cy' does not exist on type... --> | ||
| 23 | + <MyComponent data-cy="login-form" /> | ||
| 24 | +</template> | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +## Solution | ||
| 28 | + | ||
| 29 | +Configure `dataAttributes` to allow specific patterns: | ||
| 30 | + | ||
| 31 | +```json | ||
| 32 | +// tsconfig.json or tsconfig.app.json | ||
| 33 | +{ | ||
| 34 | + "vueCompilerOptions": { | ||
| 35 | + "strictTemplates": true, | ||
| 36 | + "dataAttributes": ["data-*"] | ||
| 37 | + } | ||
| 38 | +} | ||
| 39 | +``` | ||
| 40 | + | ||
| 41 | +Now all `data-*` attributes are allowed on any component. | ||
| 42 | + | ||
| 43 | +## Specific Patterns | ||
| 44 | + | ||
| 45 | +You can be more selective: | ||
| 46 | + | ||
| 47 | +```json | ||
| 48 | +{ | ||
| 49 | + "vueCompilerOptions": { | ||
| 50 | + "dataAttributes": [ | ||
| 51 | + "data-testid", | ||
| 52 | + "data-cy", | ||
| 53 | + "data-test-*" | ||
| 54 | + ] | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +This only allows the specified patterns, not all data attributes. | ||
| 60 | + | ||
| 61 | +## Common Testing Attributes | ||
| 62 | + | ||
| 63 | +For testing libraries, allow their specific attributes: | ||
| 64 | + | ||
| 65 | +| Library | Attribute | Pattern | | ||
| 66 | +|---------|-----------|---------| | ||
| 67 | +| Testing Library | `data-testid` | `"data-testid"` | | ||
| 68 | +| Cypress | `data-cy` | `"data-cy"` | | ||
| 69 | +| Playwright | `data-testid` | `"data-testid"` | | ||
| 70 | +| Generic | All data attributes | `"data-*"` | | ||
| 71 | + | ||
| 72 | +## Reference | ||
| 73 | + | ||
| 74 | +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | +--- | ||
| 2 | +title: Vue 3.5+ Deep Watch Numeric Depth | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: enables efficient array mutation watching with numeric deep option | ||
| 5 | +type: capability | ||
| 6 | +tags: watch, deep, vue-3.5, array, mutation, performance | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Vue 3.5+ Deep Watch Numeric Depth | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - enables efficient array mutation watching with numeric deep option | ||
| 12 | + | ||
| 13 | +Vue 3.5 introduced `deep: number` for watch depth control. This allows watching array mutations without the performance cost of deep traversal. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- Array mutations not triggering watch callback | ||
| 18 | +- Deep watch causing performance issues on large nested objects | ||
| 19 | +- Unaware of new Vue 3.5 feature | ||
| 20 | + | ||
| 21 | +> **Note:** TypeScript error "Type 'number' is not assignable to type 'boolean'" no longer occurs with Vue 3.5+ and current TypeScript versions. The types now correctly support numeric `deep` values. | ||
| 22 | + | ||
| 23 | +## The Feature | ||
| 24 | + | ||
| 25 | +```typescript | ||
| 26 | +// Vue 3.5+ only | ||
| 27 | +watch(items, (newVal) => { | ||
| 28 | + // Triggered on array mutations (push, pop, splice, etc.) | ||
| 29 | +}, { deep: 1 }) | ||
| 30 | +``` | ||
| 31 | + | ||
| 32 | +| deep value | Behavior | | ||
| 33 | +|------------|----------| | ||
| 34 | +| `true` | Full recursive traversal (original behavior) | | ||
| 35 | +| `false` | Only reference changes | | ||
| 36 | +| `1` | One level deep - array mutations, not nested objects | | ||
| 37 | +| `2` | Two levels deep | | ||
| 38 | +| `n` | N levels deep | | ||
| 39 | + | ||
| 40 | +## Fix | ||
| 41 | + | ||
| 42 | +**Step 1: Ensure Vue 3.5+** | ||
| 43 | +```bash | ||
| 44 | +npm install vue@^3.5.0 | ||
| 45 | +``` | ||
| 46 | + | ||
| 47 | +**Step 2: Update @vue/runtime-core types** | ||
| 48 | +```bash | ||
| 49 | +npm install -D @vue/runtime-core@latest | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +**Step 3: Use numeric depth** | ||
| 53 | +```typescript | ||
| 54 | +import { watch, ref } from 'vue' | ||
| 55 | + | ||
| 56 | +const items = ref([{ id: 1, data: { nested: 'value' } }]) | ||
| 57 | + | ||
| 58 | +// Watch array mutations only (push, pop, etc.) | ||
| 59 | +watch(items, (newItems) => { | ||
| 60 | + console.log('Array mutated') | ||
| 61 | +}, { deep: 1 }) | ||
| 62 | + | ||
| 63 | +// Won't trigger on: items.value[0].data.nested = 'new' | ||
| 64 | +// Will trigger on: items.value.push(newItem) | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +## Performance Comparison | ||
| 68 | + | ||
| 69 | +```typescript | ||
| 70 | +const largeNestedData = ref({ /* deeply nested structure */ }) | ||
| 71 | + | ||
| 72 | +// SLOW - traverses entire structure | ||
| 73 | +watch(largeNestedData, handler, { deep: true }) | ||
| 74 | + | ||
| 75 | +// FAST - only watches top-level changes | ||
| 76 | +watch(largeNestedData, handler, { deep: 1 }) | ||
| 77 | + | ||
| 78 | +// FASTEST - only reference changes | ||
| 79 | +watch(largeNestedData, handler, { deep: false }) | ||
| 80 | +``` | ||
| 81 | + | ||
| 82 | +## Alternative: watchEffect for Selective Tracking | ||
| 83 | + | ||
| 84 | +```typescript | ||
| 85 | +// Only tracks properties actually accessed | ||
| 86 | +watchEffect(() => { | ||
| 87 | + // Only re-runs when items.value.length or first item changes | ||
| 88 | + console.log(items.value.length, items.value[0]?.id) | ||
| 89 | +}) | ||
| 90 | +``` | ||
| 91 | + | ||
| 92 | +## TypeScript Note | ||
| 93 | + | ||
| 94 | +If TypeScript complains about numeric deep, ensure: | ||
| 95 | +1. Vue version is 3.5+ | ||
| 96 | +2. `@vue/runtime-core` types are updated | ||
| 97 | +3. tsconfig targets correct node_modules types | ||
| 98 | + | ||
| 99 | +## Reference | ||
| 100 | + | ||
| 101 | +- [Vue Watchers Docs](https://vuejs.org/guide/essentials/watchers.html) | ||
| 102 | +- [Vue 3.5 Release Notes](https://blog.vuejs.org/posts/vue-3-5) |
| 1 | +--- | ||
| 2 | +title: defineModel Fires Update Event with Undefined | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes runtime errors from unexpected undefined in model updates | ||
| 5 | +type: capability | ||
| 6 | +tags: defineModel, v-model, update-event, undefined, vue-3.5 | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# defineModel Fires Update Event with Undefined | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes runtime errors from unexpected undefined in model updates | ||
| 12 | + | ||
| 13 | +> **Version Note (2025):** This issue may be resolved in Vue 3.5+. Testing with Vue 3.5.26 could not reproduce the double emission with `undefined`. If you're on Vue 3.5+, verify the issue exists in your specific scenario before applying workarounds. | ||
| 14 | + | ||
| 15 | +Components using `defineModel` may fire the `@update:model-value` event with `undefined` in certain edge cases. TypeScript types don't always reflect this behavior, potentially causing runtime errors when the parent expects a non-nullable value. | ||
| 16 | + | ||
| 17 | +## Symptoms | ||
| 18 | + | ||
| 19 | +- Parent component receives `undefined` unexpectedly | ||
| 20 | +- Runtime error: "Cannot read property of undefined" | ||
| 21 | +- Type mismatch between expected `T` and received `T | undefined` | ||
| 22 | +- Issue appears when clearing/resetting the model value | ||
| 23 | + | ||
| 24 | +## Root Cause | ||
| 25 | + | ||
| 26 | +`defineModel` returns `Ref<T | undefined>` by default, even when `T` is non-nullable. The update event can fire with `undefined` when: | ||
| 27 | +- Component unmounts | ||
| 28 | +- Model is explicitly cleared | ||
| 29 | +- Internal state resets | ||
| 30 | + | ||
| 31 | +## Fix | ||
| 32 | + | ||
| 33 | +**Option 1: Use required option (Vue 3.5+)** | ||
| 34 | +```typescript | ||
| 35 | +// Returns Ref<Item> instead of Ref<Item | undefined> | ||
| 36 | +const model = defineModel<Item>({ required: true }) | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +**Option 2: Type parent handler to accept undefined** | ||
| 40 | +```vue | ||
| 41 | +<template> | ||
| 42 | + <MyComponent | ||
| 43 | + v-model="item" | ||
| 44 | + @update:model-value="handleUpdate" | ||
| 45 | + /> | ||
| 46 | +</template> | ||
| 47 | + | ||
| 48 | +<script setup lang="ts"> | ||
| 49 | +// Handle both value and undefined | ||
| 50 | +const handleUpdate = (value: Item | undefined) => { | ||
| 51 | + if (value !== undefined) { | ||
| 52 | + item.value = value | ||
| 53 | + } | ||
| 54 | +} | ||
| 55 | +</script> | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +**Option 3: Use default value in defineModel** | ||
| 59 | +```typescript | ||
| 60 | +const model = defineModel<string>({ default: '' }) | ||
| 61 | +``` | ||
| 62 | + | ||
| 63 | +## Type Declaration Pattern | ||
| 64 | + | ||
| 65 | +```typescript | ||
| 66 | +// In child component | ||
| 67 | +interface Props { | ||
| 68 | + modelValue: Item | ||
| 69 | +} | ||
| 70 | +const model = defineModel<Item>({ required: true }) | ||
| 71 | + | ||
| 72 | +// Emits will be typed as (value: Item) not (value: Item | undefined) | ||
| 73 | +``` | ||
| 74 | + | ||
| 75 | +## Reference | ||
| 76 | + | ||
| 77 | +- [vuejs/core#12817](https://github.com/vuejs/core/issues/12817) | ||
| 78 | +- [vuejs/core#10103](https://github.com/vuejs/core/issues/10103) | ||
| 79 | +- [defineModel docs](https://vuejs.org/api/sfc-script-setup.html#definemodel) |
| 1 | +--- | ||
| 2 | +title: Duplicate Vue Plugin Detection | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes cryptic build errors from Vue plugin registered twice | ||
| 5 | +type: capability | ||
| 6 | +tags: vite, plugin, vue, duplicate, config, inline | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Duplicate Vue Plugin Detection | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes cryptic build errors from Vue plugin registered twice | ||
| 12 | + | ||
| 13 | +When using Vite's JavaScript API, if the Vue plugin is loaded in `vite.config.js` and specified again in `inlineConfig`, it gets registered twice, causing cryptic build errors. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- Build produces unexpected output or fails silently | ||
| 18 | +- "Cannot read property of undefined" during build | ||
| 19 | +- Different build behavior between CLI and JavaScript API | ||
| 20 | +- Vue components render incorrectly after build | ||
| 21 | + | ||
| 22 | +## Root Cause | ||
| 23 | + | ||
| 24 | +Vite doesn't deduplicate plugins by name when merging configs. The Vue plugin's internal state gets corrupted when registered twice. | ||
| 25 | + | ||
| 26 | +## Fix | ||
| 27 | + | ||
| 28 | +**Option 1: Use configFile: false with inline plugins** | ||
| 29 | +```typescript | ||
| 30 | +import { build } from 'vite' | ||
| 31 | +import vue from '@vitejs/plugin-vue' | ||
| 32 | + | ||
| 33 | +await build({ | ||
| 34 | + configFile: false, // Don't load vite.config.js | ||
| 35 | + plugins: [vue()], | ||
| 36 | + // ... rest of config | ||
| 37 | +}) | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +**Option 2: Don't specify plugins in inlineConfig** | ||
| 41 | +```typescript | ||
| 42 | +// vite.config.js already has vue plugin | ||
| 43 | +import { build } from 'vite' | ||
| 44 | + | ||
| 45 | +await build({ | ||
| 46 | + // Don't add vue plugin here - it's in vite.config.js | ||
| 47 | + root: './src', | ||
| 48 | + build: { outDir: '../dist' } | ||
| 49 | +}) | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +**Option 3: Filter out Vue plugin before merging** | ||
| 53 | +```typescript | ||
| 54 | +import { build, loadConfigFromFile } from 'vite' | ||
| 55 | +import vue from '@vitejs/plugin-vue' | ||
| 56 | + | ||
| 57 | +const { config } = await loadConfigFromFile({ command: 'build', mode: 'production' }) | ||
| 58 | + | ||
| 59 | +// Remove existing Vue plugin | ||
| 60 | +const filteredPlugins = config.plugins?.filter( | ||
| 61 | + p => !p || (Array.isArray(p) ? false : p.name !== 'vite:vue') | ||
| 62 | +) || [] | ||
| 63 | + | ||
| 64 | +await build({ | ||
| 65 | + ...config, | ||
| 66 | + plugins: [...filteredPlugins, vue({ /* your options */ })] | ||
| 67 | +}) | ||
| 68 | +``` | ||
| 69 | + | ||
| 70 | +## Detection Script | ||
| 71 | + | ||
| 72 | +Add this to debug plugin registration: | ||
| 73 | +```typescript | ||
| 74 | +// vite.config.ts | ||
| 75 | +export default defineConfig({ | ||
| 76 | + plugins: [ | ||
| 77 | + vue(), | ||
| 78 | + { | ||
| 79 | + name: 'debug-plugins', | ||
| 80 | + configResolved(config) { | ||
| 81 | + const vuePlugins = config.plugins.filter(p => p.name?.includes('vue')) | ||
| 82 | + if (vuePlugins.length > 1) { | ||
| 83 | + console.warn('WARNING: Multiple Vue plugins detected:', vuePlugins.map(p => p.name)) | ||
| 84 | + } | ||
| 85 | + } | ||
| 86 | + } | ||
| 87 | + ] | ||
| 88 | +}) | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +## Common Scenarios | ||
| 92 | + | ||
| 93 | +| Scenario | Solution | | ||
| 94 | +|----------|----------| | ||
| 95 | +| Using `vite.createServer()` | Use `configFile: false` | | ||
| 96 | +| Build script with custom config | Don't duplicate plugins | | ||
| 97 | +| Monorepo with shared config | Check for plugin inheritance | | ||
| 98 | + | ||
| 99 | +## Reference | ||
| 100 | + | ||
| 101 | +- [Vite Issue #5335](https://github.com/vitejs/vite/issues/5335) | ||
| 102 | +- [Vite JavaScript API](https://vite.dev/guide/api-javascript.html) |
| 1 | +--- | ||
| 2 | +title: Enable Fallthrough Attributes Type Checking | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: enables type-safe fallthrough attributes in component libraries | ||
| 5 | +type: capability | ||
| 6 | +tags: fallthroughAttributes, vueCompilerOptions, component-library, wrapper-components | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Enable Fallthrough Attributes Type Checking | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - enables type-aware attribute forwarding in component libraries | ||
| 12 | + | ||
| 13 | +When building component libraries with wrapper components, enable `fallthroughAttributes` to get IDE autocomplete for attributes that will be forwarded to child elements. | ||
| 14 | + | ||
| 15 | +## What It Does | ||
| 16 | + | ||
| 17 | +Wrapper components that pass attributes to child elements can benefit from type-aware completion: | ||
| 18 | + | ||
| 19 | +```vue | ||
| 20 | +<!-- MyButton.vue - wrapper around native button --> | ||
| 21 | +<template> | ||
| 22 | + <button v-bind="$attrs"><slot /></button> | ||
| 23 | +</template> | ||
| 24 | +``` | ||
| 25 | + | ||
| 26 | +## Solution | ||
| 27 | + | ||
| 28 | +Enable `fallthroughAttributes` in your tsconfig: | ||
| 29 | + | ||
| 30 | +```json | ||
| 31 | +// tsconfig.json or tsconfig.app.json | ||
| 32 | +{ | ||
| 33 | + "vueCompilerOptions": { | ||
| 34 | + "fallthroughAttributes": true | ||
| 35 | + } | ||
| 36 | +} | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +## How It Works | ||
| 40 | + | ||
| 41 | +When `fallthroughAttributes: true`: | ||
| 42 | +- Vue Language Server analyzes which element receives `$attrs` | ||
| 43 | +- IDE autocomplete suggests valid attributes for the target element | ||
| 44 | +- Helps developers discover available attributes | ||
| 45 | + | ||
| 46 | +> **Note:** This primarily enables IDE autocomplete for valid fallthrough attributes. It does NOT reject invalid attributes as type errors - arbitrary attributes are still allowed. | ||
| 47 | + | ||
| 48 | +## Related Options | ||
| 49 | + | ||
| 50 | +Combine with `strictTemplates` for comprehensive checking: | ||
| 51 | + | ||
| 52 | +```json | ||
| 53 | +{ | ||
| 54 | + "vueCompilerOptions": { | ||
| 55 | + "strictTemplates": true, | ||
| 56 | + "fallthroughAttributes": true | ||
| 57 | + } | ||
| 58 | +} | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +## Reference | ||
| 62 | + | ||
| 63 | +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | +--- | ||
| 2 | +title: HMR Debugging for Vue SSR | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes Hot Module Replacement breaking in Vue SSR applications | ||
| 5 | +type: efficiency | ||
| 6 | +tags: vite, hmr, ssr, vue, hot-reload, server-side-rendering | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# HMR Debugging for Vue SSR | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes Hot Module Replacement breaking in Vue SSR applications | ||
| 12 | + | ||
| 13 | +Hot Module Replacement breaks when modifying Vue component `<script setup>` sections in SSR applications. Changes cause errors instead of smooth updates, requiring full page reloads. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- HMR works for `<template>` changes but breaks for `<script setup>` | ||
| 18 | +- "Cannot read property of undefined" after saving | ||
| 19 | +- Full page reload required after script changes | ||
| 20 | +- HMR works in dev:client but not dev:ssr | ||
| 21 | + | ||
| 22 | +## Root Cause | ||
| 23 | + | ||
| 24 | +SSR mode has a different transformation pipeline. The Vue plugin's HMR boundary detection doesn't handle SSR modules the same way as client modules. | ||
| 25 | + | ||
| 26 | +## Fix | ||
| 27 | + | ||
| 28 | +**Step 1: Ensure correct SSR plugin configuration** | ||
| 29 | +```typescript | ||
| 30 | +// vite.config.ts | ||
| 31 | +import { defineConfig } from 'vite' | ||
| 32 | +import vue from '@vitejs/plugin-vue' | ||
| 33 | + | ||
| 34 | +export default defineConfig({ | ||
| 35 | + plugins: [vue()], | ||
| 36 | + ssr: { | ||
| 37 | + // Don't externalize these for HMR to work | ||
| 38 | + noExternal: ['vue', '@vue/runtime-core', '@vue/runtime-dom'] | ||
| 39 | + } | ||
| 40 | +}) | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +**Step 2: Configure dev server for SSR HMR** | ||
| 44 | +```typescript | ||
| 45 | +// server.ts | ||
| 46 | +import { createServer } from 'vite' | ||
| 47 | + | ||
| 48 | +const vite = await createServer({ | ||
| 49 | + server: { middlewareMode: true }, | ||
| 50 | + appType: 'custom' | ||
| 51 | +}) | ||
| 52 | + | ||
| 53 | +// Use vite.ssrLoadModule for server-side imports | ||
| 54 | +const { render } = await vite.ssrLoadModule('/src/entry-server.ts') | ||
| 55 | + | ||
| 56 | +// Handle HMR | ||
| 57 | +vite.watcher.on('change', async (file) => { | ||
| 58 | + if (file.endsWith('.vue')) { | ||
| 59 | + // Invalidate the module | ||
| 60 | + const mod = vite.moduleGraph.getModuleById(file) | ||
| 61 | + if (mod) { | ||
| 62 | + vite.moduleGraph.invalidateModule(mod) | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | +}) | ||
| 66 | +``` | ||
| 67 | + | ||
| 68 | +**Step 3: Add HMR acceptance in entry-server** | ||
| 69 | +```typescript | ||
| 70 | +// entry-server.ts | ||
| 71 | +import { createApp } from './main' | ||
| 72 | + | ||
| 73 | +export async function render(url: string) { | ||
| 74 | + const app = createApp() | ||
| 75 | + // ... render logic | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +// Accept HMR updates | ||
| 79 | +if (import.meta.hot) { | ||
| 80 | + import.meta.hot.accept() | ||
| 81 | +} | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +## Framework-Specific Solutions | ||
| 85 | + | ||
| 86 | +### Nuxt 3 | ||
| 87 | +HMR should work out of the box. If not: | ||
| 88 | +```bash | ||
| 89 | +rm -rf .nuxt node_modules/.vite | ||
| 90 | +npm install | ||
| 91 | +npm run dev | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +### Vite SSR Template | ||
| 95 | +Ensure you're using the latest `@vitejs/plugin-vue`: | ||
| 96 | +```bash | ||
| 97 | +npm install @vitejs/plugin-vue@latest | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +## Debugging | ||
| 101 | + | ||
| 102 | +Enable verbose HMR logging: | ||
| 103 | +```typescript | ||
| 104 | +// vite.config.ts | ||
| 105 | +export default defineConfig({ | ||
| 106 | + server: { | ||
| 107 | + hmr: { | ||
| 108 | + overlay: true | ||
| 109 | + } | ||
| 110 | + }, | ||
| 111 | + logLevel: 'info' // Shows HMR updates | ||
| 112 | +}) | ||
| 113 | +``` | ||
| 114 | + | ||
| 115 | +## Known Limitations | ||
| 116 | + | ||
| 117 | +- HMR for `<script>` (not `<script setup>`) may require full reload | ||
| 118 | +- SSR components with external dependencies may not hot-reload | ||
| 119 | +- State is not preserved for SSR components (expected behavior) | ||
| 120 | + | ||
| 121 | +## Reference | ||
| 122 | + | ||
| 123 | +- [vite-plugin-vue#525](https://github.com/vitejs/vite-plugin-vue/issues/525) | ||
| 124 | +- [Vite SSR Guide](https://vite.dev/guide/ssr.html) |
| 1 | +--- | ||
| 2 | +title: moduleResolution Bundler Migration Issues | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: fixes "Cannot find module" errors after @vue/tsconfig upgrade | ||
| 5 | +type: capability | ||
| 6 | +tags: moduleResolution, bundler, tsconfig, vue-tsconfig, node, esm | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# moduleResolution Bundler Migration Issues | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - fixes "Cannot find module" errors after @vue/tsconfig upgrade | ||
| 12 | + | ||
| 13 | +Recent versions of `@vue/tsconfig` changed `moduleResolution` from `"node"` to `"bundler"`. This can break existing projects with errors like "Cannot find module 'vue'" or issues with `resolveJsonModule`. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- `Cannot find module 'vue'` or other packages | ||
| 18 | +- `Option '--resolveJsonModule' cannot be specified without 'node' module resolution` | ||
| 19 | +- Errors appear after updating `@vue/tsconfig` | ||
| 20 | +- Some third-party packages no longer resolve | ||
| 21 | + | ||
| 22 | +## Root Cause | ||
| 23 | + | ||
| 24 | +`moduleResolution: "bundler"` requires: | ||
| 25 | +1. TypeScript 5.0+ | ||
| 26 | +2. Packages to have proper `exports` field in package.json | ||
| 27 | +3. Different resolution rules than Node.js classic resolution | ||
| 28 | + | ||
| 29 | +## Fix | ||
| 30 | + | ||
| 31 | +**Option 1: Ensure TypeScript 5.0+ everywhere** | ||
| 32 | +```bash | ||
| 33 | +npm install -D typescript@^5.0.0 | ||
| 34 | +``` | ||
| 35 | + | ||
| 36 | +In monorepos, ALL packages must use TypeScript 5.0+. | ||
| 37 | + | ||
| 38 | +**Option 2: Add compatibility workaround** | ||
| 39 | +```json | ||
| 40 | +{ | ||
| 41 | + "compilerOptions": { | ||
| 42 | + "module": "ESNext", | ||
| 43 | + "moduleResolution": "bundler", | ||
| 44 | + "resolvePackageJsonExports": false | ||
| 45 | + } | ||
| 46 | +} | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +Setting `resolvePackageJsonExports: false` restores compatibility with packages that don't have proper exports. | ||
| 50 | + | ||
| 51 | +**Option 3: Revert to Node resolution** | ||
| 52 | +```json | ||
| 53 | +{ | ||
| 54 | + "compilerOptions": { | ||
| 55 | + "moduleResolution": "node" | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +## Which Packages Break? | ||
| 61 | + | ||
| 62 | +Packages break if they: | ||
| 63 | +- Lack `exports` field in package.json | ||
| 64 | +- Have incorrect `exports` configuration | ||
| 65 | +- Rely on Node.js-specific resolution behavior | ||
| 66 | + | ||
| 67 | +## Diagnosis | ||
| 68 | + | ||
| 69 | +```bash | ||
| 70 | +# Check which resolution is being used | ||
| 71 | +cat tsconfig.json | grep moduleResolution | ||
| 72 | + | ||
| 73 | +# Test if a specific module resolves | ||
| 74 | +npx tsc --traceResolution 2>&1 | grep "module-name" | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +## Reference | ||
| 78 | + | ||
| 79 | +- [vuejs/tsconfig#8](https://github.com/vuejs/tsconfig/issues/8) | ||
| 80 | +- [TypeScript moduleResolution docs](https://www.typescriptlang.org/tsconfig#moduleResolution) | ||
| 81 | +- [Vite discussion#14001](https://github.com/vitejs/vite/discussions/14001) |
| 1 | +--- | ||
| 2 | +title: Mocking Pinia Stores with Vitest | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: properly mocks Pinia stores in component tests | ||
| 5 | +type: efficiency | ||
| 6 | +tags: pinia, vitest, testing, mock, createTestingPinia, store | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Mocking Pinia Stores with Vitest | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - properly mocks Pinia stores in component tests | ||
| 12 | + | ||
| 13 | +Developers struggle to properly mock Pinia stores: `createTestingPinia` requires explicit `createSpy` configuration, and "injection Symbol(pinia) not found" errors occur without proper setup. | ||
| 14 | + | ||
| 15 | +> **Important (@pinia/testing 1.0+):** The `createSpy` option is **REQUIRED**, not optional. Omitting it throws an error: "You must configure the `createSpy` option." | ||
| 16 | + | ||
| 17 | +## Symptoms | ||
| 18 | + | ||
| 19 | +- "injection Symbol(pinia) not found" error | ||
| 20 | +- "You must configure the `createSpy` option" error | ||
| 21 | +- Actions not properly mocked | ||
| 22 | +- Store state not reset between tests | ||
| 23 | + | ||
| 24 | +## Fix | ||
| 25 | + | ||
| 26 | +**Pattern 1: Basic setup with createTestingPinia** | ||
| 27 | +```typescript | ||
| 28 | +import { mount } from '@vue/test-utils' | ||
| 29 | +import { createTestingPinia } from '@pinia/testing' | ||
| 30 | +import { vi } from 'vitest' | ||
| 31 | +import MyComponent from './MyComponent.vue' | ||
| 32 | +import { useCounterStore } from '@/stores/counter' | ||
| 33 | + | ||
| 34 | +test('component uses store', async () => { | ||
| 35 | + const wrapper = mount(MyComponent, { | ||
| 36 | + global: { | ||
| 37 | + plugins: [ | ||
| 38 | + createTestingPinia({ | ||
| 39 | + createSpy: vi.fn, // REQUIRED in @pinia/testing 1.0+ | ||
| 40 | + initialState: { | ||
| 41 | + counter: { count: 10 } // Set initial state | ||
| 42 | + } | ||
| 43 | + }) | ||
| 44 | + ] | ||
| 45 | + } | ||
| 46 | + }) | ||
| 47 | + | ||
| 48 | + // Get the store instance AFTER mounting | ||
| 49 | + const store = useCounterStore() | ||
| 50 | + | ||
| 51 | + // Actions are automatically stubbed | ||
| 52 | + await wrapper.find('button').trigger('click') | ||
| 53 | + expect(store.increment).toHaveBeenCalled() | ||
| 54 | +}) | ||
| 55 | +``` | ||
| 56 | + | ||
| 57 | +**Pattern 2: Customize action behavior** | ||
| 58 | +```typescript | ||
| 59 | +test('component handles async action', async () => { | ||
| 60 | + const wrapper = mount(MyComponent, { | ||
| 61 | + global: { | ||
| 62 | + plugins: [ | ||
| 63 | + createTestingPinia({ | ||
| 64 | + createSpy: vi.fn, | ||
| 65 | + stubActions: false // Don't stub, use real actions | ||
| 66 | + }) | ||
| 67 | + ] | ||
| 68 | + } | ||
| 69 | + }) | ||
| 70 | + | ||
| 71 | + const store = useCounterStore() | ||
| 72 | + | ||
| 73 | + // Override specific action | ||
| 74 | + store.fetchData = vi.fn().mockResolvedValue({ items: [] }) | ||
| 75 | + | ||
| 76 | + await wrapper.find('.load-button').trigger('click') | ||
| 77 | + expect(store.fetchData).toHaveBeenCalled() | ||
| 78 | +}) | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +**Pattern 3: Testing store directly** | ||
| 82 | +```typescript | ||
| 83 | +import { setActivePinia, createPinia } from 'pinia' | ||
| 84 | +import { useCounterStore } from '@/stores/counter' | ||
| 85 | + | ||
| 86 | +describe('Counter Store', () => { | ||
| 87 | + beforeEach(() => { | ||
| 88 | + setActivePinia(createPinia()) | ||
| 89 | + }) | ||
| 90 | + | ||
| 91 | + test('increments count', () => { | ||
| 92 | + const store = useCounterStore() | ||
| 93 | + expect(store.count).toBe(0) | ||
| 94 | + | ||
| 95 | + store.increment() | ||
| 96 | + expect(store.count).toBe(1) | ||
| 97 | + }) | ||
| 98 | +}) | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +## Setup Store with Vitest | ||
| 102 | + | ||
| 103 | +```typescript | ||
| 104 | +// stores/counter.ts - Setup store syntax | ||
| 105 | +export const useCounterStore = defineStore('counter', () => { | ||
| 106 | + const count = ref(0) | ||
| 107 | + const doubleCount = computed(() => count.value * 2) | ||
| 108 | + | ||
| 109 | + function increment() { | ||
| 110 | + count.value++ | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + return { count, doubleCount, increment } | ||
| 114 | +}) | ||
| 115 | + | ||
| 116 | +// Test file | ||
| 117 | +test('setup store works', async () => { | ||
| 118 | + const pinia = createTestingPinia({ | ||
| 119 | + createSpy: vi.fn, | ||
| 120 | + initialState: { | ||
| 121 | + counter: { count: 5 } | ||
| 122 | + } | ||
| 123 | + }) | ||
| 124 | + | ||
| 125 | + const wrapper = mount(MyComponent, { | ||
| 126 | + global: { plugins: [pinia] } | ||
| 127 | + }) | ||
| 128 | + | ||
| 129 | + const store = useCounterStore() | ||
| 130 | + expect(store.count).toBe(5) | ||
| 131 | + expect(store.doubleCount).toBe(10) | ||
| 132 | +}) | ||
| 133 | +``` | ||
| 134 | + | ||
| 135 | +## Reset Between Tests | ||
| 136 | + | ||
| 137 | +```typescript | ||
| 138 | +describe('Store Tests', () => { | ||
| 139 | + let pinia: Pinia | ||
| 140 | + | ||
| 141 | + beforeEach(() => { | ||
| 142 | + pinia = createTestingPinia({ | ||
| 143 | + createSpy: vi.fn | ||
| 144 | + }) | ||
| 145 | + }) | ||
| 146 | + | ||
| 147 | + afterEach(() => { | ||
| 148 | + vi.clearAllMocks() | ||
| 149 | + }) | ||
| 150 | + | ||
| 151 | + test('test 1', () => { /* ... */ }) | ||
| 152 | + test('test 2', () => { /* ... */ }) | ||
| 153 | +}) | ||
| 154 | +``` | ||
| 155 | + | ||
| 156 | +## Reference | ||
| 157 | + | ||
| 158 | +- [Pinia Testing Guide](https://pinia.vuejs.org/cookbook/testing.html) | ||
| 159 | +- [Pinia Discussion #2092](https://github.com/vuejs/pinia/discussions/2092) |
| 1 | +--- | ||
| 2 | +title: JSDoc Documentation for Script Setup Components | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: enables proper documentation for composition API components | ||
| 5 | +type: capability | ||
| 6 | +tags: jsdoc, script-setup, documentation, composition-api, component | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# JSDoc Documentation for Script Setup Components | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - enables proper documentation for composition API components | ||
| 12 | + | ||
| 13 | +`<script setup>` doesn't have an obvious place to attach JSDoc comments for the component itself. Use a dual-script pattern. | ||
| 14 | + | ||
| 15 | +## Problem | ||
| 16 | + | ||
| 17 | +**Incorrect:** | ||
| 18 | +```vue | ||
| 19 | +<script setup lang="ts"> | ||
| 20 | +/** | ||
| 21 | + * This comment doesn't appear in IDE hover or docs | ||
| 22 | + * @component | ||
| 23 | + */ | ||
| 24 | +import { ref } from 'vue' | ||
| 25 | + | ||
| 26 | +const count = ref(0) | ||
| 27 | +</script> | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +JSDoc comments inside `<script setup>` don't attach to the component export because there's no explicit export statement. | ||
| 31 | + | ||
| 32 | +## Solution | ||
| 33 | + | ||
| 34 | +Use both `<script>` and `<script setup>` blocks: | ||
| 35 | + | ||
| 36 | +**Correct:** | ||
| 37 | +```vue | ||
| 38 | +<script lang="ts"> | ||
| 39 | +/** | ||
| 40 | + * A counter component that displays and increments a value. | ||
| 41 | + * | ||
| 42 | + * @example | ||
| 43 | + * ```vue | ||
| 44 | + * <Counter :initial="5" @update="handleUpdate" /> | ||
| 45 | + * ``` | ||
| 46 | + * | ||
| 47 | + * @component | ||
| 48 | + */ | ||
| 49 | +export default {} | ||
| 50 | +</script> | ||
| 51 | + | ||
| 52 | +<script setup lang="ts"> | ||
| 53 | +import { ref } from 'vue' | ||
| 54 | + | ||
| 55 | +const props = defineProps<{ | ||
| 56 | + /** Starting value for the counter */ | ||
| 57 | + initial?: number | ||
| 58 | +}>() | ||
| 59 | + | ||
| 60 | +const emit = defineEmits<{ | ||
| 61 | + /** Emitted when counter value changes */ | ||
| 62 | + update: [value: number] | ||
| 63 | +}>() | ||
| 64 | + | ||
| 65 | +const count = ref(props.initial ?? 0) | ||
| 66 | +</script> | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +## How It Works | ||
| 70 | + | ||
| 71 | +- The regular `<script>` block's default export is merged with `<script setup>` | ||
| 72 | +- JSDoc on `export default {}` attaches to the component | ||
| 73 | +- Props and emits JSDoc in `<script setup>` still work normally | ||
| 74 | + | ||
| 75 | +## What Gets Documented | ||
| 76 | + | ||
| 77 | +| Location | Shows In | | ||
| 78 | +|----------|----------| | ||
| 79 | +| `export default {}` JSDoc | Component import hover | | ||
| 80 | +| `defineProps` JSDoc | Prop hover in templates | | ||
| 81 | +| `defineEmits` JSDoc | Event handler hover | | ||
| 82 | + | ||
| 83 | +## Reference | ||
| 84 | + | ||
| 85 | +- [Vue Language Tools Discussion #5932](https://github.com/vuejs/language-tools/discussions/5932) |
| 1 | +--- | ||
| 2 | +title: Enable Strict CSS Modules Type Checking | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: catches typos in CSS module class names at compile time | ||
| 5 | +type: capability | ||
| 6 | +tags: strictCssModules, vueCompilerOptions, css-modules, style-module | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Enable Strict CSS Modules Type Checking | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - catches typos in CSS module class names at compile time | ||
| 12 | + | ||
| 13 | +When using CSS modules with `<style module>`, Vue doesn't validate class names by default. Enable `strictCssModules` to catch typos and undefined classes. | ||
| 14 | + | ||
| 15 | +## Problem | ||
| 16 | + | ||
| 17 | +CSS module class name errors go undetected: | ||
| 18 | + | ||
| 19 | +```vue | ||
| 20 | +<script setup lang="ts"> | ||
| 21 | +// No error for typo in class name | ||
| 22 | +</script> | ||
| 23 | + | ||
| 24 | +<template> | ||
| 25 | + <div :class="$style.buttn">Click me</div> | ||
| 26 | +</template> | ||
| 27 | + | ||
| 28 | +<style module> | ||
| 29 | +.button { | ||
| 30 | + background: blue; | ||
| 31 | +} | ||
| 32 | +</style> | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +The typo `buttn` instead of `button` silently fails at runtime. | ||
| 36 | + | ||
| 37 | +## Solution | ||
| 38 | + | ||
| 39 | +Enable `strictCssModules` in your tsconfig: | ||
| 40 | + | ||
| 41 | +```json | ||
| 42 | +// tsconfig.json or tsconfig.app.json | ||
| 43 | +{ | ||
| 44 | + "vueCompilerOptions": { | ||
| 45 | + "strictCssModules": true | ||
| 46 | + } | ||
| 47 | +} | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +Now `$style.buttn` will show a type error because `buttn` doesn't exist in the CSS module. | ||
| 51 | + | ||
| 52 | +## What Gets Checked | ||
| 53 | + | ||
| 54 | +| Access | With strictCssModules | | ||
| 55 | +|--------|----------------------| | ||
| 56 | +| `$style.validClass` | OK | | ||
| 57 | +| `$style.typo` | Error: Property 'typo' does not exist | | ||
| 58 | +| `$style['dynamic']` | OK (dynamic access not checked) | | ||
| 59 | + | ||
| 60 | +## Limitations | ||
| 61 | + | ||
| 62 | +- Only checks static property access (`$style.className`) | ||
| 63 | +- Dynamic access (`$style[variable]`) is not validated | ||
| 64 | +- Only works with `<style module>`, not external CSS files | ||
| 65 | + | ||
| 66 | +## Reference | ||
| 67 | + | ||
| 68 | +- [Vue Language Tools Wiki - Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) |
| 1 | +--- | ||
| 2 | +title: unplugin-vue-components and unplugin-auto-import Type Conflicts | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: fixes component types resolving as any when using both plugins | ||
| 5 | +type: capability | ||
| 6 | +tags: unplugin-vue-components, unplugin-auto-import, types, any, dts | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# unplugin-vue-components and unplugin-auto-import Type Conflicts | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - fixes component types resolving as any when using both plugins | ||
| 12 | + | ||
| 13 | +Installing both `unplugin-vue-components` and `unplugin-auto-import` can cause component types to resolve as `any`. The generated `.d.ts` files conflict with each other. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- Components typed as `any` instead of proper component types | ||
| 18 | +- No autocomplete for component props | ||
| 19 | +- No type errors for invalid props | ||
| 20 | +- Types work when using only one plugin but break with both | ||
| 21 | + | ||
| 22 | +## Root Cause | ||
| 23 | + | ||
| 24 | +Both plugins generate declaration files (`components.d.ts` and `auto-imports.d.ts`) that can have conflicting declarations. TypeScript declaration merging fails silently. | ||
| 25 | + | ||
| 26 | +## Fix | ||
| 27 | + | ||
| 28 | +**Step 1: Ensure both .d.ts files are in tsconfig include** | ||
| 29 | +```json | ||
| 30 | +{ | ||
| 31 | + "include": [ | ||
| 32 | + "src/**/*.ts", | ||
| 33 | + "src/**/*.vue", | ||
| 34 | + "components.d.ts", | ||
| 35 | + "auto-imports.d.ts" | ||
| 36 | + ] | ||
| 37 | +} | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +**Step 2: Set explicit, different dts paths** | ||
| 41 | +```typescript | ||
| 42 | +// vite.config.ts | ||
| 43 | +import Components from 'unplugin-vue-components/vite' | ||
| 44 | +import AutoImport from 'unplugin-auto-import/vite' | ||
| 45 | + | ||
| 46 | +export default defineConfig({ | ||
| 47 | + plugins: [ | ||
| 48 | + Components({ | ||
| 49 | + dts: 'src/types/components.d.ts' // Explicit unique path | ||
| 50 | + }), | ||
| 51 | + AutoImport({ | ||
| 52 | + dts: 'src/types/auto-imports.d.ts' // Explicit unique path | ||
| 53 | + }) | ||
| 54 | + ] | ||
| 55 | +}) | ||
| 56 | +``` | ||
| 57 | + | ||
| 58 | +**Step 3: Regenerate type files** | ||
| 59 | +```bash | ||
| 60 | +# Delete existing .d.ts files | ||
| 61 | +rm components.d.ts auto-imports.d.ts | ||
| 62 | + | ||
| 63 | +# Restart dev server to regenerate | ||
| 64 | +npm run dev | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +**Step 4: Verify no duplicate declarations** | ||
| 68 | + | ||
| 69 | +Check that the same component isn't declared in both files. | ||
| 70 | + | ||
| 71 | +## Plugin Order Matters | ||
| 72 | + | ||
| 73 | +Configure Components plugin AFTER AutoImport: | ||
| 74 | +```typescript | ||
| 75 | +plugins: [ | ||
| 76 | + AutoImport({ /* ... */ }), | ||
| 77 | + Components({ /* ... */ }) // Must come after AutoImport | ||
| 78 | +] | ||
| 79 | +``` | ||
| 80 | + | ||
| 81 | +## Common Mistake: Duplicate Imports | ||
| 82 | + | ||
| 83 | +Don't configure the same import in both plugins: | ||
| 84 | +```typescript | ||
| 85 | +// Wrong - Vue imported in both | ||
| 86 | +AutoImport({ | ||
| 87 | + imports: ['vue'] | ||
| 88 | +}) | ||
| 89 | +Components({ | ||
| 90 | + resolvers: [/* includes Vue components */] | ||
| 91 | +}) | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +## Reference | ||
| 95 | + | ||
| 96 | +- [unplugin-vue-components#640](https://github.com/unplugin/unplugin-vue-components/issues/640) | ||
| 97 | +- [unplugin-auto-import docs](https://github.com/unplugin/unplugin-auto-import) |
| 1 | +--- | ||
| 2 | +title: Volar 3.0 Breaking Changes | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: fixes editor integration after Volar/vue-language-server upgrade | ||
| 5 | +type: capability | ||
| 6 | +tags: volar, vue-language-server, neovim, vscode, ide, ts_ls, vtsls | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Volar 3.0 Breaking Changes | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - fixes editor integration after Volar/vue-language-server upgrade | ||
| 12 | + | ||
| 13 | +Volar 3.0 (vue-language-server 3.x) introduced breaking changes to the language server protocol. Editors configured for Volar 2.x will break with errors like "vue_ls doesn't work with ts_ls.. it expects vtsls". | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- `vue_ls doesn't work with ts_ls` | ||
| 18 | +- TypeScript features stop working in Vue files | ||
| 19 | +- No autocomplete, type hints, or error highlighting | ||
| 20 | +- Editor shows "Language server initialization failed" | ||
| 21 | + | ||
| 22 | +## Fix by Editor | ||
| 23 | + | ||
| 24 | +### VSCode | ||
| 25 | + | ||
| 26 | +Update the "Vue - Official" extension to latest version. It manages the language server automatically. | ||
| 27 | + | ||
| 28 | +### NeoVim (nvim-lspconfig) | ||
| 29 | + | ||
| 30 | +**Option 1: Use vtsls instead of ts_ls** | ||
| 31 | +```lua | ||
| 32 | +-- Replace ts_ls/tsserver with vtsls | ||
| 33 | +require('lspconfig').vtsls.setup({}) | ||
| 34 | +require('lspconfig').volar.setup({}) | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +**Option 2: Downgrade vue-language-server** | ||
| 38 | +```bash | ||
| 39 | +npm install -g @vue/language-server@2.1.10 | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +### JetBrains IDEs | ||
| 43 | + | ||
| 44 | +Update to latest Vue plugin. If issues persist, disable and re-enable the Vue plugin. | ||
| 45 | + | ||
| 46 | +## What Changed in 3.0 | ||
| 47 | + | ||
| 48 | +| Feature | Volar 2.x | Volar 3.0 | | ||
| 49 | +|---------|-----------|-----------| | ||
| 50 | +| Package name | volar | vue_ls | | ||
| 51 | +| TypeScript integration | ts_ls/tsserver | vtsls required | | ||
| 52 | +| Hybrid mode | Optional | Default | | ||
| 53 | + | ||
| 54 | +## Workaround: Stay on 2.x | ||
| 55 | + | ||
| 56 | +If upgrading is not possible: | ||
| 57 | +```bash | ||
| 58 | +npm install -g @vue/language-server@^2.0.0 | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +Pin in your project's package.json to prevent accidental upgrades. | ||
| 62 | + | ||
| 63 | +## Reference | ||
| 64 | + | ||
| 65 | +- [vuejs/language-tools#5598](https://github.com/vuejs/language-tools/issues/5598) | ||
| 66 | +- [NeoVim Vue Setup Guide](https://dev.to/danwalsh/solved-vue-3-typescript-inlay-hint-support-in-neovim-53ej) |
| 1 | +--- | ||
| 2 | +title: Vue Template Directive Comments | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: enables fine-grained control over template type checking | ||
| 5 | +type: capability | ||
| 6 | +tags: vue-directive, vue-ignore, vue-expect-error, vue-skip, template, type-checking | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Vue Template Directive Comments | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - enables fine-grained control over template type checking | ||
| 12 | + | ||
| 13 | +Vue Language Tools supports special directive comments to control type checking behavior in templates. | ||
| 14 | + | ||
| 15 | +## Available Directives | ||
| 16 | + | ||
| 17 | +### @vue-ignore | ||
| 18 | + | ||
| 19 | +Suppress type errors for the next line: | ||
| 20 | + | ||
| 21 | +```vue | ||
| 22 | +<template> | ||
| 23 | + <!-- @vue-ignore --> | ||
| 24 | + <Component :prop="valueWithTypeError" /> | ||
| 25 | +</template> | ||
| 26 | +``` | ||
| 27 | + | ||
| 28 | +### @vue-expect-error | ||
| 29 | + | ||
| 30 | +Assert that the next line should have a type error (useful for testing): | ||
| 31 | + | ||
| 32 | +```vue | ||
| 33 | +<template> | ||
| 34 | + <!-- @vue-expect-error --> | ||
| 35 | + <Component :invalid-prop="value" /> | ||
| 36 | +</template> | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +### @vue-skip | ||
| 40 | + | ||
| 41 | +Skip type checking for an entire block: | ||
| 42 | + | ||
| 43 | +```vue | ||
| 44 | +<template> | ||
| 45 | + <!-- @vue-skip --> | ||
| 46 | + <div> | ||
| 47 | + <!-- Everything in here is not type-checked --> | ||
| 48 | + <LegacyComponent :any="props" :go="here" /> | ||
| 49 | + </div> | ||
| 50 | +</template> | ||
| 51 | +``` | ||
| 52 | + | ||
| 53 | +### @vue-generic | ||
| 54 | + | ||
| 55 | +Declare template-level generic types: | ||
| 56 | + | ||
| 57 | +```vue | ||
| 58 | +<template> | ||
| 59 | + <!-- @vue-generic {T extends string} --> | ||
| 60 | + <GenericList :items="items as T[]" /> | ||
| 61 | +</template> | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +## Use Cases | ||
| 65 | + | ||
| 66 | +- Migrating legacy components with incomplete types | ||
| 67 | +- Working with third-party components that have incorrect type definitions | ||
| 68 | +- Temporarily suppressing errors during refactoring | ||
| 69 | +- Testing that certain patterns produce expected type errors | ||
| 70 | + | ||
| 71 | +## Reference | ||
| 72 | + | ||
| 73 | +- [Vue Language Tools Wiki - Directive Comments](https://github.com/vuejs/language-tools/wiki/Directive-Comments) |
| 1 | +--- | ||
| 2 | +title: Vue Router useRoute Params Union Type Narrowing | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes "Property does not exist" errors with typed route params | ||
| 5 | +type: capability | ||
| 6 | +tags: vue-router, useRoute, unplugin-vue-router, typed-routes, params | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Vue Router useRoute Params Union Type Narrowing | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes "Property does not exist" errors with typed route params | ||
| 12 | + | ||
| 13 | +With `unplugin-vue-router` typed routes, `route.params` becomes a union of ALL page param types. TypeScript cannot narrow `Record<never, never> | { id: string }` properly, causing "Property 'id' does not exist" errors even on the correct page. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- "Property 'id' does not exist on type 'RouteParams'" | ||
| 18 | +- `route.params.id` shows as `string | undefined` everywhere | ||
| 19 | +- Union type of all route params instead of specific route | ||
| 20 | +- Type narrowing with `if (route.name === 'users-id')` doesn't work | ||
| 21 | + | ||
| 22 | +## Root Cause | ||
| 23 | + | ||
| 24 | +`unplugin-vue-router` generates a union type of all possible route params. TypeScript's control flow analysis can't narrow this union based on route name checks. | ||
| 25 | + | ||
| 26 | +## Fix | ||
| 27 | + | ||
| 28 | +**Option 1: Pass route name to useRoute (recommended)** | ||
| 29 | +```typescript | ||
| 30 | +// pages/users/[id].vue | ||
| 31 | +import { useRoute } from 'vue-router/auto' | ||
| 32 | + | ||
| 33 | +// Specify the route path for proper typing | ||
| 34 | +const route = useRoute('/users/[id]') | ||
| 35 | + | ||
| 36 | +// Now properly typed as { id: string } | ||
| 37 | +console.log(route.params.id) // string, not string | undefined | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +**Option 2: Type assertion with specific route** | ||
| 41 | +```typescript | ||
| 42 | +import { useRoute } from 'vue-router' | ||
| 43 | +import type { RouteLocationNormalized } from 'vue-router/auto-routes' | ||
| 44 | + | ||
| 45 | +const route = useRoute() as RouteLocationNormalized<'/users/[id]'> | ||
| 46 | +route.params.id // Properly typed | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +**Option 3: Define route-specific param type** | ||
| 50 | +```typescript | ||
| 51 | +// In your page component | ||
| 52 | +interface UserRouteParams { | ||
| 53 | + id: string | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +const route = useRoute() | ||
| 57 | +const { id } = route.params as UserRouteParams | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +## Required tsconfig Setting | ||
| 61 | + | ||
| 62 | +Ensure `moduleResolution: "bundler"` for unplugin-vue-router: | ||
| 63 | +```json | ||
| 64 | +{ | ||
| 65 | + "compilerOptions": { | ||
| 66 | + "moduleResolution": "bundler" | ||
| 67 | + } | ||
| 68 | +} | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +## Caveat: Route Name Format | ||
| 72 | + | ||
| 73 | +The route name matches the file path pattern: | ||
| 74 | +- `pages/users/[id].vue` → `/users/[id]` | ||
| 75 | +- `pages/posts/[slug]/comments.vue` → `/posts/[slug]/comments` | ||
| 76 | + | ||
| 77 | +## Reference | ||
| 78 | + | ||
| 79 | +- [unplugin-vue-router#337](https://github.com/posva/unplugin-vue-router/issues/337) | ||
| 80 | +- [unplugin-vue-router#176](https://github.com/posva/unplugin-vue-router/discussions/176) | ||
| 81 | +- [unplugin-vue-router TypeScript docs](https://uvr.esm.is/guide/typescript.html) |
| 1 | +--- | ||
| 2 | +title: Enable Strict Template Checking | ||
| 3 | +impact: HIGH | ||
| 4 | +impactDescription: catches undefined components and props at compile time | ||
| 5 | +type: capability | ||
| 6 | +tags: vue-tsc, typescript, type-checking, templates, vueCompilerOptions | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# Enable Strict Template Checking | ||
| 10 | + | ||
| 11 | +**Impact: HIGH** - catches undefined components and props at compile time | ||
| 12 | + | ||
| 13 | +By default, vue-tsc does not report errors for undefined components in templates. Enable `strictTemplates` to catch these issues during type checking. | ||
| 14 | + | ||
| 15 | +## Which tsconfig? | ||
| 16 | + | ||
| 17 | +Add `vueCompilerOptions` to the tsconfig that includes Vue source files. In projects with multiple tsconfigs (like those created with `create-vue`), this is typically `tsconfig.app.json`, not the root `tsconfig.json` or `tsconfig.node.json`. | ||
| 18 | + | ||
| 19 | +**Incorrect (missing strict checking):** | ||
| 20 | + | ||
| 21 | +```json | ||
| 22 | +{ | ||
| 23 | + "compilerOptions": { | ||
| 24 | + "strict": true | ||
| 25 | + } | ||
| 26 | + // vueCompilerOptions not configured - undefined components won't error | ||
| 27 | +} | ||
| 28 | +``` | ||
| 29 | + | ||
| 30 | +**Correct (strict template checking enabled):** | ||
| 31 | + | ||
| 32 | +```json | ||
| 33 | +{ | ||
| 34 | + "compilerOptions": { | ||
| 35 | + "strict": true | ||
| 36 | + }, | ||
| 37 | + "vueCompilerOptions": { | ||
| 38 | + "strictTemplates": true | ||
| 39 | + } | ||
| 40 | +} | ||
| 41 | +``` | ||
| 42 | + | ||
| 43 | +## Available Options | ||
| 44 | + | ||
| 45 | +| Option | Default | Effect | | ||
| 46 | +|--------|---------|--------| | ||
| 47 | +| `strictTemplates` | `false` | Enables all checkUnknown* options below | | ||
| 48 | +| `checkUnknownComponents` | `false` | Error on undefined/unregistered components | | ||
| 49 | +| `checkUnknownProps` | `false` | Error on props not declared in component definition | | ||
| 50 | +| `checkUnknownEvents` | `false` | Error on events not declared via `defineEmits` | | ||
| 51 | +| `checkUnknownDirectives` | `false` | Error on unregistered custom directives | | ||
| 52 | + | ||
| 53 | +## Granular Control | ||
| 54 | + | ||
| 55 | +If `strictTemplates` is too strict, enable individual checks: | ||
| 56 | + | ||
| 57 | +```json | ||
| 58 | +{ | ||
| 59 | + "vueCompilerOptions": { | ||
| 60 | + "checkUnknownComponents": true, | ||
| 61 | + "checkUnknownProps": false | ||
| 62 | + } | ||
| 63 | +} | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +## Reference | ||
| 67 | + | ||
| 68 | +- [Vue Compiler Options](https://github.com/vuejs/language-tools/wiki/Vue-Compiler-Options) | ||
| 69 | +- [Vite Vue+TS Template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-vue-ts) |
| 1 | +--- | ||
| 2 | +title: withDefaults Incorrect Default with Union Types | ||
| 3 | +impact: MEDIUM | ||
| 4 | +impactDescription: fixes incorrect default value behavior with union type props | ||
| 5 | +type: capability | ||
| 6 | +tags: withDefaults, defineProps, union-types, defaults, vue-3.5 | ||
| 7 | +--- | ||
| 8 | + | ||
| 9 | +# withDefaults Incorrect Default with Union Types | ||
| 10 | + | ||
| 11 | +**Impact: MEDIUM** - fixes spurious "Missing required prop" warning with union type props | ||
| 12 | + | ||
| 13 | +Using `withDefaults` with union types like `false | string` may produce a Vue runtime warning "Missing required prop" even when a default is provided. The runtime value IS applied correctly, but the warning can be confusing. | ||
| 14 | + | ||
| 15 | +## Symptoms | ||
| 16 | + | ||
| 17 | +- Vue warns "Missing required prop" despite default being set | ||
| 18 | +- Warning appears only with union types like `false | string` | ||
| 19 | +- TypeScript types are correct | ||
| 20 | +- Runtime value IS correct (the default is applied) | ||
| 21 | + | ||
| 22 | +## Problematic Pattern | ||
| 23 | + | ||
| 24 | +```typescript | ||
| 25 | +// This produces a spurious warning (but works at runtime) | ||
| 26 | +interface Props { | ||
| 27 | + value: false | string // Union type | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +const props = withDefaults(defineProps<Props>(), { | ||
| 31 | + value: 'default' // Runtime value IS correct, but Vue warns about missing prop | ||
| 32 | +}) | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +## Fix | ||
| 36 | + | ||
| 37 | +**Option 1: Use Reactive Props Destructure (Vue 3.5+)** | ||
| 38 | +```vue | ||
| 39 | +<script setup lang="ts"> | ||
| 40 | +interface Props { | ||
| 41 | + value: false | string | ||
| 42 | +} | ||
| 43 | + | ||
| 44 | +// Preferred in Vue 3.5+ | ||
| 45 | +const { value = 'default' } = defineProps<Props>() | ||
| 46 | +</script> | ||
| 47 | +``` | ||
| 48 | + | ||
| 49 | +**Option 2: Use runtime declaration** | ||
| 50 | +```vue | ||
| 51 | +<script setup lang="ts"> | ||
| 52 | +const props = defineProps({ | ||
| 53 | + value: { | ||
| 54 | + type: [Boolean, String] as PropType<false | string>, | ||
| 55 | + default: 'default' | ||
| 56 | + } | ||
| 57 | +}) | ||
| 58 | +</script> | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +**Option 3: Split into separate props** | ||
| 62 | +```typescript | ||
| 63 | +interface Props { | ||
| 64 | + enabled: boolean | ||
| 65 | + customValue?: string | ||
| 66 | +} | ||
| 67 | + | ||
| 68 | +const props = withDefaults(defineProps<Props>(), { | ||
| 69 | + enabled: false, | ||
| 70 | + customValue: 'default' | ||
| 71 | +}) | ||
| 72 | +``` | ||
| 73 | + | ||
| 74 | +## Why Reactive Props Destructure Works | ||
| 75 | + | ||
| 76 | +Vue 3.5's Reactive Props Destructure handles default values at the destructuring level, bypassing the type inference issues with `withDefaults`. | ||
| 77 | + | ||
| 78 | +```typescript | ||
| 79 | +// The default is applied during destructuring, not type inference | ||
| 80 | +const { prop = 'default' } = defineProps<{ prop?: string }>() | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +## Enable Reactive Props Destructure | ||
| 84 | + | ||
| 85 | +This is enabled by default in Vue 3.5+. For older versions: | ||
| 86 | +```javascript | ||
| 87 | +// vite.config.js | ||
| 88 | +export default { | ||
| 89 | + plugins: [ | ||
| 90 | + vue({ | ||
| 91 | + script: { | ||
| 92 | + propsDestructure: true | ||
| 93 | + } | ||
| 94 | + }) | ||
| 95 | + ] | ||
| 96 | +} | ||
| 97 | +``` | ||
| 98 | + | ||
| 99 | +## Reference | ||
| 100 | + | ||
| 101 | +- [vuejs/core#12897](https://github.com/vuejs/core/issues/12897) | ||
| 102 | +- [Reactive Props Destructure RFC](https://github.com/vuejs/rfcs/discussions/502) |
-
Please register or login to post a comment