@baldurpan/create-ai-workflow 0.2.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/agents-block.js +70 -0
- package/dist/bin.js +14 -0
- package/dist/check/markdown.js +47 -0
- package/dist/check/parse.js +113 -0
- package/dist/check/rules.js +245 -0
- package/dist/cli.js +109 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/install.js +76 -0
- package/dist/commands/standards-add.js +145 -0
- package/dist/commands/update.js +214 -0
- package/dist/layout.js +115 -0
- package/dist/log.js +20 -0
- package/dist/manifest.js +53 -0
- package/dist/paths.js +41 -0
- package/package.json +37 -0
- package/templates/blocks/agents-block.md +30 -0
- package/templates/claude/agents/planner.agent.md +60 -0
- package/templates/claude/agents/reviewer.agent.md +77 -0
- package/templates/context/README.md +81 -0
- package/templates/context/plan-template.md +69 -0
- package/templates/context/plan-template.notes.md +55 -0
- package/templates/context/roles/coder.md +76 -0
- package/templates/context/workflow.md +143 -0
- package/templates/skills/feature-close/SKILL.md +102 -0
- package/templates/skills/feature-implement/SKILL.md +142 -0
- package/templates/skills/feature-plan/SKILL.md +138 -0
- package/templates/skills/feature-status/SKILL.md +86 -0
- package/templates/skills/onboard/SKILL.md +122 -0
- package/templates/skills/orchestrate/SKILL.md +92 -0
- package/templates/skills/roadmap/SKILL.md +97 -0
- package/templates/standards/.source +8 -0
- package/templates/standards/README.md +104 -0
- package/templates/standards/architecture/api-design.md +212 -0
- package/templates/standards/architecture/dependency-boundaries.md +104 -0
- package/templates/standards/architecture/feature-driven.md +102 -0
- package/templates/standards/architecture/folder-structure.md +108 -0
- package/templates/standards/architecture/monorepos.md +107 -0
- package/templates/standards/architecture/refactoring.md +97 -0
- package/templates/standards/architecture/shared-code.md +81 -0
- package/templates/standards/docs/PHP-SPEC.md +1195 -0
- package/templates/standards/docs/SPEC.md +540 -0
- package/templates/standards/examples/bad/data-fetching.tsx +52 -0
- package/templates/standards/examples/bad/derived-state.tsx +60 -0
- package/templates/standards/examples/bad/feature-component.tsx +80 -0
- package/templates/standards/examples/bad/prop-drilling.tsx +70 -0
- package/templates/standards/examples/good/data-fetching.tsx +53 -0
- package/templates/standards/examples/good/derived-state.tsx +52 -0
- package/templates/standards/examples/good/feature-component.tsx +141 -0
- package/templates/standards/examples/good/invoice.schema.ts +20 -0
- package/templates/standards/examples/good/prop-drilling.tsx +83 -0
- package/templates/standards/philosophy/ai-agent-behavior.md +93 -0
- package/templates/standards/philosophy/core-principles.md +49 -0
- package/templates/standards/philosophy/incremental-abstraction.md +56 -0
- package/templates/standards/philosophy/maintainability.md +66 -0
- package/templates/standards/philosophy/readability.md +87 -0
- package/templates/standards/php/README.md +62 -0
- package/templates/standards/php/anti-patterns.md +259 -0
- package/templates/standards/php/architecture.md +241 -0
- package/templates/standards/php/frameworks.md +197 -0
- package/templates/standards/php/rules.md +205 -0
- package/templates/standards/php/security.md +190 -0
- package/templates/standards/php/testing.md +139 -0
- package/templates/standards/php/tooling.md +245 -0
- package/templates/standards/react/accessibility.md +130 -0
- package/templates/standards/react/anti-patterns.md +187 -0
- package/templates/standards/react/component-design.md +139 -0
- package/templates/standards/react/error-boundaries.md +195 -0
- package/templates/standards/react/forms.md +147 -0
- package/templates/standards/react/hooks.md +88 -0
- package/templates/standards/react/memoization.md +87 -0
- package/templates/standards/react/state-management.md +123 -0
- package/templates/standards/react/testing.md +118 -0
- package/templates/standards/react/use-effect.md +110 -0
- package/templates/standards/security/api-security.md +136 -0
- package/templates/standards/security/auth.md +112 -0
- package/templates/standards/security/secrets.md +105 -0
- package/templates/standards/security/validation.md +89 -0
- package/templates/standards/templates/.editorconfig +16 -0
- package/templates/standards/templates/.nvmrc +1 -0
- package/templates/standards/templates/AGENTS.md +46 -0
- package/templates/standards/templates/CLAUDE.md +3 -0
- package/templates/standards/templates/_dot_gitignore +50 -0
- package/templates/standards/templates/biome.json +53 -0
- package/templates/standards/templates/eslint.config.js +61 -0
- package/templates/standards/templates/project-readme.md +76 -0
- package/templates/standards/templates/pull-request-template.md +46 -0
- package/templates/standards/templates/tsconfig.json +35 -0
- package/templates/standards/tooling/biome.md +92 -0
- package/templates/standards/tooling/ci.md +128 -0
- package/templates/standards/tooling/dates.md +194 -0
- package/templates/standards/tooling/dependencies.md +90 -0
- package/templates/standards/tooling/nx.md +113 -0
- package/templates/standards/tooling/observability.md +183 -0
- package/templates/standards/tooling/prisma.md +152 -0
- package/templates/standards/tooling/shadcn.md +115 -0
- package/templates/standards/tooling/tailwind.md +135 -0
- package/templates/standards/tooling/tanstack-query.md +214 -0
- package/templates/standards/tooling/tanstack-router.md +365 -0
- package/templates/standards/tooling/tanstack.md +31 -0
- package/templates/standards/tooling/vite.md +135 -0
- package/templates/standards/typescript/anti-patterns.md +107 -0
- package/templates/standards/typescript/error-handling.md +216 -0
- package/templates/standards/typescript/naming.md +78 -0
- package/templates/standards/typescript/rules.md +159 -0
- package/templates/standards/typescript/tsconfig/base.json +32 -0
- package/templates/standards/typescript/tsconfig/nextjs.json +12 -0
- package/templates/standards/typescript/tsconfig/react.json +9 -0
- package/templates/standards/typescript/validation.md +104 -0
- package/templates/stubs/executors.md +49 -0
- package/templates/stubs/findings.md +40 -0
- package/templates/stubs/history.md +13 -0
- package/templates/stubs/roadmap.md +29 -0
- package/templates/stubs/stack.md +35 -0
- package/templates/stubs/verify.md +48 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# TanStack Router
|
|
2
|
+
|
|
3
|
+
## Core Rules
|
|
4
|
+
|
|
5
|
+
- Define routes as code (or file-based) with full type inference — no string-typed paths
|
|
6
|
+
- Validate all search params with Zod — typed and runtime-safe
|
|
7
|
+
- Load data in route loaders, consume with `useSuspenseQuery` — eliminates loading flicker on navigation
|
|
8
|
+
- Use route-level `pendingComponent` / `errorComponent` for navigation transitions; component-level `<Suspense>` / `<ErrorBoundary>` for in-page widgets
|
|
9
|
+
- Auth-guard with `beforeLoad` — redirect before the route renders, never inside the component
|
|
10
|
+
- Treat the URL as the source of truth for filters, search, pagination, and tab state
|
|
11
|
+
|
|
12
|
+
## Why Router + Query
|
|
13
|
+
|
|
14
|
+
TanStack Router and TanStack Query together change how data fetching is structured:
|
|
15
|
+
|
|
16
|
+
- **Loader prefetches data** before the component mounts
|
|
17
|
+
- **Component reads from cache** via `useSuspenseQuery` — synchronously returns cached data
|
|
18
|
+
- **No loading state on navigation** — the data is already there when the component renders
|
|
19
|
+
- **Single source of truth** — the query cache serves both loaders and components
|
|
20
|
+
|
|
21
|
+
The pattern flips the usual "fetch inside the component" model.
|
|
22
|
+
|
|
23
|
+
## Route Definition
|
|
24
|
+
|
|
25
|
+
### Code-Based Routes
|
|
26
|
+
|
|
27
|
+
Define routes as a tree of `createRoute` calls. Best for full type inference and explicit composition.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createRootRoute, createRoute, createRouter } from "@tanstack/react-router";
|
|
31
|
+
|
|
32
|
+
const rootRoute = createRootRoute({ component: RootLayout });
|
|
33
|
+
|
|
34
|
+
const invoicesRoute = createRoute({
|
|
35
|
+
getParentRoute: () => rootRoute,
|
|
36
|
+
path: "/invoices",
|
|
37
|
+
component: InvoicesPage,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const invoiceDetailRoute = createRoute({
|
|
41
|
+
getParentRoute: () => invoicesRoute,
|
|
42
|
+
path: "$invoiceId",
|
|
43
|
+
component: InvoiceDetailPage,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const routeTree = rootRoute.addChildren([invoicesRoute, invoiceDetailRoute]);
|
|
47
|
+
|
|
48
|
+
export const router = createRouter({ routeTree });
|
|
49
|
+
|
|
50
|
+
declare module "@tanstack/react-router" {
|
|
51
|
+
interface Register {
|
|
52
|
+
router: typeof router;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### File-Based Routes
|
|
58
|
+
|
|
59
|
+
For larger apps, file-based routing reduces ceremony. Routes live in `src/routes/` and the generator creates the route tree:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
src/routes/
|
|
63
|
+
__root.tsx ← root layout
|
|
64
|
+
index.tsx ← /
|
|
65
|
+
invoices/
|
|
66
|
+
index.tsx ← /invoices
|
|
67
|
+
$invoiceId.tsx ← /invoices/:invoiceId
|
|
68
|
+
_layout.tsx ← shared layout for /invoices/*
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use the `@tanstack/router-plugin/vite` plugin to regenerate the route tree on save.
|
|
72
|
+
|
|
73
|
+
**When to choose which:** code-based for libraries, embedded apps, or small projects; file-based for production apps with many routes.
|
|
74
|
+
|
|
75
|
+
## Typed Path Params
|
|
76
|
+
|
|
77
|
+
Path params are inferred from the path string:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const invoiceDetailRoute = createRoute({
|
|
81
|
+
getParentRoute: () => invoicesRoute,
|
|
82
|
+
path: "$invoiceId",
|
|
83
|
+
component: InvoiceDetailPage,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function InvoiceDetailPage() {
|
|
87
|
+
const { invoiceId } = invoiceDetailRoute.useParams(); // string, fully typed
|
|
88
|
+
return <InvoiceDetail id={invoiceId} />;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For runtime-validated params (e.g., must be a CUID), use `parseParams`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
parseParams: ({ invoiceId }) => ({ invoiceId: z.string().cuid().parse(invoiceId) }),
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Typed Search Params
|
|
99
|
+
|
|
100
|
+
Search params are the canonical place for filters, search queries, pagination, and tab selection — anywhere the state should be shareable, bookmarkable, and survive a refresh.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const invoicesRoute = createRoute({
|
|
104
|
+
getParentRoute: () => rootRoute,
|
|
105
|
+
path: "/invoices",
|
|
106
|
+
validateSearch: z.object({
|
|
107
|
+
status: z.enum(["all", "open", "paid"]).default("all"),
|
|
108
|
+
page: z.number().int().positive().default(1),
|
|
109
|
+
sort: z.enum(["dueDate", "amount"]).default("dueDate"),
|
|
110
|
+
}),
|
|
111
|
+
component: InvoicesPage,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function InvoicesPage() {
|
|
115
|
+
const { status, page, sort } = invoicesRoute.useSearch();
|
|
116
|
+
const navigate = invoicesRoute.useNavigate();
|
|
117
|
+
|
|
118
|
+
function setStatus(status: InvoiceStatus) {
|
|
119
|
+
navigate({ search: (prev) => ({ ...prev, status, page: 1 }) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return <InvoiceTable status={status} page={page} sort={sort} onStatusChange={setStatus} />;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Search params are validated on every navigation. Invalid values are coerced or rejected by the schema.
|
|
127
|
+
|
|
128
|
+
## Loaders + TanStack Query (The Core Pattern)
|
|
129
|
+
|
|
130
|
+
The recommended pattern: loaders prefetch with `queryClient.ensureQueryData`, components consume with `useSuspenseQuery`.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// 1. Define the query options outside the route so loader + component share them
|
|
134
|
+
function invoicesQueryOptions(filters: InvoicesSearch) {
|
|
135
|
+
return queryOptions({
|
|
136
|
+
queryKey: ["invoices", filters],
|
|
137
|
+
queryFn: () => fetchInvoices(filters),
|
|
138
|
+
staleTime: 30_000,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. The loader prefetches into the query cache
|
|
143
|
+
const invoicesRoute = createRoute({
|
|
144
|
+
getParentRoute: () => rootRoute,
|
|
145
|
+
path: "/invoices",
|
|
146
|
+
validateSearch: invoicesSearchSchema,
|
|
147
|
+
loaderDeps: ({ search }) => search,
|
|
148
|
+
loader: ({ context, deps }) =>
|
|
149
|
+
context.queryClient.ensureQueryData(invoicesQueryOptions(deps)),
|
|
150
|
+
component: InvoicesPage,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 3. The component reads from the cache — synchronous, no loading state
|
|
154
|
+
function InvoicesPage() {
|
|
155
|
+
const search = invoicesRoute.useSearch();
|
|
156
|
+
const { data: invoices } = useSuspenseQuery(invoicesQueryOptions(search));
|
|
157
|
+
return <InvoiceTable invoices={invoices} />;
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
What this gives you:
|
|
162
|
+
- **No loading flicker on navigation** — data is in cache before render
|
|
163
|
+
- **Stale-while-revalidate** — TanStack Query refetches in the background if stale
|
|
164
|
+
- **Cache reuse** — going back to the page hits the cache instantly
|
|
165
|
+
- **Prefetching** — `<Link preload="intent">` prefetches on hover
|
|
166
|
+
|
|
167
|
+
### Router Setup with QueryClient in Context
|
|
168
|
+
|
|
169
|
+
Pass the `queryClient` to the router so loaders can use it:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
const queryClient = new QueryClient({ /* ... */ });
|
|
173
|
+
|
|
174
|
+
const router = createRouter({
|
|
175
|
+
routeTree,
|
|
176
|
+
context: { queryClient },
|
|
177
|
+
defaultPreload: "intent",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export function App() {
|
|
181
|
+
return (
|
|
182
|
+
<QueryClientProvider client={queryClient}>
|
|
183
|
+
<RouterProvider router={router} />
|
|
184
|
+
</QueryClientProvider>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Pending and Error Components
|
|
190
|
+
|
|
191
|
+
Route-level components handle the boundary between routes. Different from in-page widgets.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const invoicesRoute = createRoute({
|
|
195
|
+
getParentRoute: () => rootRoute,
|
|
196
|
+
path: "/invoices",
|
|
197
|
+
loader: /* ... */,
|
|
198
|
+
pendingComponent: InvoicesPending,
|
|
199
|
+
errorComponent: InvoicesError,
|
|
200
|
+
component: InvoicesPage,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function InvoicesPending() {
|
|
204
|
+
return <TableSkeleton />;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function InvoicesError({ error }: { error: Error }) {
|
|
208
|
+
return (
|
|
209
|
+
<div role="alert">
|
|
210
|
+
<p>We couldn't load invoices.</p>
|
|
211
|
+
<p><small>{error.message}</small></p>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Use route-level `pendingComponent` / `errorComponent`** for the entire route transition (loader runs, then renders).
|
|
218
|
+
**Use component-level `<Suspense>` / `<ErrorBoundary>`** for independent widgets within the page that can fail or load separately (see [`../react/error-boundaries.md`](../react/error-boundaries.md)).
|
|
219
|
+
|
|
220
|
+
## Auth Guards with `beforeLoad`
|
|
221
|
+
|
|
222
|
+
`beforeLoad` runs before the loader. It is the right place for authentication and authorization:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const dashboardRoute = createRoute({
|
|
226
|
+
getParentRoute: () => rootRoute,
|
|
227
|
+
path: "/dashboard",
|
|
228
|
+
beforeLoad: ({ context, location }) => {
|
|
229
|
+
if (!context.auth.isAuthenticated) {
|
|
230
|
+
throw redirect({
|
|
231
|
+
to: "/login",
|
|
232
|
+
search: { returnTo: location.href },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
loader: /* ... */,
|
|
237
|
+
component: DashboardPage,
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`throw redirect(...)` short-circuits the route — the component never renders.
|
|
242
|
+
|
|
243
|
+
Pass `auth` through the router context (same pattern as `queryClient`).
|
|
244
|
+
|
|
245
|
+
## Code Splitting
|
|
246
|
+
|
|
247
|
+
Lazy-load route components to reduce initial bundle size:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const invoicesRoute = createRoute({
|
|
251
|
+
getParentRoute: () => rootRoute,
|
|
252
|
+
path: "/invoices",
|
|
253
|
+
component: lazyRouteComponent(() => import("./InvoicesPage")),
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
For file-based routes, prefix the file with `_` to make it lazy:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
src/routes/invoices.lazy.tsx ← lazy-loaded
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Route Context
|
|
264
|
+
|
|
265
|
+
Share data down the route tree without prop drilling:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const router = createRouter({
|
|
269
|
+
routeTree,
|
|
270
|
+
context: {
|
|
271
|
+
queryClient,
|
|
272
|
+
auth: undefined!, // filled in by the auth route
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const authRoute = createRoute({
|
|
277
|
+
getParentRoute: () => rootRoute,
|
|
278
|
+
beforeLoad: ({ context }) => ({
|
|
279
|
+
auth: { user: getCurrentUser(), isAuthenticated: !!getCurrentUser() },
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Child routes inherit and can extend the context.
|
|
285
|
+
|
|
286
|
+
## Type-Safe Navigation
|
|
287
|
+
|
|
288
|
+
`Link`, `navigate`, and `redirect` are all typed against the route tree:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
import { Link } from "@tanstack/react-router";
|
|
292
|
+
|
|
293
|
+
// Path and params are fully typed — typos and missing params are compile errors
|
|
294
|
+
<Link to="/invoices/$invoiceId" params={{ invoiceId: invoice.id }}>
|
|
295
|
+
View invoice
|
|
296
|
+
</Link>
|
|
297
|
+
|
|
298
|
+
// Search params are typed against the route's validateSearch schema
|
|
299
|
+
<Link
|
|
300
|
+
to="/invoices"
|
|
301
|
+
search={{ status: "open", page: 1 }}
|
|
302
|
+
>
|
|
303
|
+
Open invoices
|
|
304
|
+
</Link>
|
|
305
|
+
|
|
306
|
+
// Preload on hover / focus
|
|
307
|
+
<Link to="/invoices/$invoiceId" params={{ invoiceId }} preload="intent">
|
|
308
|
+
View
|
|
309
|
+
</Link>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Programmatic navigation:
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const navigate = useNavigate();
|
|
316
|
+
navigate({ to: "/invoices/$invoiceId", params: { invoiceId } });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Invalidation After Mutations
|
|
320
|
+
|
|
321
|
+
When a mutation changes data, invalidate the relevant queries and optionally navigate:
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
const { mutate } = useMutation({
|
|
325
|
+
mutationFn: createInvoice,
|
|
326
|
+
onSuccess: async (newInvoice) => {
|
|
327
|
+
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
328
|
+
navigate({ to: "/invoices/$invoiceId", params: { invoiceId: newInvoice.id } });
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
For server-driven navigation (e.g., after delete, go back to the list):
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
onSuccess: () => {
|
|
337
|
+
queryClient.invalidateQueries({ queryKey: ["invoices"] });
|
|
338
|
+
navigate({ to: "/invoices", search: { status: "all", page: 1 } });
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## DO NOT
|
|
343
|
+
|
|
344
|
+
- Define routes with stringly-typed paths — use `createRoute` or file-based routing for type inference
|
|
345
|
+
- Fetch data inside the component on mount — use the loader
|
|
346
|
+
- Use `useEffect` to read URL params — use `useSearch` / `useParams`
|
|
347
|
+
- Put search-param logic in component state and sync it to the URL — use the router's `validateSearch`
|
|
348
|
+
- Skip `validateSearch` "just for now" — invalid params will leak into your queries and cause cache pollution
|
|
349
|
+
- Build custom auth-redirect logic inside components — use `beforeLoad`
|
|
350
|
+
|
|
351
|
+
## PRIORITY
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
Loader-prefetched + Suspense queries > Component-level fetching
|
|
355
|
+
Typed search params > Component state for shareable values
|
|
356
|
+
beforeLoad redirects > Component-level redirects
|
|
357
|
+
Route errorComponent > Component-level ErrorBoundary (for route-scoped errors)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## See Also
|
|
361
|
+
|
|
362
|
+
- [`tanstack-query.md`](tanstack-query.md) — query options, mutations, invalidation
|
|
363
|
+
- [`../react/state-management.md`](../react/state-management.md) — URL state at the top of the hierarchy
|
|
364
|
+
- [`../react/error-boundaries.md`](../react/error-boundaries.md) — component-level error and loading boundaries
|
|
365
|
+
- [`../architecture/api-design.md`](../architecture/api-design.md) — API client and pagination patterns
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# TanStack
|
|
2
|
+
|
|
3
|
+
The TanStack family is the preferred toolkit for routing, server state, and forms.
|
|
4
|
+
|
|
5
|
+
| Library | Doc | Purpose |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| TanStack Router | [`tanstack-router.md`](tanstack-router.md) | Type-safe routing, loaders, typed search params |
|
|
8
|
+
| TanStack Query | [`tanstack-query.md`](tanstack-query.md) | Server state, caching, mutations |
|
|
9
|
+
| TanStack Form | [`../react/forms.md`](../react/forms.md) | Type-safe form state with schema validation |
|
|
10
|
+
|
|
11
|
+
## Why TanStack
|
|
12
|
+
|
|
13
|
+
- Each library is independent but composes cleanly with the others
|
|
14
|
+
- Type inference flows end-to-end — fewer hand-typed DTOs
|
|
15
|
+
- Schema validation (Zod) provides a consistent backbone across router, query, and form
|
|
16
|
+
- Router + Query together eliminate loading flicker on navigation — see [`tanstack-router.md`](tanstack-router.md)
|
|
17
|
+
|
|
18
|
+
## When All Three Compose
|
|
19
|
+
|
|
20
|
+
A typical data flow uses all three:
|
|
21
|
+
|
|
22
|
+
1. **Router** loads data in a `loader` via `queryClient.ensureQueryData`
|
|
23
|
+
2. **Query** caches it; components consume it via `useSuspenseQuery`
|
|
24
|
+
3. **Form** mutations call `queryClient.invalidateQueries` after submission
|
|
25
|
+
4. The Router navigates to the next route, which may already be prefetched
|
|
26
|
+
|
|
27
|
+
## See Also
|
|
28
|
+
|
|
29
|
+
- [`../react/state-management.md`](../react/state-management.md) — state hierarchy (URL > server > local > context)
|
|
30
|
+
- [`../react/forms.md`](../react/forms.md) — TanStack Form patterns
|
|
31
|
+
- [`../architecture/api-design.md`](../architecture/api-design.md) — API contracts that feed these libraries
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Vite
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Vite is the preferred build tool for React applications and libraries. It provides fast dev-server HMR and optimized production builds.
|
|
6
|
+
|
|
7
|
+
## Recommended Config
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// vite.config.ts
|
|
11
|
+
import { defineConfig } from "vite";
|
|
12
|
+
import react from "@vitejs/plugin-react";
|
|
13
|
+
import tsconfigPaths from "vite-tsconfig-paths";
|
|
14
|
+
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
plugins: [
|
|
17
|
+
react(),
|
|
18
|
+
tsconfigPaths(), // resolves ~/... aliases from tsconfig paths
|
|
19
|
+
],
|
|
20
|
+
build: {
|
|
21
|
+
target: "es2022",
|
|
22
|
+
sourcemap: true,
|
|
23
|
+
},
|
|
24
|
+
server: {
|
|
25
|
+
port: 3000,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Path Aliases
|
|
31
|
+
|
|
32
|
+
Use `~/` for absolute imports. Configure in `tsconfig.json` and resolve with `vite-tsconfig-paths`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
// tsconfig.json
|
|
36
|
+
{
|
|
37
|
+
"compilerOptions": {
|
|
38
|
+
"paths": {
|
|
39
|
+
"~/*": ["./src/*"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// Usage
|
|
47
|
+
import { InvoiceTable } from "~/features/invoices/InvoiceTable";
|
|
48
|
+
import { formatCurrency } from "~/shared/utils/format-currency";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment Variables
|
|
52
|
+
|
|
53
|
+
Vite exposes env vars prefixed with `VITE_` to the client:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
VITE_API_BASE_URL=https://api.example.com
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// Access in code
|
|
61
|
+
const apiBase = import.meta.env.VITE_API_BASE_URL;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Validate env vars at startup:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/lib/env.ts
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
|
|
70
|
+
const envSchema = z.object({
|
|
71
|
+
VITE_API_BASE_URL: z.string().url(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const env = envSchema.parse(import.meta.env);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Vitest Integration
|
|
78
|
+
|
|
79
|
+
Vitest runs inside Vite — same config, same transforms:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// vite.config.ts
|
|
83
|
+
import { defineConfig } from "vite";
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [react(), tsconfigPaths()],
|
|
87
|
+
test: {
|
|
88
|
+
environment: "jsdom",
|
|
89
|
+
setupFiles: ["./src/test/setup.ts"],
|
|
90
|
+
globals: true,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/test/setup.ts
|
|
97
|
+
import "@testing-library/jest-dom";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Code Splitting
|
|
101
|
+
|
|
102
|
+
Vite splits by dynamic import automatically:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const InvoicePage = lazy(() => import("~/features/invoices/InvoicePage"));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For manual chunk control:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// vite.config.ts
|
|
112
|
+
build: {
|
|
113
|
+
rollupOptions: {
|
|
114
|
+
output: {
|
|
115
|
+
manualChunks: {
|
|
116
|
+
vendor: ["react", "react-dom"],
|
|
117
|
+
tanstack: ["@tanstack/react-query", "@tanstack/react-router"],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## DO NOT
|
|
125
|
+
|
|
126
|
+
- Use `process.env` in Vite client code — use `import.meta.env`
|
|
127
|
+
- Commit `.env` files with secrets — use `.env.local` (gitignored)
|
|
128
|
+
- Disable sourcemaps in production without good reason
|
|
129
|
+
|
|
130
|
+
## See Also
|
|
131
|
+
|
|
132
|
+
- [`biome.md`](biome.md) — lint and format
|
|
133
|
+
- [`dependencies.md`](dependencies.md) — bundle impact tools
|
|
134
|
+
- [`../security/secrets.md`](../security/secrets.md) — env var validation
|
|
135
|
+
- [`../react/testing.md`](../react/testing.md) — Vitest runs inside Vite
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# TypeScript Anti-Patterns
|
|
2
|
+
|
|
3
|
+
## `any`
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// BAD — erases all type safety
|
|
7
|
+
function process(data: any) {
|
|
8
|
+
return data.value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// GOOD — use unknown and narrow
|
|
12
|
+
function process(data: unknown) {
|
|
13
|
+
if (typeof data === "object" && data !== null && "value" in data) {
|
|
14
|
+
return data.value;
|
|
15
|
+
}
|
|
16
|
+
throw new Error("Unexpected data shape");
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Type Assertions
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// BAD — silences the compiler without verification
|
|
24
|
+
const user = JSON.parse(raw) as User;
|
|
25
|
+
|
|
26
|
+
// GOOD — validate before using
|
|
27
|
+
const user = userSchema.parse(JSON.parse(raw));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Enums
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// BAD — compiled JS artifact, not tree-shakeable, verbose
|
|
34
|
+
enum Status {
|
|
35
|
+
Active = "ACTIVE",
|
|
36
|
+
Inactive = "INACTIVE",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// GOOD — plain union, zero runtime cost
|
|
40
|
+
type Status = "ACTIVE" | "INACTIVE";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## DTO Duplication
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// BAD — same shape defined twice
|
|
47
|
+
// api/users.ts
|
|
48
|
+
interface ApiUser { id: string; name: string; }
|
|
49
|
+
|
|
50
|
+
// frontend/types.ts
|
|
51
|
+
interface FrontendUser { id: string; name: string; }
|
|
52
|
+
|
|
53
|
+
// GOOD — single schema, inferred types
|
|
54
|
+
const userSchema = z.object({ id: z.string(), name: z.string() });
|
|
55
|
+
type User = z.infer<typeof userSchema>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Type Gymnastics
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// BAD — deeply conditional mapped types that take 10 minutes to understand
|
|
62
|
+
type DeepPartialReadonly<T> =
|
|
63
|
+
T extends object
|
|
64
|
+
? { readonly [K in keyof T]?: DeepPartialReadonly<T[K]> }
|
|
65
|
+
: T;
|
|
66
|
+
|
|
67
|
+
// If you need this, ask whether the data model is the real problem
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## `@ts-ignore` Without Explanation
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// BAD
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
doSomethingWeird(value);
|
|
76
|
+
|
|
77
|
+
// ACCEPTABLE — only when necessary, always with context
|
|
78
|
+
// @ts-expect-error — third-party type definition is wrong, see github.com/foo/bar/issues/123
|
|
79
|
+
doSomethingWeird(value);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Non-null Assertions Without Verification
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// BAD — crashes at runtime if undefined
|
|
86
|
+
const name = user!.profile!.name;
|
|
87
|
+
|
|
88
|
+
// GOOD
|
|
89
|
+
const name = user?.profile?.name ?? "Anonymous";
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## `object` or `{}` as a Type
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// BAD — accepts almost anything
|
|
96
|
+
function format(value: object): string { ... }
|
|
97
|
+
function format(value: {}): string { ... }
|
|
98
|
+
|
|
99
|
+
// GOOD — be explicit
|
|
100
|
+
function format(value: Record<string, string>): string { ... }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## See Also
|
|
104
|
+
|
|
105
|
+
- [`rules.md`](rules.md) — the positive rules these anti-patterns violate
|
|
106
|
+
- [`validation.md`](validation.md) — runtime validation for `unknown` data
|
|
107
|
+
- [`error-handling.md`](error-handling.md) — async, throwing, and preserving error context
|