@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.
Files changed (44) hide show
  1. package/bin/kumiko-build.ts +85 -0
  2. package/bin/kumiko-dev.ts +90 -0
  3. package/package.json +45 -0
  4. package/src/__tests__/build-prod-bundle.integration.ts +265 -0
  5. package/src/__tests__/build-prod-bundle.test.ts +262 -0
  6. package/src/__tests__/cache-headers.test.ts +70 -0
  7. package/src/__tests__/classify-change.test.ts +87 -0
  8. package/src/__tests__/compose-features-wiring.integration.ts +352 -0
  9. package/src/__tests__/compose-features.test.ts +81 -0
  10. package/src/__tests__/crash-tracker.test.ts +89 -0
  11. package/src/__tests__/create-kumiko-server.integration.ts +286 -0
  12. package/src/__tests__/few-shot-corpus.test.ts +311 -0
  13. package/src/__tests__/inject-schema.test.ts +62 -0
  14. package/src/__tests__/resolve-stylesheet.test.ts +90 -0
  15. package/src/__tests__/resolve-tailwind-cli.test.ts +49 -0
  16. package/src/__tests__/run-prod-app-spec.test.ts +57 -0
  17. package/src/__tests__/run-prod-app.integration.ts +535 -0
  18. package/src/__tests__/scaffold-feature.test.ts +143 -0
  19. package/src/__tests__/try-hono-first.test.ts +63 -0
  20. package/src/build-prod-bundle.ts +587 -0
  21. package/src/build-server-bundle.ts +308 -0
  22. package/src/build.ts +28 -0
  23. package/src/codegen/__tests__/run-codegen.test.ts +494 -0
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +467 -0
  25. package/src/codegen/__tests__/watch.test.ts +186 -0
  26. package/src/codegen/index.ts +17 -0
  27. package/src/codegen/render.ts +225 -0
  28. package/src/codegen/run-codegen.ts +157 -0
  29. package/src/codegen/scan-events.ts +574 -0
  30. package/src/codegen/watch.ts +127 -0
  31. package/src/compose-features.ts +128 -0
  32. package/src/crash-tracker.ts +56 -0
  33. package/src/create-kumiko-server.ts +1010 -0
  34. package/src/drizzle-config.ts +44 -0
  35. package/src/drizzle-tables-auth-mode.ts +32 -0
  36. package/src/drizzle-tables-minimal.ts +22 -0
  37. package/src/few-shot-corpus.ts +369 -0
  38. package/src/index.ts +57 -0
  39. package/src/inject-schema.ts +24 -0
  40. package/src/resolve-tailwind-cli.ts +28 -0
  41. package/src/run-dev-app.ts +290 -0
  42. package/src/run-prod-app.ts +892 -0
  43. package/src/scaffold-feature.ts +226 -0
  44. 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, Karten­haus 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
+ }