@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,128 @@
1
+ // composeFeatures — single source of truth für die Feature-Liste die
2
+ // Boot UND Schema-Generator sehen.
3
+ //
4
+ // Sowohl runDevApp als auch runProdApp mischen im auth-mode dieselben
5
+ // vier Bundled-Features dazu (config + user + tenant + auth-email-pw).
6
+ // Damit der drizzle-Schema-Generator pro App genau dieselbe Feature-
7
+ // Liste sieht wie die Runtime, leben die Komposition hier — beide
8
+ // Bootstrap-Wrapper UND der per-app drizzle/generate.ts rufen sie auf.
9
+ //
10
+ // Reihenfolge: Infrastruktur-Features (config/user/tenant) zuerst, dann
11
+ // auth-email-password, dann die App-Features. Spätere Features dürfen
12
+ // auf Frühere referenzieren (z.B. authClaims-Hooks an user/tenant).
13
+
14
+ import {
15
+ type AuthEmailPasswordOptions,
16
+ createAuthEmailPasswordFeature,
17
+ type EmailVerificationOptions,
18
+ type InviteOptions,
19
+ type PasswordResetOptions,
20
+ type SignupOptions,
21
+ } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
22
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
23
+ import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
24
+ import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
25
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
26
+
27
+ export type ComposeFeaturesOptions = {
28
+ /** When true, prepends config + user + tenant + auth-email-password
29
+ * before the app features. Mirror of "auth-mode" in run{Dev,Prod}App. */
30
+ readonly includeBundled: boolean;
31
+ /** Optional auth-feature-options durchgereicht an
32
+ * createAuthEmailPasswordFeature. Wenn passwordReset / emailVerification
33
+ * hier gesetzt sind, registriert das Feature die request-/confirm-
34
+ * Handler — sonst NICHT (500 wenn die routes via auth-routes.ts
35
+ * gemounted sind aber kein Handler dispatcht). Hand-in-hand mit dem
36
+ * passwordReset-Block in RunProdAppAuthOptions / RunDevAppAuthOptions. */
37
+ readonly authOptions?: AuthEmailPasswordOptions;
38
+ };
39
+
40
+ export function composeFeatures(
41
+ appFeatures: readonly FeatureDefinition[],
42
+ options: ComposeFeaturesOptions,
43
+ ): FeatureDefinition[] {
44
+ return options.includeBundled
45
+ ? [
46
+ createConfigFeature(),
47
+ createUserFeature(),
48
+ createTenantFeature(),
49
+ createAuthEmailPasswordFeature(options.authOptions ?? {}),
50
+ ...appFeatures,
51
+ ]
52
+ : [...appFeatures];
53
+ }
54
+
55
+ /** Shape eines beliebigen run{Prod,Dev}App-Auth-Blocks der eine
56
+ * PasswordReset/EmailVerification-Konfiguration tragen kann. Die
57
+ * Wrapper-API (PasswordResetSetup) extends die Feature-API
58
+ * (PasswordResetOptions), darum reicht ein structural-typed
59
+ * Lookup auf den auth-only-Subset. Erlaubt buildComposeAuthOptions
60
+ * mit RunProd- UND RunDev-AuthOptions zu callen ohne den Helper
61
+ * doppelt zu bauen. */
62
+ export type AuthOptionsCarrier = {
63
+ readonly passwordReset?: PasswordResetOptions;
64
+ readonly emailVerification?: EmailVerificationOptions;
65
+ readonly signup?: SignupOptions;
66
+ readonly invite?: InviteOptions;
67
+ };
68
+
69
+ /** Baut den authOptions-Block für composeFeatures aus einem
70
+ * Wrapper-Auth-Block. Reicht NUR die feature-side-Felder
71
+ * (hmacSecret, tokenTtlMinutes, mode) durch — die mail-side
72
+ * (sendResetEmail/appResetUrl) gehört in die auth-routes-config
73
+ * und wird vom Wrapper separat verdrahtet.
74
+ *
75
+ * Returnt undefined wenn weder passwordReset noch emailVerification
76
+ * gesetzt sind (composeFeatures default-deny: KEINE handler in der
77
+ * Registry registriert, /api/auth/request-password-reset etc. bleiben
78
+ * 401/404). */
79
+ export function buildComposeAuthOptions(
80
+ auth: AuthOptionsCarrier | undefined,
81
+ ): AuthEmailPasswordOptions | undefined {
82
+ if (!auth) return undefined;
83
+ const opts: { -readonly [K in keyof AuthEmailPasswordOptions]: AuthEmailPasswordOptions[K] } = {};
84
+ if (auth.passwordReset) {
85
+ const reset: { -readonly [K in keyof PasswordResetOptions]: PasswordResetOptions[K] } = {
86
+ hmacSecret: auth.passwordReset.hmacSecret,
87
+ };
88
+ if (auth.passwordReset.tokenTtlMinutes !== undefined) {
89
+ reset.tokenTtlMinutes = auth.passwordReset.tokenTtlMinutes;
90
+ }
91
+ opts.passwordReset = reset;
92
+ }
93
+ if (auth.emailVerification) {
94
+ const verify: { -readonly [K in keyof EmailVerificationOptions]: EmailVerificationOptions[K] } =
95
+ {
96
+ hmacSecret: auth.emailVerification.hmacSecret,
97
+ };
98
+ if (auth.emailVerification.tokenTtlMinutes !== undefined) {
99
+ verify.tokenTtlMinutes = auth.emailVerification.tokenTtlMinutes;
100
+ }
101
+ if (auth.emailVerification.mode !== undefined) {
102
+ verify.mode = auth.emailVerification.mode;
103
+ }
104
+ opts.emailVerification = verify;
105
+ }
106
+ if (auth.signup) {
107
+ // Plain object statt mapped-type — SignupOptions ist type-alias auf
108
+ // SignupRequestOptions, der TS-mapped-type-Pfad löst's als
109
+ // index-signature auf (TS noPropertyAccessFromIndexSignature klagt
110
+ // dann beim Property-write). Plain shape ist klar UND funktioniert.
111
+ const signup: { tokenTtlMinutes?: number } = {};
112
+ if (auth.signup.tokenTtlMinutes !== undefined) {
113
+ signup.tokenTtlMinutes = auth.signup.tokenTtlMinutes;
114
+ }
115
+ opts.signup = signup;
116
+ }
117
+ if (auth.invite) {
118
+ // Plain object analog signup (gleicher type-alias-issue).
119
+ const invite: { tokenTtlMinutes?: number } = {};
120
+ if (auth.invite.tokenTtlMinutes !== undefined) {
121
+ invite.tokenTtlMinutes = auth.invite.tokenTtlMinutes;
122
+ }
123
+ opts.invite = invite;
124
+ }
125
+ return opts.passwordReset || opts.emailVerification || opts.signup || opts.invite
126
+ ? opts
127
+ : undefined;
128
+ }
@@ -0,0 +1,56 @@
1
+ // Tracks server-Crashes in einem rollenden Zeitfenster und entscheidet,
2
+ // ob der dev-wrapper noch respawnen darf oder den Crash-Loop aufgeben
3
+ // muss. Liegt als eigener File, damit `kumiko-dev.ts` (bin-Script ohne
4
+ // Test-Setup) eine pure, injizierbare API bekommt — die Logik ist
5
+ // testbar via createCrashTracker, ohne den Wrapper-Prozess fahren zu
6
+ // müssen.
7
+ //
8
+ // Semantik: ein Crash "zählt im Fenster", wenn er innerhalb der letzten
9
+ // `windowMs` ms vor `now` registriert wurde. Wenn nach Aufnahme des
10
+ // neuen Crashes mehr als `maxCrashes` im Fenster liegen, gibt der
11
+ // Tracker `false` zurück → Wrapper soll aufgeben.
12
+
13
+ export type CrashTrackerOptions = {
14
+ readonly maxCrashes: number;
15
+ readonly windowMs: number;
16
+ };
17
+
18
+ export type CrashTracker = {
19
+ /** Crash bei `now` registrieren. Returns `true` wenn der Wrapper
20
+ * noch respawnen darf, `false` wenn das Limit überschritten ist.
21
+ *
22
+ * Der Crash wird IMMER aufgenommen, auch wenn `false` zurückkommt —
23
+ * der Caller terminiert in dem Fall normalerweise eh. Reuse mit
24
+ * "rejected → state unchanged"-Semantik wird nicht unterstützt. */
25
+ readonly noteCrash: (now: number) => boolean;
26
+ /** Anzahl Crashes im Fenster relativ zu `now` — prunt lazy, damit
27
+ * der Aufruf auch ohne vorheriges noteCrash konsistent ist. */
28
+ readonly crashCountInWindow: (now: number) => number;
29
+ };
30
+
31
+ export function createCrashTracker(options: CrashTrackerOptions): CrashTracker {
32
+ const timestamps: number[] = [];
33
+
34
+ const pruneBefore = (cutoff: number): void => {
35
+ // Crashes älter als `cutoff` raus. Gleicher Timestamp wie cutoff
36
+ // bleibt drin (`<` statt `<=`) — symmetrisch zur Fenster-Definition
37
+ // "innerhalb der letzten windowMs", also Endpoint inklusive.
38
+ while (timestamps.length > 0) {
39
+ const head = timestamps[0];
40
+ if (head !== undefined && head < cutoff) timestamps.shift();
41
+ else break;
42
+ }
43
+ };
44
+
45
+ return {
46
+ noteCrash: (now) => {
47
+ pruneBefore(now - options.windowMs);
48
+ timestamps.push(now);
49
+ return timestamps.length <= options.maxCrashes;
50
+ },
51
+ crashCountInWindow: (now) => {
52
+ pruneBefore(now - options.windowMs);
53
+ return timestamps.length;
54
+ },
55
+ };
56
+ }