@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,226 @@
1
+ // scaffoldFeature — generate a fresh feature workspace from a name.
2
+ // Used by `yarn kumiko create <name>` and (later) by the Designer when
3
+ // a tenant scaffolds a new feature inside their repo. Wraps the
4
+ // canonical-form renderer (feature-ast/render.ts) so every freshly
5
+ // scaffolded feature is born in canonical Object-Form with the
6
+ // schema-version header set.
7
+ //
8
+ // The generated workspace is intentionally minimal: a single entity
9
+ // pattern as a starter, so the user has something to point a "yarn
10
+ // kumiko dev" at and immediately see something on screen. Adding more
11
+ // patterns is the user's job (or the Designer's / AI's, on top of this
12
+ // scaffolding).
13
+
14
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import {
17
+ type FeaturePattern,
18
+ renderFeatureFile,
19
+ type SourceLocation,
20
+ } from "@cosmicdrift/kumiko-framework/engine";
21
+
22
+ // =============================================================================
23
+ // Public API
24
+ // =============================================================================
25
+
26
+ export type ScaffoldFeatureOptions = {
27
+ /** camelCase feature name. Must be a valid JS identifier. */
28
+ readonly name: string;
29
+ /**
30
+ * Absolute or repo-relative path where the feature workspace gets
31
+ * created. Defaults to `samples/recipes/<kebab-name>/` under the
32
+ * resolved repo root.
33
+ */
34
+ readonly destination?: string;
35
+ /** Repo root used to resolve the default destination. Defaults to cwd. */
36
+ readonly repoRoot?: string;
37
+ };
38
+
39
+ export type ScaffoldFeatureResult = {
40
+ readonly destination: string;
41
+ readonly featureFile: string;
42
+ readonly packageJsonFile: string;
43
+ readonly tsconfigFile: string;
44
+ readonly featureName: string;
45
+ readonly packageName: string;
46
+ };
47
+
48
+ /**
49
+ * Generate a starter feature workspace at `destination`. Throws when
50
+ * the destination already exists — refuses to overwrite. The caller is
51
+ * expected to run `yarn install` afterwards to wire the workspace.
52
+ */
53
+ export function scaffoldFeature(options: ScaffoldFeatureOptions): ScaffoldFeatureResult {
54
+ const featureName = validateFeatureName(options.name);
55
+ const repoRoot = options.repoRoot ?? process.cwd();
56
+ const kebab = camelToKebab(featureName);
57
+ const destination = resolve(
58
+ options.destination
59
+ ? resolveDestination(options.destination, repoRoot)
60
+ : join(repoRoot, "samples", "recipes", kebab),
61
+ );
62
+
63
+ if (existsSync(destination)) {
64
+ throw new Error(
65
+ `scaffoldFeature: destination already exists at ${destination} — refusing to overwrite`,
66
+ );
67
+ }
68
+
69
+ mkdirSync(join(destination, "src"), { recursive: true });
70
+
71
+ const packageName = `@cosmicdrift/kumiko-sample-${kebab}`;
72
+ const packageJson = renderPackageJson(packageName);
73
+ const packageJsonFile = join(destination, "package.json");
74
+ writeFileSync(packageJsonFile, packageJson);
75
+
76
+ const tsconfigFile = join(destination, "tsconfig.json");
77
+ writeFileSync(tsconfigFile, renderTsconfig());
78
+
79
+ const featureFile = join(destination, "src", "feature.ts");
80
+ const featureSource = renderFeatureFile({
81
+ featureName,
82
+ patterns: starterPatterns(),
83
+ });
84
+ writeFileSync(featureFile, featureSource);
85
+
86
+ return {
87
+ destination,
88
+ featureFile,
89
+ packageJsonFile,
90
+ tsconfigFile,
91
+ featureName,
92
+ packageName,
93
+ };
94
+ }
95
+
96
+ // =============================================================================
97
+ // Internal — name + path validation
98
+ // =============================================================================
99
+
100
+ const RESERVED_WORDS: ReadonlySet<string> = new Set([
101
+ // Subset of TS reserved words that would be confusing as feature names.
102
+ "default",
103
+ "delete",
104
+ "function",
105
+ "import",
106
+ "export",
107
+ "class",
108
+ "interface",
109
+ "enum",
110
+ "type",
111
+ "module",
112
+ "package",
113
+ "private",
114
+ "protected",
115
+ "public",
116
+ "static",
117
+ "void",
118
+ "null",
119
+ "true",
120
+ "false",
121
+ ]);
122
+
123
+ const NAME_RE = /^[a-z][A-Za-z0-9]*$/;
124
+
125
+ function validateFeatureName(raw: string): string {
126
+ if (!raw) {
127
+ throw new Error("scaffoldFeature: feature name is required");
128
+ }
129
+ if (!NAME_RE.test(raw)) {
130
+ throw new Error(
131
+ `scaffoldFeature: "${raw}" is not a valid feature name — use camelCase starting with a lowercase letter (e.g. "todoList")`,
132
+ );
133
+ }
134
+ if (RESERVED_WORDS.has(raw)) {
135
+ throw new Error(`scaffoldFeature: "${raw}" is a reserved word and cannot be a feature name`);
136
+ }
137
+ return raw;
138
+ }
139
+
140
+ function camelToKebab(name: string): string {
141
+ return name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
142
+ }
143
+
144
+ function resolveDestination(dest: string, repoRoot: string): string {
145
+ // Allow callers to pass either an absolute path or a repo-relative
146
+ // one — keep both ergonomic. `resolve(repoRoot, dest)` is a no-op for
147
+ // absolute paths.
148
+ return resolve(repoRoot, dest);
149
+ }
150
+
151
+ // =============================================================================
152
+ // Internal — content generators
153
+ // =============================================================================
154
+
155
+ function renderPackageJson(packageName: string): string {
156
+ return `${JSON.stringify(
157
+ {
158
+ name: packageName,
159
+ description: "Kumiko sample feature — scaffolded by `yarn kumiko create`",
160
+ private: true,
161
+ dependencies: {
162
+ "@cosmicdrift/kumiko-framework": "workspace:*",
163
+ },
164
+ },
165
+ null,
166
+ 2,
167
+ )}\n`;
168
+ }
169
+
170
+ /**
171
+ * Standard tsconfig matching the rest of the sample workspaces:
172
+ * strict, ESNext, bundler-resolution, no-emit. Without this file
173
+ * `yarn install + tsc` immediately complains about missing config —
174
+ * scaffolded features should compile cleanly out of the box.
175
+ */
176
+ function renderTsconfig(): string {
177
+ return `${JSON.stringify(
178
+ {
179
+ compilerOptions: {
180
+ strict: true,
181
+ noUncheckedIndexedAccess: true,
182
+ noPropertyAccessFromIndexSignature: true,
183
+ forceConsistentCasingInFileNames: true,
184
+ verbatimModuleSyntax: true,
185
+ target: "ESNext",
186
+ module: "ESNext",
187
+ moduleResolution: "bundler",
188
+ esModuleInterop: true,
189
+ skipLibCheck: true,
190
+ lib: ["ESNext"],
191
+ types: ["bun-types"],
192
+ noEmit: true,
193
+ },
194
+ include: ["src/**/*"],
195
+ },
196
+ null,
197
+ 2,
198
+ )}\n`;
199
+ }
200
+
201
+ // Synthetic SourceLocation — the renderer reads `.raw` only for opaque
202
+ // (closure-bearing) bodies. Static patterns like `entity` don't touch
203
+ // `source.raw` at render-time, so an empty placeholder is fine.
204
+ const SYNTHETIC_LOC: SourceLocation = {
205
+ file: "<scaffold>",
206
+ start: { line: 1, column: 1 },
207
+ end: { line: 1, column: 1 },
208
+ raw: "",
209
+ };
210
+
211
+ function starterPatterns(): readonly FeaturePattern[] {
212
+ // One entity, one field. Smallest interesting output: parses, renders,
213
+ // can be `yarn kumiko dev`'d, and gives the user something to extend.
214
+ return [
215
+ {
216
+ kind: "entity",
217
+ source: SYNTHETIC_LOC,
218
+ entityName: "item",
219
+ definition: {
220
+ fields: {
221
+ title: { type: "text", required: true },
222
+ },
223
+ },
224
+ },
225
+ ];
226
+ }
@@ -0,0 +1,46 @@
1
+ // Shared helper für die "Hono-first, SPA-fallback wenn 404"-Strategie.
2
+ // Wird von dev (createKumikoServer.handleFetch) UND prod (runProdApp's
3
+ // fetch-handler) verwendet — identische Semantik, ein helper. Ohne den
4
+ // shared-Helper drifteten die beiden Pfade silent (genau der Bug der
5
+ // legal-pages im dev-server geshadowed hat — runProdApp's docs sagten
6
+ // "Hono matched VOR fallback", dev-server tat das NICHT).
7
+ //
8
+ // Pattern:
9
+ // 1. Try app.fetch(req) — wenn Hono eine route matcht, greift sie.
10
+ // 2. 404 vom Hono-stack → null returnen, caller macht SPA-fallback.
11
+ // 3. Sonstige status (200, 401, 500, ...) → response durchreichen.
12
+ //
13
+ // req.clone() weil downstream der req body nochmal lesbar sein muss
14
+ // (POST/PUT/PATCH future-proof — heute nur GET-routes betroffen).
15
+
16
+ export type HonoLikeApp = {
17
+ // Hono.app.fetch ist `(req) => Response | Promise<Response>` (sync wenn
18
+ // alle Handler sync sind, sonst Promise). createApiEntrypoint's
19
+ // apiHandler matcht dieselbe shape. Union accepts both — wir await
20
+ // unten, das funktioniert für beide Fälle.
21
+ readonly fetch: (req: Request) => Response | Promise<Response>;
22
+ };
23
+
24
+ export type HonoFirstResult = {
25
+ /** True wenn Hono eine matchende Route hat (status !== 404).
26
+ * Caller returnt dann response direkt.
27
+ * False wenn keine Route matcht (status === 404). Caller macht den
28
+ * SPA-/static-fallback; response enthält den 404 als final-fallback
29
+ * falls auch der SPA-Pfad nichts liefert. */
30
+ readonly matched: boolean;
31
+ readonly response: Response;
32
+ };
33
+
34
+ /**
35
+ * Hono-first try: app.fetch ZUERST. Wenn matched (status !== 404), gibt
36
+ * der caller den response direkt zurück. Wenn nicht matched, fällt der
37
+ * caller in den eigenen SPA-/static-fallback zurück — der response (404)
38
+ * bleibt verfügbar als letztes Sicherheitsnetz.
39
+ *
40
+ * req.clone() weil downstream der req body nochmal lesbar sein muss
41
+ * (POST/PUT/PATCH future-proof — heute nur GET-routes betroffen).
42
+ */
43
+ export async function tryHonoFirst(app: HonoLikeApp, req: Request): Promise<HonoFirstResult> {
44
+ const response = await app.fetch(req.clone());
45
+ return { matched: response.status !== 404, response };
46
+ }