diff --git a/rules/README.md b/rules/README.md index e1a52496..e4b69f73 100644 --- a/rules/README.md +++ b/rules/README.md @@ -16,6 +16,8 @@ rules/ │ └── security.md ├── typescript/ # TypeScript/JavaScript specific ├── angular/ # Angular specific +├── vue/ # Vue 3 specific +├── nuxt/ # Nuxt 4 specific ├── python/ # Python specific ├── golang/ # Go specific ├── web/ # Web and frontend specific @@ -36,6 +38,8 @@ rules/ # Install common + one or more language-specific rule sets ./install.sh typescript ./install.sh angular +./install.sh vue +./install.sh nuxt ./install.sh python ./install.sh golang ./install.sh web @@ -70,6 +74,8 @@ cp -r rules/common ~/.claude/rules/ecc/ # Install language-specific rules based on your project's tech stack cp -r rules/typescript ~/.claude/rules/ecc/ cp -r rules/angular ~/.claude/rules/ecc/ +cp -r rules/vue ~/.claude/rules/ecc/ +cp -r rules/nuxt ~/.claude/rules/ecc/ cp -r rules/python ~/.claude/rules/ecc/ cp -r rules/golang ~/.claude/rules/ecc/ cp -r rules/web ~/.claude/rules/ecc/ diff --git a/rules/nuxt/coding-style.md b/rules/nuxt/coding-style.md new file mode 100644 index 00000000..b8a938a5 --- /dev/null +++ b/rules/nuxt/coding-style.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/nuxt.config.*" + - "**/app.config.*" + - "**/app.vue" + - "**/pages/**" + - "**/layouts/**" + - "**/middleware/**" +--- + +# Nuxt Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Nuxt specific content. + +## Directory layout + +- Default `srcDir` is `app/`. Framework files live at `app/pages/`, `app/layouts/`, `app/middleware/`, `app/plugins/`, `app/app.config.ts`. `nuxt.config.ts` and `server/` stay at project root. +- Some projects override `srcDir` to `src/` for a Feature-Sliced Design layout, remapping `dir.pages` (for example to `src/app/routes`), `dir.layouts`, and the `@`/`~` aliases. Always check `nuxt.config.ts` before assuming a path. + +## Auto-imports discipline + +- Composables in `app/composables/` and `server/utils/` auto-import. Do NOT manually import Nuxt composables (`useFetch`, `useState`, `navigateTo`) or `defineStore` / `storeToRefs`. +- Do NOT add a standalone `vue-router` dep (Nuxt bundles v5) or hand-mount `createApp` / `createPinia` / `createRouter`. The framework wires these. + +## Compiler macros + +- `definePageMeta` is a compile-time macro. Static values only, no reactive data and no side-effect calls inside it. +- Augment typed `PageMeta` via `declare module '#app'` rather than casting. + +## Config file separation + +Three distinct files, do not conflate. + +- `nuxt.config.ts` = build-time only (`routeRules`, `modules`, `nitro`, `ssr` flag). Not reactive. +- `runtimeConfig` (inside nuxt.config) = per-env runtime values, env-overridable via `NUXT_*`. Root keys are server-only, `public` keys are client-visible. +- `app/app.config.ts` = public build-fixed reactive settings (theme tokens, feature flags). No env override. NEVER secrets. + +## Head and meta + +- `app.head` in `nuxt.config.ts` takes static values only. +- Reactive meta goes through `useHead` / `useSeoMeta` in component setup, never via `app.head`. + +## Reference + +- ECC skills: `nuxt4-patterns`, `vite-patterns`, `frontend-patterns`. +- [Nuxt directory structure](https://nuxt.com/docs/guide/directory-structure/app) +- [Nuxt configuration](https://nuxt.com/docs/api/nuxt-config) diff --git a/rules/nuxt/hooks.md b/rules/nuxt/hooks.md new file mode 100644 index 00000000..a561c9da --- /dev/null +++ b/rules/nuxt/hooks.md @@ -0,0 +1,39 @@ +--- +paths: + - "**/nuxt.config.*" + - "**/app.config.*" + - "**/server/**/*.ts" + - "**/*.vue" +--- + +# Nuxt Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Nuxt specific content. + +These are Claude Code harness hooks for Nuxt work. They run via the harness, not Claude. + +## Typecheck + +- `nuxi typecheck` wraps `vue-tsc`. Requires `vue-tsc` + `typescript` dev deps. +- Run on `.vue` / `.ts` edit or pre-commit. Typecheck is project-wide, so debounce it and wrap it in a timeout (mirror `web/hooks.md`, for example `timeout 60 nuxi typecheck`) so a hung type-check is reaped instead of accumulating across fast edits. + +## Lint + +- Use the `@nuxt/eslint` module (flat-config, project-aware, generates `.nuxt/eslint.config.mjs`). +- Run `eslint .` or `eslint --fix`. This is the Nuxt-official ESLint integration, prefer it over hand-rolled configs. + +## Format + +- `prettier --write`, or enable stylistic rules in `@nuxt/eslint` to avoid a Prettier/ESLint conflict. +- Pick one formatting authority. Do not run both Prettier and ESLint stylistic at once. + +## Suggested PostToolUse chain + +- On Edit to `app/**` and `server/**`: run `eslint --fix` then `timeout 60 nuxi typecheck`. +- Order matters: lint-fix first (mutates the file), the timed typecheck second (verifies the result). Debouncing still applies. + +## Reference + +- ECC skills: `nuxt4-patterns`, `vite-patterns`. +- [@nuxt/eslint module](https://eslint.nuxt.com/) +- [nuxi typecheck](https://nuxt.com/docs/api/commands/typecheck) diff --git a/rules/nuxt/patterns.md b/rules/nuxt/patterns.md new file mode 100644 index 00000000..cb41be4b --- /dev/null +++ b/rules/nuxt/patterns.md @@ -0,0 +1,54 @@ +--- +paths: + - "**/nuxt.config.*" + - "**/app.config.*" + - "**/app.vue" + - "**/server/**/*.ts" + - "**/pages/**" + - "**/middleware/**" +--- + +# Nuxt Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Nuxt specific content. + +## Data-fetch selection + +Load-bearing. Pick by render timing, not habit. + +- `useFetch(url)` = SSR-safe, URL-first initial/first-paint data. The default. Forwards the server result through the payload so there is no hydration double-fetch. +- `useAsyncData(key, fn)` = SSR-safe, custom async logic (SDK / GraphQL / combined calls). The explicit key shares the result across components. +- `$fetch` = client interactions only (form submit, button click, POST/PUT/DELETE). NOT SSR-safe, double-fetches if used for first paint. +- Rule: `useFetch` / `useAsyncData` for anything rendered on first paint, `$fetch` only for event-driven mutations. + +## Shared state + +- `useState('key', () => init)` for SSR-safe shared state. Values must be JSON-serializable. +- NEVER `export const x = ref()` at module scope. One shared instance leaks across concurrent SSR requests and causes a memory leak. +- With `@pinia/nuxt`: Pinia for domain state, `useState` for small cross-component primitives. +- Async server-side init goes in `callOnce(async () => {...})`, not as a side effect inside `useAsyncData`. + +## Nitro server routes + +- `server/api/*.{get,post}.ts` auto-register by path + method. Handler is `defineEventHandler((event) => ...)`. +- Errors via `throw createError({ status, statusText })`. Prefer the Web-API `status` / `statusText` over deprecated `statusCode` / `statusMessage`. +- `server/middleware/` must NOT return a response. Only mutate `event.context` or set headers. + +## Route middleware + +- `app/middleware/*.ts` with `defineNuxtRouteMiddleware((to, from) => ...)`. +- Use the `to` / `from` args. Do NOT call `useRoute()` inside middleware. +- `.global` suffix runs on every route. Return `navigateTo()` to redirect, `abortNavigation()` to stop. + +## Hydration-safe rendering + +- Route off `status` (`idle | pending | success | error`) for lazy fetches. +- `useAsyncData` payload uses `devalue` (Date/Map/Set/refs survive). A `server/api` response is `JSON.stringify`-only, so define `toJSON()` for non-JSON types. +- Shrink payload with `pick` / `transform`. This reduces serialized size, it does not skip the fetch. + +## Reference + +- ECC skills: `nuxt4-patterns`, `vite-patterns`, `frontend-patterns`. +- [Nuxt data fetching](https://nuxt.com/docs/getting-started/data-fetching) +- [Nuxt state management](https://nuxt.com/docs/getting-started/state-management) +- [Nuxt server engine (Nitro)](https://nuxt.com/docs/guide/directory-structure/server) diff --git a/rules/nuxt/security.md b/rules/nuxt/security.md new file mode 100644 index 00000000..37f45bba --- /dev/null +++ b/rules/nuxt/security.md @@ -0,0 +1,48 @@ +--- +paths: + - "**/nuxt.config.*" + - "**/app.config.*" + - "**/server/**/*.ts" +--- + +# Nuxt Security + +> This file extends [common/security.md](../common/security.md) with Nuxt specific content. + +## runtimeConfig public vs private + +- Root `runtimeConfig` keys are server-only. `runtimeConfig.public` serializes into EVERY page payload (client-visible). +- Secrets go at root only. Never put secrets in `app.config.ts` or `runtimeConfig.public`, both ship to the client bundle. +- Official warning: "Be careful not to expose runtime config keys to the client-side by either rendering them or passing them to `useState`." + +## Server-route input validation + +- Use h3 validating readers. Do NOT trust raw `readBody` / `getQuery` / `getRouterParam`. + - `readValidatedBody(event, schema)` validates the body. + - `getValidatedQuery(event, schema)` validates the query. + - `getValidatedRouterParams(event, schema)` validates route params. +- All accept a validation function or a Zod schema and throw on failure. + +## SSR payload leakage + +- Anything in `useState`, `useFetch` / `useAsyncData` results, or `runtimeConfig.public` is serialized into the client payload. Never write a secret into those. +- Use `useServerSeoMeta` for server-only meta with no client cost. + +## Cookie and auth passthrough on SSR + +- Nuxt does NOT auto-attach the incoming user's cookies to outbound server-side `$fetch`. +- Forward explicitly with `useRequestFetch()` (cleanest, pre-bound to request headers) or `useRequestHeaders(['cookie'])`. +- Relay a backend `Set-Cookie` to the browser via `$fetch.raw` + `appendResponseHeader(event, 'set-cookie', ...)`. +- socket.io is client-only (`.client.ts` plugin), never SSR. + +## SSRF on server $fetch + +- Server routes run with full network egress. Never pass user-controlled input directly into a server-side `$fetch` URL or host. +- Validate the param first (h3 utilities above), allowlist the target, pin to `runtimeConfig.public.apiBase`, reject user-supplied absolute URLs. +- Auto-trigger `/security-review` only for routes that make external network requests (server `$fetch`), handle auth tokens or credentials, or perform sensitive mutations or authorization checks. Examples: SSRF-prone proxy endpoints, token exchange or password reset, admin actions. Skip benign read-only routes that only accept validated query params. + +## Reference + +- ECC skills: `security-review`, `nuxt4-patterns`. +- [Nuxt runtime config](https://nuxt.com/docs/guide/going-further/runtime-config) +- [h3 request utils](https://v1.h3.dev/utils/request) diff --git a/rules/nuxt/testing.md b/rules/nuxt/testing.md new file mode 100644 index 00000000..ac3744ff --- /dev/null +++ b/rules/nuxt/testing.md @@ -0,0 +1,49 @@ +--- +paths: + - "**/nuxt.config.*" + - "**/server/**/*.ts" + - "**/pages/**" + - "**/layouts/**" + - "**/middleware/**" +--- + +# Nuxt Testing + +> This file extends [common/testing.md](../common/testing.md) with Nuxt specific content. + +Package: `@nuxt/test-utils`. Vitest-first for unit and component tests, with built-in Playwright browser E2E support. nuxt-vitest and vitest-environment-nuxt are superseded and folded into it. + +## Setup + +- Install dev deps: `@nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core`. +- Config: `defineVitestConfig({ test: { environment: 'nuxt' } })` from `@nuxt/test-utils/config`. Use `defineVitestProject` for multi-project (separate unit / nuxt / e2e environments). +- Add `@nuxt/test-utils/module` to `nuxt.config`. Per-file opt-in via `// @vitest-environment nuxt`. + +## Runtime helpers + +Import from `@nuxt/test-utils/runtime`. + +- `mountSuspended(component, opts)` mounts in the Nuxt env with async setup + plugin injection (accepts `@vue/test-utils` mount options + `route`). +- `renderSuspended(component, opts)` is the Testing Library variant (needs `@testing-library/vue`). +- `mockNuxtImport(name, factory)` mocks auto-imports (e.g. `useState`). Once per import per file, use `vi.hoisted()`. +- `mockComponent(name, factory)` mocks by PascalCase name or path. +- `registerEndpoint(path, handler|opts)` mocks a Nitro endpoint to test server routes or stub the backend. Supports method + `once`. + +## E2E helpers + +Import from `@nuxt/test-utils/e2e`. + +- `await setup({ rootDir, server, browser, ... })` inside the describe block (manages beforeAll/afterAll). +- Then `$fetch(url)` (rendered HTML), `fetch(url)` (response object), `url(path)` (full URL with port), `createPage(url)` (Playwright). +- Playwright integration: import `expect` / `test` from `@nuxt/test-utils/playwright`. + +## What to test how + +- Composables: mock auto-imports with `mockNuxtImport`, mount a host component via `mountSuspended` to exercise `useState` / `useFetch` in the Nuxt runtime. +- Server routes: `registerEndpoint` to stub, or e2e `$fetch` / `fetch` against the real Nitro server. + +## Reference + +- ECC skills: `nuxt4-patterns`, `e2e-testing`, `vite-patterns`. +- [Nuxt testing docs](https://nuxt.com/docs/getting-started/testing) +- [@nuxt/test-utils npm](https://www.npmjs.com/package/@nuxt/test-utils) diff --git a/rules/vue/coding-style.md b/rules/vue/coding-style.md new file mode 100644 index 00000000..f6c0e31b --- /dev/null +++ b/rules/vue/coding-style.md @@ -0,0 +1,54 @@ +--- +paths: + - "**/*.vue" +--- + +# Vue Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Vue specific content. + +## SFC Structure + +- Always ` +``` + +## Reference + +- ECC skills: `frontend-patterns`, `vite-patterns`. +- Docs: https://vuejs.org/api/sfc-script-setup.html · https://vuejs.org/guide/essentials/reactivity-fundamentals.html · https://eslint.vuejs.org/ diff --git a/rules/vue/hooks.md b/rules/vue/hooks.md new file mode 100644 index 00000000..b0c8d916 --- /dev/null +++ b/rules/vue/hooks.md @@ -0,0 +1,45 @@ +--- +paths: + - "**/*.vue" + - "**/*.ts" + - "**/*.tsx" +--- + +# Vue Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Vue specific content. + +## PostToolUse Targets + +Run on `*.vue`, `*.ts`, and `*.tsx` after edits. Scope to changed files where possible. + +## Typecheck + +- Use `vue-tsc --noEmit` for SFC plus TypeScript checking. Plain `tsc` cannot read `.vue` single-file components, so it must not be the typecheck hook for this project. +- Typecheck is project-wide. Debounce or scope it so a save-on-every-keystroke loop does not stall the editor. + +## Lint and Format + +- `eslint --fix` with `eslint-plugin-vue` (flat-config `vue/vue3-recommended`) covers both template and script lint. +- `prettier --write` for formatting. Prefer Prettier-via-ESLint over a separate Prettier pass to avoid double formatting and fight loops. + +## Architecture Boundaries + +- Optional: enforce Feature-Sliced Design slice boundaries with `@feature-sliced/steiger` or `eslint-plugin-boundaries` to block deep cross-slice imports. + +## Sequencing + +```bash +# changed files only +eslint --fix "$FILE" +prettier --write "$FILE" +# project-wide, debounced +vue-tsc --noEmit +``` + +- Run lint and format per-file first, then the project-wide typecheck last so type errors reflect the formatted source. + +## Reference + +- ECC skills: `frontend-patterns`, `vite-patterns`. +- Docs: https://github.com/vuejs/language-tools (vue-tsc) · https://eslint.vuejs.org/ · https://github.com/feature-sliced/steiger diff --git a/rules/vue/patterns.md b/rules/vue/patterns.md new file mode 100644 index 00000000..35059f0f --- /dev/null +++ b/rules/vue/patterns.md @@ -0,0 +1,56 @@ +--- +paths: + - "**/*.vue" +--- + +# Vue Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Vue specific content. + +## Composables + +- The composable (`useXxx`) is the reusable-logic unit. In Feature-Sliced Design it lives in the slice `model` segment. +- Accept `MaybeRefOrGetter` inputs and normalize with `toValue`, so callers can pass a ref, a getter, or a raw value. +- Return `toRefs(reactive(...))` so consumers can destructure without losing reactivity. +- A composable that uses lifecycle hooks or `provide` / `inject` must be called inside a component `setup`, not lazily or conditionally. + +## Props, Emits, v-model + +- Type-based `defineProps()` and tuple-form `defineEmits<{ change: [id: number] }>()`. +- `defineModel('name', { default })` for two-way binding. It compiles to a prop plus an `update:*` emit. + +## Provide / Inject + +- Use `provide` / `inject` for tree-scoped data without prop drilling. +- Type-safe collision-free keys: `const key = Symbol() as InjectionKey`. +- The provider owns mutations. Expose a `readonly` ref plus an explicit updater function, never a raw mutable ref. + +## Pinia (FSD model segment) + +- Prefer setup stores: `ref` is state, `computed` is getters, `function` is actions. +- Setup stores do not get `$reset` for free. Define your own. +- Use `storeToRefs` for state and getters. Destructure actions directly off the store. +- Never persist raw auth tokens to `localStorage`. + +## vue-router + +- Lazy-load route components with dynamic `import()`. +- A global `beforeEach` auth gate keyed on `meta.requiresAuth`. Guards return `false` (cancel), a route location (redirect), or `undefined` / `true` (continue). +- Watch `() => route.params.id`, not the whole `route` object. + +## vue-query (server cache) + +- `@tanstack/vue-query` owns server-cache state. Pinia owns client state. +- Put request functions plus `queryOptions` factories in the FSD `api` segment. +- Critical: put the ref or computed ITSELF in the query key, never `.value`. Passing `.value` freezes the key and kills reactive refetch. + +```ts +useQuery({ queryKey: ['auction', id], queryFn: () => fetchAuction(toValue(id)) }) +// after a mutation +queryClient.invalidateQueries({ queryKey: ['auction', id] }) +``` + +## Reference + +- ECC skills: `frontend-patterns`, `vite-patterns`. +- Docs: https://pinia.vuejs.org/ · https://router.vuejs.org/ · https://tanstack.com/query/latest/docs/framework/vue/overview · https://vuejs.org/guide/reusability/composables.html diff --git a/rules/vue/security.md b/rules/vue/security.md new file mode 100644 index 00000000..6a04d333 --- /dev/null +++ b/rules/vue/security.md @@ -0,0 +1,46 @@ +--- +paths: + - "**/*.vue" +--- + +# Vue Security + +> This file extends [common/security.md](../common/security.md) with Vue specific content. + +## What Vue Escapes Automatically + +- Text interpolation `{{ }}` and dynamic attribute bindings (`:title`) are auto-escaped. The vectors below are NOT protected. + +## Rule No.1: Templates from Trusted Sources Only + +- Never use non-trusted content as a component template. No runtime template compilation from user input. +- No user-controlled `:is` that resolves a component from an arbitrary string. + +## v-html and Render Functions + +- `v-html` bypasses escaping and is a direct XSS vector. Avoid it on user content. +- If unavoidable, sanitize with DOMPurify (allowlist config) before binding, or render in a sandboxed iframe. Vue itself recommends sanitizing on the backend before persisting. +- Render-function and scoped-slot output carry the same risk. Passing user HTML through `h()` with `innerHTML` is `v-html` by another name. Sanitize first. + +## URL, Style, and Event Injection + +- `:href` and `:src` are not escaped. `javascript:` URLs execute. Validate the scheme, allow `http` / `https` / `mailto` only. Vue docs reference `@braintree/sanitize-url`, but sanitize on the backend before persisting. +- `:style` with user input is unsafe (CSS exfiltration). Use object syntax with whitelisted properties, never a raw user string. +- Never bind user input to `onclick`, `onfocus`, or any event attribute. + +## Client Bundle Secrets + +- Anything in `import.meta.env.VITE_*` ships to the browser. Keep API keys and tokens server-side. +- Use httpOnly cookies for session tokens. Never bundle credentials into the client. + +```vue + +
+ +
+``` + +## Reference + +- ECC skills: `frontend-patterns`, `vite-patterns`. +- Docs: https://vuejs.org/guide/best-practices/security.html · https://github.com/cure53/DOMPurify · https://github.com/braintree/sanitize-url diff --git a/rules/vue/testing.md b/rules/vue/testing.md new file mode 100644 index 00000000..7dbf9f07 --- /dev/null +++ b/rules/vue/testing.md @@ -0,0 +1,53 @@ +--- +paths: + - "**/*.vue" +--- + +# Vue Testing + +> This file extends [common/testing.md](../common/testing.md) with Vue specific content. + +## Stack + +- Vitest (Vite-native runner) plus `@vue/test-utils`. `create-vue` scaffolds `@vitejs/plugin-vue`. +- DOM environment via `happy-dom` or `jsdom`, set in `vite.config.ts` under `test.environment`. + +## Rendering and Async + +- `mount` for a full render. `shallowMount` to stub all child components. +- `trigger` and `setValue` return promises, `await` them. +- `flushPromises` flushes resolved promise handlers. `nextTick` settles the DOM after a state change. + +## What to Test + +- Test the public interface only: props, emitted events, slots, rendered output. +- Do not assert private state or internal methods, and do not rely solely on snapshots. + +## Composables + +- Composables that use only reactivity APIs unit-test directly: call the function, assert on the returned refs. +- Composables that use lifecycle hooks or `inject` must be tested through a host component. + +## Pinia + +- In components: `createTestingPinia()` from `@pinia/testing`, passed via `global.plugins`. Actions are stubbed by default, set `stubActions: false` to run them. `createSpy: vi.fn` is required under Vitest (no Jest globals). +- In isolation: `beforeEach(() => setActivePinia(createPinia()))` gives a fresh store per test and prevents state leakage. + +## Mount Config + +- `global.plugins`, `global.stubs` (stubs `Transition` / `TransitionGroup` by default), `global.mocks` (e.g. `$router`), `global.provide` (for `inject`, Symbol keys supported). +- `RouterLinkStub` stubs `router-link` without mounting a full router. + +```ts +const wrapper = mount(AuctionCard, { + props: { id: 1 }, + global: { plugins: [createTestingPinia({ createSpy: vi.fn })] }, +}) +await wrapper.find('button').trigger('click') +expect(wrapper.emitted('bid')).toBeTruthy() +``` + +## Reference + +- ECC skills: `frontend-patterns`, `vite-patterns`. +- Docs: https://test-utils.vuejs.org/api/ · https://pinia.vuejs.org/cookbook/testing.html · https://vitest.dev/