@cosmicdrift/kumiko-server-runtime 0.210.0 → 0.212.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.210.0",
3
+ "version": "0.212.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>",
@@ -80,8 +80,8 @@
80
80
  }
81
81
  },
82
82
  "dependencies": {
83
- "@cosmicdrift/kumiko-bundled-features": "0.210.0",
84
- "@cosmicdrift/kumiko-framework": "0.210.0",
83
+ "@cosmicdrift/kumiko-bundled-features": "0.212.0",
84
+ "@cosmicdrift/kumiko-framework": "0.212.0",
85
85
  "temporal-polyfill": "^0.3.2"
86
86
  },
87
87
  "publishConfig": {
@@ -12,6 +12,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import {
15
+ buildMissingTemplateError,
15
16
  type ClientEntry,
16
17
  computeBuildId,
17
18
  discoverClientEntries,
@@ -333,4 +334,49 @@ describe("build-prod-bundle/discovery edges", () => {
333
334
  await writeFile(join(workDir, "src/client_admin.tsx"), "// bad");
334
335
  expect(discoverClientEntries(workDir)).toEqual([]);
335
336
  });
337
+
338
+ // #2305: a module only imported by another client-*.tsx still matches
339
+ // the entry pattern and becomes a second bundle entry.
340
+ test("discoverClientEntries treats a plain imported module named client-<x>.tsx as its own entry", async () => {
341
+ await mkdir(join(workDir, "src"), { recursive: true });
342
+ await writeFile(join(workDir, "src/client-app.tsx"), "// real entry");
343
+ await writeFile(join(workDir, "src/client-features.tsx"), "export const clientFeatures = [];");
344
+
345
+ const entries = discoverClientEntries(workDir);
346
+
347
+ expect(entries.map((e) => e.name)).toEqual(["app", "features"]);
348
+ });
349
+ });
350
+
351
+ describe("build-prod-bundle/buildMissingTemplateError", () => {
352
+ function multiEntry(): ClientEntry {
353
+ return {
354
+ name: "features",
355
+ sourceFile: "/Users/dev/offlot-app/src/client-features.tsx",
356
+ manifestKey: "client-features.js",
357
+ htmlPath: "features.html",
358
+ };
359
+ }
360
+
361
+ test("names the discovered source file and the client-<suffix> convention", () => {
362
+ const message = buildMissingTemplateError(
363
+ { "client-features.js": "/assets/client-features-abcd.js" },
364
+ multiEntry(),
365
+ );
366
+
367
+ expect(message).toContain("src/client-features.tsx");
368
+ expect(message).toContain("client-<suffix>.tsx");
369
+ expect(message).toContain("umbenennen");
370
+ });
371
+
372
+ test("single-mode entry gets no rename hint (nothing was auto-discovered by suffix)", () => {
373
+ const message = buildMissingTemplateError(
374
+ { "client.js": "/assets/client-abcd.js" },
375
+ clientEntry(),
376
+ );
377
+
378
+ expect(message).toContain("src/client.tsx");
379
+ expect(message).not.toContain("umbenennen");
380
+ expect(message).not.toContain("client-<suffix>.tsx");
381
+ });
336
382
  });
@@ -8,7 +8,7 @@
8
8
  // dispatched ins Leere → 500.
9
9
 
10
10
  import { describe, expect, spyOn, test } from "bun:test";
