@cosmicdrift/kumiko-dev-server 0.336.0 → 0.337.1
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.
|
@@ -185,6 +185,9 @@ export type KumikoServerHandle = {
|
|
|
185
185
|
readonly stack: TestStack;
|
|
186
186
|
/** Stops the server and tears down the stack (DB + redis). */
|
|
187
187
|
readonly stop: () => Promise<void>;
|
|
188
|
+
/** BullMQ queue-name prefix of this boot: per-boot in ephemeral mode, so
|
|
189
|
+
* its `bull:<prefix>-*` keys belong to this server alone on a shared Redis. */
|
|
190
|
+
readonly jobQueueNamePrefix: string;
|
|
188
191
|
};
|
|
189
192
|
export declare const STYLESHEET_WATCH_ENV = "KUMIKO_DEV_STYLESHEET_WATCH";
|
|
190
193
|
export declare const PROD_BUNDLES_ENV = "KUMIKO_DEV_PROD_BUNDLES";
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// The companion prod entry will land at `@cosmicdrift/kumiko-framework/server`
|
|
16
16
|
// with a different options shape (clientDist, auth config, db url).
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
-
import { existsSync, mkdtempSync, statSync } from "node:fs";
|
|
18
|
+
import { existsSync, mkdtempSync, realpathSync, statSync } from "node:fs";
|
|
19
19
|
import { readFile, realpath, watch } from "node:fs/promises";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join, resolve, sep } from "node:path";
|
|
@@ -228,13 +228,14 @@ async function servePublicFile(pathname, publicDir) {
|
|
|
228
228
|
if (filePath === undefined)
|
|
229
229
|
return undefined;
|
|
230
230
|
try {
|
|
231
|
-
const bytes = await readFile(filePath);
|
|
232
231
|
// Lexical containment (above) only catches "..", not a symlink inside
|
|
233
232
|
// publicDir that points outside it on disk — realpath resolves the
|
|
234
|
-
// actual target and the same containment check runs against it.
|
|
233
|
+
// actual target and the same containment check runs against it. Read
|
|
234
|
+
// from the resolved path so the checked target is the one read.
|
|
235
235
|
const real = await realpath(filePath);
|
|
236
236
|
if (real !== publicDir && !real.startsWith(publicDir + sep))
|
|
237
237
|
return undefined;
|
|
238
|
+
const bytes = await readFile(real);
|
|
238
239
|
return { bytes, mime: publicFileMimeType(filePath) };
|
|
239
240
|
}
|
|
240
241
|
catch (err) {
|
|
@@ -756,7 +757,12 @@ export async function createKumikoServer(options) {
|
|
|
756
757
|
bundleByAssetPath.set(assetPathFor(e.name), e.name);
|
|
757
758
|
// App-root convention (same as expandWatchPatterns/resolveStylesheet above):
|
|
758
759
|
// process.cwd() is the app workspace, so public/ is its static asset dir.
|
|
759
|
-
|
|
760
|
+
// Symlink-resolved once: servePublicFile compares realpath()s against it, so
|
|
761
|
+
// a symlinked public/ would otherwise 404 every file.
|
|
762
|
+
const lexicalPublicDir = resolve(process.cwd(), "public");
|
|
763
|
+
const publicDir = existsSync(lexicalPublicDir)
|
|
764
|
+
? realpathSync(lexicalPublicDir)
|
|
765
|
+
: lexicalPublicDir;
|
|
760
766
|
// The app's cwd first, this package's own location as fallback (the cwd may
|
|
761
767
|
// not have renderer-web installed, e.g. a fixture dir).
|
|
762
768
|
const rendererWebFontsDir = resolveRendererWebFontsDir([process.cwd(), import.meta.dir]);
|
|
@@ -984,6 +990,29 @@ export async function createKumikoServer(options) {
|
|
|
984
990
|
}, watcherAbort.signal);
|
|
985
991
|
}
|
|
986
992
|
}
|
|
993
|
+
// The per-boot prefix is never reused, so its BullMQ keys (queues,
|
|
994
|
+
// repeatables, delayed jobs) would pile up in the shared Redis forever.
|
|
995
|
+
// A prefix-less duplicate: SCAN returns full key names, which the stack
|
|
996
|
+
// client's keyPrefix would otherwise prepend a second time.
|
|
997
|
+
// Takes the connection as a parameter: it must be opened before
|
|
998
|
+
// stack.cleanup() closes the stack client, yet used only after it, because
|
|
999
|
+
// closing the stack's enqueuer queues re-writes their `meta` keys.
|
|
1000
|
+
const deleteEphemeralJobQueues = async (raw) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const keys = [];
|
|
1003
|
+
for await (const batch of raw.scanStream({
|
|
1004
|
+
match: `bull:${jobQueueNamePrefix}-*`,
|
|
1005
|
+
count: 500,
|
|
1006
|
+
})) {
|
|
1007
|
+
keys.push(...batch);
|
|
1008
|
+
}
|
|
1009
|
+
if (keys.length > 0)
|
|
1010
|
+
await raw.del(...keys);
|
|
1011
|
+
}
|
|
1012
|
+
finally {
|
|
1013
|
+
raw.disconnect();
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
987
1016
|
const stop = async () => {
|
|
988
1017
|
// Watcher zuerst stoppen damit kein onChange während des Teardowns
|
|
989
1018
|
// mehr feuert (sonst können tmpdir-rmSync ein process.exit(75)
|
|
@@ -998,7 +1027,10 @@ export async function createKumikoServer(options) {
|
|
|
998
1027
|
await stack.eventDispatcher.stop();
|
|
999
1028
|
}
|
|
1000
1029
|
await devJobRunners.stop();
|
|
1030
|
+
const rawRedis = persistentDb ? undefined : stack.redis.redis.duplicate({ keyPrefix: "" });
|
|
1001
1031
|
await stack.cleanup();
|
|
1032
|
+
if (rawRedis)
|
|
1033
|
+
await deleteEphemeralJobQueues(rawRedis);
|
|
1002
1034
|
};
|
|
1003
1035
|
// --- graceful shutdown ---
|
|
1004
1036
|
// Signal handlers fire on Ctrl-C / kill. Without them, repeated dev
|
|
@@ -1041,5 +1073,5 @@ export async function createKumikoServer(options) {
|
|
|
1041
1073
|
? ` (hot reload on ${entries.length === 1 ? "client entry" : `${entries.length} entries`})`
|
|
1042
1074
|
: ""));
|
|
1043
1075
|
}
|
|
1044
|
-
return { fetch: handleFetch, server, stack, stop };
|
|
1076
|
+
return { fetch: handleFetch, server, stack, stop, jobQueueNamePrefix };
|
|
1045
1077
|
}
|
package/dist/init-deploy-cli.js
CHANGED
|
@@ -84,7 +84,8 @@ export async function runInitDeployCli({ argv, cwd, out, }) {
|
|
|
84
84
|
out.err("");
|
|
85
85
|
return 2;
|
|
86
86
|
}
|
|
87
|
-
const
|
|
87
|
+
const outDir = getStringFlag(args, "out") ?? cwd;
|
|
88
|
+
const appName = getStringFlag(args, "app") ?? defaultAppName(outDir);
|
|
88
89
|
if (!appName) {
|
|
89
90
|
out.err("");
|
|
90
91
|
out.err(' --app <name> is required (no package.json "name" found to default from).');
|
|
@@ -96,7 +97,7 @@ export async function runInitDeployCli({ argv, cwd, out, }) {
|
|
|
96
97
|
appName,
|
|
97
98
|
port: getNumberFlag(args, "port"),
|
|
98
99
|
githubOrg: getStringFlag(args, "github-org"),
|
|
99
|
-
destination:
|
|
100
|
+
destination: outDir,
|
|
100
101
|
};
|
|
101
102
|
try {
|
|
102
103
|
return check ? reportDrift(target, out) : reportScaffold(target, force, out);
|
|
@@ -6,6 +6,18 @@ import { setupTestStack, } from "@cosmicdrift/kumiko-framework/stack";
|
|
|
6
6
|
import { jobRunLoggerCallbacks } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
|
|
7
7
|
import { composeFeatures, } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
8
8
|
import { addConfigAccessorFactory } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
|
|
9
|
+
function isConfigResolver(value) {
|
|
10
|
+
return (typeof value === "object" && value !== null && "get" in value && typeof value.get === "function");
|
|
11
|
+
}
|
|
12
|
+
function resolveBaseConfigResolver(fromBase) {
|
|
13
|
+
const supplied = fromBase["configResolver"];
|
|
14
|
+
if (supplied === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!isConfigResolver(supplied)) {
|
|
17
|
+
throw new Error("extraContext.configResolver must be a ConfigResolver (an object with get()), e.g. from createConfigResolver() — got a different value; pass the instance, not a factory.");
|
|
18
|
+
}
|
|
19
|
+
return supplied;
|
|
20
|
+
}
|
|
9
21
|
export function mergeExtraContext(base, presets) {
|
|
10
22
|
if (presets.length === 0)
|
|
11
23
|
return base;
|
|
@@ -14,7 +26,7 @@ export function mergeExtraContext(base, presets) {
|
|
|
14
26
|
const merged = { ...fromBase };
|
|
15
27
|
if (presets.includes("config")) {
|
|
16
28
|
// Same precedence as mergeConfigResolverDefault (run-dev-app.ts) — a base-supplied resolver wins, factory always derives from it (fw#3313).
|
|
17
|
-
const configResolver = fromBase
|
|
29
|
+
const configResolver = resolveBaseConfigResolver(fromBase) ?? createConfigResolver();
|
|
18
30
|
Object.assign(merged, addConfigAccessorFactory({ configResolver }, deps.registry));
|
|
19
31
|
}
|
|
20
32
|
if (presets.includes("template-resolver")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.337.1",
|
|
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>",
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"kumiko-upgrade": "./bin/kumiko-upgrade.ts"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
70
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
71
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
67
|
+
"@cosmicdrift/kumiko-bundled-features": "0.337.1",
|
|
68
|
+
"@cosmicdrift/kumiko-framework": "0.337.1",
|
|
69
|
+
"@cosmicdrift/kumiko-headless": "0.337.1",
|
|
70
|
+
"@cosmicdrift/kumiko-renderer-web": "0.337.1",
|
|
71
|
+
"@cosmicdrift/kumiko-server-runtime": "0.337.1",
|
|
72
72
|
"bun-types": "^1.3.13",
|
|
73
73
|
"ts-morph": "^28.0.0",
|
|
74
74
|
"zod": "^4.4.3"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.337.1",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "createKumikoServer handle exposes the boot's jobQueueNamePrefix",
|
|
6
|
+
"detail": "dev-server: `createKumikoServer()` returns the boot's `jobQueueNamePrefix` on its handle, so callers sharing a Redis can address exactly that boot's `bull:<prefix>-*` keys.",
|
|
7
|
+
"migration": "No action needed: purely additive handle field."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.337.0",
|
|
11
|
+
"type": "fix",
|
|
12
|
+
"title": "public/ served via realpath incl. symlinked public/, ephemeral dev queues cleaned on stop, init-deploy --out app name, compose-style migrate-step network name, config preset validates configResolver; guards report correct isolation lines and time out kumiko-upgrade",
|
|
13
|
+
"detail": "The dev server serves `public/` files from the symlink-resolved path (realpath check before the read) and works when `public/` itself is a symlink. Ephemeral dev boots delete their per-boot BullMQ keys from the shared Redis on `stop()`. `kumiko-init-deploy --out <dir>` defaults the app name from that directory's `package.json`. The generated `migrate-step.sh` with `stackNetwork: \"directory\"` derives the network name like Docker Compose (lowercased, sanitised, `COMPOSE_PROJECT_NAME` wins). `setupTestStackFromFeatures` with the `config` preset throws a clear error when `extraContext.configResolver` is not a resolver. The runtime-isolation guard reports each regression at its own import line, and the upgrade-state guard no longer hangs on a stuck `kumiko-upgrade` (stdin ignored, timeout)."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.335.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -33,9 +33,11 @@ if [ -z "$STACK_NETWORK" ]; then
|
|
|
33
33
|
fi
|
|
34
34
|
{{/stackNetworkDiscover}}
|
|
35
35
|
{{#stackNetworkDirectory}}
|
|
36
|
-
|
|
36
|
+
# Compose lowercases the project name and drops characters outside [a-z0-9_-].
|
|
37
|
+
COMPOSE_PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')}"
|
|
38
|
+
STACK_NETWORK="${COMPOSE_PROJECT}_stack"
|
|
37
39
|
if ! docker network inspect "$STACK_NETWORK" >/dev/null 2>&1; then
|
|
38
|
-
echo "Stack network '$STACK_NETWORK' not found — compose project not running?" >&2
|
|
40
|
+
echo "Stack network '$STACK_NETWORK' not found (derived from COMPOSE_PROJECT_NAME or the directory name '$(basename "$PWD")') — compose project not running?" >&2
|
|
39
41
|
exit 1
|
|
40
42
|
fi
|
|
41
43
|
{{/stackNetworkDirectory}}
|