@beignet/cli 0.0.1

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.
@@ -0,0 +1,2460 @@
1
+ export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
2
+ export type PresetName = "minimal" | "standard";
3
+ export type TemplateName = "next";
4
+ export type FeatureName = "client" | "react-query" | "forms" | "openapi";
5
+ export type IntegrationName =
6
+ | "better-auth"
7
+ | "drizzle-turso"
8
+ | "inngest"
9
+ | "pino"
10
+ | "resend"
11
+ | "upstash-rate-limit";
12
+
13
+ export type TemplateFile = {
14
+ path: string;
15
+ content: string;
16
+ };
17
+
18
+ export type TemplateContext = {
19
+ name: string;
20
+ packageManager: PackageManager;
21
+ beignetVersion: string;
22
+ preset: PresetName;
23
+ features: FeatureName[];
24
+ integrations: IntegrationName[];
25
+ };
26
+
27
+ const externalVersions = {
28
+ next: "^16.2.4",
29
+ react: "^19.2.5",
30
+ reactDom: "^19.2.5",
31
+ typescript: "^5.3.0",
32
+ typesBun: "^1.3.13",
33
+ typesNode: "^20.10.0",
34
+ typesReact: "^19.0.0",
35
+ typesReactDom: "^19.0.0",
36
+ zod: "^4.0.0",
37
+ tanstackReactQuery: "^5.100.7",
38
+ hookformResolvers: "^5.0.0",
39
+ reactHookForm: "^7.74.0",
40
+ betterAuth: "^1.3.26",
41
+ drizzleKit: "^0.30.0",
42
+ drizzleOrm: "^0.38.0",
43
+ inngest: "^3.0.0",
44
+ libsqlClient: "^0.14.0",
45
+ pino: "^9.7.0",
46
+ resend: "^4.0.1",
47
+ upstashRateLimit: "^2.0.0",
48
+ upstashRedis: "^1.0.0",
49
+ };
50
+
51
+ function isStandardPreset(ctx: TemplateContext): boolean {
52
+ return ctx.preset === "standard";
53
+ }
54
+
55
+ function hasDrizzleTurso(ctx: TemplateContext): boolean {
56
+ return hasIntegration(ctx, "drizzle-turso");
57
+ }
58
+
59
+ function packageRunner(ctx: TemplateContext): string {
60
+ switch (ctx.packageManager) {
61
+ case "npm":
62
+ return "npx -p @beignet/cli beignet";
63
+ case "pnpm":
64
+ return "pnpm dlx @beignet/cli beignet";
65
+ case "yarn":
66
+ return "yarn dlx @beignet/cli beignet";
67
+ default:
68
+ return "bunx -p @beignet/cli beignet";
69
+ }
70
+ }
71
+
72
+ export const featureChoices = [
73
+ "client",
74
+ "react-query",
75
+ "forms",
76
+ "openapi",
77
+ ] as const satisfies readonly FeatureName[];
78
+
79
+ export const presetChoices = [
80
+ "minimal",
81
+ "standard",
82
+ ] as const satisfies readonly PresetName[];
83
+
84
+ export const integrationChoices = [
85
+ "better-auth",
86
+ "drizzle-turso",
87
+ "inngest",
88
+ "pino",
89
+ "resend",
90
+ "upstash-rate-limit",
91
+ ] as const satisfies readonly IntegrationName[];
92
+
93
+ const integrationDeps: Record<IntegrationName, Record<string, string>> = {
94
+ "better-auth": {
95
+ "@beignet/provider-auth-better-auth": "",
96
+ "better-auth": externalVersions.betterAuth,
97
+ },
98
+ "drizzle-turso": {
99
+ "@beignet/provider-drizzle-turso": "",
100
+ "@libsql/client": externalVersions.libsqlClient,
101
+ "drizzle-orm": externalVersions.drizzleOrm,
102
+ },
103
+ inngest: {
104
+ "@beignet/provider-inngest": "",
105
+ inngest: externalVersions.inngest,
106
+ },
107
+ pino: {
108
+ "@beignet/provider-logger-pino": "",
109
+ pino: externalVersions.pino,
110
+ },
111
+ resend: {
112
+ "@beignet/provider-mail-resend": "",
113
+ resend: externalVersions.resend,
114
+ },
115
+ "upstash-rate-limit": {
116
+ "@beignet/provider-rate-limit-upstash": "",
117
+ "@upstash/ratelimit": externalVersions.upstashRateLimit,
118
+ "@upstash/redis": externalVersions.upstashRedis,
119
+ },
120
+ };
121
+
122
+ const integrationDevDeps: Partial<
123
+ Record<IntegrationName, Record<string, string>>
124
+ > = {
125
+ "drizzle-turso": {
126
+ "drizzle-kit": externalVersions.drizzleKit,
127
+ },
128
+ };
129
+
130
+ const featureDeps: Record<FeatureName, Record<string, string>> = {
131
+ client: {},
132
+ "react-query": {
133
+ "@beignet/react-query": "",
134
+ "@tanstack/react-query": externalVersions.tanstackReactQuery,
135
+ },
136
+ forms: {
137
+ "@beignet/react-hook-form": "",
138
+ "@hookform/resolvers": externalVersions.hookformResolvers,
139
+ "react-hook-form": externalVersions.reactHookForm,
140
+ },
141
+ openapi: {
142
+ "@beignet/core": "",
143
+ },
144
+ };
145
+
146
+ function hasFeature(ctx: TemplateContext, feature: FeatureName): boolean {
147
+ return ctx.features.includes(feature);
148
+ }
149
+
150
+ function hasIntegration(
151
+ ctx: TemplateContext,
152
+ integration: IntegrationName,
153
+ ): boolean {
154
+ return ctx.integrations.includes(integration);
155
+ }
156
+
157
+ function json(value: unknown): string {
158
+ return `${JSON.stringify(value, null, "\t")}\n`;
159
+ }
160
+
161
+ function gitignore(ctx: TemplateContext): string {
162
+ const lines = [files.gitignore.trimEnd()];
163
+
164
+ if (isStandardPreset(ctx)) {
165
+ lines.push("/storage/");
166
+ }
167
+
168
+ if (hasDrizzleTurso(ctx)) {
169
+ lines.push("local.db", "local.db-*");
170
+ }
171
+
172
+ return `${lines.join("\n")}\n`;
173
+ }
174
+
175
+ function packageJson(ctx: TemplateContext): string {
176
+ const dependencies: Record<string, string> = {
177
+ "@beignet/core": ctx.beignetVersion,
178
+ "@beignet/next": ctx.beignetVersion,
179
+ next: externalVersions.next,
180
+ react: externalVersions.react,
181
+ "react-dom": externalVersions.reactDom,
182
+ zod: externalVersions.zod,
183
+ };
184
+
185
+ if (isStandardPreset(ctx)) {
186
+ dependencies["@beignet/devtools"] = ctx.beignetVersion;
187
+ dependencies["@beignet/provider-storage-local"] = ctx.beignetVersion;
188
+ }
189
+
190
+ for (const integration of ctx.integrations) {
191
+ for (const [name, version] of Object.entries(
192
+ integrationDeps[integration],
193
+ )) {
194
+ dependencies[name] = version || ctx.beignetVersion;
195
+ }
196
+ }
197
+
198
+ for (const feature of ctx.features) {
199
+ for (const [name, version] of Object.entries(featureDeps[feature])) {
200
+ dependencies[name] = version || ctx.beignetVersion;
201
+ }
202
+ }
203
+
204
+ const devDependencies: Record<string, string> = {
205
+ "@types/bun": externalVersions.typesBun,
206
+ "@types/node": externalVersions.typesNode,
207
+ "@types/react": externalVersions.typesReact,
208
+ "@types/react-dom": externalVersions.typesReactDom,
209
+ typescript: externalVersions.typescript,
210
+ };
211
+
212
+ for (const integration of ctx.integrations) {
213
+ for (const [name, version] of Object.entries(
214
+ integrationDevDeps[integration] ?? {},
215
+ )) {
216
+ devDependencies[name] = version;
217
+ }
218
+ }
219
+
220
+ const scripts: Record<string, string> = {
221
+ dev: "next dev",
222
+ build: "next build",
223
+ start: "next start",
224
+ test: "bun test",
225
+ typecheck: "tsc --noEmit",
226
+ };
227
+
228
+ if (hasDrizzleTurso(ctx)) {
229
+ scripts["db:generate"] = "drizzle-kit generate";
230
+ scripts["db:migrate"] = "drizzle-kit migrate";
231
+ }
232
+
233
+ return json({
234
+ name: ctx.name,
235
+ version: "0.1.0",
236
+ private: true,
237
+ type: "module",
238
+ scripts,
239
+ dependencies,
240
+ devDependencies,
241
+ });
242
+ }
243
+
244
+ function readme(ctx: TemplateContext): string {
245
+ if (isStandardPreset(ctx)) {
246
+ return standardReadme(ctx);
247
+ }
248
+
249
+ const install = `${ctx.packageManager} install`;
250
+ const run =
251
+ ctx.packageManager === "npm"
252
+ ? "npm run dev"
253
+ : `${ctx.packageManager} run dev`;
254
+ const featureList =
255
+ ctx.features.length > 0
256
+ ? ctx.features.map((feature) => `- \`${feature}\``).join("\n")
257
+ : "- API routes only";
258
+ const integrationList =
259
+ ctx.integrations.length > 0
260
+ ? ctx.integrations.map((integration) => `- \`${integration}\``).join("\n")
261
+ : "- None";
262
+
263
+ return `# ${ctx.name}
264
+
265
+ Beignet app scaffolded with \`@beignet/cli\`.
266
+
267
+ ## Getting started
268
+
269
+ \`\`\`bash
270
+ ${install}
271
+ ${run}
272
+ \`\`\`
273
+
274
+ Open http://localhost:3000 and start from the todos workflow.
275
+
276
+ ## Files to know
277
+
278
+ - \`features/todos/contracts.ts\` defines the API contract.
279
+ - \`features/todos/use-cases.ts\` contains application logic with input and output validation.
280
+ - \`ports/index.ts\` defines the app's external dependencies.
281
+ - \`server/index.ts\` wires contracts to handlers.
282
+ - \`app/api/[[...path]]/route.ts\` exposes contract-backed API routes to Next.js.
283
+ ${hasFeature(ctx, "react-query") ? "- `features/todos/components/todo-app.tsx` contains the starter UI.\n" : ""}
284
+
285
+ ## Selected preset
286
+
287
+ \`${ctx.preset}\`
288
+
289
+ ## Selected features
290
+
291
+ ${featureList}
292
+
293
+ ## Selected integrations
294
+
295
+ ${integrationList}
296
+ ${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}
297
+ `;
298
+ }
299
+
300
+ function standardReadme(ctx: TemplateContext): string {
301
+ const install = `${ctx.packageManager} install`;
302
+ const run =
303
+ ctx.packageManager === "npm"
304
+ ? "npm run dev"
305
+ : `${ctx.packageManager} run dev`;
306
+ const typecheck =
307
+ ctx.packageManager === "npm"
308
+ ? "npm run typecheck"
309
+ : `${ctx.packageManager} run typecheck`;
310
+ const cli = packageRunner(ctx);
311
+ const beforeDeploy = hasDrizzleTurso(ctx)
312
+ ? [
313
+ "- Create a Turso database or keep `TURSO_DB_URL=file:local.db` for local libSQL development.",
314
+ `- Run \`${ctx.packageManager} run db:generate\` and \`${ctx.packageManager} run db:migrate\` after changing the Drizzle schema.`,
315
+ ].join("\n")
316
+ : "- Replace the in-memory todo repository with a durable adapter.";
317
+
318
+ return `# ${ctx.name}
319
+
320
+ Beignet app scaffolded with \`@beignet/cli\`.
321
+
322
+ ## Getting started
323
+
324
+ \`\`\`bash
325
+ ${install}
326
+ cp .env.example .env.local
327
+ ${run}
328
+ \`\`\`
329
+
330
+ Open http://localhost:3000 for the todo workflow, http://localhost:3000/api/health for health checks, and http://localhost:3000/api/devtools while developing.
331
+
332
+ ## First checks
333
+
334
+ \`\`\`bash
335
+ # in another terminal
336
+ ${cli} routes
337
+ ${cli} lint
338
+ ${cli} doctor
339
+ ${typecheck}
340
+ \`\`\`
341
+
342
+ \`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
343
+
344
+ ## App map
345
+
346
+ - \`features/todos/contracts.ts\` owns the HTTP contract and reuses use case schemas.
347
+ - \`features/todos/use-cases/\` owns application behavior and validation.
348
+ - \`ports/\` defines app-owned dependencies.
349
+ - \`infra/\` implements ports for the selected runtime.
350
+ - \`server/routes.ts\` keeps the central route registry and OpenAPI contract list.
351
+ - \`server/\` wires context, providers, routes, hooks, and error handling.
352
+ - \`features/shared/errors.ts\` keeps application errors and route-owned error schemas together.
353
+ - \`app/storage/[...key]/route.ts\` serves public local storage objects.
354
+ - \`lib/env.ts\` validates deployment configuration at startup.
355
+ - \`lib/auth.ts\` exposes \`requireUser(ctx)\` for protected use cases.
356
+ - \`features/todos/policy.ts\` defines authorization rules registered with \`createGate(...)\`.
357
+ ${hasDrizzleTurso(ctx) ? "- `infra/db/schema/` contains the Drizzle schema, `infra/db/repositories.ts` creates app repositories, and `infra/todos/` contains the durable todo repository adapter.\n" : ""}
358
+
359
+ ## Before deploying
360
+
361
+ ${beforeDeploy}
362
+ - Keep \`/api/devtools\` development-only unless you add authentication and stricter redaction.
363
+ - Set \`APP_URL\`, \`LOG_LEVEL\`, and service-specific integration variables in your hosting environment.
364
+ - Replace anonymous auth and add use-case authorization before exposing user-owned data.
365
+ ${ctx.integrations.length > 0 ? "\nSee `docs/integrations.md` for setup notes.\n" : ""}
366
+ `;
367
+ }
368
+
369
+ function integrationsDoc(ctx: TemplateContext): string {
370
+ const lines = [
371
+ "# Integrations",
372
+ "",
373
+ "The starter app added dependencies for the integrations you selected. Runtime providers are wired in `server/providers.ts` when Beignet can do so safely; integrations that need app-owned setup list their follow-up steps below.",
374
+ "",
375
+ ];
376
+
377
+ for (const integration of ctx.integrations) {
378
+ if (integration === "better-auth") {
379
+ lines.push(
380
+ "## Better Auth",
381
+ "",
382
+ "- Package: `@beignet/provider-auth-better-auth`",
383
+ "- Peer dependency: `better-auth`",
384
+ "- The starter uses an anonymous auth adapter. Replace `createAnonymousAuth()` in `infra/app-ports.ts` with an adapter backed by your Better Auth session lookup.",
385
+ "- Keep auth behind the shared `AuthPort`, then call `requireUser(ctx)` in use cases that need a signed-in user.",
386
+ "- Put repeated ownership or role rules in policies registered with `createGate(...)`.",
387
+ "- Add the Better Auth provider only after your app has a database adapter and session setup.",
388
+ "",
389
+ );
390
+ }
391
+
392
+ if (integration === "drizzle-turso") {
393
+ lines.push(
394
+ "## Drizzle + Turso",
395
+ "",
396
+ "- Package: `@beignet/provider-drizzle-turso`",
397
+ "- Peer dependencies: `@libsql/client` and `drizzle-orm`",
398
+ "- Dev dependency: `drizzle-kit`",
399
+ "- Set `TURSO_DB_URL` and optional `TURSO_DB_AUTH_TOKEN`.",
400
+ "- The scaffold wires a typed Drizzle repository behind the `TodoRepository` port and a transaction-backed Unit of Work.",
401
+ `- Run \`${ctx.packageManager} run db:generate\` and \`${ctx.packageManager} run db:migrate\` after changing \`infra/db/schema.ts\`.`,
402
+ "",
403
+ );
404
+ }
405
+
406
+ if (integration === "inngest") {
407
+ lines.push(
408
+ "## Inngest",
409
+ "",
410
+ "- Package: `@beignet/core` (`@beignet/core/events`, `@beignet/core/jobs`)",
411
+ "- Package: `@beignet/provider-inngest`",
412
+ "- Peer dependency: `inngest`",
413
+ "- The standard preset wires `inngestProvider` in `server/providers.ts` and adds `jobs: JobDispatcherPort` to `AppPorts`.",
414
+ "- Define jobs with `createJobHandlers(...).defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.",
415
+ "- Keep direct Inngest client usage inside infrastructure code unless you intentionally add an app-owned escape-hatch port.",
416
+ "",
417
+ );
418
+ }
419
+
420
+ if (integration === "pino") {
421
+ lines.push(
422
+ "## Pino logger",
423
+ "",
424
+ "- Package: `@beignet/provider-logger-pino`",
425
+ "- Peer dependency: `pino`",
426
+ "- Use this provider when you want structured request and handler logging.",
427
+ "",
428
+ );
429
+ }
430
+
431
+ if (integration === "resend") {
432
+ lines.push(
433
+ "## Resend mail",
434
+ "",
435
+ "- Package: `@beignet/provider-mail-resend`",
436
+ "- Peer dependency: `resend`",
437
+ "- The standard preset wires `mailResendProvider` in `server/providers.ts` and adds `mailer: MailerPort` to `AppPorts`.",
438
+ "- Set `RESEND_API_KEY` and `RESEND_FROM` before starting the app.",
439
+ "",
440
+ );
441
+ }
442
+
443
+ if (integration === "upstash-rate-limit") {
444
+ lines.push(
445
+ "## Upstash rate limit",
446
+ "",
447
+ "- Package: `@beignet/provider-rate-limit-upstash`",
448
+ "- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
449
+ "- The standard preset wires `upstashRateLimitProvider` in `server/providers.ts` and adds `rateLimit: RateLimitPort` to `AppPorts`.",
450
+ "- Set `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` before starting the app.",
451
+ "",
452
+ );
453
+ }
454
+ }
455
+
456
+ return `${lines.join("\n")}\n`;
457
+ }
458
+
459
+ function envExample(ctx: TemplateContext): string {
460
+ const lines = [
461
+ "# Copy to .env.local and fill in values for your app.",
462
+ "NODE_ENV=development",
463
+ "APP_URL=http://localhost:3000",
464
+ "CRON_SECRET=",
465
+ "DEVTOOLS_ENABLED=true",
466
+ "",
467
+ ];
468
+
469
+ if (isStandardPreset(ctx)) {
470
+ lines.push(
471
+ "STORAGE_ROOT=storage/app",
472
+ "# STORAGE_PUBLIC_BASE_URL=/storage",
473
+ "",
474
+ );
475
+ }
476
+
477
+ if (hasIntegration(ctx, "pino")) {
478
+ lines.push(
479
+ "LOG_LEVEL=info",
480
+ "LOG_FORMAT=json",
481
+ `LOG_SERVICE=${ctx.name}`,
482
+ "",
483
+ );
484
+ }
485
+
486
+ if (hasIntegration(ctx, "inngest")) {
487
+ lines.push("INNGEST_APP_NAME=beignet-app", "# INNGEST_EVENT_KEY=", "");
488
+ }
489
+
490
+ if (hasIntegration(ctx, "drizzle-turso")) {
491
+ lines.push(
492
+ "# Use file:local.db for local libSQL or libsql://... for Turso cloud.",
493
+ "TURSO_DB_URL=file:local.db",
494
+ "# TURSO_DB_AUTH_TOKEN=",
495
+ "",
496
+ );
497
+ }
498
+
499
+ if (hasIntegration(ctx, "resend")) {
500
+ lines.push("RESEND_API_KEY=", "RESEND_FROM=", "");
501
+ }
502
+
503
+ if (hasIntegration(ctx, "upstash-rate-limit")) {
504
+ lines.push(
505
+ "UPSTASH_REDIS_REST_URL=",
506
+ "UPSTASH_REDIS_REST_TOKEN=",
507
+ "UPSTASH_PREFIX=ck:ratelimit",
508
+ "",
509
+ );
510
+ }
511
+
512
+ if (hasIntegration(ctx, "better-auth")) {
513
+ lines.push(
514
+ "# Better Auth needs an app-owned database adapter and a shared AuthPort adapter before it can be wired.",
515
+ "BETTER_AUTH_SECRET=",
516
+ "BETTER_AUTH_URL=http://localhost:3000",
517
+ "",
518
+ );
519
+ }
520
+
521
+ return `${lines.join("\n")}\n`;
522
+ }
523
+
524
+ function layout(ctx: TemplateContext): string {
525
+ if (!hasFeature(ctx, "react-query")) {
526
+ return `import "./globals.css";
527
+ import type { Metadata } from "next";
528
+ import type { ReactNode } from "react";
529
+
530
+ export const metadata: Metadata = {
531
+ title: "Beignet app",
532
+ description: "A contract-first Next.js app.",
533
+ };
534
+
535
+ export default function RootLayout({ children }: { children: ReactNode }) {
536
+ return (
537
+ <html lang="en">
538
+ <body>{children}</body>
539
+ </html>
540
+ );
541
+ }
542
+ `;
543
+ }
544
+
545
+ return `import "./globals.css";
546
+ import type { Metadata } from "next";
547
+ import type { ReactNode } from "react";
548
+ import { Providers } from "./providers";
549
+
550
+ export const metadata: Metadata = {
551
+ title: "Beignet app",
552
+ description: "A contract-first Next.js app.",
553
+ };
554
+
555
+ export default function RootLayout({ children }: { children: ReactNode }) {
556
+ return (
557
+ <html lang="en">
558
+ <body>
559
+ <Providers>{children}</Providers>
560
+ </body>
561
+ </html>
562
+ );
563
+ }
564
+ `;
565
+ }
566
+
567
+ function page(ctx: TemplateContext): string {
568
+ if (!hasFeature(ctx, "react-query")) {
569
+ return `export default function HomePage() {
570
+ return (
571
+ <main className="page">
572
+ <div className="shell">
573
+ <h1>Beignet app</h1>
574
+ <p className="muted">
575
+ This app starts with one contract-backed todos API route.
576
+ </p>
577
+
578
+ <section className="panel">
579
+ <h2>Try the API</h2>
580
+ <pre className="code">GET /api/todos</pre>
581
+ <p>
582
+ <a href="/api/todos">Open todos API</a>
583
+ </p>
584
+ </section>
585
+ </div>
586
+ </main>
587
+ );
588
+ }
589
+ `;
590
+ }
591
+
592
+ return `import { TodoApp } from "@/features/todos/components/todo-app";
593
+
594
+ export default function HomePage() {
595
+ return (
596
+ <main className="page">
597
+ <div className="shell">
598
+ <header className="hero">
599
+ <p className="eyebrow">Beignet starter</p>
600
+ <h1>Type-safe full-stack workflows without codegen.</h1>
601
+ <p className="muted">
602
+ Contracts, use cases, ports, server routes, a typed client, React Query,
603
+ and form validation are wired together so you can start changing product
604
+ code immediately.
605
+ </p>
606
+ </header>
607
+
608
+ <TodoApp />
609
+ </div>
610
+ </main>
611
+ );
612
+ }
613
+ `;
614
+ }
615
+
616
+ function serverProviders(ctx: TemplateContext): string {
617
+ const imports: string[] = [];
618
+ const entries: string[] = [];
619
+
620
+ if (hasIntegration(ctx, "pino")) {
621
+ imports.push(
622
+ 'import { loggerPinoProvider } from "@beignet/provider-logger-pino";',
623
+ );
624
+ entries.push("loggerPinoProvider");
625
+ }
626
+
627
+ if (hasIntegration(ctx, "inngest")) {
628
+ imports.push(
629
+ 'import { inngestProvider } from "@beignet/provider-inngest";',
630
+ );
631
+ entries.push("inngestProvider");
632
+ }
633
+
634
+ if (hasIntegration(ctx, "resend")) {
635
+ imports.push(
636
+ 'import { mailResendProvider } from "@beignet/provider-mail-resend";',
637
+ );
638
+ entries.push("mailResendProvider");
639
+ }
640
+
641
+ if (hasIntegration(ctx, "upstash-rate-limit")) {
642
+ imports.push(
643
+ 'import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";',
644
+ );
645
+ entries.push("upstashRateLimitProvider");
646
+ }
647
+
648
+ return `${imports.join("\n")}
649
+ export const providers = [
650
+ ${entries.join(",\n\t")},
651
+ ];
652
+ `;
653
+ }
654
+
655
+ function server(ctx: TemplateContext): string {
656
+ const hasProviders = ctx.integrations.some((integration) =>
657
+ ["pino", "inngest", "resend", "upstash-rate-limit"].includes(integration),
658
+ );
659
+
660
+ return `import { createNextServer } from "@beignet/next";
661
+ ${hasProviders ? 'import { providers } from "./providers";\n' : ""}import { routes } from "./routes";
662
+ import { appPorts, type AppPorts } from "@/ports";
663
+
664
+ export type AppContext = {
665
+ requestId: string;
666
+ ports: AppPorts;
667
+ };
668
+
669
+ export const server = await createNextServer<AppContext, AppPorts>({
670
+ ports: appPorts,
671
+ ${hasProviders ? "\tproviders,\n" : ""} createContext: async ({ ports }) => ({
672
+ requestId: crypto.randomUUID(),
673
+ ports,
674
+ }),
675
+ routes,
676
+ mapUnhandledError: ({ err, ctx }) => {
677
+ const ports = ctx?.ports as
678
+ | {
679
+ logger?: {
680
+ error: (
681
+ message: string,
682
+ meta?: Record<string, unknown>,
683
+ ) => void;
684
+ };
685
+ }
686
+ | undefined;
687
+
688
+ if (ports?.logger) {
689
+ const logger = ports.logger as {
690
+ error: (message: string, meta?: Record<string, unknown>) => void;
691
+ };
692
+ logger.error("Unhandled API error", {
693
+ error: err,
694
+ requestId: ctx?.requestId,
695
+ });
696
+ } else {
697
+ console.error("Unhandled API error", {
698
+ error: err,
699
+ requestId: ctx?.requestId,
700
+ });
701
+ }
702
+
703
+ return {
704
+ status: 500,
705
+ body: {
706
+ code: "INTERNAL_SERVER_ERROR",
707
+ message: "Internal server error",
708
+ requestId: ctx?.requestId,
709
+ },
710
+ };
711
+ },
712
+ });
713
+ `;
714
+ }
715
+
716
+ function todoApp(ctx: TemplateContext): string {
717
+ const listTodosQueryOptions =
718
+ ctx.preset === "standard" ? "{ query: {} }" : "";
719
+
720
+ if (hasFeature(ctx, "forms")) {
721
+ return `"use client";
722
+
723
+ import { createReactHookForm } from "@beignet/react-hook-form";
724
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
725
+ import { createTodo, listTodos } from "@/features/todos/contracts";
726
+ import { rq } from "@/client/rq";
727
+
728
+ const rhf = createReactHookForm();
729
+ const createTodoForm = rhf(createTodo);
730
+
731
+ export function TodoApp() {
732
+ const queryClient = useQueryClient();
733
+ const todosQuery = useQuery(rq(listTodos).queryOptions(${listTodosQueryOptions}));
734
+ const form = createTodoForm.useForm({
735
+ defaultValues: { title: "" },
736
+ });
737
+ const createTodoMutation = useMutation(
738
+ rq(createTodo).mutationOptions({
739
+ onSuccess: async () => {
740
+ form.reset();
741
+ await queryClient.invalidateQueries({
742
+ queryKey: rq(listTodos).key(),
743
+ });
744
+ },
745
+ }),
746
+ );
747
+
748
+ const onSubmit = form.handleSubmit((body) => {
749
+ createTodoMutation.mutate({ body });
750
+ });
751
+
752
+ return (
753
+ <section className="workspace">
754
+ <form className="composer" onSubmit={onSubmit}>
755
+ <label htmlFor="todo-title">New todo</label>
756
+ <div className="composer-row">
757
+ <input
758
+ id="todo-title"
759
+ placeholder="Ship something type-safe"
760
+ {...form.register("title")}
761
+ />
762
+ <button type="submit" disabled={createTodoMutation.isPending}>
763
+ {createTodoMutation.isPending ? "Adding" : "Add"}
764
+ </button>
765
+ </div>
766
+ {form.formState.errors.title ? (
767
+ <p className="error">{form.formState.errors.title.message}</p>
768
+ ) : null}
769
+ </form>
770
+
771
+ <div className="todos-panel">
772
+ <div className="panel-heading">
773
+ <h2>Todos</h2>
774
+ <span>{todosQuery.data?.total ?? 0} total</span>
775
+ </div>
776
+ {todosQuery.isLoading ? <p className="muted">Loading todos...</p> : null}
777
+ {todosQuery.isError ? (
778
+ <p className="error">Could not load todos.</p>
779
+ ) : null}
780
+ <ul className="todo-list">
781
+ {todosQuery.data?.todos.map((todo) => (
782
+ <li key={todo.id}>
783
+ <span>{todo.title}</span>
784
+ <small>{todo.completed ? "Done" : "Open"}</small>
785
+ </li>
786
+ ))}
787
+ </ul>
788
+ </div>
789
+ </section>
790
+ );
791
+ }
792
+ `;
793
+ }
794
+
795
+ return `"use client";
796
+
797
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
798
+ import { useState } from "react";
799
+ import { createTodo, listTodos } from "@/features/todos/contracts";
800
+ import { rq } from "@/client/rq";
801
+
802
+ export function TodoApp() {
803
+ const [title, setTitle] = useState("");
804
+ const queryClient = useQueryClient();
805
+ const todosQuery = useQuery(rq(listTodos).queryOptions(${listTodosQueryOptions}));
806
+ const createTodoMutation = useMutation(
807
+ rq(createTodo).mutationOptions({
808
+ onSuccess: async () => {
809
+ setTitle("");
810
+ await queryClient.invalidateQueries({
811
+ queryKey: rq(listTodos).key(),
812
+ });
813
+ },
814
+ }),
815
+ );
816
+
817
+ return (
818
+ <section className="workspace">
819
+ <form
820
+ className="composer"
821
+ onSubmit={(event) => {
822
+ event.preventDefault();
823
+ createTodoMutation.mutate({ body: { title } });
824
+ }}
825
+ >
826
+ <label htmlFor="todo-title">New todo</label>
827
+ <div className="composer-row">
828
+ <input
829
+ id="todo-title"
830
+ value={title}
831
+ onChange={(event) => setTitle(event.currentTarget.value)}
832
+ placeholder="Ship something type-safe"
833
+ />
834
+ <button type="submit" disabled={!title || createTodoMutation.isPending}>
835
+ {createTodoMutation.isPending ? "Adding" : "Add"}
836
+ </button>
837
+ </div>
838
+ </form>
839
+
840
+ <div className="todos-panel">
841
+ <div className="panel-heading">
842
+ <h2>Todos</h2>
843
+ <span>{todosQuery.data?.total ?? 0} total</span>
844
+ </div>
845
+ <ul className="todo-list">
846
+ {todosQuery.data?.todos.map((todo) => (
847
+ <li key={todo.id}>
848
+ <span>{todo.title}</span>
849
+ <small>{todo.completed ? "Done" : "Open"}</small>
850
+ </li>
851
+ ))}
852
+ </ul>
853
+ </div>
854
+ </section>
855
+ );
856
+ }
857
+ `;
858
+ }
859
+
860
+ const files = {
861
+ gitignore: `.next
862
+ node_modules
863
+ dist
864
+ coverage
865
+ .env
866
+ .env.local
867
+ `,
868
+ nextEnv: `/// <reference types="next" />
869
+ /// <reference types="next/image-types/global" />
870
+
871
+ // This file should not be edited.
872
+ `,
873
+ nextConfig: `/** @type {import("next").NextConfig} */
874
+ const nextConfig = {};
875
+
876
+ export default nextConfig;
877
+ `,
878
+ tsconfig: json({
879
+ compilerOptions: {
880
+ target: "ES2017",
881
+ lib: ["dom", "dom.iterable", "esnext"],
882
+ allowJs: true,
883
+ skipLibCheck: true,
884
+ strict: true,
885
+ noEmit: true,
886
+ esModuleInterop: true,
887
+ module: "esnext",
888
+ moduleResolution: "bundler",
889
+ resolveJsonModule: true,
890
+ isolatedModules: true,
891
+ jsx: "react-jsx",
892
+ incremental: true,
893
+ plugins: [{ name: "next" }],
894
+ paths: {
895
+ "@/*": ["./*"],
896
+ },
897
+ },
898
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
899
+ exclude: ["node_modules"],
900
+ }),
901
+ globals: `:root {
902
+ color-scheme: light;
903
+ background: #f8fafc;
904
+ color: #172033;
905
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
906
+ }
907
+
908
+ * {
909
+ box-sizing: border-box;
910
+ }
911
+
912
+ body {
913
+ margin: 0;
914
+ }
915
+
916
+ a {
917
+ color: #4f46e5;
918
+ }
919
+
920
+ button,
921
+ input {
922
+ font: inherit;
923
+ }
924
+
925
+ .page {
926
+ min-height: 100vh;
927
+ padding: 48px 24px 64px;
928
+ }
929
+
930
+ .shell {
931
+ max-width: 880px;
932
+ margin: 0 auto;
933
+ }
934
+
935
+ .hero {
936
+ margin-bottom: 28px;
937
+ }
938
+
939
+ .hero h1 {
940
+ max-width: 720px;
941
+ margin: 0 0 12px;
942
+ font-size: 40px;
943
+ line-height: 1.05;
944
+ letter-spacing: 0;
945
+ }
946
+
947
+ .eyebrow {
948
+ margin: 0 0 12px;
949
+ color: #4f46e5;
950
+ font-size: 13px;
951
+ font-weight: 700;
952
+ text-transform: uppercase;
953
+ letter-spacing: 0.12em;
954
+ }
955
+
956
+ .muted {
957
+ color: #475569;
958
+ }
959
+
960
+ .panel {
961
+ margin-top: 24px;
962
+ padding: 20px;
963
+ border: 1px solid #e2e8f0;
964
+ border-radius: 8px;
965
+ background: white;
966
+ }
967
+
968
+ .code {
969
+ padding: 16px;
970
+ overflow-x: auto;
971
+ border-radius: 8px;
972
+ background: #111827;
973
+ color: #f8fafc;
974
+ }
975
+
976
+ .workspace {
977
+ display: grid;
978
+ gap: 18px;
979
+ }
980
+
981
+ .composer,
982
+ .todos-panel {
983
+ padding: 18px;
984
+ border: 1px solid #dbe3ef;
985
+ border-radius: 8px;
986
+ background: #ffffff;
987
+ box-shadow: 0 1px 2px rgb(15 23 42 / 0.05);
988
+ }
989
+
990
+ .composer label,
991
+ .panel-heading h2 {
992
+ margin: 0;
993
+ color: #172033;
994
+ font-size: 15px;
995
+ font-weight: 700;
996
+ }
997
+
998
+ .composer-row {
999
+ display: flex;
1000
+ gap: 10px;
1001
+ margin-top: 10px;
1002
+ }
1003
+
1004
+ .composer input {
1005
+ width: 100%;
1006
+ min-width: 0;
1007
+ border: 1px solid #cbd5e1;
1008
+ border-radius: 8px;
1009
+ padding: 10px 12px;
1010
+ color: #172033;
1011
+ }
1012
+
1013
+ .composer button {
1014
+ border: 0;
1015
+ border-radius: 8px;
1016
+ padding: 10px 14px;
1017
+ background: #4f46e5;
1018
+ color: #ffffff;
1019
+ font-weight: 700;
1020
+ cursor: pointer;
1021
+ }
1022
+
1023
+ .composer button:disabled {
1024
+ cursor: not-allowed;
1025
+ opacity: 0.6;
1026
+ }
1027
+
1028
+ .panel-heading {
1029
+ display: flex;
1030
+ align-items: center;
1031
+ justify-content: space-between;
1032
+ gap: 16px;
1033
+ margin-bottom: 12px;
1034
+ }
1035
+
1036
+ .panel-heading span {
1037
+ color: #64748b;
1038
+ font-size: 13px;
1039
+ }
1040
+
1041
+ .todo-list {
1042
+ display: grid;
1043
+ gap: 8px;
1044
+ margin: 0;
1045
+ padding: 0;
1046
+ list-style: none;
1047
+ }
1048
+
1049
+ .todo-list li {
1050
+ display: flex;
1051
+ align-items: center;
1052
+ justify-content: space-between;
1053
+ gap: 16px;
1054
+ padding: 12px;
1055
+ border: 1px solid #e2e8f0;
1056
+ border-radius: 8px;
1057
+ background: #f8fafc;
1058
+ }
1059
+
1060
+ .todo-list small {
1061
+ color: #64748b;
1062
+ }
1063
+
1064
+ .error {
1065
+ margin: 10px 0 0;
1066
+ color: #dc2626;
1067
+ font-size: 14px;
1068
+ }
1069
+
1070
+ @media (max-width: 640px) {
1071
+ .page {
1072
+ padding: 32px 16px 48px;
1073
+ }
1074
+
1075
+ .hero h1 {
1076
+ font-size: 32px;
1077
+ }
1078
+
1079
+ .composer-row {
1080
+ flex-direction: column;
1081
+ }
1082
+ }
1083
+ `,
1084
+ appProviders: `"use client";
1085
+
1086
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1087
+ import { type ReactNode, useState } from "react";
1088
+
1089
+ export function Providers({ children }: { children: ReactNode }) {
1090
+ const [queryClient] = useState(() => new QueryClient());
1091
+
1092
+ return (
1093
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
1094
+ );
1095
+ }
1096
+ `,
1097
+ apiClient: `import { createNextClient } from "@beignet/next";
1098
+
1099
+ export const apiClient = createNextClient({
1100
+ validate: true,
1101
+ });
1102
+ `,
1103
+ rq: `import { createReactQuery } from "@beignet/react-query";
1104
+ import { apiClient } from "./api-client";
1105
+
1106
+ export const rq = createReactQuery(apiClient);
1107
+ `,
1108
+ contractsTodos: `import { createContractGroup } from "@beignet/core/contracts";
1109
+ import { z } from "zod";
1110
+
1111
+ const todos = createContractGroup().namespace("todos");
1112
+
1113
+ export const TodoSchema = z.object({
1114
+ id: z.string(),
1115
+ title: z.string().min(1),
1116
+ completed: z.boolean(),
1117
+ createdAt: z.string(),
1118
+ });
1119
+
1120
+ export const CreateTodoSchema = z.object({
1121
+ title: z.string().min(1),
1122
+ });
1123
+
1124
+ export const listTodos = todos.get("/api/todos").responses({
1125
+ 200: z.object({
1126
+ todos: z.array(TodoSchema),
1127
+ total: z.number(),
1128
+ }),
1129
+ });
1130
+
1131
+ export const createTodo = todos
1132
+ .post("/api/todos")
1133
+ .body(CreateTodoSchema)
1134
+ .responses({
1135
+ 201: TodoSchema,
1136
+ });
1137
+ `,
1138
+ ports: `import { definePorts } from "@beignet/core/ports";
1139
+ import type { z } from "zod";
1140
+ import type { CreateTodoSchema, TodoSchema } from "@/features/todos/contracts";
1141
+
1142
+ export type Todo = z.infer<typeof TodoSchema>;
1143
+ export type CreateTodoInput = z.infer<typeof CreateTodoSchema>;
1144
+
1145
+ const todos = new Map<string, Todo>();
1146
+
1147
+ function createTodo(input: CreateTodoInput): Todo {
1148
+ const todo = {
1149
+ id: crypto.randomUUID(),
1150
+ title: input.title,
1151
+ completed: false,
1152
+ createdAt: new Date().toISOString(),
1153
+ };
1154
+ todos.set(todo.id, todo);
1155
+ return todo;
1156
+ }
1157
+
1158
+ createTodo({ title: "Read the generated contract" });
1159
+ createTodo({ title: "Add your first use case" });
1160
+
1161
+ export const appPorts = definePorts({
1162
+ todos: {
1163
+ list: async () => Array.from(todos.values()),
1164
+ create: async (input: CreateTodoInput) => createTodo(input),
1165
+ },
1166
+ });
1167
+
1168
+ export type AppPorts = typeof appPorts;
1169
+ `,
1170
+ useCasesTodos: `import { createUseCase } from "@beignet/core/application";
1171
+ import { z } from "zod";
1172
+ import { CreateTodoSchema, TodoSchema } from "@/features/todos/contracts";
1173
+ import type { AppPorts } from "@/ports";
1174
+
1175
+ type AppContext = {
1176
+ requestId: string;
1177
+ ports: AppPorts;
1178
+ };
1179
+
1180
+ const useCase = createUseCase<AppContext>();
1181
+
1182
+ export const ListTodosOutputSchema = z.object({
1183
+ todos: z.array(TodoSchema),
1184
+ total: z.number(),
1185
+ });
1186
+
1187
+ export const listTodos = useCase
1188
+ .query("listTodos")
1189
+ .input(z.void())
1190
+ .output(ListTodosOutputSchema)
1191
+ .run(async ({ ctx }) => {
1192
+ const todos = await ctx.ports.todos.list();
1193
+ return { todos, total: todos.length };
1194
+ });
1195
+
1196
+ export const createTodo = useCase
1197
+ .command("createTodo")
1198
+ .input(CreateTodoSchema)
1199
+ .output(TodoSchema)
1200
+ .run(async ({ ctx, input }) => ctx.ports.todos.create(input));
1201
+ `,
1202
+ todoRoutes: `import { defineRouteGroup } from "@beignet/next";
1203
+ import type { AppContext } from "@/server";
1204
+ import { createTodo, listTodos } from "@/features/todos/use-cases";
1205
+ import * as contracts from "@/features/todos/contracts";
1206
+
1207
+ export const todoRoutes = defineRouteGroup<AppContext>({
1208
+ name: "todos",
1209
+ routes: [
1210
+ {
1211
+ contract: contracts.listTodos,
1212
+ handle: async ({ ctx }) => ({
1213
+ status: 200,
1214
+ body: await listTodos.run({ ctx, input: undefined }),
1215
+ }),
1216
+ },
1217
+ {
1218
+ contract: contracts.createTodo,
1219
+ handle: async ({ ctx, body }) => ({
1220
+ status: 201,
1221
+ body: await createTodo.run({ ctx, input: body }),
1222
+ }),
1223
+ },
1224
+ ],
1225
+ });
1226
+ `,
1227
+ serverRoutes: `import { contractsFromRoutes, defineRoutes } from "@beignet/next";
1228
+ import type { AppContext } from "@/server";
1229
+ import { todoRoutes } from "@/features/todos/routes";
1230
+
1231
+ export const routes = defineRoutes<AppContext>([
1232
+ todoRoutes,
1233
+ ]);
1234
+ export const contracts = contractsFromRoutes(routes);
1235
+ `,
1236
+ apiCatchAllRoute: `import { server } from "@/server";
1237
+
1238
+ export const GET = server.api;
1239
+ export const HEAD = server.api;
1240
+ export const OPTIONS = server.api;
1241
+ export const PATCH = server.api;
1242
+ export const POST = server.api;
1243
+ export const PUT = server.api;
1244
+ export const DELETE = server.api;
1245
+ `,
1246
+ apiOpenApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
1247
+ import { server } from "@/server";
1248
+
1249
+ export const GET = createOpenAPIHandler(server.contracts, {
1250
+ title: "Beignet starter API",
1251
+ version: "0.1.0",
1252
+ });
1253
+ `,
1254
+ productionEnv: `import { createEnv } from "@beignet/core/config";
1255
+ import { z } from "zod";
1256
+
1257
+ const BooleanEnv = z.enum(["true", "false"]).transform((value) => value === "true");
1258
+
1259
+ export const env = createEnv({
1260
+ server: {
1261
+ NODE_ENV: z
1262
+ .enum(["development", "test", "production"])
1263
+ .default("development"),
1264
+ APP_URL: z.string().url().default("http://localhost:3000"),
1265
+ CRON_SECRET: z.string().min(1).optional(),
1266
+ DEVTOOLS_ENABLED: BooleanEnv.optional(),
1267
+ LOG_LEVEL: z
1268
+ .enum(["trace", "debug", "info", "warn", "error", "fatal"])
1269
+ .default("info"),
1270
+ LOG_FORMAT: z.enum(["pretty", "json"]).default("json"),
1271
+ LOG_SERVICE: z.string().default("beignet-app"),
1272
+ },
1273
+ runtimeEnv: process.env,
1274
+ });
1275
+
1276
+ export const isProduction = env.NODE_ENV === "production";
1277
+ `,
1278
+ productionAppContext: `import type { DevtoolsPort, DevtoolsTraceContext } from "@beignet/devtools";
1279
+ import type { ActivityActor, ActivityTenant, StoragePort } from "@beignet/core/ports";
1280
+ import type { AppGate, AppPorts } from "@/ports";
1281
+ import type { AuthSession } from "@/ports/auth";
1282
+
1283
+ export type AppContext = {
1284
+ requestId: string;
1285
+ actor: ActivityActor;
1286
+ auth: AuthSession | null;
1287
+ gate: AppGate;
1288
+ ports: AppPorts & {
1289
+ devtools: DevtoolsPort;
1290
+ storage: StoragePort;
1291
+ };
1292
+ tenant?: ActivityTenant;
1293
+ } & Partial<DevtoolsTraceContext>;
1294
+ `,
1295
+ productimapUnhandledErrors: `import { createAppError, defineErrors } from "@beignet/core/errors";
1296
+
1297
+ export const errors = defineErrors({
1298
+ Unauthorized: {
1299
+ code: "UNAUTHORIZED",
1300
+ status: 401,
1301
+ message: "Authentication required",
1302
+ },
1303
+ Forbidden: {
1304
+ code: "FORBIDDEN",
1305
+ status: 403,
1306
+ message: "Forbidden",
1307
+ },
1308
+ TodoNotFound: {
1309
+ code: "TODO_NOT_FOUND",
1310
+ status: 404,
1311
+ message: "Todo not found",
1312
+ },
1313
+ });
1314
+
1315
+ export const appError = createAppError(errors);
1316
+ `,
1317
+ productionTodoSchemas: `import { z } from "zod";
1318
+
1319
+ export const TodoSchema = z.object({
1320
+ id: z.string().uuid(),
1321
+ title: z.string().min(1),
1322
+ completed: z.boolean(),
1323
+ createdAt: z.string().datetime(),
1324
+ });
1325
+
1326
+ export const ListTodosInputSchema = z.object({
1327
+ limit: z.coerce.number().int().min(1).max(100).default(20),
1328
+ offset: z.coerce.number().int().min(0).default(0),
1329
+ });
1330
+
1331
+ export const ListTodosOutputSchema = z.object({
1332
+ todos: z.array(TodoSchema),
1333
+ total: z.number().int().min(0),
1334
+ limit: z.number().int().min(1),
1335
+ offset: z.number().int().min(0),
1336
+ });
1337
+
1338
+ export const GetTodoInputSchema = z.object({
1339
+ id: z.string().uuid(),
1340
+ });
1341
+
1342
+ export const CreateTodoInputSchema = z.object({
1343
+ title: z.string().min(1).max(120),
1344
+ });
1345
+
1346
+ export type Todo = z.infer<typeof TodoSchema>;
1347
+ export type CreateTodoInput = z.infer<typeof CreateTodoInputSchema>;
1348
+ export type ListTodosInput = z.infer<typeof ListTodosInputSchema>;
1349
+ `,
1350
+ productionUseCaseBuilder: `import { createUseCase } from "@beignet/core/application";
1351
+ import { createDevtoolsUseCaseObserver } from "@beignet/devtools";
1352
+ import type { AppContext } from "@/app-context";
1353
+
1354
+ export const useCase = createUseCase<AppContext>({
1355
+ onRun: createDevtoolsUseCaseObserver<AppContext>(),
1356
+ });
1357
+ `,
1358
+ productionAuthHelpers: `import type { AppContext } from "@/app-context";
1359
+ import type { AuthSession, AuthUser } from "@/ports/auth";
1360
+ import { appError } from "@/features/shared/errors";
1361
+
1362
+ export function requireSession(ctx: AppContext): AuthSession {
1363
+ if (!ctx.auth) {
1364
+ throw appError("Unauthorized");
1365
+ }
1366
+
1367
+ return ctx.auth;
1368
+ }
1369
+
1370
+ export function requireUser(ctx: AppContext): AuthUser {
1371
+ return requireSession(ctx).user;
1372
+ }
1373
+ `,
1374
+ productionListTodosUseCase: `import { useCase } from "@/lib/use-case";
1375
+ import { ListTodosInputSchema, ListTodosOutputSchema } from "./schemas";
1376
+
1377
+ export const listTodosUseCase = useCase
1378
+ .query("todos.list")
1379
+ .input(ListTodosInputSchema)
1380
+ .output(ListTodosOutputSchema)
1381
+ .run(async ({ ctx, input }) => {
1382
+ const result = await ctx.ports.todos.list(input);
1383
+ return {
1384
+ todos: result.todos,
1385
+ total: result.total,
1386
+ limit: input.limit,
1387
+ offset: input.offset,
1388
+ };
1389
+ });
1390
+ `,
1391
+ productionGetTodoUseCase: `import { appError } from "@/features/shared/errors";
1392
+ import { useCase } from "@/lib/use-case";
1393
+ import { GetTodoInputSchema, TodoSchema } from "./schemas";
1394
+
1395
+ export const getTodoUseCase = useCase
1396
+ .query("todos.get")
1397
+ .input(GetTodoInputSchema)
1398
+ .output(TodoSchema)
1399
+ .run(async ({ ctx, input }) => {
1400
+ const todo = await ctx.ports.todos.findById(input.id);
1401
+ if (!todo) {
1402
+ throw appError("TodoNotFound", { details: { id: input.id } });
1403
+ }
1404
+
1405
+ return todo;
1406
+ });
1407
+ `,
1408
+ productionCreateTodoUseCase: `import { useCase } from "@/lib/use-case";
1409
+ import { CreateTodoInputSchema, TodoSchema } from "./schemas";
1410
+
1411
+ export const createTodoUseCase = useCase
1412
+ .command("todos.create")
1413
+ .input(CreateTodoInputSchema)
1414
+ .output(TodoSchema)
1415
+ .run(async ({ ctx, input }) => {
1416
+ await ctx.gate.authorize("todos.create");
1417
+ return ctx.ports.uow.transaction((tx) => tx.todos.create(input));
1418
+ });
1419
+ `,
1420
+ productionUseCasesIndex: `export { createTodoUseCase } from "./create-todo";
1421
+ export { getTodoUseCase } from "./get-todo";
1422
+ export { listTodosUseCase } from "./list-todos";
1423
+ export {
1424
+ CreateTodoInputSchema,
1425
+ GetTodoInputSchema,
1426
+ ListTodosInputSchema,
1427
+ ListTodosOutputSchema,
1428
+ TodoSchema,
1429
+ type CreateTodoInput,
1430
+ type ListTodosInput,
1431
+ type Todo,
1432
+ } from "./schemas";
1433
+ `,
1434
+ productionTodoRepositoryPort: `import type {
1435
+ CreateTodoInput,
1436
+ ListTodosInput,
1437
+ Todo,
1438
+ } from "@/features/todos/use-cases/schemas";
1439
+
1440
+ export type ListTodosResult = {
1441
+ todos: Todo[];
1442
+ total: number;
1443
+ };
1444
+
1445
+ export interface TodoRepository {
1446
+ list(input: ListTodosInput): Promise<ListTodosResult>;
1447
+ findById(id: string): Promise<Todo | null>;
1448
+ create(input: CreateTodoInput): Promise<Todo>;
1449
+ }
1450
+ `,
1451
+ productionAuthPort: `import type {
1452
+ AuthPort as BeignetAuthPort,
1453
+ AuthRequestLike,
1454
+ AuthSession as BeignetAuthSession,
1455
+ } from "@beignet/core/ports";
1456
+
1457
+ export type AuthRequest = AuthRequestLike;
1458
+
1459
+ export type AuthUser = {
1460
+ id: string;
1461
+ email?: string;
1462
+ name?: string;
1463
+ };
1464
+
1465
+ export type AuthSessionMetadata = {
1466
+ id?: string;
1467
+ };
1468
+
1469
+ export type AuthSession = BeignetAuthSession<
1470
+ AuthUser,
1471
+ AuthSessionMetadata
1472
+ >;
1473
+
1474
+ export type AuthPort = BeignetAuthPort<
1475
+ AuthUser,
1476
+ AuthSessionMetadata,
1477
+ AuthRequest
1478
+ >;
1479
+ `,
1480
+ productionLogger: `import type { LoggerPort } from "@beignet/core/ports";
1481
+
1482
+ export const fallbackLogger: LoggerPort = {
1483
+ trace: (message, meta) => console.trace(message, meta),
1484
+ debug: (message, meta) => console.debug(message, meta),
1485
+ info: (message, meta) => console.info(message, meta),
1486
+ warn: (message, meta) => console.warn(message, meta),
1487
+ error: (message, meta) => console.error(message, meta),
1488
+ fatal: (message, meta) => console.error(message, meta),
1489
+ child: () => fallbackLogger,
1490
+ };
1491
+ `,
1492
+ productionAnonymousAuth: `import { createAnonymousAuth as createAnonymousAuthPort } from "@beignet/core/ports";
1493
+ import type {
1494
+ AuthPort,
1495
+ AuthRequest,
1496
+ AuthSessionMetadata,
1497
+ AuthUser,
1498
+ } from "@/ports/auth";
1499
+
1500
+ export function createAnonymousAuth(): AuthPort {
1501
+ return createAnonymousAuthPort<AuthUser, AuthSessionMetadata, AuthRequest>();
1502
+ }
1503
+ `,
1504
+ productionTodoPolicy: `import { type ActivityActor, definePolicy, deny } from "@beignet/core/ports";
1505
+ import type { AuthSession } from "@/ports/auth";
1506
+
1507
+ export type AuthorizationContext = {
1508
+ actor: ActivityActor;
1509
+ auth: AuthSession | null;
1510
+ };
1511
+
1512
+ export const todoPolicy = definePolicy({
1513
+ "todos.create": (ctx: AuthorizationContext) => {
1514
+ if (ctx.actor.type === "user") return true;
1515
+ return deny("You must be signed in to create todos.");
1516
+ },
1517
+ });
1518
+ `,
1519
+ productionInMemoryTodoRepository: `import type {
1520
+ CreateTodoInput,
1521
+ ListTodosInput,
1522
+ Todo,
1523
+ } from "@/features/todos/use-cases/schemas";
1524
+ import type { TodoRepository } from "@/features/todos/ports";
1525
+
1526
+ export function createInMemoryTodoRepository(
1527
+ seed: Todo[] = [],
1528
+ ): TodoRepository {
1529
+ const todos = new Map(seed.map((todo) => [todo.id, todo]));
1530
+
1531
+ return {
1532
+ async list(input: ListTodosInput) {
1533
+ const allTodos = Array.from(todos.values()).sort((left, right) =>
1534
+ left.createdAt.localeCompare(right.createdAt),
1535
+ );
1536
+
1537
+ return {
1538
+ todos: allTodos.slice(input.offset, input.offset + input.limit),
1539
+ total: allTodos.length,
1540
+ };
1541
+ },
1542
+ async findById(id: string) {
1543
+ return todos.get(id) ?? null;
1544
+ },
1545
+ async create(input: CreateTodoInput) {
1546
+ const todo: Todo = {
1547
+ id: crypto.randomUUID(),
1548
+ title: input.title,
1549
+ completed: false,
1550
+ createdAt: new Date().toISOString(),
1551
+ };
1552
+ todos.set(todo.id, todo);
1553
+ return todo;
1554
+ },
1555
+ };
1556
+ }
1557
+ `,
1558
+ productionInfrastructurePorts: `import {
1559
+ createGate,
1560
+ createNoopUnitOfWork,
1561
+ definePorts,
1562
+ } from "@beignet/core/ports";
1563
+ import { todoPolicy } from "@/features/todos/policy";
1564
+ import { appError } from "@/features/shared/errors";
1565
+ import { createAnonymousAuth } from "./auth/anonymous-auth";
1566
+ import { fallbackLogger } from "./logger";
1567
+ import { createInMemoryTodoRepository } from "./todos/in-memory-todo-repository";
1568
+
1569
+ const todos = createInMemoryTodoRepository([
1570
+ {
1571
+ id: "00000000-0000-4000-8000-000000000001",
1572
+ title: "Review the starter boundaries",
1573
+ completed: false,
1574
+ createdAt: new Date().toISOString(),
1575
+ },
1576
+ {
1577
+ id: "00000000-0000-4000-8000-000000000002",
1578
+ title: "Replace the repository with durable persistence",
1579
+ completed: false,
1580
+ createdAt: new Date().toISOString(),
1581
+ },
1582
+ ]);
1583
+
1584
+ const gate = createGate({
1585
+ policies: [todoPolicy],
1586
+ onDeny(decision) {
1587
+ return appError("Forbidden", {
1588
+ message: decision.reason ?? "Forbidden",
1589
+ details: decision.details,
1590
+ });
1591
+ },
1592
+ });
1593
+
1594
+ export const appPorts = definePorts({
1595
+ auth: createAnonymousAuth(),
1596
+ gate,
1597
+ todos,
1598
+ logger: fallbackLogger,
1599
+ uow: createNoopUnitOfWork(() => ({
1600
+ todos,
1601
+ })),
1602
+ });
1603
+ `,
1604
+ productionInfrastructurePortsWithDrizzleTurso: `import { createGate, definePorts } from "@beignet/core/ports";
1605
+ import { todoPolicy } from "@/features/todos/policy";
1606
+ import { appError } from "@/features/shared/errors";
1607
+ import { createAnonymousAuth } from "./auth/anonymous-auth";
1608
+ import { fallbackLogger } from "./logger";
1609
+
1610
+ const gate = createGate({
1611
+ policies: [todoPolicy],
1612
+ onDeny(decision) {
1613
+ return appError("Forbidden", {
1614
+ message: decision.reason ?? "Forbidden",
1615
+ details: decision.details,
1616
+ });
1617
+ },
1618
+ });
1619
+
1620
+ export const appPorts = definePorts({
1621
+ auth: createAnonymousAuth(),
1622
+ gate,
1623
+ logger: fallbackLogger,
1624
+ });
1625
+ `,
1626
+ productionDrizzleConfig: `export default {
1627
+ schema: "./infra/db/schema/index.ts",
1628
+ out: "./drizzle",
1629
+ dialect: "sqlite",
1630
+ dbCredentials: {
1631
+ url: process.env.TURSO_DB_URL ?? "file:local.db",
1632
+ authToken: process.env.TURSO_DB_AUTH_TOKEN,
1633
+ },
1634
+ };
1635
+ `,
1636
+ productionDbSchema: `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
1637
+
1638
+ export const todos = sqliteTable("todos", {
1639
+ id: text("id").primaryKey(),
1640
+ title: text("title").notNull(),
1641
+ completed: integer("completed", { mode: "boolean" }).notNull().default(false),
1642
+ createdAt: text("created_at").notNull(),
1643
+ });
1644
+ `,
1645
+ productionDrizzleTodoRepository: `import { count, desc, eq } from "drizzle-orm";
1646
+ import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
1647
+ import type { TodoRepository } from "@/features/todos/ports";
1648
+ import type {
1649
+ CreateTodoInput,
1650
+ ListTodosInput,
1651
+ Todo,
1652
+ } from "@/features/todos/use-cases/schemas";
1653
+ import * as schema from "@/infra/db/schema";
1654
+
1655
+ type TodoRow = typeof schema.todos.$inferSelect;
1656
+
1657
+ function toTodo(row: TodoRow): Todo {
1658
+ return {
1659
+ id: row.id,
1660
+ title: row.title,
1661
+ completed: row.completed,
1662
+ createdAt: row.createdAt,
1663
+ };
1664
+ }
1665
+
1666
+ export function createDrizzleTodoRepository(
1667
+ db: DrizzleTursoDatabase<typeof schema>,
1668
+ ): TodoRepository {
1669
+ return {
1670
+ async list(input: ListTodosInput) {
1671
+ const rows = await db
1672
+ .select()
1673
+ .from(schema.todos)
1674
+ .orderBy(desc(schema.todos.createdAt))
1675
+ .limit(input.limit)
1676
+ .offset(input.offset);
1677
+ const [{ total }] = await db.select({ total: count() }).from(schema.todos);
1678
+
1679
+ return {
1680
+ todos: rows.map(toTodo),
1681
+ total,
1682
+ };
1683
+ },
1684
+ async findById(id: string) {
1685
+ const [row] = await db
1686
+ .select()
1687
+ .from(schema.todos)
1688
+ .where(eq(schema.todos.id, id))
1689
+ .limit(1);
1690
+
1691
+ return row ? toTodo(row) : null;
1692
+ },
1693
+ async create(input: CreateTodoInput) {
1694
+ const todo = {
1695
+ id: crypto.randomUUID(),
1696
+ title: input.title,
1697
+ completed: false,
1698
+ createdAt: new Date().toISOString(),
1699
+ };
1700
+ const [row] = await db.insert(schema.todos).values(todo).returning();
1701
+
1702
+ if (!row) {
1703
+ throw new Error("Failed to create todo");
1704
+ }
1705
+
1706
+ return toTodo(row);
1707
+ },
1708
+ };
1709
+ }
1710
+ `,
1711
+ productionDbRepositories: `import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
1712
+ import { createDrizzleTodoRepository } from "@/infra/todos/drizzle-todo-repository";
1713
+ import type { AppTransactionPorts } from "@/ports";
1714
+ import * as schema from "./schema";
1715
+
1716
+ export function createRepositories(
1717
+ db: DrizzleTursoDatabase<typeof schema>,
1718
+ ): Omit<AppTransactionPorts, "events"> {
1719
+ return {
1720
+ todos: createDrizzleTodoRepository(db),
1721
+ };
1722
+ }
1723
+ `,
1724
+ productionContractsTodos: `import { createContractGroup } from "@beignet/core/contracts";
1725
+ import { z } from "zod";
1726
+ import { errors } from "@/features/shared/errors";
1727
+ import {
1728
+ createTodoUseCase,
1729
+ getTodoUseCase,
1730
+ listTodosUseCase,
1731
+ } from "@/features/todos/use-cases";
1732
+
1733
+ const ErrorResponseSchema = z.object({
1734
+ code: z.string(),
1735
+ message: z.string(),
1736
+ requestId: z.string().optional(),
1737
+ });
1738
+
1739
+ const todos = createContractGroup()
1740
+ .namespace("todos")
1741
+ .errors({ Unauthorized: errors.Unauthorized })
1742
+ .responses({
1743
+ 500: ErrorResponseSchema,
1744
+ });
1745
+
1746
+ export const listTodos = todos
1747
+ .get("/api/todos")
1748
+ .query(listTodosUseCase.inputSchema)
1749
+ .responses({
1750
+ 200: listTodosUseCase.outputSchema,
1751
+ });
1752
+
1753
+ export const createTodo = todos
1754
+ .post("/api/todos")
1755
+ .body(createTodoUseCase.inputSchema)
1756
+ .errors({ Forbidden: errors.Forbidden })
1757
+ .responses({
1758
+ 201: createTodoUseCase.outputSchema,
1759
+ });
1760
+
1761
+ export const getTodo = todos
1762
+ .get("/api/todos/:id")
1763
+ .pathParams(getTodoUseCase.inputSchema)
1764
+ .errors({ TodoNotFound: errors.TodoNotFound })
1765
+ .responses({
1766
+ 200: getTodoUseCase.outputSchema,
1767
+ });
1768
+ `,
1769
+ productionTodoRoutes: `import { defineRouteGroup } from "@beignet/next";
1770
+ import type { AppContext } from "@/app-context";
1771
+ import { createTodo, getTodo, listTodos } from "@/features/todos/contracts";
1772
+ import {
1773
+ createTodoUseCase,
1774
+ getTodoUseCase,
1775
+ listTodosUseCase,
1776
+ } from "@/features/todos/use-cases";
1777
+
1778
+ export const todoRoutes = defineRouteGroup<AppContext>({
1779
+ name: "todos",
1780
+ routes: [
1781
+ {
1782
+ contract: listTodos,
1783
+ handle: async ({ ctx, query }) => ({
1784
+ status: 200,
1785
+ body: await listTodosUseCase.run({ ctx, input: query }),
1786
+ }),
1787
+ },
1788
+ {
1789
+ contract: createTodo,
1790
+ handle: async ({ ctx, body }) => ({
1791
+ status: 201,
1792
+ body: await createTodoUseCase.run({ ctx, input: body }),
1793
+ }),
1794
+ },
1795
+ {
1796
+ contract: getTodo,
1797
+ handle: async ({ ctx, path }) => ({
1798
+ status: 200,
1799
+ body: await getTodoUseCase.run({ ctx, input: path }),
1800
+ }),
1801
+ },
1802
+ ],
1803
+ });
1804
+ `,
1805
+ productionServerRoutes: `import { contractsFromRoutes, defineRoutes } from "@beignet/next";
1806
+ import type { AppContext } from "@/app-context";
1807
+ import { todoRoutes } from "@/features/todos/routes";
1808
+
1809
+ export const routes = defineRoutes<AppContext>([
1810
+ todoRoutes,
1811
+ ]);
1812
+ export const contracts = contractsFromRoutes(routes);
1813
+ `,
1814
+ productionServer: `import { createDevtoolsHooks } from "@beignet/devtools";
1815
+ import { createNextServer } from "@beignet/next";
1816
+ import {
1817
+ createAnonymousActor,
1818
+ createTenant,
1819
+ createUserActor,
1820
+ } from "@beignet/core/ports";
1821
+ import type { AppContext } from "@/app-context";
1822
+ import { appPorts } from "@/infra/app-ports";
1823
+ import { env } from "@/lib/env";
1824
+ import { providers } from "./providers";
1825
+ import { routes } from "./routes";
1826
+
1827
+ export const server = await createNextServer({
1828
+ ports: appPorts,
1829
+ providers,
1830
+ hooks: [
1831
+ createDevtoolsHooks<AppContext>({
1832
+ requestIdHeader: "x-request-id",
1833
+ }),
1834
+ ],
1835
+ createContext: async ({ req, ports }) => {
1836
+ const auth = await ports.auth.getSession(req);
1837
+ const tenantId = req.headers.get("x-tenant-id") || undefined;
1838
+ const context = {
1839
+ requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
1840
+ actor: auth
1841
+ ? createUserActor(auth.user.id, { displayName: auth.user.name })
1842
+ : createAnonymousActor(),
1843
+ auth,
1844
+ ports,
1845
+ tenant: tenantId ? createTenant(tenantId) : undefined,
1846
+ };
1847
+
1848
+ return {
1849
+ ...context,
1850
+ gate: ports.gate.bind(context),
1851
+ };
1852
+ },
1853
+ routes,
1854
+ mapUnhandledError: ({ err, ctx }) => {
1855
+ ctx?.ports.logger.error("Unhandled API error", {
1856
+ error: err,
1857
+ requestId: ctx?.requestId,
1858
+ environment: env.NODE_ENV,
1859
+ });
1860
+
1861
+ return {
1862
+ status: 500,
1863
+ body: {
1864
+ code: "INTERNAL_SERVER_ERROR",
1865
+ message: "Internal server error",
1866
+ requestId: ctx?.requestId,
1867
+ },
1868
+ };
1869
+ },
1870
+ });
1871
+ `,
1872
+ productionServerWithDrizzleTurso: `import { createDevtoolsHooks } from "@beignet/devtools";
1873
+ import { createNextServer } from "@beignet/next";
1874
+ import {
1875
+ createAnonymousActor,
1876
+ createTenant,
1877
+ createUserActor,
1878
+ } from "@beignet/core/ports";
1879
+ import { createDrizzleTursoUnitOfWork } from "@beignet/provider-drizzle-turso";
1880
+ import type { AppContext } from "@/app-context";
1881
+ import { appPorts } from "@/infra/app-ports";
1882
+ import * as schema from "@/infra/db/schema";
1883
+ import { createRepositories } from "@/infra/db/repositories";
1884
+ import { env } from "@/lib/env";
1885
+ import type { AppTransactionPorts } from "@/ports";
1886
+ import { providers } from "./providers";
1887
+ import { routes } from "./routes";
1888
+
1889
+ export const server = await createNextServer({
1890
+ ports: appPorts,
1891
+ providers,
1892
+ hooks: [
1893
+ createDevtoolsHooks<AppContext>({
1894
+ requestIdHeader: "x-request-id",
1895
+ }),
1896
+ ],
1897
+ createContext: async ({ req, ports }) => {
1898
+ const auth = await ports.auth.getSession(req);
1899
+ const tenantId = req.headers.get("x-tenant-id") || undefined;
1900
+ const repositories = createRepositories(ports.db.db);
1901
+
1902
+ const context = {
1903
+ requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
1904
+ actor: auth
1905
+ ? createUserActor(auth.user.id, { displayName: auth.user.name })
1906
+ : createAnonymousActor(),
1907
+ auth,
1908
+ ports: {
1909
+ ...ports,
1910
+ ...repositories,
1911
+ uow: createDrizzleTursoUnitOfWork<typeof schema, AppTransactionPorts>({
1912
+ db: ports.db.db,
1913
+ createTransactionPorts: (tx) => ({
1914
+ ...createRepositories(tx),
1915
+ }),
1916
+ }),
1917
+ },
1918
+ tenant: tenantId ? createTenant(tenantId) : undefined,
1919
+ };
1920
+
1921
+ return {
1922
+ ...context,
1923
+ gate: ports.gate.bind(context),
1924
+ };
1925
+ },
1926
+ routes,
1927
+ mapUnhandledError: ({ err, ctx }) => {
1928
+ ctx?.ports.logger.error("Unhandled API error", {
1929
+ error: err,
1930
+ requestId: ctx?.requestId,
1931
+ environment: env.NODE_ENV,
1932
+ });
1933
+
1934
+ return {
1935
+ status: 500,
1936
+ body: {
1937
+ code: "INTERNAL_SERVER_ERROR",
1938
+ message: "Internal server error",
1939
+ requestId: ctx?.requestId,
1940
+ },
1941
+ };
1942
+ },
1943
+ });
1944
+ `,
1945
+ productionApiCatchAllRoute: `import { server } from "@/server";
1946
+
1947
+ export const GET = server.api;
1948
+ export const HEAD = server.api;
1949
+ export const OPTIONS = server.api;
1950
+ export const PATCH = server.api;
1951
+ export const POST = server.api;
1952
+ export const PUT = server.api;
1953
+ export const DELETE = server.api;
1954
+ `,
1955
+ productionHealthRoute: `import { env } from "@/lib/env";
1956
+
1957
+ export function GET() {
1958
+ return Response.json(
1959
+ {
1960
+ status: "ok",
1961
+ environment: env.NODE_ENV,
1962
+ timestamp: new Date().toISOString(),
1963
+ },
1964
+ {
1965
+ headers: {
1966
+ "cache-control": "no-store",
1967
+ },
1968
+ },
1969
+ );
1970
+ }
1971
+ `,
1972
+ productionDevtoolsRoute: `import { createDevtoolsRoute } from "@beignet/devtools";
1973
+ import { server } from "@/server";
1974
+
1975
+ export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
1976
+ basePath: "/api/devtools",
1977
+ });
1978
+ `,
1979
+ productionStorageRoute: `import { createStorageRoute } from "@beignet/next";
1980
+ import { server } from "@/server";
1981
+
1982
+ export const { GET, HEAD } = createStorageRoute(server.ports.storage, {
1983
+ basePath: "/storage",
1984
+ });
1985
+ `,
1986
+ productionOpenApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
1987
+ import { server } from "@/server";
1988
+
1989
+ export const GET = createOpenAPIHandler(server.contracts, {
1990
+ title: "Beignet starter API",
1991
+ version: "0.1.0",
1992
+ });
1993
+ `,
1994
+ };
1995
+
1996
+ function productionProviderPortImports(ctx: TemplateContext): string[] {
1997
+ const imports: string[] = [];
1998
+ if (hasIntegration(ctx, "inngest")) imports.push("\tJobDispatcherPort,");
1999
+ if (hasIntegration(ctx, "upstash-rate-limit")) {
2000
+ imports.push("\tRateLimitPort,");
2001
+ }
2002
+ return imports;
2003
+ }
2004
+
2005
+ function productionProviderPortFields(ctx: TemplateContext): string[] {
2006
+ const fields: string[] = [];
2007
+ if (hasIntegration(ctx, "inngest")) fields.push("\tjobs: JobDispatcherPort;");
2008
+ if (hasIntegration(ctx, "resend")) fields.push("\tmailer: MailerPort;");
2009
+ if (hasIntegration(ctx, "upstash-rate-limit")) {
2010
+ fields.push("\trateLimit: RateLimitPort;");
2011
+ }
2012
+ return fields;
2013
+ }
2014
+
2015
+ function productionPorts(ctx: TemplateContext): string {
2016
+ const mailImport = hasIntegration(ctx, "resend")
2017
+ ? 'import type { MailerPort } from "@beignet/core/mail";\n'
2018
+ : "";
2019
+ const imports = [
2020
+ "\tBoundGate,",
2021
+ "\tGatePort,",
2022
+ "\tLoggerPort,",
2023
+ ...productionProviderPortImports(ctx),
2024
+ "\tUnitOfWorkPort,",
2025
+ ];
2026
+ const providerFields = productionProviderPortFields(ctx);
2027
+
2028
+ return `import type {
2029
+ ${imports.join("\n")}
2030
+ } from "@beignet/core/ports";
2031
+ ${mailImport}import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
2032
+ import type { AuthPort } from "./auth";
2033
+ import type { TodoRepository } from "@/features/todos/ports";
2034
+
2035
+ export type AppTransactionPorts = {
2036
+ todos: TodoRepository;
2037
+ };
2038
+
2039
+ export type AppGate = BoundGate<[typeof todoPolicy]>;
2040
+
2041
+ export type AppPorts = {
2042
+ auth: AuthPort;
2043
+ gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
2044
+ todos: TodoRepository;
2045
+ logger: LoggerPort;
2046
+ ${providerFields.length > 0 ? `${providerFields.join("\n")}\n` : ""} uow: UnitOfWorkPort<AppTransactionPorts>;
2047
+ };
2048
+ `;
2049
+ }
2050
+
2051
+ function productionPortsWithDrizzleTurso(ctx: TemplateContext): string {
2052
+ const mailImport = hasIntegration(ctx, "resend")
2053
+ ? 'import type { MailerPort } from "@beignet/core/mail";\n'
2054
+ : "";
2055
+ const imports = [
2056
+ "\tBoundGate,",
2057
+ "\tGatePort,",
2058
+ "\tLoggerPort,",
2059
+ ...productionProviderPortImports(ctx),
2060
+ "\tUnitOfWorkPort,",
2061
+ ];
2062
+ const providerFields = productionProviderPortFields(ctx);
2063
+
2064
+ return `import type {
2065
+ ${imports.join("\n")}
2066
+ } from "@beignet/core/ports";
2067
+ ${mailImport}import type { DbPort } from "@beignet/provider-drizzle-turso";
2068
+ import * as schema from "@/infra/db/schema";
2069
+ import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
2070
+ import type { AuthPort } from "./auth";
2071
+ import type { TodoRepository } from "@/features/todos/ports";
2072
+
2073
+ export type AppTransactionPorts = {
2074
+ todos: TodoRepository;
2075
+ };
2076
+
2077
+ export type AppGate = BoundGate<[typeof todoPolicy]>;
2078
+
2079
+ export type AppPorts = {
2080
+ auth: AuthPort;
2081
+ db: DbPort<typeof schema>;
2082
+ gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
2083
+ todos: TodoRepository;
2084
+ logger: LoggerPort;
2085
+ ${providerFields.length > 0 ? `${providerFields.join("\n")}\n` : ""} uow: UnitOfWorkPort<AppTransactionPorts>;
2086
+ };
2087
+ `;
2088
+ }
2089
+
2090
+ function productionServerProviders(ctx: TemplateContext): string {
2091
+ const imports = [
2092
+ 'import { createDevtoolsProvider } from "@beignet/devtools";',
2093
+ hasDrizzleTurso(ctx)
2094
+ ? 'import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";'
2095
+ : undefined,
2096
+ hasIntegration(ctx, "inngest")
2097
+ ? 'import { inngestProvider } from "@beignet/provider-inngest";'
2098
+ : undefined,
2099
+ 'import { loggerPinoProvider } from "@beignet/provider-logger-pino";',
2100
+ hasIntegration(ctx, "resend")
2101
+ ? 'import { mailResendProvider } from "@beignet/provider-mail-resend";'
2102
+ : undefined,
2103
+ 'import { localStorageProvider } from "@beignet/provider-storage-local";',
2104
+ hasIntegration(ctx, "upstash-rate-limit")
2105
+ ? 'import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";'
2106
+ : undefined,
2107
+ hasDrizzleTurso(ctx)
2108
+ ? 'import * as schema from "@/infra/db/schema";'
2109
+ : undefined,
2110
+ ].filter((line): line is string => Boolean(line));
2111
+
2112
+ const declarations = hasDrizzleTurso(ctx)
2113
+ ? "\nconst drizzleTursoProvider = createDrizzleTursoProvider({ schema });\n"
2114
+ : "";
2115
+ const entries = [
2116
+ "\tcreateDevtoolsProvider(),",
2117
+ "\tlocalStorageProvider,",
2118
+ "\tloggerPinoProvider,",
2119
+ hasDrizzleTurso(ctx) ? "\tdrizzleTursoProvider," : undefined,
2120
+ hasIntegration(ctx, "inngest") ? "\tinngestProvider," : undefined,
2121
+ hasIntegration(ctx, "resend") ? "\tmailResendProvider," : undefined,
2122
+ hasIntegration(ctx, "upstash-rate-limit")
2123
+ ? "\tupstashRateLimitProvider,"
2124
+ : undefined,
2125
+ ].filter((entry): entry is string => Boolean(entry));
2126
+
2127
+ return `${imports.join("\n")}
2128
+ ${declarations}
2129
+ export const providers = [
2130
+ ${entries.join("\n")}
2131
+ ];
2132
+ `;
2133
+ }
2134
+
2135
+ function productionTodosTest(ctx: TemplateContext): string {
2136
+ const portImports = [
2137
+ hasIntegration(ctx, "upstash-rate-limit")
2138
+ ? "createMemoryRateLimiter"
2139
+ : undefined,
2140
+ "createMemoryStorage",
2141
+ "createNoopUnitOfWork",
2142
+ "createUserActor",
2143
+ ].filter((item): item is string => Boolean(item));
2144
+ const extraPorts = [
2145
+ hasDrizzleTurso(ctx)
2146
+ ? "\t\t\tdb: { db: undefined as never, client: undefined as never },"
2147
+ : undefined,
2148
+ hasIntegration(ctx, "inngest")
2149
+ ? "\t\t\tjobs: { dispatch: async () => undefined },"
2150
+ : undefined,
2151
+ hasIntegration(ctx, "resend")
2152
+ ? '\t\t\tmailer: { send: async () => ({ provider: "test" }) },'
2153
+ : undefined,
2154
+ hasIntegration(ctx, "upstash-rate-limit")
2155
+ ? "\t\t\trateLimit: createMemoryRateLimiter(),"
2156
+ : undefined,
2157
+ ].filter((entry): entry is string => Boolean(entry));
2158
+
2159
+ return `import { describe, expect, it } from "bun:test";
2160
+ import { createUseCaseTester } from "@beignet/core/application";
2161
+ import { createInMemoryDevtools } from "@beignet/devtools";
2162
+ import { ${portImports.join(", ")} } from "@beignet/core/ports";
2163
+ import type { AppContext } from "@/app-context";
2164
+ import { appPorts } from "@/infra/app-ports";
2165
+ import {
2166
+ createTodoUseCase,
2167
+ getTodoUseCase,
2168
+ listTodosUseCase,
2169
+ type CreateTodoInput,
2170
+ type ListTodosInput,
2171
+ type Todo,
2172
+ } from "../use-cases";
2173
+
2174
+ function createTestTodoRepository() {
2175
+ const todos = new Map<string, Todo>();
2176
+
2177
+ return {
2178
+ async list(input: ListTodosInput) {
2179
+ const allTodos = Array.from(todos.values()).sort((left, right) =>
2180
+ left.createdAt.localeCompare(right.createdAt),
2181
+ );
2182
+
2183
+ return {
2184
+ todos: allTodos.slice(input.offset, input.offset + input.limit),
2185
+ total: allTodos.length,
2186
+ };
2187
+ },
2188
+ async findById(id: string) {
2189
+ return todos.get(id) ?? null;
2190
+ },
2191
+ async create(input: CreateTodoInput) {
2192
+ const todo: Todo = {
2193
+ id: crypto.randomUUID(),
2194
+ title: input.title,
2195
+ completed: false,
2196
+ createdAt: new Date().toISOString(),
2197
+ };
2198
+ todos.set(todo.id, todo);
2199
+ return todo;
2200
+ },
2201
+ };
2202
+ }
2203
+
2204
+ describe("todos resource", () => {
2205
+ it("creates, gets, and lists todos", async () => {
2206
+ const todos = createTestTodoRepository();
2207
+ const auth = {
2208
+ user: {
2209
+ id: "user_test",
2210
+ email: "test@example.com",
2211
+ name: "Test User",
2212
+ },
2213
+ session: { id: "session_test" },
2214
+ };
2215
+ const actor = createUserActor(auth.user.id, {
2216
+ displayName: auth.user.name,
2217
+ });
2218
+ const testPorts = {
2219
+ ...appPorts,
2220
+ todos,
2221
+ ${extraPorts.length > 0 ? `${extraPorts.join("\n")}\n` : ""} uow: createNoopUnitOfWork(() => ({ todos })) as AppContext["ports"]["uow"],
2222
+ devtools: createInMemoryDevtools(),
2223
+ storage: createMemoryStorage(),
2224
+ };
2225
+ const tester = createUseCaseTester<AppContext>(() => ({
2226
+ requestId: "test-request",
2227
+ actor,
2228
+ auth,
2229
+ gate: testPorts.gate.bind({ actor, auth }),
2230
+ ports: testPorts,
2231
+ }));
2232
+
2233
+ const ctx = await tester.ctx();
2234
+ const created = await tester.run(
2235
+ createTodoUseCase,
2236
+ { title: "First todo" },
2237
+ { ctx },
2238
+ );
2239
+ const found = await tester.run(getTodoUseCase, { id: created.id }, { ctx });
2240
+ const result = await tester.run(
2241
+ listTodosUseCase,
2242
+ { limit: 20, offset: 0 },
2243
+ { ctx },
2244
+ );
2245
+
2246
+ expect(created.title).toBe("First todo");
2247
+ expect(found.id).toBe(created.id);
2248
+ expect(result.total).toBe(1);
2249
+ expect(result.todos).toEqual([created]);
2250
+ });
2251
+ });
2252
+ `;
2253
+ }
2254
+
2255
+ function getProductionTemplateFiles(ctx: TemplateContext): TemplateFile[] {
2256
+ const usesDrizzleTurso = hasDrizzleTurso(ctx);
2257
+ const templateFiles: TemplateFile[] = [
2258
+ { path: "package.json", content: packageJson(ctx) },
2259
+ { path: "README.md", content: readme(ctx) },
2260
+ { path: ".gitignore", content: gitignore(ctx) },
2261
+ { path: ".env.example", content: envExample(ctx) },
2262
+ { path: "next-env.d.ts", content: files.nextEnv },
2263
+ { path: "next.config.js", content: files.nextConfig },
2264
+ { path: "tsconfig.json", content: files.tsconfig },
2265
+ { path: "app/globals.css", content: files.globals },
2266
+ { path: "app/layout.tsx", content: layout(ctx) },
2267
+ { path: "app/page.tsx", content: page(ctx) },
2268
+ { path: "app/providers.tsx", content: files.appProviders },
2269
+ { path: "app-context.ts", content: files.productionAppContext },
2270
+ {
2271
+ path: "app/api/[[...path]]/route.ts",
2272
+ content: files.productionApiCatchAllRoute,
2273
+ },
2274
+ { path: "app/api/health/route.ts", content: files.productionHealthRoute },
2275
+ {
2276
+ path: "app/api/devtools/[[...path]]/route.ts",
2277
+ content: files.productionDevtoolsRoute,
2278
+ },
2279
+ {
2280
+ path: "app/storage/[...key]/route.ts",
2281
+ content: files.productionStorageRoute,
2282
+ },
2283
+ {
2284
+ path: "app/api/openapi/route.ts",
2285
+ content: files.productionOpenApiRoute,
2286
+ },
2287
+ { path: "client/api-client.ts", content: files.apiClient },
2288
+ { path: "client/rq.ts", content: files.rq },
2289
+ {
2290
+ path: "features/todos/contracts.ts",
2291
+ content: files.productionContractsTodos,
2292
+ },
2293
+ { path: "features/todos/routes.ts", content: files.productionTodoRoutes },
2294
+ { path: "features/todos/components/todo-app.tsx", content: todoApp(ctx) },
2295
+ { path: "infra/logger.ts", content: files.productionLogger },
2296
+ {
2297
+ path: "infra/app-ports.ts",
2298
+ content: usesDrizzleTurso
2299
+ ? files.productionInfrastructurePortsWithDrizzleTurso
2300
+ : files.productionInfrastructurePorts,
2301
+ },
2302
+ { path: "lib/env.ts", content: files.productionEnv },
2303
+ { path: "lib/auth.ts", content: files.productionAuthHelpers },
2304
+ { path: "features/todos/policy.ts", content: files.productionTodoPolicy },
2305
+ {
2306
+ path: "infra/auth/anonymous-auth.ts",
2307
+ content: files.productionAnonymousAuth,
2308
+ },
2309
+ { path: "ports/auth.ts", content: files.productionAuthPort },
2310
+ {
2311
+ path: "ports/index.ts",
2312
+ content: usesDrizzleTurso
2313
+ ? productionPortsWithDrizzleTurso(ctx)
2314
+ : productionPorts(ctx),
2315
+ },
2316
+ {
2317
+ path: "features/todos/ports.ts",
2318
+ content: files.productionTodoRepositoryPort,
2319
+ },
2320
+ {
2321
+ path: "features/shared/errors.ts",
2322
+ content: files.productimapUnhandledErrors,
2323
+ },
2324
+ {
2325
+ path: "server/index.ts",
2326
+ content: usesDrizzleTurso
2327
+ ? files.productionServerWithDrizzleTurso
2328
+ : files.productionServer,
2329
+ },
2330
+ { path: "server/routes.ts", content: files.productionServerRoutes },
2331
+ {
2332
+ path: "server/providers.ts",
2333
+ content: productionServerProviders(ctx),
2334
+ },
2335
+ { path: "lib/use-case.ts", content: files.productionUseCaseBuilder },
2336
+ {
2337
+ path: "features/todos/use-cases/create-todo.ts",
2338
+ content: files.productionCreateTodoUseCase,
2339
+ },
2340
+ {
2341
+ path: "features/todos/use-cases/get-todo.ts",
2342
+ content: files.productionGetTodoUseCase,
2343
+ },
2344
+ {
2345
+ path: "features/todos/use-cases/list-todos.ts",
2346
+ content: files.productionListTodosUseCase,
2347
+ },
2348
+ {
2349
+ path: "features/todos/use-cases/index.ts",
2350
+ content: files.productionUseCasesIndex,
2351
+ },
2352
+ {
2353
+ path: "features/todos/use-cases/schemas.ts",
2354
+ content: files.productionTodoSchemas,
2355
+ },
2356
+ {
2357
+ path: "features/todos/tests/todos.test.ts",
2358
+ content: productionTodosTest(ctx),
2359
+ },
2360
+ { path: "docs/integrations.md", content: integrationsDoc(ctx) },
2361
+ ];
2362
+
2363
+ if (usesDrizzleTurso) {
2364
+ templateFiles.push(
2365
+ { path: "drizzle.config.ts", content: files.productionDrizzleConfig },
2366
+ {
2367
+ path: "infra/db/schema/index.ts",
2368
+ content: files.productionDbSchema,
2369
+ },
2370
+ {
2371
+ path: "infra/db/repositories.ts",
2372
+ content: files.productionDbRepositories,
2373
+ },
2374
+ {
2375
+ path: "infra/todos/drizzle-todo-repository.ts",
2376
+ content: files.productionDrizzleTodoRepository,
2377
+ },
2378
+ );
2379
+ } else {
2380
+ templateFiles.push({
2381
+ path: "infra/todos/in-memory-todo-repository.ts",
2382
+ content: files.productionInMemoryTodoRepository,
2383
+ });
2384
+ }
2385
+
2386
+ return templateFiles;
2387
+ }
2388
+
2389
+ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
2390
+ if (isStandardPreset(ctx)) {
2391
+ return getProductionTemplateFiles(ctx);
2392
+ }
2393
+
2394
+ const templateFiles = [
2395
+ { path: "package.json", content: packageJson(ctx) },
2396
+ { path: "README.md", content: readme(ctx) },
2397
+ { path: ".gitignore", content: gitignore(ctx) },
2398
+ { path: "next-env.d.ts", content: files.nextEnv },
2399
+ { path: "next.config.js", content: files.nextConfig },
2400
+ { path: "tsconfig.json", content: files.tsconfig },
2401
+ { path: "app/globals.css", content: files.globals },
2402
+ { path: "app/layout.tsx", content: layout(ctx) },
2403
+ { path: "app/page.tsx", content: page(ctx) },
2404
+ { path: "app/api/[[...path]]/route.ts", content: files.apiCatchAllRoute },
2405
+ { path: "features/todos/contracts.ts", content: files.contractsTodos },
2406
+ { path: "features/todos/routes.ts", content: files.todoRoutes },
2407
+ { path: "ports/index.ts", content: files.ports },
2408
+ { path: "server/index.ts", content: server(ctx) },
2409
+ { path: "server/routes.ts", content: files.serverRoutes },
2410
+ { path: "features/todos/use-cases.ts", content: files.useCasesTodos },
2411
+ ];
2412
+
2413
+ if (hasFeature(ctx, "client")) {
2414
+ templateFiles.push({
2415
+ path: "client/api-client.ts",
2416
+ content: files.apiClient,
2417
+ });
2418
+ }
2419
+
2420
+ if (hasFeature(ctx, "react-query")) {
2421
+ templateFiles.push(
2422
+ { path: "app/providers.tsx", content: files.appProviders },
2423
+ { path: "client/rq.ts", content: files.rq },
2424
+ { path: "features/todos/components/todo-app.tsx", content: todoApp(ctx) },
2425
+ );
2426
+ }
2427
+
2428
+ if (hasFeature(ctx, "openapi")) {
2429
+ templateFiles.push({
2430
+ path: "app/api/openapi/route.ts",
2431
+ content: files.apiOpenApiRoute,
2432
+ });
2433
+ }
2434
+
2435
+ if (
2436
+ ctx.integrations.some((integration) =>
2437
+ ["pino", "inngest", "resend", "upstash-rate-limit"].includes(integration),
2438
+ )
2439
+ ) {
2440
+ templateFiles.push({
2441
+ path: "server/providers.ts",
2442
+ content: serverProviders(ctx),
2443
+ });
2444
+ }
2445
+
2446
+ if (ctx.integrations.length > 0) {
2447
+ templateFiles.push(
2448
+ {
2449
+ path: ".env.example",
2450
+ content: envExample(ctx),
2451
+ },
2452
+ {
2453
+ path: "docs/integrations.md",
2454
+ content: integrationsDoc(ctx),
2455
+ },
2456
+ );
2457
+ }
2458
+
2459
+ return templateFiles;
2460
+ }