11
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
11
+ import { defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { composeFeatures } from "../compose-features";
13
13
 
14
14
  const noopFeature = defineFeature("noop-app", () => {});
@@ -130,6 +130,38 @@ describe("composeFeatures", () => {
130
130
  expect(handlerNames).toContain("logout");
131
131
  });
132
132
 
133
+ // fw#2223 regression: without this wiring, includeBundled apps that DO
134
+ // configure authOptions.invite still got /members with no invite button —
135
+ // the screen is bound to auth-email-password's invite-create handler,
136
+ // which only exists when opts.invite is set (feature.ts:238-239).
137
+ test("authOptions.invite → tenant bekommt invite-Screen + Toolbar-Action; ohne invite bootet sauber ohne beides", () => {
138
+ const withInvite = composeFeatures([noopFeature], {
139
+ includeBundled: true,
140
+ authOptions: { invite: { appUrl: "https://app/invite" } },
141
+ });
142
+ const tenantWith = withInvite.find((f) => f.name === "tenant");
143
+ expect(tenantWith).toBeDefined();
144
+ if (!tenantWith) return;
145
+ const membersScreenWith = tenantWith.screens["members"];
146
+ if (membersScreenWith?.type !== "projectionList") {
147
+ throw new Error("expected members screen to be projectionList");
148
+ }
149
+ expect(membersScreenWith.toolbarActions?.some((a) => a.id === "invite")).toBe(true);
150
+ expect(tenantWith.screens["invite-create"]).toBeDefined();
151
+
152
+ const withoutInvite = composeFeatures([noopFeature], { includeBundled: true });
153
+ const tenantWithout = withoutInvite.find((f) => f.name === "tenant");
154
+ expect(tenantWithout).toBeDefined();
155
+ if (!tenantWithout) return;
156
+ const membersScreenWithout = tenantWithout.screens["members"];
157
+ if (membersScreenWithout?.type !== "projectionList") {
158
+ throw new Error("expected members screen to be projectionList");
159
+ }
160
+ expect(membersScreenWithout.toolbarActions ?? []).toHaveLength(0);
161
+ expect(tenantWithout.screens["invite-create"]).toBeUndefined();
162
+ expect(() => validateBoot(withoutInvite)).not.toThrow();
163
+ });
164
+
133
165
  test("app feature duplicating a bundled name is dropped (no createRegistry crash)", () => {
134
166
  // create-kumiko-app's picker hands back createAuthEmailPasswordFeature()
135
167
  // because the user ticked it in the recommended set; runDevApp then adds
@@ -536,7 +536,9 @@ async function renderHtml(
536
536
  return injectAssetTags(template, manifest, entry, buildInfo);
537
537
  }
538
538
 
539
- function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry): string {
539
+ // @internal exported for unit tests only. See #2305: the message must
540
+ // name the source file and the filename convention.
541
+ export function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry): string {
540
542
  const cssLine = manifest["styles.css"]
541
543
  ? ` <link rel="stylesheet" href="/styles.css" />\n`
542
544
  : "";
@@ -544,8 +546,22 @@ function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry):
544
546
  ? // html-ok: Error-Hilfetext (Tag-Snippet als Text), wird nie gerendert.
545
547
  ` <script type="module" src="/${entry.manifestKey}"></script>\n`
546
548
  : "";
549
+ const sourceBasename = basenameOf(entry.sourceFile);
550
+ // Single-mode entries always carry manifestKey "client.js"; any other
551
+ // value came from discoverMultiClientEntries' filename match.
552
+ const isMultiEntry = entry.manifestKey !== "client.js";
553
+ const discoveryHint = isMultiEntry
554
+ ? `"src/${sourceBasename}" wurde automatisch als eigener Bundle-Entry erkannt — ` +
555
+ `jede Datei nach dem Muster src/client-<suffix>.tsx zählt als Entry (hier: Suffix "${entry.name}"). ` +
556
+ `War das nicht beabsichtigt, z. B. weil die Datei nur ein von einem anderen client-*.tsx ` +
557
+ `importiertes Modul ist: Datei umbenennen (ohne "client-"-Präfix), dann verschwindet der Entry.\n` +
558
+ `\n` +
559
+ `War es beabsichtigt:\n`
560
+ : "";
547
561
  return (
548
- `[kumiko build] kein ${entry.htmlPath} gefunden, aber es gibt JS/CSS-Output.\n` +
562
+ `[kumiko build] kein ${entry.htmlPath} gefunden für Entry "src/${sourceBasename}", ` +
563
+ `aber es gibt JS/CSS-Output.\n` +
564
+ discoveryHint +
549
565
  `Leg ein public/${basenameOf(entry.htmlPath)} oder ${basenameOf(entry.htmlPath)} im App-Root an, z. B.:\n` +
550
566
  `\n` +
551
567
  `<!doctype html>\n` +
@@ -78,7 +78,11 @@ export function composeFeatures(
78
78
  const bundled = [
79
79
  createConfigFeature(),
80
80
  createUserFeature(),
81
- createTenantFeature(),
81
+ // inviteScreen mirrors authOptions.invite: that's the exact option that
82
+ // makes auth-email-password register the invite-create write-handler
83
+ // the /members invite drawer is bound to — without this, includeBundled
84
+ // apps that DO configure invite still get /members with no invite button.
85
+ createTenantFeature({ inviteScreen: Boolean(authOptions?.invite) }),
82
86
  createAuthEmailPasswordFeature(authOptions ?? {}),
83
87
  // signup-request/signup-confirm are registered whenever authOptions.signup
84
88
  // is set (see above), but the handler itself no-ops unless the companion