@cosmicdrift/kumiko-dev-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kumiko-build.ts +85 -0
- package/bin/kumiko-dev.ts +90 -0
- package/package.json +45 -0
- package/src/__tests__/build-prod-bundle.integration.ts +265 -0
- package/src/__tests__/build-prod-bundle.test.ts +262 -0
- package/src/__tests__/cache-headers.test.ts +70 -0
- package/src/__tests__/classify-change.test.ts +87 -0
- package/src/__tests__/compose-features-wiring.integration.ts +352 -0
- package/src/__tests__/compose-features.test.ts +81 -0
- package/src/__tests__/crash-tracker.test.ts +89 -0
- package/src/__tests__/create-kumiko-server.integration.ts +286 -0
- package/src/__tests__/few-shot-corpus.test.ts +311 -0
- package/src/__tests__/inject-schema.test.ts +62 -0
- package/src/__tests__/resolve-stylesheet.test.ts +90 -0
- package/src/__tests__/resolve-tailwind-cli.test.ts +49 -0
- package/src/__tests__/run-prod-app-spec.test.ts +57 -0
- package/src/__tests__/run-prod-app.integration.ts +535 -0
- package/src/__tests__/scaffold-feature.test.ts +143 -0
- package/src/__tests__/try-hono-first.test.ts +63 -0
- package/src/build-prod-bundle.ts +587 -0
- package/src/build-server-bundle.ts +308 -0
- package/src/build.ts +28 -0
- package/src/codegen/__tests__/run-codegen.test.ts +494 -0
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +467 -0
- package/src/codegen/__tests__/watch.test.ts +186 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/render.ts +225 -0
- package/src/codegen/run-codegen.ts +157 -0
- package/src/codegen/scan-events.ts +574 -0
- package/src/codegen/watch.ts +127 -0
- package/src/compose-features.ts +128 -0
- package/src/crash-tracker.ts +56 -0
- package/src/create-kumiko-server.ts +1010 -0
- package/src/drizzle-config.ts +44 -0
- package/src/drizzle-tables-auth-mode.ts +32 -0
- package/src/drizzle-tables-minimal.ts +22 -0
- package/src/few-shot-corpus.ts +369 -0
- package/src/index.ts +57 -0
- package/src/inject-schema.ts +24 -0
- package/src/resolve-tailwind-cli.ts +28 -0
- package/src/run-dev-app.ts +290 -0
- package/src/run-prod-app.ts +892 -0
- package/src/scaffold-feature.ts +226 -0
- package/src/try-hono-first.ts +46 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// runDevApp — high-level dev-server wrapper für Sample-Apps und
|
|
2
|
+
// Showcases. Mischt die Standard-Features (config/user/tenant/auth-
|
|
3
|
+
// email-password) automatisch dazu wenn auth-mode aktiv ist, wired das
|
|
4
|
+
// AuthRoutesConfig + Login-Error-Map, und ruft seedAdmin() im
|
|
5
|
+
// onAfterSetup. Reduziert den Sample-Bootstrap von 50 Zeilen auf 5-10.
|
|
6
|
+
//
|
|
7
|
+
// Auto-mix passiert NUR im auth-mode. Ohne `auth`-Block bleibt der
|
|
8
|
+
// Server im Auto-Mint-JWT-Modus (Dev-Default) und mischt nichts dazu —
|
|
9
|
+
// Showcases die nur eine Domain demonstrieren wollen ohne Login-Flow
|
|
10
|
+
// haben so nicht plötzlich vier zusätzliche Features in der Registry.
|
|
11
|
+
//
|
|
12
|
+
// Wer maximale Kontrolle braucht (z.B. abweichende Auth-Wiring,
|
|
13
|
+
// alternativer Membership-Query, eigener LoginRateLimiter): geht direkt
|
|
14
|
+
// auf `createKumikoServer` aus @cosmicdrift/kumiko-dev-server.
|
|
15
|
+
|
|
16
|
+
import { AuthErrors, AuthHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
17
|
+
import {
|
|
18
|
+
type SeedAdminOptions,
|
|
19
|
+
seedAdmin,
|
|
20
|
+
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
21
|
+
import { createConfigResolver } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
22
|
+
import {
|
|
23
|
+
createSessionCallbacks,
|
|
24
|
+
type SessionCallbacks,
|
|
25
|
+
} from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
26
|
+
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
27
|
+
|
|
28
|
+
import type { SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
29
|
+
import type { FeatureDefinition, SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
30
|
+
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
31
|
+
|
|
32
|
+
import { watchAndRegenerate } from "./codegen";
|
|
33
|
+
import { buildComposeAuthOptions, composeFeatures } from "./compose-features";
|
|
34
|
+
import {
|
|
35
|
+
type CreateKumikoServerOptions,
|
|
36
|
+
createKumikoServer,
|
|
37
|
+
type KumikoServerHandle,
|
|
38
|
+
} from "./create-kumiko-server";
|
|
39
|
+
|
|
40
|
+
// Re-export der shared Auth-Setup-Types damit Apps nur einen Import-Pfad
|
|
41
|
+
// brauchen. PasswordResetSetup / EmailVerificationSetup leben in
|
|
42
|
+
// run-prod-app.ts (single source of truth) — hier nur durchgereicht.
|
|
43
|
+
export type {
|
|
44
|
+
EmailVerificationSetup,
|
|
45
|
+
InviteSetup,
|
|
46
|
+
PasswordResetSetup,
|
|
47
|
+
SignupSetup,
|
|
48
|
+
} from "./run-prod-app";
|
|
49
|
+
|
|
50
|
+
import type {
|
|
51
|
+
EmailVerificationSetup,
|
|
52
|
+
InviteSetup,
|
|
53
|
+
PasswordResetSetup,
|
|
54
|
+
SignupSetup,
|
|
55
|
+
} from "./run-prod-app";
|
|
56
|
+
|
|
57
|
+
export type RunDevAppAuthOptions = {
|
|
58
|
+
/** Admin user to seed at boot. Idempotent — re-runs in persistent-DB
|
|
59
|
+
* mode reuse the existing user. */
|
|
60
|
+
readonly admin: SeedAdminOptions;
|
|
61
|
+
/** Optional override of the login error → HTTP status map. Default
|
|
62
|
+
* maps invalidCredentials → 401, noMembership → 403. */
|
|
63
|
+
readonly loginErrorStatusMap?: Readonly<Record<string, number>>;
|
|
64
|
+
/** Opt-in: revocable server-side sessions. Caller MUSS
|
|
65
|
+
* `createSessionsFeature()` zu `features` adden — runDevApp wired
|
|
66
|
+
* hier nur die Auth-Callbacks (creator/revoker/checker) gegen
|
|
67
|
+
* stack.db, plus sessionStrictMode=true.
|
|
68
|
+
*
|
|
69
|
+
* Standardverhalten ohne diese Option: stateless JWTs ohne sid,
|
|
70
|
+
* Logout ist client-side cookie-clear, Kartenhaus existing-Apps
|
|
71
|
+
* bleibt unangefasst. */
|
|
72
|
+
readonly sessions?: {
|
|
73
|
+
readonly expiresInMs?: number;
|
|
74
|
+
};
|
|
75
|
+
/** Password-reset flow. Wenn gesetzt werden /api/auth/request-password-
|
|
76
|
+
* reset + /api/auth/reset-password als Public-Routes gemounted UND
|
|
77
|
+
* der request/confirm-Handler im auth-email-password-Feature wird
|
|
78
|
+
* registriert. Symmetrisch zu RunProdAppAuthOptions.passwordReset. */
|
|
79
|
+
readonly passwordReset?: PasswordResetSetup;
|
|
80
|
+
/** Email-verification flow. Symmetric zu passwordReset. */
|
|
81
|
+
readonly emailVerification?: EmailVerificationSetup;
|
|
82
|
+
/** Self-Signup flow (Magic-Link). Symmetric zu RunProdAppAuthOptions. */
|
|
83
|
+
readonly signup?: SignupSetup;
|
|
84
|
+
/** Tenant-Invite flow (Magic-Link). Symmetric. */
|
|
85
|
+
readonly invite?: InviteSetup;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Hook for app-specific seeding (demo data, fixtures). Runs after the
|
|
89
|
+
* admin (when auth is active) in declared order. */
|
|
90
|
+
export type SeedFn = (stack: TestStack) => Promise<void>;
|
|
91
|
+
|
|
92
|
+
export type RunDevAppOptions = {
|
|
93
|
+
/** App-spezifische Features. Im auth-mode werden config/user/tenant/
|
|
94
|
+
* auth-email-password automatisch dazu gemischt — KEIN doppeltes
|
|
95
|
+
* manuelles Hinzufügen nötig. */
|
|
96
|
+
readonly features: readonly FeatureDefinition[];
|
|
97
|
+
/** Pfad zum Browser-Entry-Modul. Bun.build bündelt es zu /client.js.
|
|
98
|
+
* Mutually exclusive mit `clientEntries`. */
|
|
99
|
+
readonly clientEntry?: string;
|
|
100
|
+
/** Multi-Entry-Mode: pro Entry ein eigenes Bundle (`/client-<name>.js`)
|
|
101
|
+
* + ein eigenes HTML-Template. `hostDispatch` wählt zur Request-Zeit
|
|
102
|
+
* welcher Entry kommt. Symmetric zur kumiko-build-Convention
|
|
103
|
+
* `src/client-<name>.tsx`. Mutually exclusive mit `clientEntry`. */
|
|
104
|
+
readonly clientEntries?: CreateKumikoServerOptions["clientEntries"];
|
|
105
|
+
/** Multi-Entry-Mode: Routing per Request. Wird für Multi-Entry mit
|
|
106
|
+
* geforderten — sonst weiß der Server nicht welche HTML er liefern
|
|
107
|
+
* soll. */
|
|
108
|
+
readonly hostDispatch?: CreateKumikoServerOptions["hostDispatch"];
|
|
109
|
+
/** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer-web/styles.css`
|
|
110
|
+
* wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
|
|
111
|
+
readonly stylesheet?: string | false;
|
|
112
|
+
/** Eigenes HTML-Template; sonst minimal-Default (#root + client.js).
|
|
113
|
+
* Im Multi-Entry-Mode ist es das Fallback-Template, wenn ein einzelner
|
|
114
|
+
* Entry kein eigenes htmlPath setzt. */
|
|
115
|
+
readonly htmlPath?: string;
|
|
116
|
+
/** Listen-Port. Default 4173 (oder $PORT). */
|
|
117
|
+
readonly port?: number;
|
|
118
|
+
/** Extra-Verzeichnisse für den File-Watcher (Trigger Hot-Reload). */
|
|
119
|
+
readonly watchDirs?: readonly string[];
|
|
120
|
+
/** SIGINT/SIGTERM-Handler installieren (Default true; in Tests auf
|
|
121
|
+
* false damit repeated boots keine Listener akkumulieren). */
|
|
122
|
+
readonly installSignalHandlers?: boolean;
|
|
123
|
+
/** Auth-Mode: Standard-Features dazu, Auth-Routes wired, seedAdmin
|
|
124
|
+
* läuft im onAfterSetup. Ohne `auth` läuft der Server im Auto-Mint-
|
|
125
|
+
* JWT-Modus. */
|
|
126
|
+
readonly auth?: RunDevAppAuthOptions;
|
|
127
|
+
/** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
|
|
128
|
+
* Array-Reihenfolge. */
|
|
129
|
+
readonly seeds?: readonly SeedFn[];
|
|
130
|
+
/** Extra-AppContext-Keys. Im auth-mode wird `configResolver` automatisch
|
|
131
|
+
* hinzugefügt — kein Override durch den Caller nötig. */
|
|
132
|
+
readonly extraContext?: CreateKumikoServerOptions["extraContext"];
|
|
133
|
+
/** Anonymous-Access für Public-Endpoints — Requests ohne JWT laufen
|
|
134
|
+
* als Pseudo-User mit Rolle `anonymous` durch, wenn der Handler die
|
|
135
|
+
* Rolle in `access.roles` führt. */
|
|
136
|
+
readonly anonymousAccess?: CreateKumikoServerOptions["anonymousAccess"];
|
|
137
|
+
/** App-eigene HTTP-Routes (z.B. /feed.xml, /sitemap.xml) — wird ans
|
|
138
|
+
* Hono-app gehängt, läuft VOR dem static-asset-Pfad. Symmetrisch zur
|
|
139
|
+
* gleichnamigen Option in runProdApp. */
|
|
140
|
+
readonly extraRoutes?: CreateKumikoServerOptions["extraRoutes"];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle> {
|
|
144
|
+
// Codegen + File-Watcher — schreibt `<appRoot>/.kumiko/types.generated.d.ts`
|
|
145
|
+
// + `define.ts` aus den r.defineEvent-Aufrufen der App, einmal beim
|
|
146
|
+
// Boot UND danach bei jeder relevanten Änderung unter `<appRoot>/src/`.
|
|
147
|
+
// Idempotent (writeIfChanged) — der TS-Sprachserver kriegt nur einen
|
|
148
|
+
// Reload-Tick wenn sich tatsächlich was geändert hat.
|
|
149
|
+
//
|
|
150
|
+
// App-Root ist process.cwd() (yarn-dev läuft vom App-Workspace). Der
|
|
151
|
+
// Watcher läuft solange der Dev-Server lebt; close() bei Shutdown
|
|
152
|
+
// wird über das createKumikoServer-Handle implizit erledigt (Bun's
|
|
153
|
+
// process-exit räumt fs.watch-handles auf).
|
|
154
|
+
watchAndRegenerate({ appRoot: process.cwd() });
|
|
155
|
+
|
|
156
|
+
// Auto-mix Standard-Features im auth-mode via composeFeatures (single
|
|
157
|
+
// source of truth — auch runProdApp und der per-app drizzle-Schema-
|
|
158
|
+
// Generator nutzen denselben Helper, damit Migration und Runtime nie
|
|
159
|
+
// auseinanderdriften können).
|
|
160
|
+
const composeAuthOptions = buildComposeAuthOptions(options.auth);
|
|
161
|
+
const features = composeFeatures(options.features, {
|
|
162
|
+
includeBundled: !!options.auth,
|
|
163
|
+
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// configResolver-default fürs config-feature — im auth-mode immer
|
|
167
|
+
// hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
|
|
168
|
+
// wird gewrap't damit der spread auf das aufgerufene Result greift,
|
|
169
|
+
// nicht auf die function selbst (no-op).
|
|
170
|
+
const extraContext = options.auth
|
|
171
|
+
? mergeConfigResolverDefault(options.extraContext)
|
|
172
|
+
: options.extraContext;
|
|
173
|
+
|
|
174
|
+
// Sessions opt-in: Holder lebt im closure, `createSessionCallbacks`
|
|
175
|
+
// kennt erst nach setupTestStack die echte db-connection. Inline
|
|
176
|
+
// statt @cosmicdrift/kumiko-framework/testing's createLateBoundHolder zu reusen,
|
|
177
|
+
// weil dev-server (dev-runtime) keine Tooling aus framework/testing
|
|
178
|
+
// (test-runtime) importieren darf — Runtime-Isolation Guard.
|
|
179
|
+
// Server-Start passiert NACH onAfterSetup (siehe create-kumiko-server.ts),
|
|
180
|
+
// daher ist `sessionCallbacks` zur ersten Login-Request konkret.
|
|
181
|
+
let sessionCallbacks: SessionCallbacks | undefined;
|
|
182
|
+
const requireSessions = (): SessionCallbacks => {
|
|
183
|
+
if (!sessionCallbacks) {
|
|
184
|
+
throw new Error("[runDevApp] session-callbacks accessed before onAfterSetup");
|
|
185
|
+
}
|
|
186
|
+
return sessionCallbacks;
|
|
187
|
+
};
|
|
188
|
+
const sessionAuthFragment =
|
|
189
|
+
options.auth?.sessions !== undefined
|
|
190
|
+
? {
|
|
191
|
+
sessionCreator: (user: SessionUser, meta: SessionMetadata) =>
|
|
192
|
+
requireSessions().sessionCreator(user, meta),
|
|
193
|
+
sessionRevoker: (sid: string) => requireSessions().sessionRevoker(sid),
|
|
194
|
+
sessionChecker: (sid: string, userId: string) =>
|
|
195
|
+
requireSessions().sessionChecker(sid, userId),
|
|
196
|
+
// strict-mode: jede neue Plattform-App startet ohne legacy-
|
|
197
|
+
// JWTs ohne sid, daher safe als Default. Wer Sessions opt-in
|
|
198
|
+
// wählt, will explizite Server-side Revocation — strict-mode
|
|
199
|
+
// ist der einzige Modus der das tatsächlich erzwingt.
|
|
200
|
+
sessionStrictMode: true,
|
|
201
|
+
}
|
|
202
|
+
: {};
|
|
203
|
+
|
|
204
|
+
return createKumikoServer({
|
|
205
|
+
features,
|
|
206
|
+
...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
|
|
207
|
+
...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
|
|
208
|
+
...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
|
|
209
|
+
...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
|
|
210
|
+
...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
|
|
211
|
+
...(options.port !== undefined && { port: options.port }),
|
|
212
|
+
...(options.watchDirs !== undefined && { watchDirs: options.watchDirs }),
|
|
213
|
+
...(options.installSignalHandlers !== undefined && {
|
|
214
|
+
installSignalHandlers: options.installSignalHandlers,
|
|
215
|
+
}),
|
|
216
|
+
...(extraContext !== undefined && { extraContext }),
|
|
217
|
+
...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
|
|
218
|
+
...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
|
|
219
|
+
...(options.auth && {
|
|
220
|
+
auth: {
|
|
221
|
+
membershipQuery: TenantQueries.memberships,
|
|
222
|
+
loginHandler: AuthHandlers.login,
|
|
223
|
+
loginErrorStatusMap: options.auth.loginErrorStatusMap ?? {
|
|
224
|
+
[AuthErrors.invalidCredentials]: 401,
|
|
225
|
+
[AuthErrors.noMembership]: 403,
|
|
226
|
+
},
|
|
227
|
+
...sessionAuthFragment,
|
|
228
|
+
...(options.auth.passwordReset && {
|
|
229
|
+
passwordReset: {
|
|
230
|
+
requestHandler: AuthHandlers.requestPasswordReset,
|
|
231
|
+
confirmHandler: AuthHandlers.resetPassword,
|
|
232
|
+
sendResetEmail: options.auth.passwordReset.sendResetEmail,
|
|
233
|
+
appResetUrl: options.auth.passwordReset.appResetUrl,
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
...(options.auth.emailVerification && {
|
|
237
|
+
emailVerification: {
|
|
238
|
+
requestHandler: AuthHandlers.requestEmailVerification,
|
|
239
|
+
confirmHandler: AuthHandlers.verifyEmail,
|
|
240
|
+
sendVerificationEmail: options.auth.emailVerification.sendVerificationEmail,
|
|
241
|
+
appVerifyUrl: options.auth.emailVerification.appVerifyUrl,
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
...(options.auth.signup && {
|
|
245
|
+
signup: {
|
|
246
|
+
requestHandler: AuthHandlers.signupRequest,
|
|
247
|
+
confirmHandler: AuthHandlers.signupConfirm,
|
|
248
|
+
sendActivationEmail: options.auth.signup.sendActivationEmail,
|
|
249
|
+
appActivationUrl: options.auth.signup.appActivationUrl,
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
...(options.auth.invite && {
|
|
253
|
+
invite: {
|
|
254
|
+
acceptHandler: AuthHandlers.inviteAccept,
|
|
255
|
+
acceptWithLoginHandler: AuthHandlers.inviteAcceptWithLogin,
|
|
256
|
+
signupCompleteHandler: AuthHandlers.inviteSignupComplete,
|
|
257
|
+
sendInviteEmail: options.auth.invite.sendInviteEmail,
|
|
258
|
+
appAcceptUrl: options.auth.invite.appAcceptUrl,
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
onAfterSetup: async (stack) => {
|
|
264
|
+
if (options.auth?.sessions !== undefined) {
|
|
265
|
+
const expiresInMs = options.auth.sessions.expiresInMs;
|
|
266
|
+
sessionCallbacks = createSessionCallbacks({
|
|
267
|
+
db: stack.db,
|
|
268
|
+
...(expiresInMs !== undefined && { expiresInMs }),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (options.auth) {
|
|
272
|
+
await seedAdmin(stack.db, options.auth.admin);
|
|
273
|
+
}
|
|
274
|
+
for (const seed of options.seeds ?? []) {
|
|
275
|
+
await seed(stack);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mergeConfigResolverDefault(
|
|
282
|
+
ctx: CreateKumikoServerOptions["extraContext"],
|
|
283
|
+
): CreateKumikoServerOptions["extraContext"] {
|
|
284
|
+
const defaults = { configResolver: createConfigResolver() };
|
|
285
|
+
if (ctx === undefined) return defaults;
|
|
286
|
+
if (typeof ctx === "function") {
|
|
287
|
+
return (deps) => ({ ...defaults, ...ctx(deps) });
|
|
288
|
+
}
|
|
289
|
+
return { ...defaults, ...ctx };
|
|
290
|
+
}
|