@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,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
|
+
}
|