@3lineas/grooveskills 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/{chunk-SP3ESDM5.js → chunk-2VONHHPV.js} +80 -17
- package/dist/chunk-2VONHHPV.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/skills/controller-orchestration-queues/SKILL.md +62 -63
- package/skills/controller-orchestration-queues/references/controller-queue-examples.md +66 -42
- package/skills/error-handling-logging/SKILL.md +64 -68
- package/skills/error-handling-logging/templates/app-error.template.md +10 -1
- package/skills/hono/SKILL.md +73 -66
- package/skills/hono/templates/router.template.md +18 -26
- package/skills/react-core/SKILL.md +45 -112
- package/skills/testing-vitest/SKILL.md +68 -61
- package/skills/testing-vitest/references/backend-d1-testing-pyramid.md +69 -0
- package/skills/typescript-clean/SKILL.md +40 -20
- package/skills/valibot-validation/SKILL.md +63 -20
- package/skills/workers-rpc-precision/SKILL.md +129 -0
- package/skills/workers-rpc-precision/references/python-worker-rpc-examples.md +98 -0
- package/dist/chunk-SP3ESDM5.js.map +0 -1
package/skills/hono/SKILL.md
CHANGED
|
@@ -3,96 +3,103 @@ name: hono
|
|
|
3
3
|
description: Use when building or modifying Hono APIs, endpoints, middlewares, or actions. TRIGGER when the project imports 'hono', when modifying routes in src/modules/ or actions in src/actions/, or when the user mentions Hono, API endpoints, or routing. Always validate with Valibot (* as v from 'valibot') and adhere to the 'best-practices' skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Hono Architecture &
|
|
6
|
+
# Hono Architecture & Thin Route Standards
|
|
7
7
|
|
|
8
|
-
Standardized guide for building robust, scalable
|
|
8
|
+
Standardized guide for building robust, scalable Hono applications with isolated domain layers, strict Valibot validation, typed Actions, and thin route wrappers.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## 1. Golden Rules (Non-negotiable)
|
|
13
13
|
|
|
14
14
|
1. **Standard Schema Validator with Valibot**:
|
|
15
|
-
- Always use **Valibot** (`* as v from "valibot"`) with `@hono/
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
15
|
+
- Always use **Valibot** (`* as v from "valibot"`) with `@hono/valibot-validator` (`vValidator`) or `@hono/standard-validator` (`sValidator`).
|
|
16
|
+
- Use `validationHook` to intercept schema issues and emit a standardized `400 Bad Request` with `ActionResponse<never>`.
|
|
17
|
+
|
|
18
|
+
2. **Strict Route Wrapper (`createRouteHandler`)**:
|
|
19
|
+
- Endpoints must be thin controllers. Wrap routes with `createRouteHandler({ outputSchema, status }, handler)`.
|
|
20
|
+
- Handler automatically receives `{ db, logger, env, input, ctx }`, handles try/catch normalization to `AppError`, and returns serialized `ActionResponse<T>`.
|
|
21
|
+
|
|
22
|
+
3. **Strict Identifier Naming**:
|
|
23
|
+
- The Hono context parameter is **ALWAYS** named `ctx` (NEVER `c`).
|
|
24
|
+
- Catch errors are **ALWAYS** named `error` or `caughtError` (NEVER `e` or `err`).
|
|
25
|
+
|
|
22
26
|
4. **Independent Route per File by Domain**:
|
|
23
|
-
- Every single endpoint resides in its own file (`src/modules/{domain}/{action}.ts`).
|
|
24
|
-
- Group them under the domain router `src/modules/{domain}/{domain}.router.ts
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
9. **Unified Error Handling**:
|
|
36
|
-
- Throw typed `AppError` instances for business failures.
|
|
37
|
-
- Wrap I/O operations in `try / catch`, log with structured `Logger`, and re-throw or propagate to `globalErrorHandler`.
|
|
38
|
-
10. **Courteous and Professional User Feedback**:
|
|
39
|
-
- Return polite, engaging, and solution-oriented messages in English (`ActionResponse<T>`).
|
|
27
|
+
- Every single endpoint resides in its own file (`src/routes/modules/{domain}/{action}.route.ts`).
|
|
28
|
+
- Group them under the domain router `src/routes/modules/{domain}/{domain}.router.ts` and mount in root router.
|
|
29
|
+
|
|
30
|
+
5. **Business Logic Exclusively in Actions / Pipelines**:
|
|
31
|
+
- All business logic lives in `src/actions/{domain}/{action}.action.ts`.
|
|
32
|
+
- Never execute SQL, D1, Drizzle, or Prisma queries directly inside route handlers.
|
|
33
|
+
|
|
34
|
+
6. **Hard Size Limit: <200 Lines Max**:
|
|
35
|
+
- Keep route endpoints under 60 lines and routers under 50 lines.
|
|
36
|
+
|
|
37
|
+
7. **Unified Client Response (`ActionResponse<T>`)**:
|
|
38
|
+
- Every API endpoint response (success or failure) conforms to `ActionResponse<T>` (`success`, `message`, `data`, `error`).
|
|
40
39
|
|
|
41
40
|
---
|
|
42
41
|
|
|
43
42
|
## 2. Directory Taxonomy
|
|
44
43
|
|
|
45
44
|
```text
|
|
46
|
-
|
|
47
|
-
├──
|
|
48
|
-
│
|
|
49
|
-
│
|
|
50
|
-
│
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
├── schemas/ # Valibot schemas
|
|
56
|
-
│ └── auth/
|
|
57
|
-
│ └── register-user.schema.ts # Strict input schema (< 50 lines)
|
|
58
|
-
├── errors/
|
|
59
|
-
│ ├── app-error.ts # Base application error with auto-logging
|
|
60
|
-
│ └── error-handler.ts # Central Hono error handler
|
|
61
|
-
└── routers/
|
|
62
|
-
└── api.router.ts # Root router exporting ApiRouter type for RPC
|
|
45
|
+
src/
|
|
46
|
+
├── routes/
|
|
47
|
+
│ ├── index.ts # Main apiRouter mounting domain routers
|
|
48
|
+
│ └── modules/{domain}/
|
|
49
|
+
│ ├── {action}.route.ts # Atomic endpoint (<60 lines)
|
|
50
|
+
│ └── {domain}.router.ts # Domain assembly router (<50 lines)
|
|
51
|
+
├── dtos/{domain}/ # Valibot schemas (<50 lines)
|
|
52
|
+
├── actions/{domain}/ # Atomic business logic (<100 lines)
|
|
53
|
+
└── types/ # Isolated domain contracts (<50 lines)
|
|
63
54
|
```
|
|
64
55
|
|
|
65
56
|
---
|
|
66
57
|
|
|
67
|
-
## 3.
|
|
58
|
+
## 3. Thin Route Pattern Example
|
|
68
59
|
|
|
69
60
|
```ts
|
|
70
|
-
import {
|
|
71
|
-
import
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
import { Hono } from "hono";
|
|
62
|
+
import { vValidator } from "@hono/valibot-validator";
|
|
63
|
+
import type { AppContext } from "@/types/context/app-context.type";
|
|
64
|
+
import type { CreateUserInput } from "@/types/users/create-user-input.type";
|
|
65
|
+
import { CreateUserDto } from "@/dtos/users/create-user.dto";
|
|
66
|
+
import { UserResponseDto } from "@/dtos/users/user-response.dto";
|
|
67
|
+
import { registerUserAction } from "@/actions/users/register-user.action";
|
|
68
|
+
import { createRouteHandler } from "@/common/utils/route-handler";
|
|
69
|
+
import { validationHook } from "@/common/utils/validation";
|
|
70
|
+
|
|
71
|
+
export const createUserRoute = new Hono<AppContext>().post(
|
|
72
|
+
"/",
|
|
73
|
+
vValidator("json", CreateUserDto, validationHook),
|
|
74
|
+
createRouteHandler<unknown, typeof UserResponseDto, CreateUserInput>(
|
|
75
|
+
{ outputSchema: UserResponseDto, status: 201 },
|
|
76
|
+
async ({ db, input }) => {
|
|
77
|
+
return await registerUserAction(db, input!);
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
);
|
|
85
81
|
```
|
|
86
82
|
|
|
87
83
|
---
|
|
88
84
|
|
|
89
|
-
## 4.
|
|
85
|
+
## 4. Domain Router Assembly
|
|
90
86
|
|
|
91
87
|
```ts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
import { Hono } from "hono";
|
|
89
|
+
import type { AppContext } from "@/types/context/app-context.type";
|
|
90
|
+
import { createUserRoute } from "./create-user.route";
|
|
91
|
+
import { listUsersRoute } from "./list-users.route";
|
|
92
|
+
|
|
93
|
+
export const usersRouter = new Hono<AppContext>()
|
|
94
|
+
.route("/", createUserRoute)
|
|
95
|
+
.route("/", listUsersRoute);
|
|
98
96
|
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 5. Templates
|
|
101
|
+
|
|
102
|
+
- `./templates/router.template.md`: Atomic route template with `createRouteHandler`.
|
|
103
|
+
- `./templates/action.template.md`: Single responsibility Action template.
|
|
104
|
+
- `./templates/schema.template.md`: Valibot schema template with UX microcopy.
|
|
105
|
+
- `./templates/error-handler.template.md`: Centralized error handler template.
|
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
# Hono Route Template
|
|
2
2
|
|
|
3
|
-
Slim HTTP controller that validates request body with Valibot and delegates
|
|
3
|
+
Slim HTTP controller that validates request body with Valibot via `validationHook`, uses `createRouteHandler`, and delegates to the Action.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { Hono } from "hono";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
7
|
+
import { vValidator } from "@hono/valibot-validator";
|
|
8
|
+
import type { AppContext } from "@/types/context/app-context.type";
|
|
9
|
+
import type { RegisterUserInput } from "@/types/auth/register-user-input.type";
|
|
10
|
+
import { RegisterUserDto } from "@/dtos/auth/register-user.dto";
|
|
11
|
+
import { UserResponseDto } from "@/dtos/auth/user-response.dto";
|
|
12
|
+
import { registerUserAction } from "@/actions/auth/register-user.action";
|
|
13
|
+
import { createRouteHandler } from "@/common/utils/route-handler";
|
|
14
|
+
import { validationHook } from "@/common/utils/validation";
|
|
11
15
|
|
|
12
|
-
export
|
|
13
|
-
db: DatabaseConnection;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const registerUserRoute = new Hono<{ Variables: CustomVariables }>().post(
|
|
16
|
+
export const registerUserRoute = new Hono<AppContext>().post(
|
|
17
17
|
"/",
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
success: true,
|
|
28
|
-
message: "Welcome aboard! Your account has been created successfully.",
|
|
29
|
-
data: createdUser,
|
|
30
|
-
},
|
|
31
|
-
201
|
|
32
|
-
);
|
|
33
|
-
}
|
|
18
|
+
vValidator("json", RegisterUserDto, validationHook),
|
|
19
|
+
createRouteHandler<unknown, typeof UserResponseDto, RegisterUserInput>(
|
|
20
|
+
{ outputSchema: UserResponseDto, status: 201 },
|
|
21
|
+
async ({ db, input }) => {
|
|
22
|
+
const createdUser = await registerUserAction(db, input!);
|
|
23
|
+
return createdUser;
|
|
24
|
+
}
|
|
25
|
+
)
|
|
34
26
|
);
|
|
35
27
|
```
|
|
@@ -11,158 +11,91 @@ Canonical reference for React fundamentals in GrooveSkills projects: function co
|
|
|
11
11
|
|
|
12
12
|
## 1. Golden Rules (Non-negotiable)
|
|
13
13
|
|
|
14
|
-
1. **
|
|
14
|
+
1. **Strictly Prohibited: `React.FC` and `React.FunctionComponent`**:
|
|
15
|
+
- In modern React (React 18 / 19), **NEVER** type components with `React.FC` or `React.FunctionComponent`.
|
|
16
|
+
- Declare standard functions and type `props` directly in the parameter list.
|
|
17
|
+
- For components requiring children, type `children: ReactNode` explicitly in the props interface.
|
|
18
|
+
|
|
19
|
+
2. **One Component per File & < 200 Lines**:
|
|
15
20
|
- Every exported React component resides in its own dedicated `.tsx` file and never exceeds **200 lines**.
|
|
16
21
|
- Extract sub-components or custom hooks (`use-*.ts`) into atomic files before reaching the limit.
|
|
17
|
-
|
|
22
|
+
|
|
23
|
+
3. **Domain Custom Hooks for Form Encapsulation**:
|
|
24
|
+
- Form components must encapsulate their own state using domain hooks (e.g. `useLoginForm`, `useRegisterForm`) instead of bubbling raw `form` and `onSubmit` props up to higher-level screen containers.
|
|
25
|
+
|
|
26
|
+
4. **Mandatory Path Aliases (`@/...`) & Zero Barrel Files**:
|
|
18
27
|
- Import components, hooks, schemas, and utilities directly via path aliases (`@/components/ui/card`, `@/hooks/use-window-width`).
|
|
19
28
|
- Never create `index.ts` or `index.tsx` barrel files to re-export components or hooks.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- Treat `props` and `state` as strictly immutable.
|
|
23
|
-
4. **Early Returns for UI States (No Nested Ternaries)**:
|
|
29
|
+
|
|
30
|
+
5. **Early Returns for UI States (No Nested Ternaries)**:
|
|
24
31
|
- Handle loading, error, and empty states with top-level guard clauses (`if (!data) return <EmptyState />;`) instead of deeply nested JSX conditionals.
|
|
25
|
-
|
|
26
|
-
- Call hooks unconditionally at the top level of function components or custom hooks—never inside loops, conditions, or nested functions (except React 19's `use()`, covered in `react-advanced`).
|
|
32
|
+
|
|
27
33
|
6. **Immutable State Updates & Functional Updaters**:
|
|
28
34
|
- Never mutate arrays or objects in place (`items.push()`). Always pass a new reference (`[...items, next]`).
|
|
29
35
|
- Use functional updaters (`setCount((prev) => prev + step)`) whenever next state depends on previous state.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- Reserve manual `useMemo` and `useCallback` for stabilizing references passed to `useEffect` dependency arrays or for genuinely expensive computations in uncompiled modules.
|
|
33
|
-
8. **Stable Identity Keys in Lists**:
|
|
36
|
+
|
|
37
|
+
7. **Stable Identity Keys in Lists**:
|
|
34
38
|
- Always provide a stable, unique `key` derived from domain identity (`item.id`), never the array index (`i`) for dynamic or reorderable lists.
|
|
35
|
-
9. **Controlled Inputs with Single Source of Truth**:
|
|
36
|
-
- Derive input `value` from state and handle updates via `onChange`, validating user input with **Valibot** (`* as v from "valibot"`) where applicable.
|
|
37
39
|
|
|
38
40
|
---
|
|
39
41
|
|
|
40
|
-
## 2.
|
|
41
|
-
|
|
42
|
-
Name components in `PascalCase`. Return a single root element or Fragment (`<>...</>`). Use guard clauses to keep the primary render path flat.
|
|
42
|
+
## 2. Component Typing Standards
|
|
43
43
|
|
|
44
44
|
```tsx
|
|
45
|
-
import {
|
|
45
|
+
import type { ReactNode } from "react";
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// ✅ Component with props
|
|
48
|
+
export interface UserAvatarProps {
|
|
48
49
|
name: string;
|
|
49
|
-
|
|
50
|
-
isActive: boolean;
|
|
51
|
-
step?: number;
|
|
50
|
+
avatarUrl?: string;
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
export function
|
|
55
|
-
const [count, setCount] = useState<number>(0);
|
|
56
|
-
|
|
57
|
-
if (!isActive) {
|
|
58
|
-
return <p className="text-muted">This account is currently inactive.</p>;
|
|
59
|
-
}
|
|
60
|
-
|
|
53
|
+
export function UserAvatar({ name, avatarUrl }: UserAvatarProps) {
|
|
61
54
|
return (
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</button>
|
|
67
|
-
</section>
|
|
55
|
+
<div className="flex items-center gap-2">
|
|
56
|
+
{avatarUrl ? <img src={avatarUrl} alt={name} /> : null}
|
|
57
|
+
<span>{name}</span>
|
|
58
|
+
</div>
|
|
68
59
|
);
|
|
69
60
|
}
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## 3. Core Hooks Guide
|
|
75
|
-
|
|
76
|
-
- **`useState`**: Holds reactive local state (`const [isOpen, setIsOpen] = useState<boolean>(false);`).
|
|
77
|
-
- **`useEffect`**: Synchronizes with external systems (DOM listeners, timers, subscriptions). Always declare accurate dependencies and return a cleanup function.
|
|
78
|
-
- **`useRef`**: Holds a mutable value across renders without causing re-renders, or references a DOM node.
|
|
79
|
-
- **`useMemo` & `useCallback`**: Deliberate escape hatches to stabilize references for `useEffect` dependency arrays or heavy computations in uncompiled components.
|
|
80
|
-
- **`useContext`**: Reads shared state from a `Context` higher in the tree (`<ThemeContext value={theme}>` in React 19).
|
|
81
|
-
|
|
82
|
-
```tsx
|
|
83
|
-
import { useEffect, useRef, useState } from "react";
|
|
84
|
-
|
|
85
|
-
export function AutoFocusWindowTracker() {
|
|
86
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
87
|
-
const [width, setWidth] = useState<number>(() => window.innerWidth);
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
inputRef.current?.focus();
|
|
91
|
-
const handleResize = (): void => setWidth(window.innerWidth);
|
|
92
|
-
window.addEventListener("resize", handleResize);
|
|
93
|
-
return () => window.removeEventListener("resize", handleResize);
|
|
94
|
-
}, []);
|
|
95
|
-
|
|
96
|
-
return <input ref={inputRef} placeholder={`Viewport: ${width}px`} />;
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## 4. Composition, Lists, and Controlled Inputs
|
|
103
|
-
|
|
104
|
-
Favor composition via `children` over bloated configuration props, use stable domain keys (`todo.id`) for lists, and bind controlled inputs to state:
|
|
105
|
-
|
|
106
|
-
```tsx
|
|
107
|
-
import { useState, type ReactNode } from "react";
|
|
108
|
-
|
|
109
|
-
export interface TodoItem {
|
|
110
|
-
id: string;
|
|
111
|
-
text: string;
|
|
112
|
-
done: boolean;
|
|
113
|
-
}
|
|
114
61
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
todos,
|
|
118
|
-
children,
|
|
119
|
-
}: {
|
|
62
|
+
// ✅ Component with children
|
|
63
|
+
export interface CardProps {
|
|
120
64
|
title: string;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}) {
|
|
124
|
-
const [filter, setFilter] = useState<string>("");
|
|
125
|
-
|
|
126
|
-
if (todos.length === 0) {
|
|
127
|
-
return <p>Your task list is clear. Add a new item to get started.</p>;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const visible = todos.filter((t) => t.text.toLowerCase().includes(filter.toLowerCase()));
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
}
|
|
131
67
|
|
|
68
|
+
export function Card({ title, children }: CardProps) {
|
|
132
69
|
return (
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
<input
|
|
136
|
-
value={filter}
|
|
137
|
-
onChange={(event) => setFilter(event.target.value)}
|
|
138
|
-
placeholder="Filter tasks..."
|
|
139
|
-
/>
|
|
140
|
-
<ul>
|
|
141
|
-
{visible.map((todo) => (
|
|
142
|
-
<li key={todo.id}>{todo.done ? <s>{todo.text}</s> : todo.text}</li>
|
|
143
|
-
))}
|
|
144
|
-
</ul>
|
|
70
|
+
<section className="rounded-lg border p-4">
|
|
71
|
+
<h3 className="font-semibold text-lg">{title}</h3>
|
|
145
72
|
{children}
|
|
146
|
-
</
|
|
73
|
+
</section>
|
|
147
74
|
);
|
|
148
75
|
}
|
|
76
|
+
|
|
77
|
+
// ✅ Component without props
|
|
78
|
+
export function EmptyState() {
|
|
79
|
+
return <p className="text-muted-foreground text-sm">No items found.</p>;
|
|
80
|
+
}
|
|
149
81
|
```
|
|
150
82
|
|
|
151
83
|
---
|
|
152
84
|
|
|
153
|
-
##
|
|
85
|
+
## 3. Anti-Patterns to Avoid
|
|
154
86
|
|
|
155
87
|
| Anti-Pattern | Why It Fails | Correct Pattern |
|
|
156
88
|
| :--- | :--- | :--- |
|
|
89
|
+
| `export const Button: React.FC<Props>` | Verbose, breaks TS generics, legacy implicit children | `export function Button({ label }: ButtonProps)` |
|
|
157
90
|
| Mutating state in place (`items.push(next)`) | Same array reference; React skips re-render | Create a new reference: `setItems((prev) => [...prev, next])` |
|
|
158
|
-
| Calling hooks after `if` or inside loops | Breaks React's deterministic hook call order | Call hooks unconditionally at
|
|
91
|
+
| Calling hooks after `if` or inside loops | Breaks React's deterministic hook call order | Call hooks unconditionally at top level |
|
|
159
92
|
| Using array index `key={i}` on dynamic lists | Misattributes component state on insert, delete, or sort | Use a stable domain identifier: `key={item.id}` |
|
|
160
|
-
| Fetching or deriving synchronous state inside `useEffect` | Triggers an extra render pass and causes UI flicker | Derive values inline during render; use `use()` / Suspense or Actions for async data (`react-advanced`) |
|
|
161
93
|
| Re-exporting components via `components/index.ts` | Violates Zero Barrel Files and harms tree-shaking | Import directly from `@/components/ui/card` |
|
|
162
94
|
|
|
163
95
|
---
|
|
164
96
|
|
|
165
|
-
##
|
|
97
|
+
## 4. Sibling Skills
|
|
166
98
|
|
|
167
99
|
- **`best-practices`**: Universal clean architecture, `< 200` lines limit, `@/` path aliases, strict typing, and courteous user feedback.
|
|
168
|
-
- **`react-advanced`**: React 19 platform features, React Compiler
|
|
100
|
+
- **`react-advanced`**: React 19 platform features, React Compiler auto-memoization, concurrent rendering, and RSC boundaries.
|
|
101
|
+
- **`react-create-page`**: Page coordinator architecture, custom domain hooks, and Valibot forms.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing-vitest
|
|
3
|
-
description: Clean, deterministic unit and
|
|
3
|
+
description: Clean, deterministic unit, integration, and HTTP route testing with Vitest. Enforces Arrange-Act-Assert (AAA), behavioral testing over implementation details, in-memory SQLite/D1 fakes over brittle mocks, four-tier testing pyramid, and strict assertion contracts. TRIGGER when writing, maintaining, or fixing automated tests (*.test.ts, *.spec.ts), test utilities, or testing infrastructure with Vitest.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Vitest Testing Standards
|
|
6
|
+
# Vitest Testing Standards & 4-Tier Backend Pyramid
|
|
7
7
|
|
|
8
|
-
Guidelines for authoring reliable, fast, and maintainable unit and
|
|
8
|
+
Guidelines for authoring reliable, fast, and maintainable unit, integration, and route tests with **Vitest**.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -22,88 +22,95 @@ Guidelines for authoring reliable, fast, and maintainable unit and integration t
|
|
|
22
22
|
- `// Assert`: Verify outputs and invariants.
|
|
23
23
|
|
|
24
24
|
3. **In-Memory Fakes Over Brittle Mocks**:
|
|
25
|
-
- Prefer in-memory fakes (e.g., `
|
|
26
|
-
- Only mock external boundaries (network HTTP calls, system clocks, third-party APIs).
|
|
25
|
+
- Prefer in-memory fakes (e.g., `testDb` backed by `:memory:` SQLite / D1 emulator) over mock chains.
|
|
26
|
+
- Only mock external boundaries (network HTTP calls, system clocks, third-party payment APIs).
|
|
27
27
|
|
|
28
|
-
4. **
|
|
29
|
-
-
|
|
30
|
-
-
|
|
28
|
+
4. **Four-Tier Testing Pyramid**:
|
|
29
|
+
- **Tier 1 (DTOs / Schemas)**: Test constraint validations, sanitization, trims, and invalid payload rejections.
|
|
30
|
+
- **Tier 2 (Actions)**: Test isolated business logic functions with typed context.
|
|
31
|
+
- **Tier 3 (Transactions)**: Test multi-mutation atomic batching and rollback mechanics.
|
|
32
|
+
- **Tier 4 (HTTP Routes)**: Mount thin routes on `testApp`, inject `testDb`, and verify HTTP lifecycle with `testApp.request()`.
|
|
31
33
|
|
|
32
|
-
5. **
|
|
33
|
-
-
|
|
34
|
-
-
|
|
34
|
+
5. **Mandatory Response Envelope Validation (`ActionResponse<T>`)**:
|
|
35
|
+
- Route tests must assert that responses adhere to `ActionResponse<T>` (`success`, `message`, `data`, `error`).
|
|
36
|
+
- Verify that sensitive fields (e.g. `passwordHash`) are stripped by output whitelisting schemas.
|
|
35
37
|
|
|
36
|
-
6. **
|
|
37
|
-
-
|
|
38
|
-
- Avoid vague names like `it("works")` or `it("test 1")`.
|
|
38
|
+
6. **Hard Size Limit: <200 Lines**:
|
|
39
|
+
- Keep test files focused and under 200 lines (target: 30-100 lines).
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
42
|
-
## 2.
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Act
|
|
55
|
-
const finalPrice = calculateDiscount(originalPrice, isPremium);
|
|
43
|
+
## 2. Directory Taxonomy
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
packages/testing/src/backend/ (or tests/)
|
|
47
|
+
├── setup.ts # Global testDb initialization & migrations
|
|
48
|
+
├── mocks/
|
|
49
|
+
│ └── in-memory-d1.ts # In-memory SQLite emulator applying migrations
|
|
50
|
+
└── {domain}/
|
|
51
|
+
├── {entity}-dto.test.ts # Tier 1: Schema validation tests
|
|
52
|
+
├── {domain}-actions.test.ts# Tier 2: Pipeline actions unit tests
|
|
53
|
+
└── {action}.route.test.ts # Tier 4: Route integration tests
|
|
54
|
+
```
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
expect(finalPrice).toBe(85);
|
|
59
|
-
});
|
|
56
|
+
---
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
// Arrange & Act
|
|
63
|
-
const finalPrice = calculateDiscount(50, false);
|
|
58
|
+
## 3. Four-Tier Pattern Examples
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
### Tier 1: DTO Validation Test
|
|
61
|
+
```ts
|
|
62
|
+
import { describe, it, expect } from "vitest";
|
|
63
|
+
import * as v from "valibot";
|
|
64
|
+
import { CreateUserDto } from "@/dtos/users/create-user.dto";
|
|
65
|
+
|
|
66
|
+
describe("Users DTO Validation", () => {
|
|
67
|
+
it("should accept valid payload and lowercase email", () => {
|
|
68
|
+
const input = { fullName: "Carlos Santana", email: "CARLOS@EXAMPLE.COM" };
|
|
69
|
+
const parsed = v.parse(CreateUserDto, input);
|
|
70
|
+
expect(parsed.email).toBe("carlos@example.com");
|
|
67
71
|
});
|
|
68
72
|
|
|
69
|
-
it("should
|
|
70
|
-
|
|
71
|
-
expect(() => calculateDiscount(-10, true)).toThrowError(/Price must be non-negative/);
|
|
73
|
+
it("should reject invalid email", () => {
|
|
74
|
+
expect(() => v.parse(CreateUserDto, { fullName: "Carlos", email: "invalid" })).toThrow();
|
|
72
75
|
});
|
|
73
76
|
});
|
|
74
77
|
```
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## 3. Mocking External Boundaries Safely
|
|
79
|
-
|
|
79
|
+
### Tier 4: Route Integration Test with `testApp.request()`
|
|
80
80
|
```ts
|
|
81
|
-
import { describe, it, expect, vi
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
import { describe, it, expect, vi } from "vitest";
|
|
82
|
+
import { Hono } from "hono";
|
|
83
|
+
import { createUserRoute } from "@/routes/modules/users/create-user.route";
|
|
84
|
+
import { testDb } from "@/backend/setup";
|
|
85
|
+
import type { AppContext } from "@/types/context/app-context.type";
|
|
86
|
+
import type { ActionResponse } from "@/types/common/action-response.type";
|
|
87
|
+
|
|
88
|
+
describe("POST /users Route Integration", () => {
|
|
89
|
+
const testApp = new Hono<AppContext>();
|
|
90
|
+
testApp.use("*", async (ctx, next) => {
|
|
91
|
+
ctx.set("db", testDb);
|
|
92
|
+
ctx.set("logger", { info: vi.fn(), warn: vi.fn(), error: vi.fn() } as any);
|
|
93
|
+
await next();
|
|
87
94
|
});
|
|
95
|
+
testApp.route("/users", createUserRoute);
|
|
88
96
|
|
|
89
|
-
it("should
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
it("should return 201 with ActionResponse contract", async () => {
|
|
98
|
+
const response = await testApp.request("/users", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ fullName: "Valeria Gomez", email: "valeria@example.com" }),
|
|
93
102
|
});
|
|
94
|
-
vi.stubGlobal("fetch", fakeFetch);
|
|
95
|
-
|
|
96
|
-
const rate = await fetchExchangeRate("EUR", "USD");
|
|
97
103
|
|
|
98
|
-
expect(
|
|
99
|
-
|
|
104
|
+
expect(response.status).toBe(201);
|
|
105
|
+
const json = (await response.json()) as ActionResponse<{ id: string; passwordHash?: string }>;
|
|
106
|
+
expect(json.success).toBe(true);
|
|
107
|
+
expect(json.data?.passwordHash).toBeUndefined();
|
|
100
108
|
});
|
|
101
109
|
});
|
|
102
110
|
```
|
|
103
111
|
|
|
104
112
|
---
|
|
105
113
|
|
|
106
|
-
## 4.
|
|
114
|
+
## 4. Deep References
|
|
107
115
|
|
|
108
|
-
-
|
|
109
|
-
- **`error-handling-logging`**: Testing expected `AppError` throws and error payloads.
|
|
116
|
+
- `./references/backend-d1-testing-pyramid.md`: Comprehensive guide for In-Memory SQLite/D1 setup and test matrices.
|