From ff8b1c4b372b92d0d603955e2d21d08cd99678a9 Mon Sep 17 00:00:00 2001 From: Bujidao <3317431882@qq.com> Date: Fri, 12 Jun 2026 17:53:49 +0800 Subject: [PATCH] feat(rules): add Vue architecture patterns and security rules Add rules/vue/patterns.md: - Presentational vs Container component design - Provide/Inject, Scoped Slots, Teleport (with 3.5+ defer prop) - State management decision tree and Pinia Setup Store patterns - Vue Router navigation guards, lazy loading, reactive route params - v-for/v-if patterns, v-model (Vue 3.4+ defineModel) - Scoped CSS (:deep, :slotted), KeepAlive with max, Dynamic Components - Vue 3.5+ new APIs: useId(), data-allow-mismatch, Suspense - Nuxt-specific patterns and Vue 2 migration notes Add rules/vue/security.md: - v-html XSS audit (DOMPurify sanitization checklist) - Unsafe URL binding validation (javascript:/data: scheme prevention) - Custom directive innerHTML injection - Secret exposure via VITE_ prefix and Nuxt runtimeConfig - Nuxt Nitro server API input validation with zod - localStorage/sessionStorage token risks, SSR browser API guards - target=_blank rel=noopener, CSP minimum policy - Prototype pollution, source maps in production - Vue 3.5+ SSR hydration mismatch security notes --- rules/vue/patterns.md | 412 ++++++++++++++++++++++++++++++++++++++++++ rules/vue/security.md | 250 +++++++++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 rules/vue/patterns.md create mode 100644 rules/vue/security.md diff --git a/rules/vue/patterns.md b/rules/vue/patterns.md new file mode 100644 index 00000000..30698993 --- /dev/null +++ b/rules/vue/patterns.md @@ -0,0 +1,412 @@ +--- +paths: + - "**/*.vue" + - "**/components/**/*.ts" + - "**/composables/**/*.ts" + - "**/stores/**/*.ts" + - "**/pages/**/*.vue" +--- +# Vue Patterns + +> This file extends [typescript/patterns.md](../typescript/patterns.md) and [common/patterns.md](../common/patterns.md) with Vue-specific architecture patterns. For composable rules see [hooks.md](./hooks.md). + +## Component Design Principles + +### Presentational vs Container + +Split large views into container (data-fetching, state, orchestration) and presentational (props-in, events-out) components. + +```vue + + + + + + + +``` + +### Provide / Inject + +Use for dependency injection (not state management). Ideal for: theme, locale, configuration, plugin API surfaces. + +```ts +// Provider — in a parent or plugin +const theme = ref("light"); +provide("theme", readonly(theme)); + +// Consumer — in any descendant +const theme = inject>("theme"); +``` + +- Always use `readonly()` when providing to prevent child mutations. +- Use `Symbol` keys for injection to avoid name collisions. +- Document the injection key type with a shared constant. + +### Scoped Slots + +Use scoped slots when a child component owns data but the parent controls rendering. + +```vue + + + + + +``` + +## State Management + +### Decision Tree + +1. **Component-local**: `ref()` / `reactive()` inside the component +2. **Shared between parent + few children**: Lift to parent, pass via props + emits +3. **Shared across distant branches, infrequent updates**: `provide` / `inject` +4. **Global, shared, complex**: Pinia store +5. **Server-derived data**: Composables wrapping `fetch` / `useFetch` (Nuxt) / TanStack Query (Vue Query) + +### Pinia Patterns + +```ts +// stores/useUserStore.ts +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import { getUser, updateUser } from "@/api/user"; + +export const useUserStore = defineStore("user", () => { + // State + const currentUser = ref(null); + const isLoading = ref(false); + const error = ref(null); + + // Getters (computed) + const isLoggedIn = computed(() => currentUser.value !== null); + const displayName = computed(() => + currentUser.value ? currentUser.value.name : "Guest" + ); + + // Actions + async function fetchUser(id: string) { + isLoading.value = true; + error.value = null; + try { + currentUser.value = await getUser(id); + } catch (e) { + error.value = e as Error; + } finally { + isLoading.value = false; + } + } + + return { currentUser, isLoading, error, isLoggedIn, displayName, fetchUser }; +}); +``` + +- Prefer **Setup Store** syntax (Composition API) over Options Store. +- Store actions are the ONLY place to mutate state — no direct `store.$patch` in components for complex logic. +- Every async action must handle loading, success, and error states. +- Keep stores focused on one domain — split auth, user, cart, etc. into separate stores. + +## Vue Router Patterns + +### Navigation Guards + +```ts +// Global guard +router.beforeEach((to, from) => { + const store = useUserStore(); + if (to.meta.requiresAuth && !store.isLoggedIn) { + return { name: "login", query: { redirect: to.fullPath } }; + } +}); +``` + +- Always provide a redirect path so the user returns to their intended destination after login. +- Route guards should not have side effects beyond navigation decisions. +- Use `beforeEnter` on routes for route-specific checks; `beforeEach` for global ones. + +### Lazy Loading + +```ts +const routes = [ + { + path: "/dashboard", + component: () => import("@/pages/Dashboard.vue"), // lazy + }, + { + path: "/settings", + component: () => import("@/pages/Settings.vue"), + // Provide loading/error components + meta: { + __loadingComponent: LoadingSpinner, + __errorComponent: ErrorView, + }, + }, +]; +``` + +### Route Params inside Same Component + +```vue + +``` + +## List Rendering + +### `v-for` with Stable Keys + +```vue + +
+ {{ item.name }} +
+ + +
+ {{ item.name }} +
+ + +
+ +
+ + + + +``` + +## Forms + +### v-model Patterns + +```vue + + + +``` + +### Form Validation + +For non-trivial forms, use a vetted library: + +- **VeeValidate** — declarative validation rules, form-level context. +- **FormKit** — schema-based forms with built-in validation. +- **Custom with composable** — for simple cases only. + +```ts +// Anti-pattern: manual validation in component +const errors = ref([]); +function submit() { + errors.value = []; + if (!email.value.includes("@")) errors.value.push("Invalid email"); + // ... fragile, not reusable, no i18n +} +``` + +### Event Handling + +```vue + +
+ +
+ + + + + + +Link +``` + +## Scoped CSS + +```vue + +``` + +- Always use ` +``` + +## Teleport + +Use `` for modals, tooltips, notifications — content that must escape parent overflow/z-index constraints. + +```vue + +``` + +**Vue 3.5+**: `` supports `defer` prop for deferred mounting. This allows teleporting to a target element that is rendered later in the same render cycle: + +```vue + + +

