@cosmicdrift/kumiko-dev-server 0.287.0 → 0.289.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.289.0",
|
|
4
4
|
"description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -59,9 +59,10 @@
|
|
|
59
59
|
"kumiko-upgrade": "./bin/kumiko-upgrade.ts"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
63
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
64
|
-
"@cosmicdrift/kumiko-
|
|
62
|
+
"@cosmicdrift/kumiko-bundled-features": "0.289.0",
|
|
63
|
+
"@cosmicdrift/kumiko-framework": "0.289.0",
|
|
64
|
+
"@cosmicdrift/kumiko-headless": "0.289.0",
|
|
65
|
+
"@cosmicdrift/kumiko-server-runtime": "0.289.0",
|
|
65
66
|
"ts-morph": "^28.0.0"
|
|
66
67
|
},
|
|
67
68
|
"publishConfig": {
|
|
@@ -509,6 +509,83 @@ describe("createKumikoServer — hot-reload broadcast", () => {
|
|
|
509
509
|
});
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
// kumiko-framework#3026: runDevApp gains resolvePageHead parity with
|
|
513
|
+
// runProdApp — both now call the same @cosmicdrift/kumiko-headless/apex
|
|
514
|
+
// resolveAndInjectPageHead, so a resolver produces the same tags in the
|
|
515
|
+
// same order under dev as under prod (see run-prod-app-static-files.test.ts's
|
|
516
|
+
// "resolver meta → og-tags appear" case for the prod-side equivalent). The
|
|
517
|
+
// real 300ms-timeout path is covered once, in headless's own unit test for
|
|
518
|
+
// resolveAndInjectPageHead (same shared function) — no need to re-run a slow
|
|
519
|
+
// timeout wait here too.
|
|
520
|
+
describe("createKumikoServer resolvePageHead", () => {
|
|
521
|
+
test("resolver meta → head tags injected, same tags + order as runProdApp", async () => {
|
|
522
|
+
handle = await createKumikoServer({
|
|
523
|
+
features: [probeFeature],
|
|
524
|
+
port: 0,
|
|
525
|
+
installSignalHandlers: false,
|
|
526
|
+
resolvePageHead: async () => ({
|
|
527
|
+
title: "Vehicle X",
|
|
528
|
+
description: "A car",
|
|
529
|
+
ogImage: "https://x/i.png",
|
|
530
|
+
}),
|
|
531
|
+
});
|
|
532
|
+
const res = await handle.fetch(new Request("http://localhost/"));
|
|
533
|
+
expect(res.status).toBe(200);
|
|
534
|
+
const body = await res.text();
|
|
535
|
+
expect(body).not.toContain("<title>Kumiko</title>");
|
|
536
|
+
expect((body.match(/kumiko-page-head/g) ?? []).length).toBe(1);
|
|
537
|
+
expect(body).toContain("<title>Vehicle X</title>");
|
|
538
|
+
expect(body).toContain('<meta name="description" content="A car" />');
|
|
539
|
+
expect(body).toContain('<meta property="og:title" content="Vehicle X" />');
|
|
540
|
+
expect(body).toContain('<meta property="og:description" content="A car" />');
|
|
541
|
+
expect(body).toContain('<meta property="og:image" content="https://x/i.png" />');
|
|
542
|
+
|
|
543
|
+
const titleIdx = body.indexOf("<title>Vehicle X</title>");
|
|
544
|
+
const descIdx = body.indexOf('<meta name="description"');
|
|
545
|
+
const ogTitleIdx = body.indexOf('<meta property="og:title"');
|
|
546
|
+
const ogDescIdx = body.indexOf('<meta property="og:description"');
|
|
547
|
+
const ogImageIdx = body.indexOf('<meta property="og:image"');
|
|
548
|
+
expect(titleIdx).toBeLessThan(descIdx);
|
|
549
|
+
expect(descIdx).toBeLessThan(ogTitleIdx);
|
|
550
|
+
expect(ogTitleIdx).toBeLessThan(ogDescIdx);
|
|
551
|
+
expect(ogDescIdx).toBeLessThan(ogImageIdx);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("resolver throws → 200 with the unchanged default shell, not a 500", async () => {
|
|
555
|
+
handle = await createKumikoServer({
|
|
556
|
+
features: [probeFeature],
|
|
557
|
+
port: 0,
|
|
558
|
+
installSignalHandlers: false,
|
|
559
|
+
resolvePageHead: async () => {
|
|
560
|
+
throw new Error("boom");
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
const res = await handle.fetch(new Request("http://localhost/"));
|
|
564
|
+
expect(res.status).toBe(200);
|
|
565
|
+
const body = await res.text();
|
|
566
|
+
expect(body).not.toContain("kumiko-page-head");
|
|
567
|
+
expect(body).toMatch(/<div id="root">/);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("resolver returns null → 200 with the unchanged default shell", async () => {
|
|
571
|
+
handle = await createKumikoServer({
|
|
572
|
+
features: [probeFeature],
|
|
573
|
+
port: 0,
|
|
574
|
+
installSignalHandlers: false,
|
|
575
|
+
resolvePageHead: async () => null,
|
|
576
|
+
});
|
|
577
|
+
const res = await handle.fetch(new Request("http://localhost/"));
|
|
578
|
+
expect(res.status).toBe(200);
|
|
579
|
+
expect(await res.text()).not.toContain("kumiko-page-head");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("no resolvePageHead configured → no page-head marker in the response", async () => {
|
|
583
|
+
const h = await boot();
|
|
584
|
+
const res = await h.fetch(new Request("http://localhost/"));
|
|
585
|
+
expect(await res.text()).not.toContain("kumiko-page-head");
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
512
589
|
// kumiko-framework#2435: tryHonoFirst used to decide "no route matched"
|
|
513
590
|
// purely from the status code — a matched httpRoute answering 404 on
|
|
514
591
|
// purpose (e.g. default-deny reads) was indistinguishable from an
|
package/src/changes.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.289.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "runDevApp gains resolvePageHead for dev/e2e parity with runProdApp",
|
|
6
|
+
"detail": "RunDevAppOptions/CreateKumikoServerOptions gain resolvePageHead, same PageHeadResolver signature as RunProdAppOptions. Applied to every templated HTML response (default single-entry shell and a host-dispatched \"html\" entry) via the shared @cosmicdrift/kumiko-headless/apex resolveAndInjectPageHead — same 300ms timeout, same error/null/timeout-falls-back-to-unchanged-200-shell semantics as prod (kumiko-framework#3026). The dev-only static-html hostDispatch kind (raw file passthrough, no bundle/schema injection either) is unaffected. No behavior change for apps that don't set resolvePageHead."
|
|
7
|
+
}
|
|
8
|
+
]
|
|
@@ -20,8 +20,17 @@ import { readFile, realpath, watch } from "node:fs/promises";
|
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join, resolve, sep } from "node:path";
|
|
22
22
|
import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import {
|
|
24
|
+
type AuthRoutesConfig,
|
|
25
|
+
buildRequestContextDataFromRequest,
|
|
26
|
+
generateToken,
|
|
27
|
+
requestContext,
|
|
28
|
+
} from "@cosmicdrift/kumiko-framework/api";
|
|
29
|
+
import {
|
|
30
|
+
buildAppSchema,
|
|
31
|
+
createAnonymousUser,
|
|
32
|
+
type FeatureDefinition,
|
|
33
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
25
34
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
26
35
|
import {
|
|
27
36
|
pushEntityProjectionTables,
|
|
@@ -30,6 +39,11 @@ import {
|
|
|
30
39
|
type TestStackOptions,
|
|
31
40
|
TestUsers,
|
|
32
41
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
42
|
+
import {
|
|
43
|
+
type PageHeadResolver,
|
|
44
|
+
type PageHeadSystemQuery,
|
|
45
|
+
resolveAndInjectPageHead,
|
|
46
|
+
} from "@cosmicdrift/kumiko-headless/apex";
|
|
33
47
|
import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
|
|
34
48
|
import { buildBunServeOptions } from "@cosmicdrift/kumiko-server-runtime/bun-serve-options";
|
|
35
49
|
import {
|
|
@@ -209,6 +223,16 @@ export type CreateKumikoServerOptions = {
|
|
|
209
223
|
* /og-image, …) Vorrang vor dem Dev-Asset-Pfad haben. `deps` statt
|
|
210
224
|
* `ctx` weil dies kein HandlerContext ist — kein user/tenant. */
|
|
211
225
|
readonly extraRoutes?: (app: import("hono").Hono, deps: ExtraRoutesSystemDeps) => void;
|
|
226
|
+
/** Per-request head-metadata resolver (Open-Graph/title/description) —
|
|
227
|
+
* same option, same `PageHeadResolver` signature, and same shared
|
|
228
|
+
* resolve+inject call (`resolveAndInjectPageHead`) as `runProdApp`'s
|
|
229
|
+
* `resolvePageHead`, so dev and e2e exercise the exact code prod does
|
|
230
|
+
* (kumiko-framework#3026). Consulted for every templated HTML response
|
|
231
|
+
* (the default single-entry shell and a host-dispatched `"html"` entry),
|
|
232
|
+
* right before it's served. On error, `null`, or a timeout, the shell
|
|
233
|
+
* ships unchanged with status 200 — never a 500. `systemQuery` inside
|
|
234
|
+
* the resolver runs as the anonymous role, same as prod. */
|
|
235
|
+
readonly resolvePageHead?: PageHeadResolver;
|
|
212
236
|
};
|
|
213
237
|
|
|
214
238
|
export type KumikoServerHandle = {
|
|
@@ -918,7 +942,15 @@ export async function createKumikoServer(
|
|
|
918
942
|
// bestimmt nachdem er hostDispatch evaluiert hat. Ohne hostDispatch
|
|
919
943
|
// ist es immer "client" mit Schema-Inject true (Single-Entry-Default
|
|
920
944
|
// damit der Client TypeScript-Schemas findet).
|
|
921
|
-
|
|
945
|
+
//
|
|
946
|
+
// resolvePageHead goes through the same headless resolveAndInjectPageHead
|
|
947
|
+
// that runProdApp uses, so dev and e2e exercise the one timeout and
|
|
948
|
+
// fallback path instead of a second copy of it (#3026).
|
|
949
|
+
const htmlResponse = async (
|
|
950
|
+
entryName: string,
|
|
951
|
+
doInjectSchema: boolean,
|
|
952
|
+
req: Request,
|
|
953
|
+
): Promise<Response> => {
|
|
922
954
|
const template = htmlTemplates.get(entryName) ?? defaultTemplate;
|
|
923
955
|
const headers = new Headers();
|
|
924
956
|
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
@@ -931,6 +963,19 @@ export async function createKumikoServer(
|
|
|
931
963
|
let html = injectReload(template);
|
|
932
964
|
if (stylesheetPath !== undefined) html = injectStylesheet(html);
|
|
933
965
|
if (doInjectSchema) html = injectSchema(html, appSchemaJson);
|
|
966
|
+
if (options.resolvePageHead !== undefined) {
|
|
967
|
+
const url = new URL(req.url);
|
|
968
|
+
const host = req.headers.get("host") ?? url.host;
|
|
969
|
+
const systemQuery: PageHeadSystemQuery = (type, payload, tenantId) =>
|
|
970
|
+
requestContext.run(requestContext.get() ?? buildRequestContextDataFromRequest(req), () =>
|
|
971
|
+
stack.dispatcher.query(type, payload, createAnonymousUser(tenantId)),
|
|
972
|
+
);
|
|
973
|
+
html = await resolveAndInjectPageHead(html, options.resolvePageHead, {
|
|
974
|
+
path: url.pathname,
|
|
975
|
+
host,
|
|
976
|
+
systemQuery,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
934
979
|
return new Response(html, { headers });
|
|
935
980
|
};
|
|
936
981
|
|
|
@@ -1058,9 +1103,9 @@ export async function createKumikoServer(
|
|
|
1058
1103
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
1059
1104
|
});
|
|
1060
1105
|
}
|
|
1061
|
-
return htmlResponse(dispatch.entryName, dispatch.injectSchema ?? true);
|
|
1106
|
+
return htmlResponse(dispatch.entryName, dispatch.injectSchema ?? true, req);
|
|
1062
1107
|
}
|
|
1063
|
-
return htmlResponse("client", true);
|
|
1108
|
+
return htmlResponse("client", true, req);
|
|
1064
1109
|
}
|
|
1065
1110
|
|
|
1066
1111
|
// Static assets under public/ — see tryServePublicAsset's own comment
|
package/src/run-dev-app.ts
CHANGED
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
|
|
75
75
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
76
76
|
import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
77
|
+
import type { PageHeadResolver } from "@cosmicdrift/kumiko-headless/apex";
|
|
77
78
|
import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
|
|
78
79
|
import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
|
|
79
80
|
import {
|
|
@@ -187,6 +188,11 @@ export type RunDevAppOptions = {
|
|
|
187
188
|
* geforderten — sonst weiß der Server nicht welche HTML er liefern
|
|
188
189
|
* soll. */
|
|
189
190
|
readonly hostDispatch?: CreateKumikoServerOptions["hostDispatch"];
|
|
191
|
+
/** Per-request head-metadata resolver — same option, same signature as
|
|
192
|
+
* `runProdApp`'s `resolvePageHead`, so `bun dev` and every e2e that boots
|
|
193
|
+
* through `runDevApp` show the same resolved head prod does. See
|
|
194
|
+
* `CreateKumikoServerOptions.resolvePageHead` for the full contract. */
|
|
195
|
+
readonly resolvePageHead?: PageHeadResolver;
|
|
190
196
|
/** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer-web/styles.css`
|
|
191
197
|
* wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
|
|
192
198
|
readonly stylesheet?: string | false;
|
|
@@ -482,6 +488,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
482
488
|
...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
|
|
483
489
|
...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
|
|
484
490
|
...(options.hostDispatch !== undefined && { hostDispatch: options.hostDispatch }),
|
|
491
|
+
...(options.resolvePageHead !== undefined && { resolvePageHead: options.resolvePageHead }),
|
|
485
492
|
...(options.stylesheet !== undefined && { stylesheet: options.stylesheet }),
|
|
486
493
|
...(options.htmlPath !== undefined && { htmlPath: options.htmlPath }),
|
|
487
494
|
...(options.port !== undefined && { port: options.port }),
|