@cosmicdrift/kumiko-dev-server 0.371.1 → 0.373.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.
@@ -1,49 +1,57 @@
1
1
  #!/usr/bin/env bun
2
- // kumiko-build — Production-Build für Kumiko-Apps. Convention-driven:
2
+ // kumiko-build — production build for Kumiko apps. Convention-driven:
3
3
  //
4
4
  // package.json "kumiko.clientEntries"/"kumiko.clientEntry",
5
- // or src/client.tsx | src/styles.css | public/ → Client-Bundle (dist/)
6
- // bin/main.ts → Server-Bundle (dist-server/)
5
+ // or src/client.tsx | src/styles.css | public/ → client bundle (dist/)
6
+ // bin/main.ts → server bundle (dist-server/,
7
+ // incl. bun.lock seeded from the app's bun.lock)
7
8
  //
8
- // Beide werden gebaut wenn die jeweiligen Conventions getroffen sind, sonst
9
- // übersprungen. Ein Workspace mit nur bin/main.ts kriegt nur den Server-
10
- // Bundle, ein Browser-only-Sample nur den Client.
9
+ // Both are built when their conventions match, otherwise skipped. A workspace
10
+ // with only bin/main.ts gets just the server bundle, a browser-only sample
11
+ // just the client.
11
12
  //
12
13
  // {
13
14
  // "scripts": { "build": "kumiko-build" }
14
15
  // }
15
16
  //
16
- // Optionaler Pfad-Parameter überschreibt cwd:
17
+ // An optional path argument overrides cwd:
17
18
  // kumiko-build samples/apps/showcase
19
+ //
20
+ // Extra server entry points (repeatable, paths relative to the app dir), each
21
+ // emitted as dist-server/<basename>.js:
22
+ // kumiko-build --entry bin/remote-probe.ts
18
23
 
19
24
  import { existsSync } from "node:fs";
20
- import { join, resolve } from "node:path";
25
+ import { join, relative, resolve } from "node:path";
21
26
  import {
22
27
  buildProdBundle,
23
28
  buildServerBundle,
24
29
  discoverServerEntry,
25
30
  formatBuildResult,
26
31
  formatServerBuildResult,
32
+ parseKumikoBuildArgs,
27
33
  readClientEntriesConfig,
28
34
  readExtraRuntimeExternals,
29
35
  resolveClientEntries,
36
+ writeServerLockfile,
30
37
  } from "@cosmicdrift/kumiko-dev-server/cli";
31
38
 
32
39
  if (process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
33
40
  process.stdout.write(
34
- "Usage: kumiko-build [app-dir] build client (dist/) and server (dist-server/) bundles\n",
41
+ "Usage: kumiko-build [app-dir] [--entry <path>]... build client (dist/) and server (dist-server/) bundles; --entry adds a server entry point (dist-server/<basename>.js)\n",
35
42
  );
36
43
  process.exit(0);
37
44
  }
38
45
 
39
- const explicit = process.argv.slice(2).find((arg) => !arg.startsWith("-"));
40
- const cwd = explicit ? resolve(process.cwd(), explicit) : process.cwd();
41
-
42
46
  const red = "\x1b[31m";
43
47
  const yellow = "\x1b[33m";
48
+ const dim = "\x1b[2m";
44
49
  const reset = "\x1b[0m";
45
50
 
