create-sprinkles 0.2.3
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/dist/bin.mjs +295 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.mjs +180 -0
- package/package.json +45 -0
- package/templates/react-router-convex/.env.local +2 -0
- package/templates/react-router-convex/convex/schema.ts +9 -0
- package/templates/react-router-rsc/app/home.tsx.hbs +31 -0
- package/templates/react-router-rsc/app/root.tsx.hbs +55 -0
- package/templates/react-router-rsc/tsconfig.json.hbs +31 -0
- package/templates/react-router-rsc/workers/entry.rsc.tsx +44 -0
- package/templates/react-router-rsc/workers/entry.ssr.tsx +41 -0
- package/templates/react-router-rsc/wrangler.rsc.jsonc.hbs +25 -0
- package/templates/react-router-rsc/wrangler.ssr.jsonc.hbs +14 -0
- package/templates/react-router-rsc-content-layer/app/content.config.ts.hbs +26 -0
- package/templates/react-router-rsc-content-layer/content-layer/api.ts +350 -0
- package/templates/react-router-rsc-content-layer/content-layer/codegen.ts +89 -0
- package/templates/react-router-rsc-content-layer/content-layer/config.ts +20 -0
- package/templates/react-router-rsc-content-layer/content-layer/digest.ts +6 -0
- package/templates/react-router-rsc-content-layer/content-layer/frontmatter.ts +19 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/file.ts +55 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/glob.ts +82 -0
- package/templates/react-router-rsc-content-layer/content-layer/loaders/index.ts +2 -0
- package/templates/react-router-rsc-content-layer/content-layer/plugin.ts +419 -0
- package/templates/react-router-rsc-content-layer/content-layer/resolve-hook.js +12 -0
- package/templates/react-router-rsc-content-layer/content-layer/runtime.ts +73 -0
- package/templates/react-router-rsc-content-layer/content-layer/store.ts +59 -0
- package/templates/react-router-spa/app/home.tsx.hbs +7 -0
- package/templates/react-router-spa/app/root.tsx.hbs +60 -0
- package/templates/react-router-spa/tsconfig.json.hbs +26 -0
- package/templates/react-router-spa/wrangler.jsonc.hbs +9 -0
- package/templates/react-router-ssr/app/home.tsx.hbs +21 -0
- package/templates/react-router-ssr/app/root.tsx.hbs +105 -0
- package/templates/react-router-ssr/convex/schema.ts +7 -0
- package/templates/react-router-ssr/tsconfig.json.hbs +28 -0
- package/templates/react-router-ssr/wrangler.jsonc.hbs +13 -0
- package/templates/react-router-ssr-convex/app/lib/client.ts +19 -0
- package/templates/react-router-ssr-convex/app/tanstack-query-integration/middleware.ts +18 -0
- package/templates/react-router-ssr-convex/app/tanstack-query-integration/query-preloader.ts +125 -0
- package/templates/react-shared/app/routes.ts.hbs +3 -0
- package/templates/react-shared/app/styles/tailwind.css +1 -0
- package/templates/react-shared/react-compiler.plugin.ts.hbs +10 -0
- package/templates/react-shared/react-router.config.ts.hbs +9 -0
- package/templates/shared/.gitignore.hbs +23 -0
- package/templates/shared/.node-version +1 -0
- package/templates/shared/.vscode/extensions.json.hbs +8 -0
- package/templates/shared/.vscode/settings.json.hbs +72 -0
- package/templates/shared/AGENTS.md.hbs +599 -0
- package/templates/shared/README.md.hbs +24 -0
- package/templates/shared/package.json.hbs +41 -0
- package/templates/shared/vite.config.ts.hbs +384 -0
- package/templates/ts-package/src/index.ts +3 -0
- package/templates/ts-package/tests/index.test.ts +9 -0
- package/templates/ts-package/tsconfig.json +18 -0
- package/templates/ts-package-cli/bin/index.ts.hbs +1 -0
- package/templates/ts-package-cli/src/cli.ts.hbs +37 -0
- package/templates/ts-package-generator/bin/create.ts.hbs +2 -0
- package/templates/ts-package-generator/src/template.ts.hbs +22 -0
- package/templates/ts-package-sea/sea-config.json.hbs +2 -0
- package/templates/ts-package-sea/src/sea-entry.ts.hbs +4 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# Contributor Guidelines
|
|
2
|
+
|
|
3
|
+
## Technology Overview
|
|
4
|
+
|
|
5
|
+
- **Development runtime**: Node.js (v24)
|
|
6
|
+
- **Package manager**: pnpm
|
|
7
|
+
- **Toolchain**: Vite+ (`vp`)
|
|
8
|
+
- **Bundler**: Vite 8 (with Rolldown)
|
|
9
|
+
{{#if isReactRouter}}
|
|
10
|
+
- **JavaScript framework**: React 19
|
|
11
|
+
- **Meta-framework**: React Router
|
|
12
|
+
{{#if isSPA}}
|
|
13
|
+
- **Rendering**: Single-Page Application (SPA)
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#if isSSR}}
|
|
16
|
+
- **Rendering**: Server-Side Rendering (SSR)
|
|
17
|
+
{{/if}}
|
|
18
|
+
{{#if isRSC}}
|
|
19
|
+
- **Rendering**: React Server Components (RSC)
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if hasConvex}}
|
|
22
|
+
- **Backend**: Convex
|
|
23
|
+
- **Data fetching**: TanStack Query with @convex-dev/react-query
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if isRSC}}
|
|
26
|
+
{{#if hasContentLayer}}
|
|
27
|
+
- **Content**: Content-layer plugin with MDX
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{/if}}
|
|
30
|
+
- **Styling**: Tailwind CSS v4
|
|
31
|
+
{{/if}}
|
|
32
|
+
- **Formatter**: Oxfmt
|
|
33
|
+
- **Linter**: Oxlint
|
|
34
|
+
{{#if isReactRouter}}
|
|
35
|
+
- **Deployment runtime**: Cloudflare `workerd`
|
|
36
|
+
{{/if}}
|
|
37
|
+
|
|
38
|
+
## Common Commands
|
|
39
|
+
|
|
40
|
+
- Dev server: `vp dev` (**NEVER** run this yourself)
|
|
41
|
+
- Build: `vp build`
|
|
42
|
+
- Format: `vp fmt`
|
|
43
|
+
- Lint: `vp lint`
|
|
44
|
+
- Typecheck: `vp run typecheck`
|
|
45
|
+
- Project check: `vp check`
|
|
46
|
+
- Tests: `vp test`
|
|
47
|
+
{{#if isPackage}}
|
|
48
|
+
- Pack library: `vp pack`
|
|
49
|
+
- Watch mode: `vp run dev`
|
|
50
|
+
{{/if}}
|
|
51
|
+
|
|
52
|
+
## Debugging Approach
|
|
53
|
+
|
|
54
|
+
- Use `console.log` with clear prefixes during development
|
|
55
|
+
- **NEVER** attempt to run the dev server yourself
|
|
56
|
+
- If you need to debug, add `console.log` statements, tell the user where to look for the output (client, server, both, etc.), and they will paste back the results
|
|
57
|
+
|
|
58
|
+
## Node.js
|
|
59
|
+
|
|
60
|
+
- Always use the `node:` prefix for Node built-in module imports
|
|
61
|
+
- Format code with `vp fmt` before committing
|
|
62
|
+
- Run `vp run typecheck` and fix all info, warnings, and errors
|
|
63
|
+
- Prefer `unknown` over `any` unless absolutely necessary
|
|
64
|
+
- Use JSDoc/TSDoc comments for public APIs
|
|
65
|
+
- Document important lines of code with a single line comment
|
|
66
|
+
- Prefer `jsr:@std/*` for standard library packages
|
|
67
|
+
- Use relative imports for local modules
|
|
68
|
+
- Prefer import aliases (`~/*`) over deep relative imports
|
|
69
|
+
- Validate environment variables at startup using `node:assert`
|
|
70
|
+
|
|
71
|
+
{{#if isReactRouter}}
|
|
72
|
+
## React 19
|
|
73
|
+
|
|
74
|
+
### Document Metadata in Components
|
|
75
|
+
|
|
76
|
+
React 19 supports rendering `<title>`, `<meta>`, and `<link>` tags directly inside components. React hoists them to `<head>` automatically:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
export function ProductPage({ product }) {
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<title>`${product.name} | Store`</title>
|
|
83
|
+
<meta name="description" content={product.description} />
|
|
84
|
+
<link rel="canonical" href={`/products/${product.id}`} />
|
|
85
|
+
<main>
|
|
86
|
+
<h1>{product.name}</h1>
|
|
87
|
+
</main>
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `use()` for Reading Promises and Context
|
|
94
|
+
|
|
95
|
+
`use()` reads the value of a promise or context. Unlike hooks, `use()` can be called inside loops and conditionals:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
import { use } from "react";
|
|
99
|
+
|
|
100
|
+
// Reading a promise (suspends until resolved)
|
|
101
|
+
function ProductDetails({ productPromise }) {
|
|
102
|
+
let product = use(productPromise);
|
|
103
|
+
return <h1>{product.name}</h1>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// With TanStack Query, use useQuery alongside use() for query promises:
|
|
107
|
+
import { useQuery } from "@tanstack/react-query";
|
|
108
|
+
|
|
109
|
+
function ProductDetails({ queryOptions }) {
|
|
110
|
+
let { promise } = useQuery(queryOptions);
|
|
111
|
+
let product = use(promise);
|
|
112
|
+
|
|
113
|
+
return <h1>{product.name}</h1>;
|
|
114
|
+
}
|
|
115
|
+
{{#if isRSC}}
|
|
116
|
+
|
|
117
|
+
// Reading data from a server component in a client component:
|
|
118
|
+
// Server component passes a promise as a prop
|
|
119
|
+
export async function ServerComponent() {
|
|
120
|
+
let dataPromise = fetchData(); // Don't await - pass as promise
|
|
121
|
+
return <ClientDisplay dataPromise={dataPromise} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Client component uses use() to read it
|
|
125
|
+
"use client";
|
|
126
|
+
function ClientDisplay({ dataPromise }) {
|
|
127
|
+
let data = use(dataPromise);
|
|
128
|
+
return <div>{data.value}</div>;
|
|
129
|
+
}
|
|
130
|
+
{{/if}}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `useActionState()` for Form Actions
|
|
134
|
+
|
|
135
|
+
`useActionState()` manages form submission state with automatic pending tracking:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
"use client";
|
|
139
|
+
|
|
140
|
+
import { useActionState } from "react";
|
|
141
|
+
|
|
142
|
+
function LoginForm() {
|
|
143
|
+
let [state, action, isPending] = useActionState(
|
|
144
|
+
async (prevState, formData: FormData) => {
|
|
145
|
+
let result = await login(formData);
|
|
146
|
+
if (result.error) return { error: result.error };
|
|
147
|
+
return { success: true };
|
|
148
|
+
},
|
|
149
|
+
{ error: null },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<form action={action}>
|
|
154
|
+
<input name="email" type="email" />
|
|
155
|
+
<input name="password" type="password" />
|
|
156
|
+
{state.error && <p>{state.error}</p>}
|
|
157
|
+
<button disabled={isPending} type="submit">
|
|
158
|
+
{isPending ? "Logging in..." : "Log in"}
|
|
159
|
+
</button>
|
|
160
|
+
</form>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `useOptimistic()` for Optimistic Updates
|
|
166
|
+
|
|
167
|
+
`useOptimistic()` shows a temporary state while an async action is pending:
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
"use client";
|
|
171
|
+
|
|
172
|
+
import { useOptimistic, useActionState } from "react";
|
|
173
|
+
|
|
174
|
+
function CartItem({ item, updateQuantity }) {
|
|
175
|
+
let [optimisticQuantity, setOptimisticQuantity] = useOptimistic(
|
|
176
|
+
item.quantity,
|
|
177
|
+
(_current, newQuantity: number) => newQuantity,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
let [, action, isPending] = useActionState(
|
|
181
|
+
async (_prev, formData: FormData) => {
|
|
182
|
+
let newQuantity = Number(formData.get("quantity"));
|
|
183
|
+
setOptimisticQuantity(newQuantity);
|
|
184
|
+
return updateQuantity(item.id, newQuantity);
|
|
185
|
+
},
|
|
186
|
+
null,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<form action={action}>
|
|
191
|
+
<input
|
|
192
|
+
defaultValue={optimisticQuantity}
|
|
193
|
+
name="quantity"
|
|
194
|
+
type="number"
|
|
195
|
+
/>
|
|
196
|
+
<span style=\{{ opacity: isPending ? 0.5 : 1 }}>{item.name}</span>
|
|
197
|
+
</form>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `<form action={fn}>` for Direct Form Actions
|
|
203
|
+
|
|
204
|
+
React 19 allows passing a function directly to a form's `action` prop. This works with both client functions and server actions:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// Client-side action
|
|
208
|
+
function SearchForm() {
|
|
209
|
+
let navigate = useNavigate();
|
|
210
|
+
|
|
211
|
+
async function handleSearch(formData: FormData) {
|
|
212
|
+
let query = formData.get("q") as string;
|
|
213
|
+
navigate(`/search?q=${query}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<form action={handleSearch}>
|
|
218
|
+
<input name="q" placeholder="Search..." type="search" />
|
|
219
|
+
<button type="submit">Search</button>
|
|
220
|
+
</form>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
{{#if isRSC}}
|
|
224
|
+
|
|
225
|
+
// Server action (RSC)
|
|
226
|
+
export async function ServerComponent() {
|
|
227
|
+
async function createItem(formData: FormData) {
|
|
228
|
+
"use server";
|
|
229
|
+
let name = formData.get("name") as string;
|
|
230
|
+
await db.insert({ name });
|
|
231
|
+
redirect("/items");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<form action={createItem}>
|
|
236
|
+
<input name="name" />
|
|
237
|
+
<button type="submit">Create</button>
|
|
238
|
+
</form>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
{{/if}}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## React Router
|
|
245
|
+
|
|
246
|
+
### Critical Package Guidelines
|
|
247
|
+
|
|
248
|
+
**Use these packages:**
|
|
249
|
+
|
|
250
|
+
- `react-router` - Main package for routing components and hooks
|
|
251
|
+
- `@react-router/dev` - Development tools and route configuration
|
|
252
|
+
- `@cloudflare/vite-plugin` - Cloudflare server adapter
|
|
253
|
+
|
|
254
|
+
**NEVER use these packages:**
|
|
255
|
+
|
|
256
|
+
- `react-router-dom` - Legacy package, use `react-router` instead
|
|
257
|
+
- `@remix-run/*` - Old packages, replaced by `@react-router/*`
|
|
258
|
+
- React Router v6 patterns - Completely different architecture
|
|
259
|
+
|
|
260
|
+
### Hooks
|
|
261
|
+
|
|
262
|
+
These hooks are acceptable when needed:
|
|
263
|
+
|
|
264
|
+
- `useLocation`, `useNavigation`, `useNavigate`
|
|
265
|
+
- `useParams`, `useSearchParams`
|
|
266
|
+
- `useSubmit` (for `GET` actions only)
|
|
267
|
+
- `useMatch` / `useMatches`
|
|
268
|
+
- `useRevalidator`
|
|
269
|
+
|
|
270
|
+
**NEVER use these hooks** (data should flow through loaders, actions, and server components instead):
|
|
271
|
+
|
|
272
|
+
- `useLoaderData`, `useActionData`
|
|
273
|
+
- `useAsyncValue`, `useAsyncError`
|
|
274
|
+
- `useFormAction`
|
|
275
|
+
- `useFetcher`
|
|
276
|
+
|
|
277
|
+
### Route Configuration (`app/routes.ts`)
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
|
281
|
+
|
|
282
|
+
export default [
|
|
283
|
+
index("routes/home.tsx"),
|
|
284
|
+
route("about", "routes/about.tsx"),
|
|
285
|
+
route("products/:id", "routes/product.tsx", [
|
|
286
|
+
index("routes/product-overview.tsx"),
|
|
287
|
+
route("reviews", "routes/product-reviews.tsx"),
|
|
288
|
+
]),
|
|
289
|
+
] satisfies RouteConfig;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Route Type Imports
|
|
293
|
+
|
|
294
|
+
**ALWAYS use `./+types/[routeName]` for route type imports.**
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// CORRECT - always use this pattern:
|
|
298
|
+
import type { Route } from "./+types/product-details";
|
|
299
|
+
|
|
300
|
+
// WRONG - NEVER use relative parent paths:
|
|
301
|
+
// import type { Route } from "../+types/product-details";
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**If you see TypeScript errors about missing `./+types/[routeName]` modules:**
|
|
305
|
+
|
|
306
|
+
1. **IMMEDIATELY run `vp run typecheck`** to generate the types
|
|
307
|
+
2. **Or run `vp run typegen:react-router`** to generate types
|
|
308
|
+
3. **NEVER try to "fix" it by changing the import path**
|
|
309
|
+
|
|
310
|
+
Types are generated by `@react-router/dev` in `./+types/[routeName]` relative to each route file. The dev server also generates types automatically if the user is running it.
|
|
311
|
+
|
|
312
|
+
### Type-Safe URL Generation with `href()`
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
import { Link, href } from "react-router";
|
|
316
|
+
|
|
317
|
+
// Static routes
|
|
318
|
+
<Link to={href("/products/new")}>New Product</Link>
|
|
319
|
+
|
|
320
|
+
// Dynamic routes - automatic type safety
|
|
321
|
+
<Link to={href("/products/:id", { id: product.id })}>View</Link>
|
|
322
|
+
|
|
323
|
+
// Works with redirects too
|
|
324
|
+
return redirect(href("/products/:id", { id: newProduct.id }));
|
|
325
|
+
|
|
326
|
+
// NEVER manually construct URLs:
|
|
327
|
+
// <Link to={`/products/${product.id}`}>Product</Link> // WRONG
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
{{#if isRSC}}
|
|
331
|
+
### Route Module Pattern (RSC)
|
|
332
|
+
|
|
333
|
+
Routes export `ServerComponent` instead of `default`. All route components (`ServerComponent`, `ErrorBoundary`, `HydrateFallback`, `Layout`) are server components.
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
import type { Route } from "./+types/route";
|
|
337
|
+
import { Outlet } from "react-router";
|
|
338
|
+
|
|
339
|
+
export async function ServerComponent({ loaderData, params }: Route.ComponentProps) {
|
|
340
|
+
let product = await getProduct(params.id);
|
|
341
|
+
|
|
342
|
+
async function buyProduct(formData: FormData) {
|
|
343
|
+
"use server";
|
|
344
|
+
await updateProduct(formData);
|
|
345
|
+
return redirect(href("/products/:id", { id: params.id }));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<form action={buyProduct}>
|
|
350
|
+
<h2>{product.name}</h2>
|
|
351
|
+
<button type="submit">Buy Now</button>
|
|
352
|
+
</form>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
For client interactivity, extract to `"use client"` modules:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// counter.tsx
|
|
361
|
+
"use client";
|
|
362
|
+
|
|
363
|
+
import { useState } from "react";
|
|
364
|
+
|
|
365
|
+
export function Counter() {
|
|
366
|
+
let [count, setCount] = useState(0);
|
|
367
|
+
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
// route.tsx
|
|
373
|
+
import { Counter } from "./counter.tsx";
|
|
374
|
+
|
|
375
|
+
export function ServerComponent() {
|
|
376
|
+
return (
|
|
377
|
+
<>
|
|
378
|
+
<h1>Counter</h1>
|
|
379
|
+
<Counter />
|
|
380
|
+
</>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### `.server`/`.client` Module Naming
|
|
386
|
+
|
|
387
|
+
Support for `.server.ts(x)` and `.client.ts(x)` file naming is **not supported** in RSC Framework Mode. Use the `"server-only"` and `"client-only"` side-effect imports instead:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// app/utils/db.ts
|
|
391
|
+
import "server-only";
|
|
392
|
+
|
|
393
|
+
// This module will cause a build error if accidentally imported on the client
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
> `@vitejs/plugin-rsc` handles these imports internally — the `server-only` and `client-only` npm packages do not need to be installed.
|
|
397
|
+
|
|
398
|
+
### MDX Route Support
|
|
399
|
+
|
|
400
|
+
MDX routes are supported with `@mdx-js/rollup` v3.1.1+. Components exported from MDX routes must be valid in RSC environments (no client hooks). Extract interactive components to `"use client"` modules.
|
|
401
|
+
|
|
402
|
+
### Unsupported RSC Config Options
|
|
403
|
+
|
|
404
|
+
The following `react-router.config.ts` options are not yet supported in RSC Framework Mode:
|
|
405
|
+
|
|
406
|
+
- `buildEnd`, `prerender`, `presets`, `routeDiscovery`, `serverBundles`
|
|
407
|
+
- `ssr: false` (SPA Mode)
|
|
408
|
+
- `future.unstable_splitRouteModules`, `future.unstable_subResourceIntegrity`
|
|
409
|
+
|
|
410
|
+
{{else}}
|
|
411
|
+
### Route Module Pattern
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
import type { Route } from "./+types/product";
|
|
415
|
+
|
|
416
|
+
{{#if isSPA}}
|
|
417
|
+
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
|
418
|
+
let product = await fetchProduct(params.id);
|
|
419
|
+
return { product };
|
|
420
|
+
}
|
|
421
|
+
{{else}}
|
|
422
|
+
export async function loader({ params }: Route.LoaderArgs) {
|
|
423
|
+
return { product: await getProduct(params.id) };
|
|
424
|
+
}
|
|
425
|
+
{{/if}}
|
|
426
|
+
|
|
427
|
+
export default function Product({ loaderData }: Route.ComponentProps) {
|
|
428
|
+
return <h1>{loaderData.product.name}</h1>;
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
{{/if}}
|
|
433
|
+
### Layout Routes with `<Outlet />`
|
|
434
|
+
|
|
435
|
+
For layout routes that have child routes, **ALWAYS** use `<Outlet />` to render child routes:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
import { Outlet } from "react-router";
|
|
439
|
+
|
|
440
|
+
{{#if isRSC}}
|
|
441
|
+
export function ServerComponent() {
|
|
442
|
+
{{else}}
|
|
443
|
+
export default function Layout() {
|
|
444
|
+
{{/if}}
|
|
445
|
+
return (
|
|
446
|
+
<div className="layout">
|
|
447
|
+
<nav>{/* Navigation */}</nav>
|
|
448
|
+
<main>
|
|
449
|
+
<Outlet />
|
|
450
|
+
</main>
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Error Boundaries
|
|
457
|
+
|
|
458
|
+
Only set up `ErrorBoundary` for routes if the user explicitly asks. All errors bubble up to the `ErrorBoundary` in `root.tsx` by default.
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { isRouteErrorResponse } from "react-router";
|
|
462
|
+
|
|
463
|
+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
464
|
+
if (isRouteErrorResponse(error)) {
|
|
465
|
+
return (
|
|
466
|
+
<div>
|
|
467
|
+
<h1>{error.status} {error.statusText}</h1>
|
|
468
|
+
<p>{error.data}</p>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<div>
|
|
475
|
+
<h1>Oops!</h1>
|
|
476
|
+
<p>{error.message}</p>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### File Organization
|
|
483
|
+
|
|
484
|
+
React Router uses **explicit route configuration** in `app/routes.ts`. You are NOT constrained by file-based routing conventions. Use descriptive, kebab-case file names organized by feature.
|
|
485
|
+
|
|
486
|
+
### Correct Imports
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { Link, Form, Outlet, NavLink } from "react-router";
|
|
490
|
+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
|
491
|
+
import { data, redirect, href } from "react-router";
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
{{#if isSPA}}
|
|
495
|
+
### SPA Mode
|
|
496
|
+
|
|
497
|
+
This project runs as a single-page application with `ssr: false` in `react-router.config.ts`. All rendering happens on the client. Use `clientLoader` for data loading.
|
|
498
|
+
|
|
499
|
+
{{#if hasConvex}}
|
|
500
|
+
### Convex Integration
|
|
501
|
+
|
|
502
|
+
The app uses Convex with TanStack Query via `@convex-dev/react-query`. The provider stack is in `root.tsx`:
|
|
503
|
+
|
|
504
|
+
- `ConvexProvider` wraps the app with `ConvexReactClient`
|
|
505
|
+
- `QueryClientProvider` wraps with a `QueryClient` configured with Convex's hash and query functions
|
|
506
|
+
- Use `queryClient.ensureQueryData()` in `clientLoader` to pre-populate the cache
|
|
507
|
+
{{/if}}
|
|
508
|
+
|
|
509
|
+
{{/if}}
|
|
510
|
+
{{#if isSSR}}
|
|
511
|
+
### SSR Mode
|
|
512
|
+
|
|
513
|
+
This project uses server-side rendering deployed to Cloudflare Workers.
|
|
514
|
+
|
|
515
|
+
{{#if hasConvex}}
|
|
516
|
+
### Convex Integration
|
|
517
|
+
|
|
518
|
+
The app uses Convex with TanStack Query via middleware-based `QueryClient` hydration:
|
|
519
|
+
|
|
520
|
+
- `middleware.ts` - Sets up the query client per request via `setQueryClient`
|
|
521
|
+
- `query-preloader.ts` - Provides `createPreloader` for server-side data loading and `useDehydratedState` for client hydration
|
|
522
|
+
- `lib/client.ts` - Creates the base `QueryClient` with Convex integration
|
|
523
|
+
|
|
524
|
+
**Route pattern with createPreloader:**
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { createPreloader } from "~/tanstack-query-integration/query-preloader.ts";
|
|
528
|
+
|
|
529
|
+
export const loader = createPreloader<Route.LoaderArgs, void>(async ({ preload }) => {
|
|
530
|
+
await preload(yourQueryOptions);
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
{{/if}}
|
|
534
|
+
|
|
535
|
+
{{/if}}
|
|
536
|
+
{{/if}}
|
|
537
|
+
## General Rules
|
|
538
|
+
|
|
539
|
+
### Correctness
|
|
540
|
+
|
|
541
|
+
- Remove anything that isn't used - unused imports, parameters, and members
|
|
542
|
+
{{#if isReactRouter}}
|
|
543
|
+
- Exhaust every hook dependency in React hook dependency arrays
|
|
544
|
+
- Call React hooks only at the top level of a component (never in loops, conditions, or nested functions)
|
|
545
|
+
{{/if}}
|
|
546
|
+
- Use only valid CSS selectors - real pseudo-classes, pseudo-elements, and type selectors only
|
|
547
|
+
|
|
548
|
+
### Suspicious Code
|
|
549
|
+
|
|
550
|
+
- Skip the `any` shortcut - prefer precise TypeScript types
|
|
551
|
+
{{#if isReactRouter}}
|
|
552
|
+
- Hands off `document.cookie` - manipulating cookies directly is forbidden; use React Router's cookie utilities instead
|
|
553
|
+
{{/if}}
|
|
554
|
+
|
|
555
|
+
### Performance
|
|
556
|
+
|
|
557
|
+
- Compile regexes once - declare regular expressions at module scope, not inside hot functions
|
|
558
|
+
|
|
559
|
+
### Style & Consistency
|
|
560
|
+
|
|
561
|
+
- Stick to ES modules - no `require` or CommonJS patterns
|
|
562
|
+
- Prefer `import type` for type-only imports
|
|
563
|
+
- Use `node:` protocol for Node.js built-in imports
|
|
564
|
+
- Use `T[]` shorthand array syntax
|
|
565
|
+
- Don't reassign function parameters
|
|
566
|
+
- Favor `let` over `const` for bindings, unless declaring a top-level constant with ALL_CAPS
|
|
567
|
+
- One `let` per line - declare variables individually
|
|
568
|
+
- Skip non-null assertions - rewrite code so `!` isn't necessary
|
|
569
|
+
- Avoid `enum` - choose unions, objects, or literal types
|
|
570
|
+
- Use `trimStart`/`trimEnd` instead of `trimLeft`/`trimRight`
|
|
571
|
+
- Default parameters go last
|
|
572
|
+
{{#if isReactRouter}}
|
|
573
|
+
- Self-close empty JSX elements (`<Component />` not `<Component></Component>`)
|
|
574
|
+
- Export only the component and whitelisted helpers (`loader`, `action`, `meta`, etc.) from route files
|
|
575
|
+
{{/if}}
|
|
576
|
+
- No unused template literals - convert to quotes if not interpolating
|
|
577
|
+
- Use `slice` instead of `substr`
|
|
578
|
+
- Flatten simple `if`/`else if` chains
|
|
579
|
+
- Kill useless `else` blocks when the `if` branch returns or throws
|
|
580
|
+
- Leverage `as const` for immutability where appropriate
|
|
581
|
+
|
|
582
|
+
## Using Vite+
|
|
583
|
+
|
|
584
|
+
This project uses Vite+, a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`.
|
|
585
|
+
|
|
586
|
+
### Common Pitfalls
|
|
587
|
+
|
|
588
|
+
- **Using the package manager directly:** Do not use pnpm, npm, or Yarn directly. Vite+ handles all package manager operations.
|
|
589
|
+
- **Always use Vite commands to run tools:** Don't attempt to run `vp vitest` or `vp oxlint`. Use `vp test` and `vp lint` instead.
|
|
590
|
+
- **Running scripts:** Vite+ commands take precedence over `package.json` scripts. If there is a conflict, run scripts using `vp run <script>`.
|
|
591
|
+
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly:** Vite+ wraps these tools.
|
|
592
|
+
- **Use vpx for one-off binaries:** Use `vpx` instead of `npx`, `pnpm dlx`, or `vp dlx`.
|
|
593
|
+
- **Import from `vite-plus`:** Instead of importing from `vite` or `vitest`, import from `vite-plus`. For example: `import { defineConfig } from 'vite-plus'` or `import { expect, test } from 'vite-plus/test'`.
|
|
594
|
+
- **Type-Aware Linting:** No need to install `oxlint-tsgolint`, `vp lint --type-aware` works out of the box.
|
|
595
|
+
|
|
596
|
+
### Review Checklist
|
|
597
|
+
|
|
598
|
+
- [ ] Run `vp install` after pulling remote changes and before getting started
|
|
599
|
+
- [ ] Run `vp check` and `vp test` to validate changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# {{repository}}
|
|
2
|
+
|
|
3
|
+
{{#if isSPA}}
|
|
4
|
+
A React Router single-page application{{#if hasConvex}} with Convex and TanStack Query{{/if}}.
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if isSSR}}
|
|
7
|
+
A server-rendered React Router application{{#if hasConvex}} with Convex and TanStack Query{{/if}}.
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if isRSC}}
|
|
10
|
+
A React Router application with React Server Components{{#if hasContentLayer}} and content-layer{{/if}}.
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if isPackage}}
|
|
13
|
+
A TypeScript package.
|
|
14
|
+
{{/if}}
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
{{#if isReactRouter}}
|
|
20
|
+
vp dev
|
|
21
|
+
{{else}}
|
|
22
|
+
vp run dev
|
|
23
|
+
{{/if}}
|
|
24
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
{{#if isPackage}}
|
|
3
|
+
"name": "@{{owner}}/{{repository}}",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
{{else}}
|
|
6
|
+
"name": "{{repository}}",
|
|
7
|
+
"private": true,
|
|
8
|
+
{{/if}}
|
|
9
|
+
"type": "module",
|
|
10
|
+
{{#if isPackage}}
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"prepublishOnly": "vp run build"
|
|
21
|
+
},
|
|
22
|
+
{{else}}
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "vpx react-router typegen"
|
|
25
|
+
},
|
|
26
|
+
{{/if}}
|
|
27
|
+
"packageManager": "pnpm@10.32.1",
|
|
28
|
+
"pnpm": {
|
|
29
|
+
{{#if isReactRouter}}
|
|
30
|
+
"onlyBuiltDependencies": [
|
|
31
|
+
"esbuild",
|
|
32
|
+
"sharp",
|
|
33
|
+
"workerd"
|
|
34
|
+
],
|
|
35
|
+
{{/if}}
|
|
36
|
+
"overrides": {
|
|
37
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
38
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|