@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.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-schema-check.ts +7 -0
- package/package.json +12 -7
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/build-server-bundle.test.ts +62 -0
- package/src/__tests__/compose-stacks.test.ts +271 -0
- package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
- package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
- package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/few-shot-corpus.test.ts +16 -11
- package/src/__tests__/merge-extra-context.test.ts +56 -0
- package/src/__tests__/resolve-stylesheet.test.ts +16 -0
- package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
- package/src/__tests__/scaffold-app-feature.test.ts +59 -6
- package/src/__tests__/scaffold-app.test.ts +34 -2
- package/src/__tests__/schema-apply.integration.test.ts +5 -2
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
- package/src/__tests__/walkthrough.integration.test.ts +22 -6
- package/src/build-server-bundle.ts +33 -15
- package/src/build.ts +1 -2
- package/src/codegen/__tests__/render-codegen.test.ts +2 -1
- package/src/codegen/__tests__/run-codegen.test.ts +2 -2
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
- package/src/codegen/__tests__/watch.test.ts +1 -2
- package/src/codegen/render.ts +6 -1
- package/src/codegen/scan-events.ts +7 -5
- package/src/codegen/watch.ts +7 -4
- package/src/compose-stacks.ts +184 -0
- package/src/create-kumiko-server.ts +28 -5
- package/src/few-shot-corpus.ts +4 -3
- package/src/index.ts +30 -12
- package/src/run-dev-app.ts +195 -61
- package/src/scaffold-app-feature.ts +118 -15
- package/src/scaffold-app.ts +151 -79
- package/src/scaffold-demo-tasks.ts +233 -0
- package/src/schema-apply.ts +15 -2
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +61 -0
- package/src/welcome-banner.ts +1 -4
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -311
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -129
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -840
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/apply-boot-seeds.ts +0 -18
- package/src/build-prod-bundle.ts +0 -697
- package/src/compose-features.ts +0 -145
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -24
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app.ts +0 -1492
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
|
@@ -68,6 +68,7 @@ async function readMountedFeatures(runConfigPath: string): Promise<Set<string>>
|
|
|
68
68
|
const mod = (await import(runConfigPath)) as {
|
|
69
69
|
APP_FEATURES?: ReadonlyArray<{ name: string }>;
|
|
70
70
|
HAS_AUTH?: boolean;
|
|
71
|
+
HAS_SIGNUP?: boolean;
|
|
71
72
|
};
|
|
72
73
|
if (!mod.APP_FEATURES) {
|
|
73
74
|
throw new Error(
|
|
@@ -85,6 +86,12 @@ async function readMountedFeatures(runConfigPath: string): Promise<Set<string>>
|
|
|
85
86
|
if (mod.HAS_AUTH ?? true) {
|
|
86
87
|
for (const name of implicitAuthModeFeatureNames()) set.add(name);
|
|
87
88
|
}
|
|
89
|
+
// auth-self-registration is only prepended when authOptions.signup is set
|
|
90
|
+
// (composeFeatures) — opt-in via HAS_SIGNUP so apps without signup don't
|
|
91
|
+
// get a stale-registry false positive (#1521).
|
|
92
|
+
if (mod.HAS_SIGNUP === true) {
|
|
93
|
+
set.add("auth-self-registration");
|
|
94
|
+
}
|
|
88
95
|
return set;
|
|
89
96
|
}
|
|
90
97
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Not shipped into production node_modules — see @cosmicdrift/kumiko-server-runtime for the prod boot path.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
7
|
"repository": {
|
|
@@ -26,9 +26,13 @@
|
|
|
26
26
|
"types": "./src/build.ts",
|
|
27
27
|
"default": "./src/build.ts"
|
|
28
28
|
},
|
|
29
|
-
"./compose-
|
|
30
|
-
"types": "./src/compose-
|
|
31
|
-
"default": "./src/compose-
|
|
29
|
+
"./compose-stacks": {
|
|
30
|
+
"types": "./src/compose-stacks.ts",
|
|
31
|
+
"default": "./src/compose-stacks.ts"
|
|
32
|
+
},
|
|
33
|
+
"./setup-test-stack-from-features": {
|
|
34
|
+
"types": "./src/setup-test-stack-from-features.ts",
|
|
35
|
+
"default": "./src/setup-test-stack-from-features.ts"
|
|
32
36
|
},
|
|
33
37
|
"./schema-apply": {
|
|
34
38
|
"types": "./src/schema-apply.ts",
|
|
@@ -50,8 +54,9 @@
|
|
|
50
54
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
51
55
|
},
|
|
52
56
|
"dependencies": {
|
|
53
|
-
"@cosmicdrift/kumiko-bundled-features": "
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "
|
|
57
|
+
"@cosmicdrift/kumiko-bundled-features": "2.0.0",
|
|
58
|
+
"@cosmicdrift/kumiko-framework": "2.0.0",
|
|
59
|
+
"@cosmicdrift/kumiko-server-runtime": "2.0.0",
|
|
55
60
|
"ts-morph": "^28.0.0"
|
|
56
61
|
},
|
|
57
62
|
"publishConfig": {
|
|
@@ -19,7 +19,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { dirname, join, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { buildProdBundle } from "
|
|
22
|
+
import { buildProdBundle } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
|
|
23
23
|
|
|
24
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
25
|
const __dirname = dirname(__filename);
|
|
@@ -55,10 +55,72 @@ describe("buildServerBundle (multi-entry + splitting)", () => {
|
|
|
55
55
|
expect(pkg.scripts.start).toBe("bun run server.js");
|
|
56
56
|
expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-kit");
|
|
57
57
|
expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-orm");
|
|
58
|
+
// Positive counterpart to the negative checks above — a runtime
|
|
59
|
+
// external accidentally dropped from RUNTIME_EXTERNALS (into
|
|
60
|
+
// BUILD_ONLY_EXTERNALS instead) would slip through silently. This
|
|
61
|
+
// is the exact regression class behind the money-horse prod crash
|
|
62
|
+
// ("Cannot find package 'meilisearch'").
|
|
63
|
+
expect(Object.keys(pkg.dependencies)).toContain("meilisearch");
|
|
58
64
|
|
|
59
65
|
expect(result.totalBytes).toBeGreaterThan(0);
|
|
60
66
|
} finally {
|
|
61
67
|
rmSync(dir, { recursive: true, force: true });
|
|
62
68
|
}
|
|
63
69
|
});
|
|
70
|
+
|
|
71
|
+
test("pins runtime-deps from node_modules/@cosmicdrift/* for consumer apps without a packages/ dir", async () => {
|
|
72
|
+
// Reproduziert #1217: findRepoRoot liefert für eine Consumer-App die
|
|
73
|
+
// App-Wurzel selbst (kein packages/-Ordner) — die Version muss aus den
|
|
74
|
+
// installierten node_modules/@cosmicdrift/*-Packages kommen, nicht "*".
|
|
75
|
+
const dir = makeFixture();
|
|
76
|
+
try {
|
|
77
|
+
const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework");
|
|
78
|
+
mkdirSync(frameworkPkgDir, { recursive: true });
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(frameworkPkgDir, "package.json"),
|
|
81
|
+
`${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" } })}\n`,
|
|
82
|
+
);
|
|
83
|
+
const bundledFeaturesPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-bundled-features");
|
|
84
|
+
mkdirSync(bundledFeaturesPkgDir, { recursive: true });
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(bundledFeaturesPkgDir, "package.json"),
|
|
87
|
+
`${JSON.stringify({ name: "@cosmicdrift/kumiko-bundled-features", dependencies: { ioredis: "^5.4.1" } })}\n`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") });
|
|
91
|
+
|
|
92
|
+
expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0");
|
|
93
|
+
expect(result.runtimeDeps["ioredis"]).toBe("^5.4.1");
|
|
94
|
+
} finally {
|
|
95
|
+
rmSync(dir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("prefers node_modules/@cosmicdrift/* over packages/* when both declare a version", async () => {
|
|
100
|
+
// Framework-monorepo checkout: both the installed node_modules package
|
|
101
|
+
// (symlinked in real life, diverging here) and packages/framework exist.
|
|
102
|
+
// node_modules must win — it reflects what actually gets shipped/installed
|
|
103
|
+
// for the consumer, packages/framework is only a repo-root fallback.
|
|
104
|
+
const dir = makeFixture();
|
|
105
|
+
try {
|
|
106
|
+
const frameworkPkgDir = join(dir, "node_modules/@cosmicdrift/kumiko-framework");
|
|
107
|
+
mkdirSync(frameworkPkgDir, { recursive: true });
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(frameworkPkgDir, "package.json"),
|
|
110
|
+
`${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.58.0" } })}\n`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
mkdirSync(join(dir, "packages/framework"), { recursive: true });
|
|
114
|
+
writeFileSync(
|
|
115
|
+
join(dir, "packages/framework/package.json"),
|
|
116
|
+
`${JSON.stringify({ name: "@cosmicdrift/kumiko-framework", dependencies: { meilisearch: "^0.30.0" } })}\n`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") });
|
|
120
|
+
|
|
121
|
+
expect(result.runtimeDeps["meilisearch"]).toBe("^0.58.0");
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(dir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
64
126
|
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
3
|
+
import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
5
|
+
import {
|
|
6
|
+
composeFileStack,
|
|
7
|
+
composeGdprStack,
|
|
8
|
+
composeIdentityStack,
|
|
9
|
+
composeMailStack,
|
|
10
|
+
composeOpsStack,
|
|
11
|
+
composePagesStack,
|
|
12
|
+
composeRendererStack,
|
|
13
|
+
composeUserDataRightsStack,
|
|
14
|
+
stackFeatureNames,
|
|
15
|
+
} from "../compose-stacks";
|
|
16
|
+
|
|
17
|
+
const TEST_MFA = {
|
|
18
|
+
setupTokenSecret: "compose-stacks-mfa-setup-secret-at-least-32b!!",
|
|
19
|
+
challengeTokenSecret: "compose-stacks-mfa-challenge-secret-32b!!",
|
|
20
|
+
issuer: "Kumiko ComposeStacks",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
describe("composeStacks", () => {
|
|
24
|
+
test("composeRendererStack → template-resolver, renderer-foundation, renderer-simple", () => {
|
|
25
|
+
expect(stackFeatureNames(composeRendererStack())).toEqual([
|
|
26
|
+
"template-resolver",
|
|
27
|
+
"renderer-foundation",
|
|
28
|
+
"renderer-simple",
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("composePagesStack → text-content + legal-pages", () => {
|
|
33
|
+
expect(stackFeatureNames(composePagesStack())).toEqual(["text-content", "legal-pages"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("composeMailStack mounts foundation + selected transports", () => {
|
|
37
|
+
expect(stackFeatureNames(composeMailStack({ transports: ["inmemory", "smtp"] }))).toEqual([
|
|
38
|
+
"mail-foundation",
|
|
39
|
+
"mail-transport-inmemory",
|
|
40
|
+
"mail-transport-smtp",
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("composeFileStack mounts foundation + providers + files entity", () => {
|
|
45
|
+
expect(stackFeatureNames(composeFileStack({ providers: ["inmemory", "s3"] }))).toEqual([
|
|
46
|
+
"file-foundation",
|
|
47
|
+
"file-provider-inmemory",
|
|
48
|
+
"file-provider-s3",
|
|
49
|
+
"files",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("composeGdprStack retention-first (money-horse / publicstatus pattern)", () => {
|
|
54
|
+
expect(stackFeatureNames(composeGdprStack({ tenantLifecycle: true, sessions: true }))).toEqual([
|
|
55
|
+
"data-retention",
|
|
56
|
+
"compliance-profiles",
|
|
57
|
+
"tenant-lifecycle",
|
|
58
|
+
"auth-foundation",
|
|
59
|
+
"sessions",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("composeGdprStack compliance-first (studio pattern)", () => {
|
|
64
|
+
expect(stackFeatureNames(composeGdprStack({ order: "compliance-first" }))).toEqual([
|
|
65
|
+
"compliance-profiles",
|
|
66
|
+
"data-retention",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("composeUserDataRightsStack → udr + defaults", () => {
|
|
71
|
+
expect(stackFeatureNames(composeUserDataRightsStack())).toEqual([
|
|
72
|
+
"user-data-rights",
|
|
73
|
+
"user-data-rights-defaults",
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("composeOpsStack defaults delivery + audit + jobs", () => {
|
|
78
|
+
expect(stackFeatureNames(composeOpsStack())).toEqual(["delivery", "audit", "jobs"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("composeOpsStack rateLimiting opt-in", () => {
|
|
82
|
+
expect(stackFeatureNames(composeOpsStack({ rateLimiting: true }))).toEqual([
|
|
83
|
+
"delivery",
|
|
84
|
+
"audit",
|
|
85
|
+
"jobs",
|
|
86
|
+
"rate-limiting",
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("composeIdentityStack defaults auth-foundation + sessions", () => {
|
|
91
|
+
expect(stackFeatureNames(composeIdentityStack())).toEqual(["auth-foundation", "sessions"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("composeIdentityStack mounts auth-mfa when mfa options given", () => {
|
|
95
|
+
expect(stackFeatureNames(composeIdentityStack({ mfa: TEST_MFA }))).toEqual([
|
|
96
|
+
"auth-foundation",
|
|
97
|
+
"sessions",
|
|
98
|
+
"auth-mfa",
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("composeIdentityStack sessions:false + mfa → auth-mfa only", () => {
|
|
103
|
+
expect(stackFeatureNames(composeIdentityStack({ sessions: false, mfa: TEST_MFA }))).toEqual([
|
|
104
|
+
"auth-mfa",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("composeIdentityStack + composeGdprStack(sessions) → duplicate feature rejected at boot", () => {
|
|
109
|
+
const names = stackFeatureNames([
|
|
110
|
+
...composeIdentityStack(),
|
|
111
|
+
...composeGdprStack({ sessions: true }),
|
|
112
|
+
]);
|
|
113
|
+
expect(names.filter((n) => n === "sessions")).toHaveLength(2);
|
|
114
|
+
expect(() =>
|
|
115
|
+
validateBoot(
|
|
116
|
+
composeFeatures(
|
|
117
|
+
[
|
|
118
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
119
|
+
...composeIdentityStack(),
|
|
120
|
+
...composeGdprStack({ sessions: true }),
|
|
121
|
+
],
|
|
122
|
+
{ includeBundled: true },
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
).toThrow(/duplicate feature/i);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/** Snapshot, not a live parity check: intended block combinations, mirrored by hand from studio/money-horse/publicstatus run-configs. A drifted run-config stays green here. */
|
|
130
|
+
describe("composeStacks boots for real", () => {
|
|
131
|
+
test("studio-shaped combined stack passes validateBoot (not just name-list comparison)", () => {
|
|
132
|
+
const features = composeFeatures(
|
|
133
|
+
[
|
|
134
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
135
|
+
...composeOpsStack({ rateLimiting: true }),
|
|
136
|
+
...composePagesStack(),
|
|
137
|
+
...composeMailStack({ transports: ["inmemory"] }),
|
|
138
|
+
...composeFileStack({ providers: ["inmemory"] }),
|
|
139
|
+
...composeGdprStack({ order: "compliance-first", sessions: true }),
|
|
140
|
+
...composeUserDataRightsStack(),
|
|
141
|
+
],
|
|
142
|
+
{ includeBundled: true },
|
|
143
|
+
);
|
|
144
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("money-horse-shaped composeGdprStack({sessions:true}) without a tokenVerifier provider passes boot", () => {
|
|
148
|
+
// Session-only stacks (#1570): composeGdprStack({sessions:true}) mounts
|
|
149
|
+
// auth-foundation + sessions (sessionStore). Cookie auth does not need a
|
|
150
|
+
// tokenVerifier; money-horse/solon run-configs pass none and must boot.
|
|
151
|
+
const features = composeFeatures([...composeGdprStack({ sessions: true })], {
|
|
152
|
+
includeBundled: true,
|
|
153
|
+
});
|
|
154
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("composeGdprStack({sessions:true, providers:[...]}) passes boot with a tokenVerifier mounted", () => {
|
|
158
|
+
const features = composeFeatures(
|
|
159
|
+
[
|
|
160
|
+
...composeGdprStack({
|
|
161
|
+
sessions: true,
|
|
162
|
+
providers: [createPersonalAccessTokensFeature({ scopes: {} })],
|
|
163
|
+
}),
|
|
164
|
+
],
|
|
165
|
+
{ includeBundled: true },
|
|
166
|
+
);
|
|
167
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// auth-mfa's encrypted fields need a runtime KEK (env or configureEntityFieldEncryption
|
|
171
|
+
// + secrets). Name-list + saas-identity-wire.integration.test.ts cover identity boot.
|
|
172
|
+
test("identity stack names compose with ops + renderer under includeBundled", () => {
|
|
173
|
+
const features = composeFeatures(
|
|
174
|
+
[
|
|
175
|
+
...composeIdentityStack({ mfa: TEST_MFA }),
|
|
176
|
+
...composeOpsStack(),
|
|
177
|
+
...composeRendererStack(),
|
|
178
|
+
...composeMailStack({ transports: ["inmemory"] }),
|
|
179
|
+
],
|
|
180
|
+
{ includeBundled: true },
|
|
181
|
+
);
|
|
182
|
+
const names = stackFeatureNames(features);
|
|
183
|
+
expect(names).toContain("auth-email-password");
|
|
184
|
+
expect(names).toContain("sessions");
|
|
185
|
+
expect(names).toContain("auth-mfa");
|
|
186
|
+
expect(names).toContain("delivery");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("composeStacks intended block names (snapshot, not live parity)", () => {
|
|
191
|
+
test("studio SaaS blocks are covered by presets", () => {
|
|
192
|
+
const names = stackFeatureNames([
|
|
193
|
+
...composeOpsStack({ rateLimiting: true }),
|
|
194
|
+
...composePagesStack(),
|
|
195
|
+
...composeMailStack({ transports: ["inmemory", "smtp"] }),
|
|
196
|
+
...composeFileStack({ providers: ["inmemory", "s3"] }),
|
|
197
|
+
...composeGdprStack({ order: "compliance-first" }),
|
|
198
|
+
...composeUserDataRightsStack(),
|
|
199
|
+
]);
|
|
200
|
+
for (const expected of [
|
|
201
|
+
"delivery",
|
|
202
|
+
"audit",
|
|
203
|
+
"jobs",
|
|
204
|
+
"rate-limiting",
|
|
205
|
+
"text-content",
|
|
206
|
+
"legal-pages",
|
|
207
|
+
"mail-foundation",
|
|
208
|
+
"file-foundation",
|
|
209
|
+
"files",
|
|
210
|
+
"compliance-profiles",
|
|
211
|
+
"data-retention",
|
|
212
|
+
"user-data-rights",
|
|
213
|
+
"user-data-rights-defaults",
|
|
214
|
+
]) {
|
|
215
|
+
expect(names).toContain(expected);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("money-horse GDPR + file block names", () => {
|
|
220
|
+
const names = stackFeatureNames([
|
|
221
|
+
...composePagesStack(),
|
|
222
|
+
...composeRendererStack(),
|
|
223
|
+
...composeOpsStack({ delivery: true, audit: false, jobs: false }),
|
|
224
|
+
...composeGdprStack({ sessions: true }),
|
|
225
|
+
...composeFileStack({ providers: ["s3-env"] }),
|
|
226
|
+
...composeUserDataRightsStack(),
|
|
227
|
+
]);
|
|
228
|
+
for (const expected of [
|
|
229
|
+
"text-content",
|
|
230
|
+
"legal-pages",
|
|
231
|
+
"data-retention",
|
|
232
|
+
"compliance-profiles",
|
|
233
|
+
"sessions",
|
|
234
|
+
"file-foundation",
|
|
235
|
+
"file-provider-s3-env",
|
|
236
|
+
"files",
|
|
237
|
+
"user-data-rights",
|
|
238
|
+
]) {
|
|
239
|
+
expect(names).toContain(expected);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("publicstatus core stack block names", () => {
|
|
244
|
+
const names = stackFeatureNames([
|
|
245
|
+
...composePagesStack(),
|
|
246
|
+
...composeGdprStack({ tenantLifecycle: true }),
|
|
247
|
+
...composeOpsStack({ delivery: true, audit: true, jobs: true }),
|
|
248
|
+
...composeRendererStack(),
|
|
249
|
+
...composeFileStack({ providers: ["inmemory"] }),
|
|
250
|
+
...composeUserDataRightsStack(),
|
|
251
|
+
]);
|
|
252
|
+
for (const expected of [
|
|
253
|
+
"text-content",
|
|
254
|
+
"legal-pages",
|
|
255
|
+
"tenant-lifecycle",
|
|
256
|
+
"audit",
|
|
257
|
+
"delivery",
|
|
258
|
+
"jobs",
|
|
259
|
+
"renderer-simple",
|
|
260
|
+
"files",
|
|
261
|
+
]) {
|
|
262
|
+
expect(names).toContain(expected);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("identity stack covers sessions + auth-mfa", () => {
|
|
267
|
+
const names = stackFeatureNames(composeIdentityStack({ mfa: TEST_MFA }));
|
|
268
|
+
expect(names).toContain("sessions");
|
|
269
|
+
expect(names).toContain("auth-mfa");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// createKumikoServer error / graceful-degradation paths — boot rejects,
|
|
2
|
+
// stylesheet pipeline failures, missing CSS route.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import { createKumikoServer } from "../create-kumiko-server";
|
|
10
|
+
|
|
11
|
+
const emptyFeature = defineFeature("dev-server-errors-probe", () => {});
|
|
12
|
+
|
|
13
|
+
describe("createKumikoServer — client bundle failure", () => {
|
|
14
|
+
test("broken clientEntry rejects at boot with client bundle failed", async () => {
|
|
15
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-bundle-fail-"));
|
|
16
|
+
const entry = join(tmpDir, "client.tsx");
|
|
17
|
+
writeFileSync(entry, "const x = {{{\n");
|
|
18
|
+
try {
|
|
19
|
+
await expect(
|
|
20
|
+
createKumikoServer({
|
|
21
|
+
features: [emptyFeature],
|
|
22
|
+
port: 0,
|
|
23
|
+
installSignalHandlers: false,
|
|
24
|
+
clientEntry: entry,
|
|
25
|
+
stylesheet: false,
|
|
26
|
+
}),
|
|
27
|
+
).rejects.toThrow(/client bundle failed|Bundle failed/);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("_buildBundle throw propagates at boot", async () => {
|
|
34
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-stub-fail-"));
|
|
35
|
+
const entry = join(tmpDir, "client.tsx");
|
|
36
|
+
writeFileSync(entry, "// noop\n");
|
|
37
|
+
try {
|
|
38
|
+
await expect(
|
|
39
|
+
createKumikoServer({
|
|
40
|
+
features: [emptyFeature],
|
|
41
|
+
port: 0,
|
|
42
|
+
installSignalHandlers: false,
|
|
43
|
+
clientEntry: entry,
|
|
44
|
+
stylesheet: false,
|
|
45
|
+
_buildBundle: async () => {
|
|
46
|
+
throw new Error("stub build blew up");
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
).rejects.toThrow(/stub build blew up/);
|
|
50
|
+
} finally {
|
|
51
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Option/error matrix for createKumikoServer — pins boot-time rejects
|
|
2
|
+
// that happen before Postgres/Redis wiring (normalizeEntries).
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { createKumikoServer } from "../create-kumiko-server";
|
|
7
|
+
|
|
8
|
+
const emptyFeature = defineFeature("prod-packaging-options-probe", () => {});
|
|
9
|
+
|
|
10
|
+
describe("createKumikoServer — option matrix", () => {
|
|
11
|
+
test("clientEntry + clientEntries → mutually exclusive error", async () => {
|
|
12
|
+
await expect(
|
|
13
|
+
createKumikoServer({
|
|
14
|
+
features: [emptyFeature],
|
|
15
|
+
clientEntry: "src/client.tsx",
|
|
16
|
+
clientEntries: [{ name: "admin", sourceFile: "src/client-admin.tsx" }],
|
|
17
|
+
installSignalHandlers: false,
|
|
18
|
+
port: 0,
|
|
19
|
+
}),
|
|
20
|
+
).rejects.toThrow(/mutually exclusive/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("clientEntries without hostDispatch → error", async () => {
|
|
24
|
+
await expect(
|
|
25
|
+
createKumikoServer({
|
|
26
|
+
features: [emptyFeature],
|
|
27
|
+
clientEntries: [{ name: "admin", sourceFile: "src/client-admin.tsx" }],
|
|
28
|
+
installSignalHandlers: false,
|
|
29
|
+
port: 0,
|
|
30
|
+
}),
|
|
31
|
+
).rejects.toThrow(/hostDispatch/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
@@ -336,3 +336,130 @@ describe("createKumikoServer extraRoutes-deps", () => {
|
|
|
336
336
|
expect(body.data?.roles).toContain("SystemAdmin");
|
|
337
337
|
});
|
|
338
338
|
});
|
|
339
|
+
|
|
340
|
+
describe("createKumikoServer — stylesheet tailwind failure (graceful)", () => {
|
|
341
|
+
test("missing stylesheet entry boots without CSS — GET /styles.css → 404", async () => {
|
|
342
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-css-fail-"));
|
|
343
|
+
const entry = join(tmpDir, "client.tsx");
|
|
344
|
+
writeFileSync(entry, "export const x = 1;\n");
|
|
345
|
+
try {
|
|
346
|
+
handle = await createKumikoServer({
|
|
347
|
+
features: [probeFeature],
|
|
348
|
+
port: 0,
|
|
349
|
+
installSignalHandlers: false,
|
|
350
|
+
clientEntry: entry,
|
|
351
|
+
stylesheet: join(tmpDir, "does-not-exist.css"),
|
|
352
|
+
_buildBundle: async () => ({ js: "// stub", map: "" }),
|
|
353
|
+
});
|
|
354
|
+
const cssRes = await handle.fetch(new Request("http://localhost/styles.css"));
|
|
355
|
+
expect(cssRes.status).toBe(404);
|
|
356
|
+
expect(await cssRes.text()).toBe("no stylesheet");
|
|
357
|
+
} finally {
|
|
358
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("createKumikoServer — real Bun.build (buildClient)", () => {
|
|
364
|
+
test("clientEntry without _buildBundle produces a JS bundle via Bun.build", async () => {
|
|
365
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-real-build-"));
|
|
366
|
+
const entry = join(tmpDir, "client.tsx");
|
|
367
|
+
// Minimal entry Bun.build can emit — no JSX, no imports.
|
|
368
|
+
writeFileSync(entry, "export const ping = 1;\n");
|
|
369
|
+
// Empty dirs matching a glob — exercises expandWatchPatterns without
|
|
370
|
+
// writing files (avoids process.exit(75) from bare .tsx events).
|
|
371
|
+
mkdirSync(join(tmpDir, "pkg-a"));
|
|
372
|
+
mkdirSync(join(tmpDir, "pkg-b"));
|
|
373
|
+
try {
|
|
374
|
+
handle = await createKumikoServer({
|
|
375
|
+
features: [probeFeature],
|
|
376
|
+
port: 0,
|
|
377
|
+
installSignalHandlers: false,
|
|
378
|
+
clientEntry: entry,
|
|
379
|
+
stylesheet: false,
|
|
380
|
+
watchDirs: [join(tmpDir, "pkg-*")],
|
|
381
|
+
});
|
|
382
|
+
const res = await handle.fetch(new Request("http://localhost/client.js"));
|
|
383
|
+
expect(res.status).toBe(200);
|
|
384
|
+
expect(res.headers.get("content-type")).toMatch(/application\/javascript/);
|
|
385
|
+
const body = await res.text();
|
|
386
|
+
expect(body.length).toBeGreaterThan(0);
|
|
387
|
+
expect(body).toMatch(/ping/);
|
|
388
|
+
|
|
389
|
+
// Stop the watcher before teardown rmSync deletes clientEntry out from
|
|
390
|
+
// under it — a bare "client.tsx" delete event classifies as "restart"
|
|
391
|
+
// (classifyChange only special-cases endsWith("/client.tsx")) and
|
|
392
|
+
// process.exit(75) kills the whole bun test runner mid-suite.
|
|
393
|
+
await handle.stop();
|
|
394
|
+
handle = undefined;
|
|
395
|
+
} finally {
|
|
396
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("createKumikoServer — hot-reload broadcast", () => {
|
|
402
|
+
test("file change under web/ rebuilds and broadcasts SSE reload", async () => {
|
|
403
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-watch-"));
|
|
404
|
+
const entry = join(tmpDir, "client.tsx");
|
|
405
|
+
const webDir = join(tmpDir, "web");
|
|
406
|
+
mkdirSync(webDir);
|
|
407
|
+
writeFileSync(entry, "export const ping = 1;\n");
|
|
408
|
+
|
|
409
|
+
let builds = 0;
|
|
410
|
+
try {
|
|
411
|
+
handle = await createKumikoServer({
|
|
412
|
+
features: [probeFeature],
|
|
413
|
+
port: 0,
|
|
414
|
+
installSignalHandlers: false,
|
|
415
|
+
clientEntry: entry,
|
|
416
|
+
stylesheet: false,
|
|
417
|
+
// Only the entry dir is watched (no extra watchDirs) — a nested
|
|
418
|
+
// web/page.tsx event arrives as "web/page.tsx" → hot-reload.
|
|
419
|
+
// Watching web/ separately would fire bare "page.tsx" → restart
|
|
420
|
+
// → process.exit(75) and kill the test runner.
|
|
421
|
+
_buildBundle: async () => {
|
|
422
|
+
builds += 1;
|
|
423
|
+
return { js: `// build-${builds}`, map: "" };
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const sseRes = await handle.fetch(new Request("http://localhost/_reload"));
|
|
428
|
+
expect(sseRes.status).toBe(200);
|
|
429
|
+
const reader = sseRes.body?.getReader();
|
|
430
|
+
expect(reader).toBeDefined();
|
|
431
|
+
if (!reader) return;
|
|
432
|
+
|
|
433
|
+
await reader.read(); // drain connected comment
|
|
434
|
+
|
|
435
|
+
const initialBuilds = builds;
|
|
436
|
+
writeFileSync(join(webDir, "page.tsx"), "export const x = 1;\n");
|
|
437
|
+
|
|
438
|
+
const deadline = Date.now() + 3000;
|
|
439
|
+
let sawReload = false;
|
|
440
|
+
while (Date.now() < deadline && !sawReload) {
|
|
441
|
+
const readPromise = reader.read();
|
|
442
|
+
const timeout = new Promise<{ done: true; value: undefined }>((resolve) =>
|
|
443
|
+
setTimeout(() => resolve({ done: true, value: undefined }), 200),
|
|
444
|
+
);
|
|
445
|
+
const { value, done } = await Promise.race([readPromise, timeout]);
|
|
446
|
+
if (done || value === undefined) continue;
|
|
447
|
+
const chunk = new TextDecoder().decode(value);
|
|
448
|
+
if (chunk.includes("event: reload")) sawReload = true;
|
|
449
|
+
}
|
|
450
|
+
await reader.cancel();
|
|
451
|
+
|
|
452
|
+
expect(builds).toBeGreaterThan(initialBuilds);
|
|
453
|
+
expect(sawReload).toBe(true);
|
|
454
|
+
|
|
455
|
+
const js = await handle.fetch(new Request("http://localhost/client.js"));
|
|
456
|
+
expect(await js.text()).toMatch(/build-/);
|
|
457
|
+
|
|
458
|
+
// Abort watchers before teardown rmSync can fire a restart event.
|
|
459
|
+
await handle.stop();
|
|
460
|
+
handle = undefined;
|
|
461
|
+
} finally {
|
|
462
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
});
|
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { formatBuildResult } from "
|
|
5
|
+
import { formatBuildResult } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
|
|
6
6
|
import { discoverServerEntry } from "../build-server-bundle";
|
|
7
7
|
|
|
8
8
|
describe("discoverServerEntry", () => {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { describe, expect, it } from "bun:test";
|
|
8
8
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
9
|
import { composeEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
|
|
10
|
+
import { runProdApp } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
10
11
|
import { z } from "zod";
|
|
11
12
|
import { frameworkCoreEnvSchema } from "../env-schema";
|
|
12
13
|
import * as devServerPublicApi from "../index";
|
|
13
|
-
import { runProdApp } from "../run-prod-app";
|
|
14
14
|
|
|
15
15
|
const secretsFeature = defineFeature("secrets", (r) => {
|
|
16
16
|
r.envSchema(
|