@beignet/cli 0.0.3 → 0.0.5
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/CHANGELOG.md +219 -0
- package/README.md +379 -61
- package/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +20 -0
- package/dist/ansi.js.map +1 -0
- package/dist/choices.d.ts +49 -0
- package/dist/choices.d.ts.map +1 -0
- package/dist/choices.js +53 -0
- package/dist/choices.js.map +1 -0
- package/dist/completion.d.ts +47 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +123 -0
- package/dist/completion.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +41 -0
- package/dist/create-prompts.d.ts.map +1 -0
- package/dist/create-prompts.js +78 -0
- package/dist/create-prompts.js.map +1 -0
- package/dist/create.d.ts +10 -5
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +28 -47
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +37 -2
- package/dist/db.js.map +1 -1
- package/dist/github-annotations.d.ts +18 -0
- package/dist/github-annotations.d.ts.map +1 -0
- package/dist/github-annotations.js +22 -0
- package/dist/github-annotations.js.map +1 -0
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +665 -603
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +21 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +1938 -131
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +20 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +17 -0
- package/dist/lib.js.map +1 -0
- package/dist/lint.d.ts +10 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +340 -33
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +20 -3
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +1896 -382
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +24 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +138 -0
- package/dist/outbox.js.map +1 -0
- package/dist/schedule.d.ts +36 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +155 -0
- package/dist/schedule.js.map +1 -0
- package/dist/task.d.ts +26 -0
- package/dist/task.d.ts.map +1 -0
- package/dist/task.js +106 -0
- package/dist/task.js.map +1 -0
- package/dist/templates/base.d.ts +13 -0
- package/dist/templates/base.d.ts.map +1 -0
- package/dist/templates/base.js +300 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/db.d.ts +14 -0
- package/dist/templates/db.d.ts.map +1 -0
- package/dist/templates/db.js +962 -0
- package/dist/templates/db.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +123 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/server.d.ts +26 -0
- package/dist/templates/server.d.ts.map +1 -0
- package/dist/templates/server.js +520 -0
- package/dist/templates/server.js.map +1 -0
- package/dist/templates/shadcn.d.ts +5 -0
- package/dist/templates/shadcn.d.ts.map +1 -0
- package/dist/templates/shadcn.js +555 -0
- package/dist/templates/shadcn.js.map +1 -0
- package/dist/templates/shared.d.ts +49 -0
- package/dist/templates/shared.d.ts.map +1 -0
- package/dist/templates/shared.js +57 -0
- package/dist/templates/shared.js.map +1 -0
- package/dist/templates/shell.d.ts +5 -0
- package/dist/templates/shell.d.ts.map +1 -0
- package/dist/templates/shell.js +1190 -0
- package/dist/templates/shell.js.map +1 -0
- package/dist/templates/todos.d.ts +17 -0
- package/dist/templates/todos.d.ts.map +1 -0
- package/dist/templates/todos.js +607 -0
- package/dist/templates/todos.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +18 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -8
- package/src/ansi.ts +30 -0
- package/src/choices.ts +83 -0
- package/src/completion.ts +169 -0
- package/src/config.ts +16 -0
- package/src/create-prompts.ts +114 -0
- package/src/create.ts +40 -64
- package/src/db.ts +60 -4
- package/src/github-annotations.ts +37 -0
- package/src/index.ts +1075 -817
- package/src/inspect.ts +2859 -115
- package/src/lib.ts +45 -0
- package/src/lint.ts +493 -39
- package/src/make.ts +2389 -406
- package/src/outbox.ts +249 -0
- package/src/schedule.ts +272 -0
- package/src/task.ts +169 -0
- package/src/templates/base.ts +377 -0
- package/src/templates/db.ts +963 -0
- package/src/templates/index.ts +148 -0
- package/src/templates/server.ts +528 -0
- package/src/templates/shadcn.ts +570 -0
- package/src/templates/shared.ts +90 -0
- package/src/templates/shell.ts +1219 -0
- package/src/templates/todos.ts +607 -0
- package/src/version.ts +20 -0
- package/dist/create-bin.d.ts +0 -3
- package/dist/create-bin.d.ts.map +0 -1
- package/dist/create-bin.js +0 -9
- package/dist/create-bin.js.map +0 -1
- package/dist/templates.d.ts +0 -55
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js +0 -3520
- package/dist/templates.js.map +0 -1
- package/src/create-bin.ts +0 -11
- package/src/templates.ts +0 -3774
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
IntegrationName,
|
|
3
|
+
PackageManager,
|
|
4
|
+
TemplateName,
|
|
5
|
+
} from "../choices.js";
|
|
6
|
+
export { integrationChoices } from "../choices.js";
|
|
7
|
+
export type { TemplateContext, TemplateFile } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
baseFiles,
|
|
11
|
+
envExample,
|
|
12
|
+
integrationsDoc,
|
|
13
|
+
packageJson,
|
|
14
|
+
readme,
|
|
15
|
+
} from "./base.js";
|
|
16
|
+
import { dbFiles } from "./db.js";
|
|
17
|
+
import {
|
|
18
|
+
infrastructurePorts,
|
|
19
|
+
ports,
|
|
20
|
+
serverFiles,
|
|
21
|
+
serverProviders,
|
|
22
|
+
} from "./server.js";
|
|
23
|
+
import { shadcnTemplateFiles } from "./shadcn.js";
|
|
24
|
+
import type { TemplateContext, TemplateFile } from "./shared.js";
|
|
25
|
+
import { shellTemplateFiles } from "./shell.js";
|
|
26
|
+
import { todosFiles } from "./todos.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render all template files for a new Beignet app.
|
|
30
|
+
*
|
|
31
|
+
* There is one starter: a full-stack app with a typed API, Better Auth,
|
|
32
|
+
* Drizzle/libSQL persistence, pino logging, devtools, and a shadcn UI shell.
|
|
33
|
+
* `ctx.api` swaps the UI shell for a minimal API landing page and a reduced
|
|
34
|
+
* typed client.
|
|
35
|
+
*/
|
|
36
|
+
export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
|
|
37
|
+
const templateFiles: TemplateFile[] = [
|
|
38
|
+
{ path: "package.json", content: packageJson(ctx) },
|
|
39
|
+
{ path: "README.md", content: readme(ctx) },
|
|
40
|
+
{ path: ".gitignore", content: baseFiles.gitignore },
|
|
41
|
+
{ path: ".env.example", content: envExample(ctx) },
|
|
42
|
+
{ path: "next-env.d.ts", content: baseFiles.nextEnv },
|
|
43
|
+
{ path: "next.config.js", content: baseFiles.nextConfig },
|
|
44
|
+
{ path: "tsconfig.json", content: baseFiles.tsconfig },
|
|
45
|
+
{ path: "app-context.ts", content: serverFiles.appContext },
|
|
46
|
+
{
|
|
47
|
+
path: "app/api/[[...path]]/route.ts",
|
|
48
|
+
content: serverFiles.apiCatchAllRoute,
|
|
49
|
+
},
|
|
50
|
+
{ path: "app/api/health/route.ts", content: serverFiles.healthRoute },
|
|
51
|
+
{
|
|
52
|
+
path: "app/api/devtools/[[...path]]/route.ts",
|
|
53
|
+
content: serverFiles.devtoolsRoute,
|
|
54
|
+
},
|
|
55
|
+
{ path: "app/api/openapi/route.ts", content: serverFiles.openApiRoute },
|
|
56
|
+
{
|
|
57
|
+
path: "app/api/auth/[...all]/route.ts",
|
|
58
|
+
content: serverFiles.authRoute,
|
|
59
|
+
},
|
|
60
|
+
{ path: "features/shared/errors.ts", content: serverFiles.sharedErrors },
|
|
61
|
+
{ path: "features/todos/contracts.ts", content: todosFiles.contracts },
|
|
62
|
+
{ path: "features/todos/schemas.ts", content: todosFiles.schemas },
|
|
63
|
+
{ path: "features/todos/routes.ts", content: todosFiles.routes },
|
|
64
|
+
{ path: "features/todos/policy.ts", content: todosFiles.policy },
|
|
65
|
+
{ path: "features/todos/ports.ts", content: todosFiles.ports },
|
|
66
|
+
{
|
|
67
|
+
path: "features/todos/use-cases/create-todo.ts",
|
|
68
|
+
content: todosFiles.createTodoUseCase,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: "features/todos/use-cases/delete-todo.ts",
|
|
72
|
+
content: todosFiles.deleteTodoUseCase,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
path: "features/todos/use-cases/list-todos.ts",
|
|
76
|
+
content: todosFiles.listTodosUseCase,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
path: "features/todos/use-cases/update-todo.ts",
|
|
80
|
+
content: todosFiles.updateTodoUseCase,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: "features/todos/use-cases/index.ts",
|
|
84
|
+
content: todosFiles.useCasesIndex,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
path: "features/todos/tests/helpers.ts",
|
|
88
|
+
content: todosFiles.testHelpers,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
path: "features/todos/tests/todos.test.ts",
|
|
92
|
+
content: todosFiles.useCaseTest,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
path: "features/todos/tests/routes.test.ts",
|
|
96
|
+
content: todosFiles.routesTest,
|
|
97
|
+
},
|
|
98
|
+
{ path: "ports/index.ts", content: ports(ctx) },
|
|
99
|
+
{ path: "ports/auth.ts", content: serverFiles.authPort },
|
|
100
|
+
{ path: "infra/app-ports.ts", content: infrastructurePorts(ctx) },
|
|
101
|
+
{ path: "infra/db/provider.ts", content: dbFiles.dbProvider },
|
|
102
|
+
{ path: "infra/db/repositories.ts", content: dbFiles.dbRepositories },
|
|
103
|
+
{ path: "infra/db/reset.ts", content: dbFiles.dbReset },
|
|
104
|
+
{ path: "infra/db/test-database.ts", content: dbFiles.dbTestDatabase },
|
|
105
|
+
{ path: "infra/db/schema/index.ts", content: dbFiles.dbSchema },
|
|
106
|
+
{
|
|
107
|
+
path: "infra/todos/drizzle-todo-repository.ts",
|
|
108
|
+
content: dbFiles.drizzleTodoRepository,
|
|
109
|
+
},
|
|
110
|
+
{ path: "drizzle.config.ts", content: dbFiles.drizzleConfig },
|
|
111
|
+
{ path: "drizzle/0000_starter.sql", content: dbFiles.starterMigrationSql },
|
|
112
|
+
{
|
|
113
|
+
path: "drizzle/meta/_journal.json",
|
|
114
|
+
content: dbFiles.starterMigrationJournal,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: "drizzle/meta/0000_snapshot.json",
|
|
118
|
+
content: dbFiles.starterMigrationSnapshot,
|
|
119
|
+
},
|
|
120
|
+
{ path: "lib/env.ts", content: serverFiles.env },
|
|
121
|
+
{ path: "lib/auth.ts", content: serverFiles.authHelpers },
|
|
122
|
+
{ path: "lib/better-auth.ts", content: serverFiles.betterAuth },
|
|
123
|
+
{ path: "lib/use-case.ts", content: serverFiles.useCaseBuilder },
|
|
124
|
+
{ path: "server/index.ts", content: serverFiles.server },
|
|
125
|
+
{ path: "server/context.ts", content: serverFiles.serverContext },
|
|
126
|
+
{ path: "server/routes.ts", content: serverFiles.serverRoutes },
|
|
127
|
+
{ path: "server/providers.ts", content: serverProviders(ctx) },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (ctx.api) {
|
|
131
|
+
templateFiles.push(
|
|
132
|
+
{ path: "app/layout.tsx", content: serverFiles.apiLayout },
|
|
133
|
+
{ path: "app/page.tsx", content: serverFiles.apiPage },
|
|
134
|
+
{ path: "client/index.ts", content: serverFiles.apiClientIndex },
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
templateFiles.push(...shadcnTemplateFiles(), ...shellTemplateFiles(ctx));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (ctx.integrations.length > 0) {
|
|
141
|
+
templateFiles.push({
|
|
142
|
+
path: "docs/integrations.md",
|
|
143
|
+
content: integrationsDoc(ctx),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return templateFiles;
|
|
148
|
+
}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { hasIntegration, type TemplateContext } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
export function serverProviders(ctx: TemplateContext): string {
|
|
4
|
+
const imports = [
|
|
5
|
+
'import type { beignetServerOnly } from "@beignet/core/server-only";',
|
|
6
|
+
'import { createDevtoolsProvider } from "@beignet/devtools";',
|
|
7
|
+
'import { createAuthBetterAuthProvider } from "@beignet/provider-auth-better-auth";',
|
|
8
|
+
'import { createDrizzleTursoProvider } from "@beignet/provider-drizzle-turso";',
|
|
9
|
+
hasIntegration(ctx, "inngest")
|
|
10
|
+
? 'import { inngestProvider } from "@beignet/provider-inngest";'
|
|
11
|
+
: undefined,
|
|
12
|
+
'import { loggerPinoProvider } from "@beignet/provider-logger-pino";',
|
|
13
|
+
hasIntegration(ctx, "resend")
|
|
14
|
+
? 'import { mailResendProvider } from "@beignet/provider-mail-resend";'
|
|
15
|
+
: undefined,
|
|
16
|
+
hasIntegration(ctx, "upstash-rate-limit")
|
|
17
|
+
? 'import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";'
|
|
18
|
+
: undefined,
|
|
19
|
+
'import * as schema from "@/infra/db/schema";',
|
|
20
|
+
'import { starterDatabaseProvider } from "@/infra/db/provider";',
|
|
21
|
+
'import { auth } from "@/lib/better-auth";',
|
|
22
|
+
'import type { AuthSessionMetadata, AuthUser } from "@/ports/auth";',
|
|
23
|
+
].filter((line): line is string => Boolean(line));
|
|
24
|
+
|
|
25
|
+
const entries = [
|
|
26
|
+
"\tcreateDevtoolsProvider(),",
|
|
27
|
+
"\tcreateAuthBetterAuthProvider<AuthUser, AuthSessionMetadata>(auth),",
|
|
28
|
+
"\tloggerPinoProvider,",
|
|
29
|
+
"\tdrizzleTursoProvider,",
|
|
30
|
+
"\tstarterDatabaseProvider,",
|
|
31
|
+
hasIntegration(ctx, "inngest") ? "\tinngestProvider," : undefined,
|
|
32
|
+
hasIntegration(ctx, "resend") ? "\tmailResendProvider," : undefined,
|
|
33
|
+
hasIntegration(ctx, "upstash-rate-limit")
|
|
34
|
+
? "\tupstashRateLimitProvider,"
|
|
35
|
+
: undefined,
|
|
36
|
+
].filter((entry): entry is string => Boolean(entry));
|
|
37
|
+
|
|
38
|
+
return `${imports.join("\n")}
|
|
39
|
+
|
|
40
|
+
const drizzleTursoProvider = createDrizzleTursoProvider({ schema });
|
|
41
|
+
|
|
42
|
+
export const providers = [
|
|
43
|
+
${entries.join("\n")}
|
|
44
|
+
] as const;
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ports(ctx: TemplateContext): string {
|
|
49
|
+
const usesInngest = hasIntegration(ctx, "inngest");
|
|
50
|
+
const usesResend = hasIntegration(ctx, "resend");
|
|
51
|
+
const usesUpstash = hasIntegration(ctx, "upstash-rate-limit");
|
|
52
|
+
const coreImports = [
|
|
53
|
+
"\tBoundGate,",
|
|
54
|
+
"\tGatePort,",
|
|
55
|
+
...(usesInngest ? ["\tJobDispatcherPort,"] : []),
|
|
56
|
+
"\tLoggerPort,",
|
|
57
|
+
...(usesUpstash ? ["\tRateLimitPort,"] : []),
|
|
58
|
+
"\tUnitOfWorkPort,",
|
|
59
|
+
];
|
|
60
|
+
const mailImport = usesResend
|
|
61
|
+
? 'import type { MailerPort } from "@beignet/core/mail";\n'
|
|
62
|
+
: "";
|
|
63
|
+
|
|
64
|
+
return `import type { IdempotencyPort } from "@beignet/core/idempotency";
|
|
65
|
+
${mailImport}import type {
|
|
66
|
+
${coreImports.join("\n")}
|
|
67
|
+
} from "@beignet/core/ports";
|
|
68
|
+
import type { AuthorizationContext, todoPolicy } from "@/features/todos/policy";
|
|
69
|
+
import type { TodoRepository } from "@/features/todos/ports";
|
|
70
|
+
import type { AuthPort } from "./auth";
|
|
71
|
+
|
|
72
|
+
export type AppTransactionPorts = {
|
|
73
|
+
idempotency: IdempotencyPort;
|
|
74
|
+
todos: TodoRepository;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type AppGate = BoundGate<[typeof todoPolicy]>;
|
|
78
|
+
|
|
79
|
+
export type AppPorts = {
|
|
80
|
+
auth: AuthPort;
|
|
81
|
+
gate: GatePort<AuthorizationContext, [typeof todoPolicy]>;
|
|
82
|
+
idempotency: IdempotencyPort;
|
|
83
|
+
${usesInngest ? "\tjobs: JobDispatcherPort;\n" : ""} logger: LoggerPort;
|
|
84
|
+
${usesResend ? "\tmailer: MailerPort;\n" : ""}${usesUpstash ? "\trateLimit: RateLimitPort;\n" : ""} todos: TodoRepository;
|
|
85
|
+
uow: UnitOfWorkPort<AppTransactionPorts>;
|
|
86
|
+
};
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function infrastructurePorts(ctx: TemplateContext): string {
|
|
91
|
+
const deferred = [
|
|
92
|
+
'\t\t"auth",',
|
|
93
|
+
'\t\t"idempotency",',
|
|
94
|
+
hasIntegration(ctx, "inngest") ? '\t\t"jobs",' : undefined,
|
|
95
|
+
'\t\t"logger",',
|
|
96
|
+
hasIntegration(ctx, "resend") ? '\t\t"mailer",' : undefined,
|
|
97
|
+
hasIntegration(ctx, "upstash-rate-limit") ? '\t\t"rateLimit",' : undefined,
|
|
98
|
+
'\t\t"todos",',
|
|
99
|
+
'\t\t"uow",',
|
|
100
|
+
].filter((entry): entry is string => Boolean(entry));
|
|
101
|
+
|
|
102
|
+
return `import { createGate, definePorts } from "@beignet/core/ports";
|
|
103
|
+
import { todoPolicy } from "@/features/todos/policy";
|
|
104
|
+
import { appError } from "@/features/shared/errors";
|
|
105
|
+
import type { AppPorts } from "@/ports";
|
|
106
|
+
|
|
107
|
+
const gate = createGate({
|
|
108
|
+
policies: [todoPolicy],
|
|
109
|
+
onDeny(decision) {
|
|
110
|
+
return appError("Forbidden", {
|
|
111
|
+
message: decision.reason ?? "Forbidden",
|
|
112
|
+
details: decision.details,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* App ports the server boots with.
|
|
119
|
+
*
|
|
120
|
+
* Bound ports are app-owned. Every deferred key is contributed by a provider
|
|
121
|
+
* in server/providers.ts during startup; createNextServer(...) fails boot if
|
|
122
|
+
* any of them is still unbound after providers have started.
|
|
123
|
+
*/
|
|
124
|
+
export const appPorts = definePorts<AppPorts>()({
|
|
125
|
+
bound: {
|
|
126
|
+
gate,
|
|
127
|
+
},
|
|
128
|
+
deferred: [
|
|
129
|
+
${deferred.join("\n")}
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const files = {
|
|
136
|
+
env: `import { createEnv } from "@beignet/core/config";
|
|
137
|
+
import { z } from "zod";
|
|
138
|
+
|
|
139
|
+
const BooleanEnv = z.enum(["true", "false"]).transform((value) => value === "true");
|
|
140
|
+
|
|
141
|
+
export const env = createEnv({
|
|
142
|
+
server: {
|
|
143
|
+
NODE_ENV: z
|
|
144
|
+
.enum(["development", "test", "production"])
|
|
145
|
+
.default("development"),
|
|
146
|
+
APP_URL: z.string().url().default("http://localhost:3000"),
|
|
147
|
+
DEVTOOLS_ENABLED: BooleanEnv.optional(),
|
|
148
|
+
BETTER_AUTH_SECRET: z
|
|
149
|
+
.string()
|
|
150
|
+
.min(32)
|
|
151
|
+
.default("local-dev-better-auth-secret-change-me"),
|
|
152
|
+
BETTER_AUTH_URL: z.string().url().default("http://localhost:3000"),
|
|
153
|
+
BETTER_AUTH_TRUSTED_ORIGINS: z.string().optional(),
|
|
154
|
+
TURSO_DB_URL: z.string().default("file:local.db"),
|
|
155
|
+
TURSO_DB_AUTH_TOKEN: z.string().optional(),
|
|
156
|
+
LOG_LEVEL: z
|
|
157
|
+
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
|
158
|
+
.default("info"),
|
|
159
|
+
LOG_FORMAT: z.enum(["pretty", "json"]).default("json"),
|
|
160
|
+
LOG_SERVICE: z.string().default("beignet-app"),
|
|
161
|
+
},
|
|
162
|
+
runtimeEnv: process.env,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const isProduction = env.NODE_ENV === "production";
|
|
166
|
+
|
|
167
|
+
export const isDevtoolsEnabled =
|
|
168
|
+
env.DEVTOOLS_ENABLED ?? process.env.NODE_ENV !== "production";
|
|
169
|
+
`,
|
|
170
|
+
appContext: `import type { ActivityActor, ActivityTenant } from "@beignet/core/ports";
|
|
171
|
+
import type { InferProviderPorts } from "@beignet/core/providers";
|
|
172
|
+
import type { TraceContext } from "@beignet/core/tracing";
|
|
173
|
+
import type { AppGate, AppPorts } from "@/ports";
|
|
174
|
+
import type { AuthSession } from "@/ports/auth";
|
|
175
|
+
import type { providers } from "@/server/providers";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* App ports plus the ports contributed by the server's providers at startup.
|
|
179
|
+
*/
|
|
180
|
+
export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>;
|
|
181
|
+
|
|
182
|
+
export type AppContext = {
|
|
183
|
+
requestId: string;
|
|
184
|
+
actor: ActivityActor;
|
|
185
|
+
auth: AuthSession | null;
|
|
186
|
+
gate: AppGate;
|
|
187
|
+
ports: AppRuntimePorts;
|
|
188
|
+
tenant?: ActivityTenant;
|
|
189
|
+
} & Partial<TraceContext>;
|
|
190
|
+
`,
|
|
191
|
+
sharedErrors: `import { createAppError, defineErrors } from "@beignet/core/errors";
|
|
192
|
+
|
|
193
|
+
export const errors = defineErrors({
|
|
194
|
+
Unauthorized: {
|
|
195
|
+
code: "UNAUTHORIZED",
|
|
196
|
+
status: 401,
|
|
197
|
+
message: "Authentication required",
|
|
198
|
+
},
|
|
199
|
+
Forbidden: {
|
|
200
|
+
code: "FORBIDDEN",
|
|
201
|
+
status: 403,
|
|
202
|
+
message: "Forbidden",
|
|
203
|
+
},
|
|
204
|
+
TodoNotFound: {
|
|
205
|
+
code: "TODO_NOT_FOUND",
|
|
206
|
+
status: 404,
|
|
207
|
+
message: "Todo not found",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
export const appError = createAppError(errors);
|
|
212
|
+
`,
|
|
213
|
+
useCaseBuilder: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
214
|
+
import { createUseCase } from "@beignet/core/application";
|
|
215
|
+
import type { AppContext } from "@/app-context";
|
|
216
|
+
|
|
217
|
+
export const useCase = createUseCase<AppContext>();
|
|
218
|
+
`,
|
|
219
|
+
authHelpers: `import type { AppContext } from "@/app-context";
|
|
220
|
+
import type { AuthSession, AuthUser } from "@/ports/auth";
|
|
221
|
+
import { appError } from "@/features/shared/errors";
|
|
222
|
+
|
|
223
|
+
export function requireSession(ctx: AppContext): AuthSession {
|
|
224
|
+
if (!ctx.auth) {
|
|
225
|
+
throw appError("Unauthorized");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ctx.auth;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function requireUser(ctx: AppContext): AuthUser {
|
|
232
|
+
return requireSession(ctx).user;
|
|
233
|
+
}
|
|
234
|
+
`,
|
|
235
|
+
authPort: `import type {
|
|
236
|
+
AuthPort as BeignetAuthPort,
|
|
237
|
+
AuthRequestLike,
|
|
238
|
+
AuthSession as BeignetAuthSession,
|
|
239
|
+
} from "@beignet/core/ports";
|
|
240
|
+
|
|
241
|
+
export type AuthRequest = AuthRequestLike;
|
|
242
|
+
|
|
243
|
+
export type AuthUser = {
|
|
244
|
+
id: string;
|
|
245
|
+
email?: string;
|
|
246
|
+
name?: string;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export type AuthSessionMetadata = {
|
|
250
|
+
id?: string;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export type AuthSession = BeignetAuthSession<
|
|
254
|
+
AuthUser,
|
|
255
|
+
AuthSessionMetadata
|
|
256
|
+
>;
|
|
257
|
+
|
|
258
|
+
export type AuthPort = BeignetAuthPort<
|
|
259
|
+
AuthUser,
|
|
260
|
+
AuthSessionMetadata,
|
|
261
|
+
AuthRequest
|
|
262
|
+
>;
|
|
263
|
+
`,
|
|
264
|
+
serverRoutes: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
265
|
+
import { contractsFromRoutes, defineRoutes } from "@beignet/next";
|
|
266
|
+
import type { AppContext } from "@/app-context";
|
|
267
|
+
import { todoRoutes } from "@/features/todos/routes";
|
|
268
|
+
|
|
269
|
+
export const routes = defineRoutes<AppContext>([
|
|
270
|
+
todoRoutes,
|
|
271
|
+
]);
|
|
272
|
+
export const contracts = contractsFromRoutes(routes);
|
|
273
|
+
`,
|
|
274
|
+
serverContext: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
275
|
+
import {
|
|
276
|
+
type ActivityActor,
|
|
277
|
+
createAnonymousActor,
|
|
278
|
+
createServiceActor,
|
|
279
|
+
createTenant,
|
|
280
|
+
createUserActor,
|
|
281
|
+
} from "@beignet/core/ports";
|
|
282
|
+
import { defineServerContext } from "@beignet/core/server";
|
|
283
|
+
import type { TraceContext } from "@beignet/core/tracing";
|
|
284
|
+
import type { AppContext, AppRuntimePorts } from "@/app-context";
|
|
285
|
+
|
|
286
|
+
export type AppServiceContextInput =
|
|
287
|
+
| {
|
|
288
|
+
actor?: ActivityActor;
|
|
289
|
+
tenantId?: string;
|
|
290
|
+
}
|
|
291
|
+
| undefined;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Context blueprint shared by the runtime server and route tests.
|
|
295
|
+
*/
|
|
296
|
+
export const appContext = defineServerContext<AppContext, AppRuntimePorts>()({
|
|
297
|
+
gate: (ports) => ports.gate,
|
|
298
|
+
request: async ({ req, ports, requestId, trace }) => {
|
|
299
|
+
const auth = await ports.auth.getSession(req);
|
|
300
|
+
const tenantId = req.headers.get("x-tenant-id") || undefined;
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
requestId,
|
|
304
|
+
actor: auth
|
|
305
|
+
? createUserActor(auth.user.id, { displayName: auth.user.name })
|
|
306
|
+
: createAnonymousActor(),
|
|
307
|
+
auth,
|
|
308
|
+
...trace,
|
|
309
|
+
ports,
|
|
310
|
+
tenant: tenantId ? createTenant(tenantId) : undefined,
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
service: ({
|
|
314
|
+
ports,
|
|
315
|
+
input,
|
|
316
|
+
requestId,
|
|
317
|
+
trace,
|
|
318
|
+
}: {
|
|
319
|
+
ports: AppRuntimePorts;
|
|
320
|
+
input: AppServiceContextInput;
|
|
321
|
+
requestId: string;
|
|
322
|
+
trace: TraceContext;
|
|
323
|
+
}) => ({
|
|
324
|
+
requestId,
|
|
325
|
+
actor: input?.actor ?? createServiceActor("beignet-service"),
|
|
326
|
+
auth: null,
|
|
327
|
+
...trace,
|
|
328
|
+
ports,
|
|
329
|
+
tenant: createTenant(input?.tenantId ?? "tenant_default"),
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
`,
|
|
333
|
+
server: `import type { beignetServerOnly } from "@beignet/core/server-only";
|
|
334
|
+
import { createNextServer } from "@beignet/next";
|
|
335
|
+
import { createIdempotencyHooks } from "@beignet/core/server";
|
|
336
|
+
import type { AppContext } from "@/app-context";
|
|
337
|
+
import { appPorts } from "@/infra/app-ports";
|
|
338
|
+
import { env } from "@/lib/env";
|
|
339
|
+
import { appContext } from "./context";
|
|
340
|
+
import { providers } from "./providers";
|
|
341
|
+
import { routes } from "./routes";
|
|
342
|
+
|
|
343
|
+
export const server = await createNextServer({
|
|
344
|
+
ports: appPorts,
|
|
345
|
+
providers,
|
|
346
|
+
providerConfig: {
|
|
347
|
+
"drizzle-turso": {
|
|
348
|
+
DB_URL: env.TURSO_DB_URL,
|
|
349
|
+
DB_AUTH_TOKEN: env.TURSO_DB_AUTH_TOKEN,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
hooks: [createIdempotencyHooks<AppContext>()],
|
|
353
|
+
context: appContext,
|
|
354
|
+
routes,
|
|
355
|
+
mapUnhandledError: ({ err, ctx }) => {
|
|
356
|
+
ctx?.ports.logger.error("Unhandled API error", {
|
|
357
|
+
error: err,
|
|
358
|
+
requestId: ctx?.requestId,
|
|
359
|
+
environment: env.NODE_ENV,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
status: 500,
|
|
364
|
+
body: {
|
|
365
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
366
|
+
message: "Internal server error",
|
|
367
|
+
requestId: ctx?.requestId,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
`,
|
|
373
|
+
apiCatchAllRoute: `import { server } from "@/server";
|
|
374
|
+
|
|
375
|
+
export const GET = server.api;
|
|
376
|
+
export const HEAD = server.api;
|
|
377
|
+
export const OPTIONS = server.api;
|
|
378
|
+
export const PATCH = server.api;
|
|
379
|
+
export const POST = server.api;
|
|
380
|
+
export const PUT = server.api;
|
|
381
|
+
export const DELETE = server.api;
|
|
382
|
+
`,
|
|
383
|
+
healthRoute: `import { env } from "@/lib/env";
|
|
384
|
+
|
|
385
|
+
export function GET() {
|
|
386
|
+
return Response.json(
|
|
387
|
+
{
|
|
388
|
+
status: "ok",
|
|
389
|
+
environment: env.NODE_ENV,
|
|
390
|
+
timestamp: new Date().toISOString(),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
headers: {
|
|
394
|
+
"cache-control": "no-store",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
`,
|
|
400
|
+
devtoolsRoute: `import { createDevtoolsRoute } from "@beignet/devtools";
|
|
401
|
+
import { env } from "@/lib/env";
|
|
402
|
+
import { server } from "@/server";
|
|
403
|
+
|
|
404
|
+
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
|
|
405
|
+
basePath: "/api/devtools",
|
|
406
|
+
enabled: env.DEVTOOLS_ENABLED,
|
|
407
|
+
});
|
|
408
|
+
`,
|
|
409
|
+
openApiRoute: `import { createOpenAPIHandler } from "@beignet/next";
|
|
410
|
+
import { server } from "@/server";
|
|
411
|
+
|
|
412
|
+
export const GET = createOpenAPIHandler(server.contracts, {
|
|
413
|
+
title: "Beignet starter API",
|
|
414
|
+
version: "0.1.0",
|
|
415
|
+
});
|
|
416
|
+
`,
|
|
417
|
+
authRoute: `import { toNextJsHandler } from "better-auth/next-js";
|
|
418
|
+
import { auth } from "@/lib/better-auth";
|
|
419
|
+
|
|
420
|
+
export const { GET, POST } = toNextJsHandler(auth);
|
|
421
|
+
`,
|
|
422
|
+
betterAuth: `import { createClient } from "@libsql/client";
|
|
423
|
+
import { betterAuth } from "better-auth";
|
|
424
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
425
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
426
|
+
import * as schema from "@/infra/db/schema";
|
|
427
|
+
import { env } from "@/lib/env";
|
|
428
|
+
|
|
429
|
+
const client = createClient({
|
|
430
|
+
url: env.TURSO_DB_URL,
|
|
431
|
+
authToken: env.TURSO_DB_AUTH_TOKEN,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const db = drizzle(client, { schema });
|
|
435
|
+
|
|
436
|
+
const trustedOrigins = [
|
|
437
|
+
env.APP_URL,
|
|
438
|
+
env.BETTER_AUTH_URL,
|
|
439
|
+
...(env.BETTER_AUTH_TRUSTED_ORIGINS?.split(",")
|
|
440
|
+
.map((origin) => origin.trim())
|
|
441
|
+
.filter(Boolean) ?? []),
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
export const auth = betterAuth({
|
|
445
|
+
baseURL: env.BETTER_AUTH_URL,
|
|
446
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
447
|
+
trustedOrigins: [...new Set(trustedOrigins)],
|
|
448
|
+
database: drizzleAdapter(db, {
|
|
449
|
+
provider: "sqlite",
|
|
450
|
+
schema,
|
|
451
|
+
}),
|
|
452
|
+
emailAndPassword: {
|
|
453
|
+
enabled: true,
|
|
454
|
+
},
|
|
455
|
+
user: {
|
|
456
|
+
changeEmail: {
|
|
457
|
+
enabled: true,
|
|
458
|
+
},
|
|
459
|
+
deleteUser: {
|
|
460
|
+
enabled: true,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
`,
|
|
465
|
+
// API-only scaffold frontend: a minimal layout and landing page that point
|
|
466
|
+
// at the contract-backed API surface, plus a typed client without React
|
|
467
|
+
// Query. The full-stack scaffold gets its frontend from templates/shell.ts.
|
|
468
|
+
apiLayout: `import type { Metadata } from "next";
|
|
469
|
+
import type { ReactNode } from "react";
|
|
470
|
+
|
|
471
|
+
export const metadata: Metadata = {
|
|
472
|
+
title: "Beignet API",
|
|
473
|
+
description: "A contract-first Beignet API.",
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
477
|
+
return (
|
|
478
|
+
<html lang="en">
|
|
479
|
+
<body
|
|
480
|
+
style={{
|
|
481
|
+
margin: 0,
|
|
482
|
+
fontFamily:
|
|
483
|
+
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
{children}
|
|
487
|
+
</body>
|
|
488
|
+
</html>
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
`,
|
|
492
|
+
apiPage: `export default function HomePage() {
|
|
493
|
+
return (
|
|
494
|
+
<main style={{ margin: "0 auto", maxWidth: "640px", padding: "64px 24px" }}>
|
|
495
|
+
<h1>Beignet API starter</h1>
|
|
496
|
+
<p>
|
|
497
|
+
This app is API-only. Contract-backed routes live under{" "}
|
|
498
|
+
<code>/api</code>, and the todos API requires a signed-in Better Auth
|
|
499
|
+
user.
|
|
500
|
+
</p>
|
|
501
|
+
<ul style={{ display: "grid", gap: "8px", paddingLeft: "20px" }}>
|
|
502
|
+
<li>
|
|
503
|
+
<a href="/api/health">/api/health</a> — health check
|
|
504
|
+
</li>
|
|
505
|
+
<li>
|
|
506
|
+
<a href="/api/openapi">/api/openapi</a> — OpenAPI document
|
|
507
|
+
</li>
|
|
508
|
+
<li>
|
|
509
|
+
<a href="/api/devtools">/api/devtools</a> — devtools (development
|
|
510
|
+
only)
|
|
511
|
+
</li>
|
|
512
|
+
<li>
|
|
513
|
+
<a href="https://beignetjs.com">Beignet docs</a>
|
|
514
|
+
</li>
|
|
515
|
+
</ul>
|
|
516
|
+
</main>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
apiClientIndex: `import { createClient } from "@beignet/core/client";
|
|
521
|
+
|
|
522
|
+
export const apiClient = createClient({
|
|
523
|
+
validateInput: true,
|
|
524
|
+
});
|
|
525
|
+
`,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
export { files as serverFiles };
|