Teleported content

+
+
+``` + +## KeepAlive + +Cache component state when toggling between views. Always set `:max` to control memory. + +```vue + +``` + +## `useId()` (Vue 3.5+) + +Generate unique, SSR-stable IDs for form elements and accessibility attributes: + +```vue + + +``` + +- IDs are unique per application instance and stable across server/client rendering. +- Prefer `useId()` over manual ID generation to avoid SSR hydration mismatches. + +## `data-allow-mismatch` (Vue 3.5+) + +Suppress unavoidable server/client value mismatch warnings: + +```vue +{{ date.toLocaleString() }} + +{{ clientOnlyValue }} +``` + +Allowed types: `text`, `children`, `class`, `style`, `attribute`. + +## Suspense (Experimental / Vue 3.3+) + +```vue + +``` + +## Dynamic Components + +```vue + + + +``` + +## Out of Scope (Pointer Sections) + +### Nuxt-specific Patterns + +Nuxt auto-imports, server routes, Nitro, modules, and build configuration are treated as a separate framework concern. When adding deep Nuxt-specific patterns, see `skills/nuxt4-patterns/` if present, or propose a dedicated `rules/nuxt/` track. + +### Vue 2 / Migration + +Options API, `Vue.extend`, `Vue.directive`, filters, and event bus patterns belong to migration documentation. New code should target Vue 3 Composition API. + +## Skill Reference + +For Vue deep dives see `skills/vue-patterns/SKILL.md`. For cross-framework frontend concerns see `skills/frontend-patterns/SKILL.md`. For accessibility see `skills/accessibility/SKILL.md`. diff --git a/rules/vue/security.md b/rules/vue/security.md new file mode 100644 index 00000000..699b4426 --- /dev/null +++ b/rules/vue/security.md @@ -0,0 +1,250 @@ +--- +paths: + - "**/*.vue" + - "**/components/**/*.ts" + - "**/composables/**/*.ts" + - "**/pages/**/*.vue" + - "**/server/**/*.ts" +--- +# Vue Security + +> This file extends [typescript/security.md](../typescript/security.md) and [common/security.md](../common/security.md) with Vue-specific security rules. + +## XSS via `v-html` + +CRITICAL. `v-html` sets `innerHTML` directly — Vue deliberately named it to look dangerous. + +```vue + +
+ + +
{{ userBio }}
+ + + + +``` + +Audit checklist for every `v-html` usage: + +- Is the input always under our control? Document the source. +- If user-derived: is it sanitized at the same call site? +- Is the sanitizer allowlisting tags, not denylisting? +- Consider `eslint-plugin-vue` rule `vue/no-v-html` to flag all usages. + +## Unsafe URL Bindings + +```vue + +Visit +