create-reactor 0.1.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 +123 -0
- package/create-app.mjs +712 -0
- package/lib/build.mjs +434 -0
- package/lib/pm.mjs +85 -0
- package/lib/presets.mjs +122 -0
- package/lib/templates/ai-docs.mjs +80 -0
- package/lib/templates/app.mjs +961 -0
- package/lib/templates/backend.mjs +715 -0
- package/lib/templates/base.mjs +671 -0
- package/lib/templates/biome.mjs +107 -0
- package/lib/templates/extras.mjs +360 -0
- package/lib/templates/features.mjs +463 -0
- package/lib/templates/quality.mjs +159 -0
- package/lib/templates/readme.mjs +351 -0
- package/lib/templates/security.mjs +70 -0
- package/lib/templates/server.mjs +141 -0
- package/lib/templates/state.mjs +192 -0
- package/package.json +52 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Generated project README.md and the post-create "next steps" checklist.
|
|
2
|
+
import { AI_MODELS } from "./backend.mjs";
|
|
3
|
+
import { aiLabel, stackList } from "./app.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ordered list of manual steps the user must do after generation.
|
|
7
|
+
* Each entry: { title, details: string[] } (details are shell commands or notes).
|
|
8
|
+
*/
|
|
9
|
+
export function nextSteps(c) {
|
|
10
|
+
const steps = [];
|
|
11
|
+
|
|
12
|
+
if (!c.install) {
|
|
13
|
+
steps.push({
|
|
14
|
+
title: "Install dependencies",
|
|
15
|
+
details: [`cd ${c.name}`, c.pmInstallLabel],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (c.backend === "convex") {
|
|
20
|
+
steps.push({
|
|
21
|
+
title: "Connect Convex (one-time: logs in, creates your dev deployment)",
|
|
22
|
+
details: [`cd ${c.name}`, c.pmRunLabel("setup")],
|
|
23
|
+
});
|
|
24
|
+
steps.push({
|
|
25
|
+
title: "Import sample data into the tasks table (optional)",
|
|
26
|
+
details: [`${c.pmDlxLabel("convex")} import --table tasks sampleData.jsonl`],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (c.backend === "supabase") {
|
|
31
|
+
steps.push({
|
|
32
|
+
title: "Create a Supabase project and copy the API credentials",
|
|
33
|
+
details: [
|
|
34
|
+
"https://supabase.com/dashboard -> New project",
|
|
35
|
+
"Project Settings -> API -> copy URL and anon key into .env.local",
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (c.auth === "better-auth") {
|
|
41
|
+
steps.push({
|
|
42
|
+
title: "Configure Better Auth on your Convex deployment",
|
|
43
|
+
details: [
|
|
44
|
+
`${c.pmDlxLabel("convex")} env set BETTER_AUTH_SECRET <random-32+-char-string>`,
|
|
45
|
+
`${c.pmDlxLabel("convex")} env set SITE_URL http://localhost:5173`,
|
|
46
|
+
"Then fill VITE_CONVEX_SITE_URL in .env.local (your VITE_CONVEX_URL with .cloud -> .site)",
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (c.auth === "clerk") {
|
|
52
|
+
const details = [
|
|
53
|
+
"https://dashboard.clerk.com -> Create application",
|
|
54
|
+
"Copy the Publishable Key into .env.local as VITE_CLERK_PUBLISHABLE_KEY",
|
|
55
|
+
];
|
|
56
|
+
if (c.backend === "convex") {
|
|
57
|
+
details.push(
|
|
58
|
+
'Clerk Dashboard -> JWT templates -> New template -> "Convex" (keep the name "convex")',
|
|
59
|
+
"Copy the template's Issuer URL, then run:",
|
|
60
|
+
`${c.pmDlxLabel("convex")} env set CLERK_JWT_ISSUER_DOMAIN <issuer-url>`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
steps.push({ title: "Set up Clerk", details });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (c.auth === "convex-auth") {
|
|
67
|
+
steps.push({
|
|
68
|
+
title: "Generate Convex Auth keys (one-time)",
|
|
69
|
+
details: [
|
|
70
|
+
`${c.pmDlxLabel("@convex-dev/auth")}`,
|
|
71
|
+
"(this sets JWT_PRIVATE_KEY / JWKS on your Convex deployment)",
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (c.auth === "supabase-auth") {
|
|
77
|
+
steps.push({
|
|
78
|
+
title: "Enable an auth provider in Supabase",
|
|
79
|
+
details: [
|
|
80
|
+
"Supabase Dashboard -> Authentication -> Providers -> enable GitHub (or email)",
|
|
81
|
+
"The starter's sign-in button uses the GitHub OAuth provider",
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (c.orm !== "none") {
|
|
87
|
+
const ormName = c.orm === "drizzle" ? "Drizzle" : "Prisma";
|
|
88
|
+
const dbDetails = {
|
|
89
|
+
neon: [
|
|
90
|
+
"Create a free Postgres project at https://neon.tech",
|
|
91
|
+
"Copy the connection string into DATABASE_URL in .env",
|
|
92
|
+
],
|
|
93
|
+
docker: ["docker compose up -d # starts local Postgres (DATABASE_URL in .env is pre-filled)"],
|
|
94
|
+
turso: [
|
|
95
|
+
"Local dev works out of the box (TURSO_DATABASE_URL=file:./local.db in .env)",
|
|
96
|
+
"For a hosted DB: install the Turso CLI, run `turso db create`, fill the URL + auth token in .env",
|
|
97
|
+
],
|
|
98
|
+
supabase: ["Supabase -> Project Settings -> Database -> copy the connection string into DATABASE_URL in .env"],
|
|
99
|
+
other: ["Set DATABASE_URL in .env to your Postgres connection string"],
|
|
100
|
+
}[c.dbProvider ?? "other"];
|
|
101
|
+
steps.push({
|
|
102
|
+
title: `Point ${ormName} at your database`,
|
|
103
|
+
details: [...dbDetails, `${c.pmRunLabel("db:push")} # push the starter schema to your database`],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (c.extras.includes("redis")) {
|
|
108
|
+
steps.push({
|
|
109
|
+
title: "Create an Upstash Redis database (rate limiting / caching)",
|
|
110
|
+
details: [
|
|
111
|
+
"https://console.upstash.com -> Create database",
|
|
112
|
+
"Copy the REST URL + token into .env",
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (c.extras.includes("stripe")) {
|
|
118
|
+
steps.push({
|
|
119
|
+
title: "Add your Stripe keys",
|
|
120
|
+
details: [
|
|
121
|
+
"https://dashboard.stripe.com/test/apikeys",
|
|
122
|
+
"Publishable key -> VITE_STRIPE_PUBLISHABLE_KEY in .env.local",
|
|
123
|
+
"Secret key -> STRIPE_SECRET_KEY in .env (server-side only)",
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (c.extras.includes("resend")) {
|
|
129
|
+
steps.push({
|
|
130
|
+
title: "Add your Resend API key (transactional email)",
|
|
131
|
+
details: [
|
|
132
|
+
"https://resend.com/api-keys -> Create API key -> RESEND_API_KEY in .env",
|
|
133
|
+
"Email templates live in src/emails/ — send them from your backend",
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (c.extras.includes("posthog")) {
|
|
139
|
+
steps.push({
|
|
140
|
+
title: "Connect PostHog analytics (optional)",
|
|
141
|
+
details: ["https://app.posthog.com -> Project Settings -> copy the API key into VITE_POSTHOG_KEY in .env.local"],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (c.extras.includes("e2e")) {
|
|
146
|
+
steps.push({
|
|
147
|
+
title: "Install Playwright browsers (one-time, ~100MB)",
|
|
148
|
+
details: [`${c.pmDlxLabel("playwright")} install chromium`, `Then run E2E tests with: ${c.pmRunLabel("test:e2e")}`],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (c.extras.includes("deploy")) {
|
|
153
|
+
steps.push({
|
|
154
|
+
title: "Deploy when ready",
|
|
155
|
+
details: [
|
|
156
|
+
"Vercel: vercel deploy (vercel.json is configured)",
|
|
157
|
+
"Netlify: netlify deploy (netlify.toml is configured)",
|
|
158
|
+
`Docker: docker build -t ${c.name} . && docker run -p 8080:80 ${c.name}`,
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (c.ai !== "none") {
|
|
164
|
+
const ai = AI_MODELS[c.ai];
|
|
165
|
+
if (c.backend === "convex") {
|
|
166
|
+
steps.push({
|
|
167
|
+
title: `Set your ${aiLabel(c.ai)} API key on the Convex deployment`,
|
|
168
|
+
details: [
|
|
169
|
+
`Get a key: ${ai.keysUrl}`,
|
|
170
|
+
`${c.pmDlxLabel("convex")} env set ${ai.envKey} <your-key>`,
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
steps.push({
|
|
175
|
+
title: `Set ${ai.envKey} in .env.local`,
|
|
176
|
+
details: [
|
|
177
|
+
`Get a key: ${ai.keysUrl}`,
|
|
178
|
+
"See examples/ai.ts — AI calls must run server-side",
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
steps.push({
|
|
185
|
+
title: c.backend === "hono" ? "Start the dev servers (Vite + tRPC API together)" : "Start the dev server",
|
|
186
|
+
details: [`cd ${c.name}`, c.pmRunLabel("dev"), "Open http://localhost:5173"],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return steps;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function projectReadme(c) {
|
|
193
|
+
const stack = stackList(c);
|
|
194
|
+
const steps = nextSteps(c);
|
|
195
|
+
|
|
196
|
+
const stepsMd = steps
|
|
197
|
+
.map((s, i) => {
|
|
198
|
+
const details = s.details.map((d) => (d.startsWith("http") ? d : "```sh\n" + d + "\n```")).join("\n");
|
|
199
|
+
return `### ${i + 1}. ${s.title}\n\n${details}`;
|
|
200
|
+
})
|
|
201
|
+
.join("\n\n");
|
|
202
|
+
|
|
203
|
+
const scripts = [
|
|
204
|
+
["`dev`", c.backend === "convex" ? "Run Vite + Convex dev servers together" : "Start the Vite dev server"],
|
|
205
|
+
["`build`", "Production build + typecheck"],
|
|
206
|
+
["`preview`", "Preview the production build locally"],
|
|
207
|
+
["`lint`", "Run ESLint"],
|
|
208
|
+
["`typecheck`", "Run TypeScript without emitting"],
|
|
209
|
+
];
|
|
210
|
+
if (c.backend === "convex") {
|
|
211
|
+
scripts.splice(1, 0, ["`setup`", "One-time Convex login + dev deployment provisioning"]);
|
|
212
|
+
}
|
|
213
|
+
if (c.orm === "drizzle") {
|
|
214
|
+
scripts.push(
|
|
215
|
+
["`db:generate`", "Generate SQL migrations from the Drizzle schema"],
|
|
216
|
+
["`db:migrate`", "Apply migrations"],
|
|
217
|
+
["`db:push`", "Push schema directly to the database (prototyping)"],
|
|
218
|
+
["`db:studio`", "Open Drizzle Studio"],
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (c.orm === "prisma") {
|
|
222
|
+
scripts.push(
|
|
223
|
+
["`db:push`", "Push the Prisma schema to the database"],
|
|
224
|
+
["`db:generate`", "Regenerate the Prisma client"],
|
|
225
|
+
["`db:studio`", "Open Prisma Studio"],
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (c.extras.includes("testing")) {
|
|
229
|
+
scripts.push(["`test`", "Run tests once (Vitest)"], ["`test:watch`", "Run tests in watch mode"]);
|
|
230
|
+
}
|
|
231
|
+
if (c.extras.includes("e2e")) {
|
|
232
|
+
scripts.push(["`test:e2e`", "Run Playwright E2E tests"], ["`test:e2e:ui`", "Playwright interactive UI mode"]);
|
|
233
|
+
}
|
|
234
|
+
if (c.extras.includes("fallow")) {
|
|
235
|
+
scripts.push(
|
|
236
|
+
["`quality`", "Fallow: dead code + duplication + complexity + health score"],
|
|
237
|
+
["`quality:audit`", "Fallow: audit only the code you changed (run before a PR)"],
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (c.extras.includes("knip")) {
|
|
241
|
+
scripts.push(["`knip`", "Find unused files, exports and dependencies"]);
|
|
242
|
+
}
|
|
243
|
+
if (c.extras.includes("prettier")) {
|
|
244
|
+
scripts.push(["`format`", "Format all files with Prettier"]);
|
|
245
|
+
}
|
|
246
|
+
if (c.extras.includes("husky")) {
|
|
247
|
+
scripts.push(["`prepare`", "Set up git hooks (runs automatically on install)"]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const structure = [];
|
|
251
|
+
if (c.extras.includes("ci")) structure.push(".github/workflows/ci.yml # lint + build + test on every push");
|
|
252
|
+
if (c.extras.includes("husky")) structure.push(".husky/ # git hooks (lint-staged on commit)");
|
|
253
|
+
if (c.backend === "hono") structure.push("server/ # Hono + tRPC API server (typed end-to-end)");
|
|
254
|
+
structure.push("src/");
|
|
255
|
+
if (c.router === "tanstack") {
|
|
256
|
+
structure.push(" routes/ # file-based routes (TanStack Router)");
|
|
257
|
+
structure.push(" routeTree.gen.ts # generated — do not edit");
|
|
258
|
+
}
|
|
259
|
+
structure.push(" components/ # app components");
|
|
260
|
+
structure.push(" ui/ # shadcn/ui components");
|
|
261
|
+
structure.push(" lib/ # utilities" + (c.backend === "supabase" ? " + supabase client" : ""));
|
|
262
|
+
if (c.auth === "supabase-auth") structure.push(" hooks/ # use-session and friends");
|
|
263
|
+
if (c.extras.includes("zustand")) structure.push(" stores/ # zustand stores");
|
|
264
|
+
if (c.extras.includes("testing")) structure.push(" test/ # vitest setup + sample tests");
|
|
265
|
+
structure.push(" main.tsx # entry: providers are composed here");
|
|
266
|
+
if (c.backend === "convex") {
|
|
267
|
+
structure.push("convex/ # backend functions, schema" + (c.auth !== "none" ? ", auth" : ""));
|
|
268
|
+
structure.push(" _generated/ # generated by `convex dev` — do not edit");
|
|
269
|
+
}
|
|
270
|
+
if (c.orm === "drizzle") {
|
|
271
|
+
structure.push("db/ # drizzle schema + server-side db client");
|
|
272
|
+
structure.push("drizzle.config.ts");
|
|
273
|
+
}
|
|
274
|
+
if (c.orm === "prisma") structure.push("prisma/ # prisma schema");
|
|
275
|
+
if (c.extras.includes("redis") && c.orm !== "drizzle") structure.push("db/ # upstash redis + rate limiter (server-side)");
|
|
276
|
+
if (c.dbProvider === "docker") structure.push("docker-compose.yml # local Postgres 17");
|
|
277
|
+
if (c.secure) {
|
|
278
|
+
const secFile = { bun: "bunfig.toml", pnpm: "pnpm-workspace.yaml", npm: ".npmrc" }[c.pm];
|
|
279
|
+
structure.push(`${secFile.padEnd(22)}# supply-chain protection (7-day package cooldown)`);
|
|
280
|
+
}
|
|
281
|
+
if (c.ai !== "none" && c.backend !== "convex") structure.push("examples/ai.ts # server-side AI SDK example");
|
|
282
|
+
|
|
283
|
+
const docLinks = [
|
|
284
|
+
"- [Vite](https://vite.dev)",
|
|
285
|
+
"- [Tailwind CSS](https://tailwindcss.com/docs)",
|
|
286
|
+
"- [shadcn/ui](https://ui.shadcn.com)",
|
|
287
|
+
];
|
|
288
|
+
if (c.router === "tanstack") docLinks.push("- [TanStack Router](https://tanstack.com/router)");
|
|
289
|
+
if (c.router === "react-router") docLinks.push("- [React Router](https://reactrouter.com)");
|
|
290
|
+
if (c.backend === "convex") docLinks.push("- [Convex](https://docs.convex.dev)");
|
|
291
|
+
if (c.backend === "supabase") docLinks.push("- [Supabase](https://supabase.com/docs)");
|
|
292
|
+
if (c.backend === "hono") docLinks.push("- [Hono](https://hono.dev)", "- [tRPC](https://trpc.io/docs)");
|
|
293
|
+
if (c.orm === "drizzle") docLinks.push("- [Drizzle ORM](https://orm.drizzle.team)");
|
|
294
|
+
if (c.orm === "prisma") docLinks.push("- [Prisma](https://www.prisma.io/docs)");
|
|
295
|
+
if (c.dbProvider === "neon") docLinks.push("- [Neon](https://neon.com/docs)");
|
|
296
|
+
if (c.dbProvider === "turso") docLinks.push("- [Turso](https://docs.turso.tech)");
|
|
297
|
+
if (c.auth === "clerk") docLinks.push("- [Clerk](https://clerk.com/docs)");
|
|
298
|
+
if (c.auth === "better-auth") docLinks.push("- [Better Auth](https://better-auth.com/docs)", "- [Convex + Better Auth](https://labs.convex.dev/better-auth)");
|
|
299
|
+
if (c.auth === "convex-auth") docLinks.push("- [Convex Auth](https://labs.convex.dev/auth)");
|
|
300
|
+
if (c.extras.includes("redis")) docLinks.push("- [Upstash Redis](https://upstash.com/docs/redis)");
|
|
301
|
+
if (c.ai !== "none") docLinks.push("- [AI SDK](https://ai-sdk.dev/docs)");
|
|
302
|
+
if (c.extras.includes("query")) docLinks.push("- [TanStack Query](https://tanstack.com/query)");
|
|
303
|
+
if (c.extras.includes("table")) docLinks.push("- [TanStack Table](https://tanstack.com/table)");
|
|
304
|
+
if (c.extras.includes("zustand")) docLinks.push("- [Zustand](https://zustand.docs.pmnd.rs)");
|
|
305
|
+
if (c.extras.includes("forms"))
|
|
306
|
+
docLinks.push("- [React Hook Form](https://react-hook-form.com) + [Zod](https://zod.dev)");
|
|
307
|
+
if (c.extras.includes("motion")) docLinks.push("- [Motion](https://motion.dev)");
|
|
308
|
+
if (c.extras.includes("dates")) docLinks.push("- [date-fns](https://date-fns.org)");
|
|
309
|
+
if (c.extras.includes("testing"))
|
|
310
|
+
docLinks.push("- [Vitest](https://vitest.dev) + [Testing Library](https://testing-library.com)");
|
|
311
|
+
if (c.extras.includes("e2e")) docLinks.push("- [Playwright](https://playwright.dev)");
|
|
312
|
+
if (c.extras.includes("fallow")) docLinks.push("- [Fallow](https://docs.fallow.tools)");
|
|
313
|
+
if (c.extras.includes("sentry")) docLinks.push("- [Sentry for React](https://docs.sentry.io/platforms/javascript/guides/react/)");
|
|
314
|
+
|
|
315
|
+
return `# ${c.name}
|
|
316
|
+
|
|
317
|
+
Generated with **create-reactor**.
|
|
318
|
+
|
|
319
|
+
${stack.map((s) => `\`${s}\``).join(" · ")}
|
|
320
|
+
|
|
321
|
+
## Getting started
|
|
322
|
+
|
|
323
|
+
${stepsMd}
|
|
324
|
+
|
|
325
|
+
## Scripts
|
|
326
|
+
|
|
327
|
+
| Script | What it does |
|
|
328
|
+
| ------ | ------------ |
|
|
329
|
+
${scripts.map(([k, v]) => `| ${k} | ${v} |`).join("\n")}
|
|
330
|
+
|
|
331
|
+
Run scripts with \`${c.pmRunLabel("<script>")}\`.
|
|
332
|
+
|
|
333
|
+
## Project structure
|
|
334
|
+
|
|
335
|
+
\`\`\`
|
|
336
|
+
${structure.join("\n")}
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
## Add more UI components
|
|
340
|
+
|
|
341
|
+
\`\`\`sh
|
|
342
|
+
${c.pmDlxLabel("shadcn@latest")} add dialog dropdown-menu table
|
|
343
|
+
\`\`\`
|
|
344
|
+
|
|
345
|
+
Browse all components at https://ui.shadcn.com/docs/components.
|
|
346
|
+
|
|
347
|
+
## Docs
|
|
348
|
+
|
|
349
|
+
${docLinks.join("\n")}
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Supply-chain security: minimum package release age ("cooldown") per package manager.
|
|
2
|
+
//
|
|
3
|
+
// Why: the 2025 npm supply-chain attacks (chalk/debug compromise, Shai-Hulud worm)
|
|
4
|
+
// were all detected and removed within hours/days of publish. Refusing to install
|
|
5
|
+
// brand-new package versions blocks this entire attack class.
|
|
6
|
+
//
|
|
7
|
+
// Units differ per package manager — do not copy values across:
|
|
8
|
+
// bun -> seconds (604800 = 7 days), bunfig.toml, requires bun >= 1.3
|
|
9
|
+
// pnpm -> minutes (10080 = 7 days), pnpm-workspace.yaml, requires pnpm >= 11
|
|
10
|
+
// npm -> days (7), .npmrc, requires npm >= 11.10
|
|
11
|
+
|
|
12
|
+
/** bunfig.toml (bun) */
|
|
13
|
+
export function bunfig() {
|
|
14
|
+
return `# Supply-chain protection: only install package versions published at least
|
|
15
|
+
# 7 days ago. Malicious package versions are almost always detected and removed
|
|
16
|
+
# within hours/days of publish — the cooldown blocks them entirely.
|
|
17
|
+
#
|
|
18
|
+
# Needs a package that was published today (e.g. an urgent security fix)?
|
|
19
|
+
# Add it to the exclusion list below.
|
|
20
|
+
#
|
|
21
|
+
# Requires bun >= 1.3 — older versions ignore these settings.
|
|
22
|
+
[install]
|
|
23
|
+
minimumReleaseAge = 604800 # seconds (7 days)
|
|
24
|
+
minimumReleaseAgeExcludes = []
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** pnpm-workspace.yaml (pnpm) */
|
|
29
|
+
export function pnpmWorkspace() {
|
|
30
|
+
return `# Supply-chain protection: only install package versions published at least
|
|
31
|
+
# 7 days ago (units: minutes). Malicious package versions are almost always
|
|
32
|
+
# detected and removed within hours/days of publish.
|
|
33
|
+
#
|
|
34
|
+
# Needs a package that was published today? Add it to the exclusion list.
|
|
35
|
+
#
|
|
36
|
+
# Requires pnpm >= 11 (pnpm 11 defaults to 1 day; this raises it to 7).
|
|
37
|
+
minimumReleaseAge: 10080
|
|
38
|
+
minimumReleaseAgeExclude: []
|
|
39
|
+
|
|
40
|
+
# Block a package if its trust level drops compared to earlier releases
|
|
41
|
+
trustPolicy: no-downgrade
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** .npmrc (npm) */
|
|
46
|
+
export function npmrc() {
|
|
47
|
+
return `# Supply-chain protection: only install package versions published at least
|
|
48
|
+
# 7 days ago. Requires npm >= 11.10 (older versions ignore this setting).
|
|
49
|
+
min-release-age=7
|
|
50
|
+
|
|
51
|
+
# Stricter (optional): refuse to run dependency postinstall scripts — the top
|
|
52
|
+
# malware vector. Uncomment if you want maximum protection; some packages
|
|
53
|
+
# (native bindings) may then need: npm rebuild <package>
|
|
54
|
+
# ignore-scripts=true
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns { filename, content } for the chosen package manager, or null. */
|
|
59
|
+
export function securityFile(pm) {
|
|
60
|
+
switch (pm) {
|
|
61
|
+
case "bun":
|
|
62
|
+
return { filename: "bunfig.toml", content: bunfig() };
|
|
63
|
+
case "pnpm":
|
|
64
|
+
return { filename: "pnpm-workspace.yaml", content: pnpmWorkspace() };
|
|
65
|
+
case "npm":
|
|
66
|
+
return { filename: ".npmrc", content: npmrc() };
|
|
67
|
+
default:
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Hono + tRPC v11 backend: a small typed API server living in server/,
|
|
2
|
+
// consumed by the Vite SPA through @trpc/tanstack-react-query.
|
|
3
|
+
|
|
4
|
+
/** server/router.ts — the tRPC router (this is where the API lives). */
|
|
5
|
+
export function trpcRouter() {
|
|
6
|
+
return `import { initTRPC } from "@trpc/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
const t = initTRPC.create();
|
|
10
|
+
|
|
11
|
+
/** Building blocks for your API — use these to add more routers/procedures. @public */
|
|
12
|
+
export const router = t.router;
|
|
13
|
+
/** @public */
|
|
14
|
+
export const publicProcedure = t.procedure;
|
|
15
|
+
|
|
16
|
+
export const appRouter = router({
|
|
17
|
+
hello: publicProcedure.input(z.string().nullish()).query(({ input }) => {
|
|
18
|
+
return { greeting: \`Hello \${input ?? "World"}!\` };
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
// Example list endpoint — replace with real data (database, external API, ...)
|
|
22
|
+
list: publicProcedure.query(() => {
|
|
23
|
+
return [
|
|
24
|
+
{ id: 1, name: "Ada Lovelace" },
|
|
25
|
+
{ id: 2, name: "Grace Hopper" },
|
|
26
|
+
{ id: 3, name: "Alan Turing" },
|
|
27
|
+
];
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type AppRouter = typeof appRouter;
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** server/index.ts — Hono server hosting the tRPC router. */
|
|
36
|
+
export function honoServer() {
|
|
37
|
+
return `import { serve } from "@hono/node-server";
|
|
38
|
+
import { trpcServer } from "@hono/trpc-server";
|
|
39
|
+
import { Hono } from "hono";
|
|
40
|
+
import { cors } from "hono/cors";
|
|
41
|
+
import { appRouter } from "./router";
|
|
42
|
+
|
|
43
|
+
const app = new Hono();
|
|
44
|
+
|
|
45
|
+
// CORS must be registered BEFORE the tRPC middleware.
|
|
46
|
+
// Tighten the origin list before deploying.
|
|
47
|
+
app.use(
|
|
48
|
+
"/trpc/*",
|
|
49
|
+
cors({
|
|
50
|
+
origin: "http://localhost:5173",
|
|
51
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
52
|
+
allowMethods: ["GET", "POST", "OPTIONS"],
|
|
53
|
+
credentials: true,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
app.use(
|
|
58
|
+
"/trpc/*",
|
|
59
|
+
trpcServer({
|
|
60
|
+
endpoint: "/trpc",
|
|
61
|
+
router: appRouter,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const port = 3001;
|
|
66
|
+
serve({ fetch: app.fetch, port });
|
|
67
|
+
console.log(\`tRPC server running on http://localhost:\${port}/trpc\`);
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** src/lib/trpc.ts — typed client + TanStack Query integration. */
|
|
72
|
+
export function trpcClient() {
|
|
73
|
+
return `import { QueryClient } from "@tanstack/react-query";
|
|
74
|
+
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
|
75
|
+
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
|
76
|
+
import type { AppRouter } from "../../server/router";
|
|
77
|
+
|
|
78
|
+
export const queryClient = new QueryClient();
|
|
79
|
+
|
|
80
|
+
const trpcClient = createTRPCClient<AppRouter>({
|
|
81
|
+
links: [httpBatchLink({ url: "http://localhost:3001/trpc" })],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
|
85
|
+
client: trpcClient,
|
|
86
|
+
queryClient,
|
|
87
|
+
});
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** src/components/trpc-demo.tsx — demo card calling the tRPC API. */
|
|
92
|
+
export function trpcDemo(c) {
|
|
93
|
+
return `import { useQuery } from "@tanstack/react-query";
|
|
94
|
+
import { trpc } from "@/lib/trpc";
|
|
95
|
+
import { Badge } from "@/components/ui/badge";
|
|
96
|
+
import {
|
|
97
|
+
Card,
|
|
98
|
+
CardContent,
|
|
99
|
+
CardDescription,
|
|
100
|
+
CardHeader,
|
|
101
|
+
CardTitle,
|
|
102
|
+
} from "@/components/ui/card";
|
|
103
|
+
|
|
104
|
+
export function TrpcDemo() {
|
|
105
|
+
const hello = useQuery(trpc.hello.queryOptions("tRPC"));
|
|
106
|
+
const list = useQuery(trpc.list.queryOptions());
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Card>
|
|
110
|
+
<CardHeader>
|
|
111
|
+
<CardTitle>tRPC API</CardTitle>
|
|
112
|
+
<CardDescription>
|
|
113
|
+
End-to-end typed API from <code>server/router.ts</code>.
|
|
114
|
+
</CardDescription>
|
|
115
|
+
</CardHeader>
|
|
116
|
+
<CardContent className="space-y-4">
|
|
117
|
+
{hello.isError ? (
|
|
118
|
+
<p className="text-muted-foreground text-sm">
|
|
119
|
+
Could not reach the API server. Start everything with{" "}
|
|
120
|
+
<code>${c.pmRunLabel("dev")}</code> (runs Vite + the tRPC server together).
|
|
121
|
+
</p>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
124
|
+
<p className="text-sm font-medium">
|
|
125
|
+
{hello.isPending ? "Loading…" : hello.data.greeting}
|
|
126
|
+
</p>
|
|
127
|
+
<div className="flex flex-wrap gap-2">
|
|
128
|
+
{list.data?.map((person) => (
|
|
129
|
+
<Badge key={person.id} variant="secondary">
|
|
130
|
+
{person.name}
|
|
131
|
+
</Badge>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
</CardContent>
|
|
137
|
+
</Card>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|