@cosmicdrift/kumiko-server-runtime 0.193.1 → 0.195.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-server-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.195.0",
|
|
4
4
|
"description": "Production server-boot runtime for Kumiko apps: connections, schema-drift-gate, seeds, lifecycle, graceful shutdown. Symmetric to kumiko-dev-server's runDevApp, without dev/scaffold/codegen tooling.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
80
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
79
|
+
"@cosmicdrift/kumiko-bundled-features": "0.195.0",
|
|
80
|
+
"@cosmicdrift/kumiko-framework": "0.195.0",
|
|
81
81
|
"temporal-polyfill": "^0.3.2"
|
|
82
82
|
},
|
|
83
83
|
"publishConfig": {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// injectSchema
|
|
2
|
-
// server (static-fallback index.html)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// injectSchema is shared by the dev-server (every HTML response) and
|
|
2
|
+
// prod-server (static-fallback index.html) paths. A bug here would be a
|
|
3
|
+
// silent production failure: createKumikoApp wouldn't find
|
|
4
|
+
// `window.__KUMIKO_SCHEMA__` and would mount empty. Tests pin idempotency
|
|
5
|
+
// plus the two insertion points (before the /client.js tag or before </body>).
|
|
6
6
|
|
|
7
7
|
import { describe, expect, test } from "bun:test";
|
|
8
8
|
import { injectSchema } from "../inject-schema";
|
|
@@ -15,11 +15,20 @@ describe("injectSchema", () => {
|
|
|
15
15
|
const html = '<html><body><script src="/client.js" defer></script></body></html>';
|
|
16
16
|
const out = injectSchema(html, SCHEMA);
|
|
17
17
|
expect(out).toContain(TAG);
|
|
18
|
-
// Schema
|
|
19
|
-
//
|
|
18
|
+
// Schema MUST come before the client bundle — otherwise createKumikoApp()
|
|
19
|
+
// runs before window.__KUMIKO_SCHEMA__ is set.
|
|
20
20
|
expect(out.indexOf(TAG)).toBeLessThan(out.indexOf('<script src="/client.js"'));
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
test("HTML mit module-form /client.js-Tag: Schema-Tag wird DAVOR eingefügt", () => {
|
|
24
|
+
const html = '<html><body><script type="module" src="/client.js"></script></body></html>';
|
|
25
|
+
const out = injectSchema(html, SCHEMA);
|
|
26
|
+
expect(out).toContain(TAG);
|
|
27
|
+
// Dev templates use `type="module"`, not the classic form — insertion
|
|
28
|
+
// must match this tag pattern too.
|
|
29
|
+
expect(out.indexOf(TAG)).toBeLessThan(out.indexOf('<script type="module" src="/client.js"'));
|
|
30
|
+
});
|
|
31
|
+
|
|
23
32
|
test("HTML ohne /client.js-Tag: Schema-Tag wird vor </body> eingefügt", () => {
|
|
24
33
|
const html = "<html><body><div id=root></div></body></html>";
|
|
25
34
|
const out = injectSchema(html, SCHEMA);
|
|
@@ -36,8 +45,8 @@ describe("injectSchema", () => {
|
|
|
36
45
|
test("Idempotent: bei bereits injectem Schema kein zweiter Tag", () => {
|
|
37
46
|
const html = `<html><body>${TAG}</body></html>`;
|
|
38
47
|
const out = injectSchema(html, '{"features":[{"differentSchema":true}]}');
|
|
39
|
-
// Original
|
|
40
|
-
//
|
|
48
|
+
// Original tag stays, no second tag added — the marker check otherwise
|
|
49
|
+
// prevents stacking tags on repeated reads.
|
|
41
50
|
expect(out).toBe(html);
|
|
42
51
|
});
|
|
43
52
|
|
|
@@ -53,10 +62,39 @@ describe("injectSchema", () => {
|
|
|
53
62
|
});
|
|
54
63
|
const html = '<html><body><script src="/client.js"></script></body></html>';
|
|
55
64
|
const out = injectSchema(html, complex);
|
|
56
|
-
// Sanity:
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
65
|
+
// Sanity: the injected script must be valid JS (object-literal syntax,
|
|
66
|
+
// no HTML-reserved chars in the JSON that would break the <script>
|
|
67
|
+
// block — JSON.stringify doesn't escape /, < etc., but the standard
|
|
68
|
+
// chars used here are harmless).
|
|
60
69
|
expect(out).toContain(`window.__KUMIKO_SCHEMA__=${complex}`);
|
|
61
70
|
});
|
|
71
|
+
|
|
72
|
+
test("Schema mit $-Replacement-Patterns bleibt byte-identisch (kein HTML-Splicing)", () => {
|
|
73
|
+
// String.prototype.replace() interprets $$, $&, $`, $' specially in the
|
|
74
|
+
// REPLACEMENT argument. The injected schema JSON string lands there — an
|
|
75
|
+
// i18n label containing "$'" or similar would otherwise splice parts of
|
|
76
|
+
// `html` into the script tag. A replacer-function argument sidesteps that.
|
|
77
|
+
const dollarSchema = '{"label":"$\' tail $$ amp $& end"}';
|
|
78
|
+
const dollarTag = `<script>window.__KUMIKO_SCHEMA__=${dollarSchema};</script>`;
|
|
79
|
+
const html = '<html><body><script src="/client.js"></script></body></html>';
|
|
80
|
+
const out = injectSchema(html, dollarSchema);
|
|
81
|
+
// Insertion point is right before the /client.js tag, not the start of
|
|
82
|
+
// the document — built via indexOf/slice, not .replace(), so the
|
|
83
|
+
// expected value doesn't itself run through the same $-pattern footgun.
|
|
84
|
+
const clientScriptIdx = html.indexOf('<script src="/client.js"');
|
|
85
|
+
const expected = html.slice(0, clientScriptIdx) + dollarTag + html.slice(clientScriptIdx);
|
|
86
|
+
expect(out).toBe(expected);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Schema mit $-Replacement-Patterns, Insertion vor </body>", () => {
|
|
90
|
+
const dollarSchema = '{"label":"$\' tail $$ amp $& end"}';
|
|
91
|
+
const dollarTag = `<script>window.__KUMIKO_SCHEMA__=${dollarSchema};</script>`;
|
|
92
|
+
const html = "<html><body><div id=root></div></body></html>";
|
|
93
|
+
const out = injectSchema(html, dollarSchema);
|
|
94
|
+
// Built via slice/concat, not .replace() — the expected value must not
|
|
95
|
+
// itself run through the same $-pattern footgun the fix removes.
|
|
96
|
+
const bodyIdx = html.indexOf("</body>");
|
|
97
|
+
const expected = html.slice(0, bodyIdx) + dollarTag + html.slice(bodyIdx);
|
|
98
|
+
expect(out).toBe(expected);
|
|
99
|
+
});
|
|
62
100
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Regression guard for startDevJobRunners' lane default: a job registered
|
|
2
|
+
// WITHOUT an explicit runIn used to get no consumer at all in the dev server
|
|
3
|
+
// (the old `lanes` filter dropped `undefined` lanes), so it sat in the queue
|
|
4
|
+
// forever. `runners.length` alone can't catch that — a lane with no consumer
|
|
5
|
+
// still produces a runner object. This dispatches a real job and waits for
|
|
6
|
+
// its side effect to prove the lane is actually being consumed.
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
createJobsFeature,
|
|
10
|
+
jobRunLogsTable,
|
|
11
|
+
jobRunsTable,
|
|
12
|
+
} from "@cosmicdrift/kumiko-bundled-features/jobs";
|
|
13
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
14
|
+
import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
+
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
16
|
+
import {
|
|
17
|
+
createTestDb,
|
|
18
|
+
createTestRedis,
|
|
19
|
+
type TestDb,
|
|
20
|
+
type TestRedis,
|
|
21
|
+
unsafePushTables,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
+
import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
24
|
+
import { startDevJobRunners } from "../job-run-logger";
|
|
25
|
+
|
|
26
|
+
let testDb: TestDb;
|
|
27
|
+
let testRedis: TestRedis;
|
|
28
|
+
let db: DbConnection;
|
|
29
|
+
|
|
30
|
+
const jobRuns: Array<{ readonly note: string }> = [];
|
|
31
|
+
|
|
32
|
+
// No `runIn` — the exact case the lane-default regression hits.
|
|
33
|
+
const probeFeature = defineFeature("probe", (r) => {
|
|
34
|
+
r.job("noteIt", { trigger: { manual: true } }, async (payload) => {
|
|
35
|
+
jobRuns.push({ note: (payload as { note: string }).note });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
testDb = await createTestDb();
|
|
41
|
+
testRedis = await createTestRedis();
|
|
42
|
+
db = testDb.db;
|
|
43
|
+
await unsafePushTables(db, { jobRunsTable, jobRunLogsTable });
|
|
44
|
+
await createEventsTable(db);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await testDb.cleanup();
|
|
49
|
+
await testRedis.cleanup();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("startDevJobRunners", () => {
|
|
53
|
+
test("job without explicit runIn gets a consumer and actually executes", async () => {
|
|
54
|
+
const registry = createRegistry([probeFeature, createJobsFeature()]);
|
|
55
|
+
|
|
56
|
+
const { runners, stop } = await startDevJobRunners({
|
|
57
|
+
registry,
|
|
58
|
+
db,
|
|
59
|
+
context: {},
|
|
60
|
+
redisUrl: testRedis.redisUrl,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
expect(runners.length).toBe(1);
|
|
65
|
+
const runner = runners[0];
|
|
66
|
+
if (runner === undefined) throw new Error("expected a runner");
|
|
67
|
+
|
|
68
|
+
await runner.dispatch("probe:job:note-it", { note: "hello-from-dev-runner" });
|
|
69
|
+
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(jobRuns.some((r) => r.note === "hello-from-dev-runner")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
} finally {
|
|
74
|
+
await stop();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
package/src/build-prod-bundle.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// wenn nur clientEntry da ist und kein eigenes CSS)
|
|
12
12
|
// public/ → rsync 1:1 (kein Hash — User-bewusste URLs)
|
|
13
13
|
// public/index.html | index.html → Template, Placeholder-Tags ersetzt:
|
|
14
|
-
// <script src="/client.js"> → /assets/client-<hash>.js
|
|
14
|
+
// <script type="module" src="/client.js"> → /assets/client-<hash>.js
|
|
15
15
|
// <link href="/styles.css"> → /assets/styles-<hash>.css
|
|
16
16
|
// (kein HTML, vanilla) → Default-HTML ohne Asset-Tags
|
|
17
17
|
//
|
|
@@ -569,8 +569,8 @@ function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry):
|
|
|
569
569
|
//
|
|
570
570
|
// Convention: das HTML-Template MUSS Placeholder-Tags für jedes Asset
|
|
571
571
|
// dieses Entries enthalten:
|
|
572
|
-
// - `<script src="/client.js">` für single-mode entry "client"
|
|
573
|
-
// - `<script src="/client-<name>.js">` für multi-mode entry "<name>"
|
|
572
|
+
// - `<script type="module" src="/client.js">` für single-mode entry "client"
|
|
573
|
+
// - `<script type="module" src="/client-<name>.js">` für multi-mode entry "<name>"
|
|
574
574
|
// - `<link href="/styles.css">` für styles (gemeinsam über alle entries)
|
|
575
575
|
// Der Build ersetzt sie durch die gehashten URLs.
|
|
576
576
|
//
|
package/src/inject-schema.ts
CHANGED
|
@@ -20,11 +20,14 @@ function scriptSafeJsonHtml(json: string): string {
|
|
|
20
20
|
return json.replace(/</g, "\\u003c");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const CLIENT_SCRIPT_TAG_RE = /<script\b[^>]*\ssrc="\/client\.js"/;
|
|
24
|
+
|
|
23
25
|
export function injectSchema(html: string, schemaJson: string): string {
|
|
24
26
|
if (html.includes("__KUMIKO_SCHEMA__")) return html;
|
|
25
27
|
const tag = `<script>window.__KUMIKO_SCHEMA__=${scriptSafeJsonHtml(schemaJson)};</script>`;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const clientScriptMatch = html.match(CLIENT_SCRIPT_TAG_RE);
|
|
29
|
+
if (clientScriptMatch) {
|
|
30
|
+
return html.replace(CLIENT_SCRIPT_TAG_RE, (m) => `${tag}${m}`);
|
|
28
31
|
}
|
|
29
|
-
return html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
|
|
32
|
+
return html.includes("</body>") ? html.replace("</body>", () => `${tag}</body>`) : html + tag;
|
|
30
33
|
}
|