46
51
  try {
52
+ const buildArgs = parseKumikoBuildArgs(process.argv.slice(2));
53
+ const cwd = buildArgs.appDir ? resolve(process.cwd(), buildArgs.appDir) : process.cwd();
54
+
47
55
  // Read once up front — resolveClientEntries can throw (mutual exclusion,
48
56
  // malformed declaration, unmigrated legacy client-*.tsx files) and that
49
57
  // must surface through the same ✗/exit(1) path as every other build error.
@@ -77,10 +85,16 @@ try {
77
85
  const result = await buildServerBundle({
78
86
  cwd,
79
87
  ...(extraRuntimeExternals.length > 0 && { extraRuntimeExternals }),
88
+ ...(buildArgs.extraEntries.length > 0 && { extraEntries: buildArgs.extraEntries }),
80
89
  });
81
90
  const ms = Math.round(performance.now() - t0);
91
+ const lock = await writeServerLockfile({ cwd, outDir: result.outDir });
92
+ const lockLine =
93
+ lock.mode === "seeded"
94
+ ? ` ${dim}bun.lock seeded from ${relative(cwd, lock.seedLockfile) || "bun.lock"}${reset}`
95
+ : ` ${yellow}!${reset} bun.lock resolved fresh – no bun.lock found above ${cwd}`;
82
96
  // biome-ignore lint/suspicious/noConsole: CLI-Output, einziger Weg
83
- console.log(formatServerBuildResult(result, ms));
97
+ console.log(`${formatServerBuildResult(result, ms)}\n${lockLine}`);
84
98
  }
85
99
  } catch (err) {
86
100
  // biome-ignore lint/suspicious/noConsole: CLI-Output, einziger Weg
@@ -8,6 +8,11 @@ export type BuildServerBundleOptions = {
8
8
  readonly extraRuntimeExternals?: readonly string[];
9
9
  /** Zusätzliche Build-only-Externals. App-spezifisch — Default leer. */
10
10
  readonly extraBuildOnlyExternals?: readonly string[];
11
+ /** Additional entry points (paths relative to cwd, e.g. "bin/remote-probe.ts"),
12
+ * each emitted as `<basename>.js` next to server.js and sharing its chunks.
13
+ * A missing file or a basename that collides with a built-in entry or another
14
+ * extra entry throws. */
15
+ readonly extraEntries?: readonly string[];
11
16
  };
12
17
  export type BuildServerBundleEntry = {
13
18
  readonly file: string;
@@ -15,7 +20,7 @@ export type BuildServerBundleEntry = {
15
20
  };
16
21
  export type BuildServerBundleResult = {
17
22
  readonly outDir: string;
18
- /** Named entry files (server.js, plus kumiko.js, worker.js and bootstrap.js when present). */
23
+ /** Named entry files (server.js, plus kumiko.js, worker.js, bootstrap.js and extra entries when present). */
19
24
  readonly entries: readonly BuildServerBundleEntry[];
20
25
  /** Geteilte Framework-Chunks (von splitting). */
21
26
  readonly chunks: readonly BuildServerBundleEntry[];
@@ -25,6 +30,13 @@ export type BuildServerBundleResult = {
25
30
  };
26
31
  export declare function buildServerBundle(options?: BuildServerBundleOptions): Promise<BuildServerBundleResult>;
27
32
  export declare function discoverServerEntry(cwd: string): string | undefined;
33
+ export type KumikoBuildArgs = {
34
+ readonly appDir: string | undefined;
35
+ readonly extraEntries: readonly string[];
36
+ };
37
+ /** Parse `kumiko-build [app-dir] [--entry <path>]...` (also `--entry=<path>`).
38
+ * Unknown flags and a missing `--entry` value throw instead of being ignored. */
39
+ export declare function parseKumikoBuildArgs(args: readonly string[]): KumikoBuildArgs;
28
40
  /** Read `package.json#kumiko.extraRuntimeExternals` for kumiko-build (#1484).
29
41
  * Invalid / missing shapes → empty list (CLI stays convention-driven). */
30
42
  export declare function readExtraRuntimeExternals(cwd: string): readonly string[];
@@ -12,11 +12,14 @@
12
12
  // background worker as its own process from the same image)
13
13
  // bin/bootstrap.ts → dist-server/bootstrap.js (optional, next to bin/main.ts:
14
14
  // one-shot runBootstrap provisioning, run as a k8s Job)
15
+ // extraEntries[i] → dist-server/<basename>.js (opt-in, e.g. a probe script
16
+ // `bin/remote-probe.ts`; CLI: repeatable `--entry <path>`)
15
17
  // dist-server/package.json → runtime-deps mit gepinnten Versionen
16
18
  //
17
19
  // All entries are built in ONE Bun.build call with `splitting`: the framework
18
- // lands once as shared chunk-*.js and server.js, kumiko.js, worker.js and
19
- // bootstrap.js are thin entries importing it. Separate bundles would inline ~14 MB per entry.
20
+ // lands once as shared chunk-*.js and server.js, kumiko.js, worker.js,
21
+ // bootstrap.js and the extra entries are thin entries importing it. Separate
22
+ // bundles would inline ~14 MB per entry.
20
23
  //
21
24
  // Output:
22
25
  //
@@ -25,8 +28,10 @@
25
28
  // kumiko.js ← Migrate-CLI-Entry (wenn bin/kumiko.ts gefunden)
26
29
  // worker.js ← worker entry (if bin/worker.ts sits next to main.ts)
27
30
  // bootstrap.js ← provisioning entry (if bin/bootstrap.ts sits next to main.ts)
31
+ // <basename>.js ← one per extraEntries path
28
32
  // chunk-*.js ← geteilte Framework-Chunks
29
33
  // package.json ← runtime-deps mit gepinnten Versionen
34
+ // bun.lock ← written by writeServerLockfile (kumiko-build), not by buildServerBundle
30
35
  //
31
36
  // Externals — Pakete die NICHT ins Bundle gebakt werden, sondern via
32
37
  // `bun install` im Runtime-Container nachgeholt werden:
@@ -41,7 +46,7 @@
41
46
  // resolution-during-build ab. NICHT in runtime-deps.
42
47
  import { existsSync, readFileSync, statSync } from "node:fs";
43
48
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
44
- import { dirname, join, resolve } from "node:path";
49
+ import { basename, dirname, extname, join, resolve } from "node:path";
45
50
  import { parseJsonOrThrow, parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
46
51
  const hasBun = typeof Bun !== "undefined";
47
52
  // Pakete die das Bundle zur Laufzeit weiter referenziert (`from "<pkg>"` nach
@@ -85,6 +90,33 @@ const BUILD_ONLY_EXTERNALS = [
85
90
  // ist die Runtime-Convention; der Entry wird von nichts importiert (nur die
86
91
  // chunks werden referenziert, per relativem Pfad), darum ist der Rename sicher.
87
92
  const ENTRY_RENAMES = { "main.js": "server.js" };
93
+ const OPTIONAL_SIBLING_ENTRY_NAMES = ["worker", "bootstrap"];
94
+ // Output names the built-in entries occupy ("main" is renamed to server.js).
95
+ const RESERVED_ENTRY_OUTPUT_NAMES = new Set([
96
+ "server.js",
97
+ "main.js",
98
+ "kumiko.js",
99
+ ...OPTIONAL_SIBLING_ENTRY_NAMES.map((name) => `${name}.js`),
100
+ ]);
101
+ function resolveExtraEntries(cwd, extraEntries) {
102
+ const claimedOutputNames = new Map();
103
+ return extraEntries.map((entry) => {
104
+ const absolute = resolve(cwd, entry);
105
+ if (!existsSync(absolute) || !statSync(absolute).isFile()) {
106
+ throw new Error(`[buildServerBundle] extra entry "${entry}" does not exist (resolved to ${absolute}).`);
107
+ }
108
+ const outputName = `${basename(absolute, extname(absolute))}.js`;
109
+ if (RESERVED_ENTRY_OUTPUT_NAMES.has(outputName)) {
110
+ throw new Error(`[buildServerBundle] extra entry "${entry}" would emit ${outputName}, which a built-in entry already uses. Rename the file.`);
111
+ }
112
+ const previous = claimedOutputNames.get(outputName);
113
+ if (previous !== undefined) {
114
+ throw new Error(`[buildServerBundle] extra entries "${previous}" and "${entry}" both emit ${outputName}. Rename one of the files.`);
115
+ }
116
+ claimedOutputNames.set(outputName, entry);
117
+ return absolute;
118
+ });
119
+ }
88
120
  export async function buildServerBundle(options = {}) {
89
121
  if (!hasBun) {
90
122
  throw new Error("buildServerBundle requires Bun runtime (Bun.build is missing).");
@@ -100,6 +132,7 @@ export async function buildServerBundle(options = {}) {
100
132
  if (kumikoCli && !existsSync(kumikoCli)) {
101
133
  throw new Error(`[buildServerBundle] Repo-Root erkannt (${repoRoot}), aber bin/kumiko.ts fehlt — kann Migrate-CLI nicht bündeln.`);
102
134
  }
135
+ const extraEntryPaths = resolveExtraEntries(cwd, options.extraEntries ?? []);
103
136
  const externals = [
104
137
  ...RUNTIME_EXTERNALS,
105
138
  ...BUILD_ONLY_EXTERNALS,
@@ -110,10 +143,13 @@ export async function buildServerBundle(options = {}) {
110
143
  await mkdir(outDir, { recursive: true });
111
144
  // Ein Bun.build-Call mit allen Entries + splitting → das Framework wird
112
145
  // einmal als shared chunk abgelegt statt pro Entry inlined.
113
- const optionalSiblingEntries = ["worker", "bootstrap"]
114
- .map((baseName) => discoverSiblingEntry(dirname(serverEntry), baseName))
115
- .filter((path) => path !== undefined);
116
- const entrypoints = [serverEntry, ...(kumikoCli ? [kumikoCli] : []), ...optionalSiblingEntries];
146
+ const optionalSiblingEntries = OPTIONAL_SIBLING_ENTRY_NAMES.map((baseName) => discoverSiblingEntry(dirname(serverEntry), baseName)).filter((path) => path !== undefined);
147
+ const entrypoints = [
148
+ serverEntry,
149
+ ...(kumikoCli ? [kumikoCli] : []),
150
+ ...optionalSiblingEntries,
151
+ ...extraEntryPaths,
152
+ ];
117
153
  const result = await Bun.build({
118
154
  entrypoints,
119
155
  outdir: outDir,
@@ -188,6 +224,34 @@ function findRepoRoot(start) {
188
224
  }
189
225
  return undefined;
190
226
  }
227
+ /** Parse `kumiko-build [app-dir] [--entry <path>]...` (also `--entry=<path>`).
228
+ * Unknown flags and a missing `--entry` value throw instead of being ignored. */
229
+ export function parseKumikoBuildArgs(args) {
230
+ let appDir;
231
+ const extraEntries = [];
232
+ for (let i = 0; i < args.length; i++) {
233
+ const arg = args[i];
234
+ if (arg === undefined)
235
+ break;
236
+ if (arg === "--entry" || arg.startsWith("--entry=")) {
237
+ const value = arg === "--entry" ? args[++i] : arg.slice("--entry=".length);
238
+ if (value === undefined || value === "") {
239
+ throw new Error("kumiko-build: --entry needs a path, e.g. --entry bin/remote-probe.ts");
240
+ }
241
+ extraEntries.push(value);
242
+ }
243
+ else if (arg.startsWith("-")) {
244
+ throw new Error(`kumiko-build: unknown option ${arg}`);
245
+ }
246
+ else if (appDir === undefined) {
247
+ appDir = arg;
248
+ }
249
+ else {
250
+ throw new Error(`kumiko-build: unexpected argument ${arg}`);
251
+ }
252
+ }
253
+ return { appDir, extraEntries };
254
+ }
191
255
  /** Read `package.json#kumiko.extraRuntimeExternals` for kumiko-build (#1484).
192
256
  * Invalid / missing shapes → empty list (CLI stays convention-driven). */
193
257
  export function readExtraRuntimeExternals(cwd) {
package/dist/build.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { ASSETS_DIR, type BuildManifest, type BuildProdBundleOptions, type BuildResult, buildProdBundle, type ClientEntry, type ClientEntryDeclaration, discoverHtmlTemplate, formatBuildResult, injectAssetTags, readClientEntriesConfig, resolveClientEntries, } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
2
- export { type BuildServerBundleEntry, type BuildServerBundleOptions, type BuildServerBundleResult, buildServerBundle, discoverServerEntry, formatServerBuildResult, readExtraRuntimeExternals, } from "./build-server-bundle.ts";
2
+ export { type BuildServerBundleEntry, type BuildServerBundleOptions, type BuildServerBundleResult, buildServerBundle, discoverServerEntry, formatServerBuildResult, type KumikoBuildArgs, parseKumikoBuildArgs, readExtraRuntimeExternals, } from "./build-server-bundle.ts";
3
+ export { type ServerLockfileResult, type WriteServerLockfileOptions, writeServerLockfile, } from "./write-server-lockfile.ts";
package/dist/build.js CHANGED
@@ -2,4 +2,5 @@
2
2
  // damit der Main-Barrel kein Bun-Toolchain-Bundle in Production-Reads zieht
3
3
  // (z.B. wenn drizzle-kit die App-Config unter Node lädt).
4
4
  export { ASSETS_DIR, buildProdBundle, discoverHtmlTemplate, formatBuildResult, injectAssetTags, readClientEntriesConfig, resolveClientEntries, } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
5
- export { buildServerBundle, discoverServerEntry, formatServerBuildResult, readExtraRuntimeExternals, } from "./build-server-bundle.js";
5
+ export { buildServerBundle, discoverServerEntry, formatServerBuildResult, parseKumikoBuildArgs, readExtraRuntimeExternals, } from "./build-server-bundle.js";
6
+ export { writeServerLockfile, } from "./write-server-lockfile.js";
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildResult, formatServerBuildResult, readClientEntriesConfig, readExtraRuntimeExternals, resolveClientEntries, } from "./build.ts";
1
+ export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildResult, formatServerBuildResult, parseKumikoBuildArgs, readClientEntriesConfig, readExtraRuntimeExternals, resolveClientEntries, writeServerLockfile, } from "./build.ts";
2
2
  export { createCrashTracker } from "./crash-tracker.ts";
3
3
  export { runInitDeployCli } from "./init-deploy-cli.ts";
package/dist/cli.js CHANGED
@@ -2,6 +2,6 @@
2
2
  // the package self-reference (`@cosmicdrift/kumiko-dev-server/cli`) so they
3
3
  // resolve to src in the workspace and to dist once installed from the
4
4
  // tarball, where `src` is not shipped.
5
- export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildResult, formatServerBuildResult, readClientEntriesConfig, readExtraRuntimeExternals, resolveClientEntries, } from "./build.js";
5
+ export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildResult, formatServerBuildResult, parseKumikoBuildArgs, readClientEntriesConfig, readExtraRuntimeExternals, resolveClientEntries, writeServerLockfile, } from "./build.js";
6
6
  export { createCrashTracker } from "./crash-tracker.js";
7
7
  export { runInitDeployCli } from "./init-deploy-cli.js";
@@ -23,12 +23,13 @@ import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-
23
23
  import { AUTH_COOKIE_BASE, CSRF_COOKIE_BASE, createClientIpResolver, generateToken, sessionCookieName, } from "@cosmicdrift/kumiko-framework/api";
24
24
  import { buildAppSchema } from "@cosmicdrift/kumiko-framework/engine";
25
25
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
26
+ import { rankPreferredLocales } from "@cosmicdrift/kumiko-framework/i18n";
26
27
  import { createMetricsForFactory } from "@cosmicdrift/kumiko-framework/observability";
27
28
  import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
28
29
  import { resolveAndInjectPageHead, } from "@cosmicdrift/kumiko-headless/apex";
29
30
  import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
30
31
  import { buildBunServeOptions, resolveDerivedMaxRequestBodySize, } from "@cosmicdrift/kumiko-server-runtime/bun-serve-options";
31
- import { makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
32
+ import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
32
33
  import { RENDERER_WEB_FONT_FILE_PATTERN, RENDERER_WEB_FONTS_URL_PREFIX, resolveRendererWebFontsDir, } from "@cosmicdrift/kumiko-server-runtime/renderer-web-fonts";
33
34
  import { buildRequestBoundSystemQuery } from "@cosmicdrift/kumiko-server-runtime/request-bound-system-query";
34
35
  import { canResolveTailwindStylesheet, resolveTailwindCli, } from "@cosmicdrift/kumiko-server-runtime/resolve-tailwind-cli";
@@ -633,6 +634,7 @@ export async function createKumikoServer(options) {
633
634
  registry: stack.registry,
634
635
  metricsFor: createMetricsForFactory(stack.observability.meter),
635
636
  dispatchSystemWrite: makeDispatchSystemWrite(stack.dispatcher),
637
+ dispatchSystemQuery: makeDispatchSystemQuery(stack.dispatcher),
636
638
  });
637
639
  }
638
640
  // Hook für Caller-spezifische Tables + Seed. Läuft nach den Entity-
@@ -755,6 +757,10 @@ export async function createKumikoServer(options) {
755
757
  html = await resolveAndInjectPageHead(html, options.resolvePageHead, {
756
758
  path: url.pathname,
757
759
  host,
760
+ preferredLocales: rankPreferredLocales({
761
+ queryLang: url.searchParams.get("lang"),
762
+ acceptLanguage: req.headers.get("accept-language"),
763
+ }),
758
764
  systemQuery: buildDevSystemQuery(req, socketAddress),
759
765
  requestSignal: req.signal,
760
766
  });
@@ -211,7 +211,9 @@ function compareImportKeys(a, b) {
211
211
  function isRelativeSpecifier(specifier) {
212
212
  return specifier.startsWith("./") || specifier.startsWith("../");
213
213
  }
214
- function renderImportLine({ specifier, names, typeOnly }) {
214
+ function renderImportLine({ specifier, names, typeOnly, defaultName }) {
215
+ if (defaultName !== undefined)
216
+ return `import ${defaultName} from "${specifier}";`;
215
217
  const sorted = [...names].sort(compareImportKeys);
216
218
  const keyword = typeOnly === true ? "import type" : "import";
217
219
  const oneLine = `${keyword} { ${sorted.join(", ")} } from "${specifier}";`;
@@ -355,6 +357,7 @@ function renderMain(appName) {
355
357
  names: ["requireKmsWiring", "resolveKmsWiring"],
356
358
  },
357
359
  { specifier: "../src/run-config", names: ["APP_FEATURES"] },
360
+ { specifier: "../kumiko.config", names: [], defaultName: "appConfig" },
358
361
  { specifier: "./env-schema", names: ["envSchema"] },
359
362
  ]);
360
363
  const authBlock = renderAdminAuthBlock(appName, [
@@ -385,6 +388,7 @@ const kmsWiring =
385
388
 
386
389
  await runProdApp({
387
390
  features: APP_FEATURES,
391
+ ...(appConfig.validateBootOptions && { validateBootOptions: appConfig.validateBootOptions }),
388
392
  envSchema,
389
393
  staticDir: "./dist",
390
394
  ...kmsWiring,
@@ -398,6 +402,7 @@ function renderServer(appName) {
398
402
  { specifier: "@cosmicdrift/kumiko-dev-server", names: ["runDevApp"] },
399
403
  { specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
400
404
  { specifier: "../src/run-config", names: ["APP_FEATURES"] },
405
+ { specifier: "../kumiko.config", names: [], defaultName: "appConfig" },
401
406
  { specifier: "../src/seed", names: ["seedDemoTasks"] },
402
407
  ]);
403
408
  // Dev-only emailVerified skips the login verify gate for the seeded admin;
@@ -420,6 +425,7 @@ const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
420
425
 
421
426
  await runDevApp({
422
427
  features: APP_FEATURES,
428
+ ...(appConfig.validateBootOptions && { validateBootOptions: appConfig.validateBootOptions }),
423
429
  welcomeBanner: true,
424
430
  clientEntry: "./src/client.tsx",
425
431
  seeds: [seedDemoTasks],
@@ -0,0 +1,15 @@
1
+ export type WriteServerLockfileOptions = {
2
+ /** App directory: the search for a seed bun.lock starts here and walks up. */
3
+ readonly cwd: string;
4
+ /** Server bundle directory holding the generated package.json. */
5
+ readonly outDir: string;
6
+ };
7
+ export type ServerLockfileResult = {
8
+ readonly lockfile: string;
9
+ } & ({
10
+ readonly mode: "seeded";
11
+ readonly seedLockfile: string;
12
+ } | {
13
+ readonly mode: "resolved";
14
+ });
15
+ export declare function writeServerLockfile(options: WriteServerLockfileOptions): Promise<ServerLockfileResult>;
@@ -0,0 +1,47 @@
1
+ // Pins dist-server/ to the dependency versions the app was tested with. The runtime image
2
+ // installs from dist-server/package.json, whose ranges alone resolve to whatever is newest
3
+ // on the registry at image-build time. Seeding the app's bun.lock keeps already-locked
4
+ // packages at their locked versions; packages the app lock does not carry resolve fresh.
5
+ import { existsSync } from "node:fs";
6
+ import { copyFile } from "node:fs/promises";
7
+ import { dirname, join, resolve } from "node:path";
8
+ function findSeedLockfile(startDir) {
9
+ let dir = resolve(startDir);
10
+ for (;;) {
11
+ const candidate = join(dir, "bun.lock");
12
+ if (existsSync(candidate))
13
+ return candidate;
14
+ const parent = dirname(dir);
15
+ if (parent === dir)
16
+ return undefined;
17
+ dir = parent;
18
+ }
19
+ }
20
+ export async function writeServerLockfile(options) {
21
+ const outDir = resolve(options.outDir);
22
+ const lockfile = join(outDir, "bun.lock");
23
+ const seedLockfile = findSeedLockfile(options.cwd);
24
+ if (seedLockfile !== undefined)
25
+ await copyFile(seedLockfile, lockfile);
26
+ // process.execPath: the lock format has to match the Bun that built the bundle.
27
+ const install = Bun.spawn({
28
+ cmd: [process.execPath, "install", "--lockfile-only"],
29
+ cwd: outDir,
30
+ stdout: "pipe",
31
+ stderr: "pipe",
32
+ });
33
+ const [exitCode, stdout, stderr] = await Promise.all([
34
+ install.exited,
35
+ new Response(install.stdout).text(),
36
+ new Response(install.stderr).text(),
37
+ ]);
38
+ if (exitCode !== 0) {
39
+ throw new Error(`bun install --lockfile-only failed in ${outDir} (exit ${exitCode}); shipping dist-server without a lockfile would leave runtime versions unpinned.\n${stderr.trim() || stdout.trim()}`);
40
+ }
41
+ if (!existsSync(lockfile)) {
42
+ throw new Error(`bun install --lockfile-only did not write ${lockfile}`);
43
+ }
44
+ return seedLockfile === undefined
45
+ ? { mode: "resolved", lockfile }
46
+ : { mode: "seeded", seedLockfile, lockfile };
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.371.1",
3
+ "version": "0.373.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding. 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>",
@@ -72,14 +72,14 @@
72
72
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
73
73
  },
74
74
  "dependencies": {
75
- "@cosmicdrift/kumiko-bundled-features": "0.371.1",
76
- "@cosmicdrift/kumiko-framework": "0.371.1",
77
- "@cosmicdrift/kumiko-headless": "0.371.1",
78
- "@cosmicdrift/kumiko-renderer-mail-html": "0.371.1",
79
- "@cosmicdrift/kumiko-renderer-web": "0.371.1",
80
- "@cosmicdrift/kumiko-server-runtime": "0.371.1",
81
- "@cosmicdrift/kumiko-testing": "0.371.1",
82
- "@cosmicdrift/kumiko-types": "0.371.1",
75
+ "@cosmicdrift/kumiko-bundled-features": "0.373.0",
76
+ "@cosmicdrift/kumiko-framework": "0.373.0",
77
+ "@cosmicdrift/kumiko-headless": "0.373.0",
78
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.373.0",
79
+ "@cosmicdrift/kumiko-renderer-web": "0.373.0",
80
+ "@cosmicdrift/kumiko-server-runtime": "0.373.0",
81
+ "@cosmicdrift/kumiko-testing": "0.373.0",
82
+ "@cosmicdrift/kumiko-types": "0.373.0",
83
83
  "bun-types": "^1.3.13",
84
84
  "ts-morph": "^28.0.0",
85
85
  "zod": "^4.4.3"
package/src/changes.json CHANGED
@@ -1,4 +1,17 @@
1
1
  [
2
+ {
3
+ "version": "0.373.0",
4
+ "type": "improvement",
5
+ "title": "kumiko-build pins dist-server dependencies with a bun.lock seeded from the app's bun.lock",
6
+ "detail": "The lock is written by writeServerLockfile after the bundle build and needs network or a warm Bun cache. The scaffolded Dockerfile runs bun install --production --frozen-lockfile in the runtime stage.",
7
+ "migration": "Apps with their own Dockerfile should run `bun install --production --frozen-lockfile` in dist-server instead of `bun install --production`."
8
+ },
9
+ {
10
+ "version": "0.372.0",
11
+ "type": "improvement",
12
+ "title": "kumiko-build --entry and buildServerBundle extraEntries bundle additional server entry points as dist-server/<basename>.js",
13
+ "detail": "Opt-in per file, not every bin/ helper module; the output shares the chunk files with server.js. Unknown kumiko-build options now fail the build."
14
+ },
2
15
  {
3
16
  "version": "0.371.0",
4
17
  "type": "improvement",
@@ -60,10 +60,11 @@ WORKDIR /app
60
60
 
61
61
  RUN addgroup -S app && adduser -S app -G app
62
62
 
63
- # Server bundle + its minimal package.json. `bun install --production`
64
- # only pulls the actual externals.
63
+ # Server bundle + its minimal package.json and the bun.lock kumiko-build wrote
64
+ # next to it. `--frozen-lockfile` installs exactly the locked externals and
65
+ # fails on drift instead of resolving newer versions.
65
66
  COPY --from=build --chown=app:app /app/dist-server ./
66
- RUN bun install --production
67
+ RUN bun install --production --frozen-lockfile
67
68
 
68
69
  # Client build (static assets) — served by runProdApp.staticDir.
69
70
  COPY --from=build --chown=app:app /app/dist ./dist
@@ -9,4 +9,7 @@ import appConfig from "../kumiko.config";
9
9
 
10
10
  export const FEATURES: readonly FeatureDefinition[] = allAppFeatures(appConfig);
11
11
 
12
+ // `kumiko check` validates the boot with the options kumiko.config.ts declares.
13
+ export const VALIDATE_BOOT_OPTIONS = appConfig.validateBootOptions;
14
+
12
15
  export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);