@cosmicdrift/kumiko-dev-server 0.72.0 → 0.73.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 +3 -3
- package/src/__tests__/welcome-banner.test.ts +42 -0
- package/src/index.ts +1 -0
- package/src/run-dev-app.ts +29 -1
- package/src/welcome-banner.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
54
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
53
|
+
"@cosmicdrift/kumiko-bundled-features": "0.73.0",
|
|
54
|
+
"@cosmicdrift/kumiko-framework": "0.73.0",
|
|
55
55
|
"ts-morph": "^28.0.0"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { renderWelcomeBanner } from "../welcome-banner";
|
|
3
|
+
|
|
4
|
+
describe("renderWelcomeBanner", () => {
|
|
5
|
+
test("includes URL + admin login + features dir + docs link", () => {
|
|
6
|
+
const banner = renderWelcomeBanner({
|
|
7
|
+
url: "http://localhost:3000",
|
|
8
|
+
admin: { email: "admin@demo.test", password: "changeme" },
|
|
9
|
+
});
|
|
10
|
+
expect(banner).toContain("http://localhost:3000");
|
|
11
|
+
expect(banner).toContain("admin@demo.test");
|
|
12
|
+
expect(banner).toContain("changeme");
|
|
13
|
+
expect(banner).toContain("src/features/");
|
|
14
|
+
expect(banner).toContain("docs.kumiko.rocks");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("box rows are aligned (all the same printable width)", () => {
|
|
18
|
+
const banner = renderWelcomeBanner({
|
|
19
|
+
url: "http://localhost:3000",
|
|
20
|
+
admin: { email: "a@b.c", password: "x" },
|
|
21
|
+
});
|
|
22
|
+
const rows = banner.split("\n");
|
|
23
|
+
const widths = rows.map((r) => [...r].length);
|
|
24
|
+
expect(new Set(widths).size).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("admin is optional (no-auth dev runs still get a banner)", () => {
|
|
28
|
+
const banner = renderWelcomeBanner({ url: "http://localhost:3000" });
|
|
29
|
+
expect(banner).toContain("http://localhost:3000");
|
|
30
|
+
expect(banner).not.toContain("Login als");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("featuresDir + docsUrl overridable", () => {
|
|
34
|
+
const banner = renderWelcomeBanner({
|
|
35
|
+
url: "http://localhost:3000",
|
|
36
|
+
featuresDir: "app/modules/",
|
|
37
|
+
docsUrl: "https://internal.example.com/dev",
|
|
38
|
+
});
|
|
39
|
+
expect(banner).toContain("app/modules/");
|
|
40
|
+
expect(banner).toContain("internal.example.com/dev");
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -69,3 +69,4 @@ export type {
|
|
|
69
69
|
export { scaffoldDeploy } from "./scaffold-deploy";
|
|
70
70
|
export type { ScaffoldFeatureOptions, ScaffoldFeatureResult } from "./scaffold-feature";
|
|
71
71
|
export { scaffoldFeature } from "./scaffold-feature";
|
|
72
|
+
export { renderWelcomeBanner, type WelcomeBannerInput } from "./welcome-banner";
|
package/src/run-dev-app.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
createKumikoServer,
|
|
52
52
|
type KumikoServerHandle,
|
|
53
53
|
} from "./create-kumiko-server";
|
|
54
|
+
import { renderWelcomeBanner } from "./welcome-banner";
|
|
54
55
|
|
|
55
56
|
// Re-export der shared Auth-Setup-Types damit Apps nur einen Import-Pfad
|
|
56
57
|
// brauchen. PasswordResetSetup / EmailVerificationSetup leben in
|
|
@@ -177,6 +178,13 @@ export type RunDevAppOptions = {
|
|
|
177
178
|
* in einem seed-fn, weil die runtime stack.db braucht und die seed-
|
|
178
179
|
* Funktionen nach setupTestStack laufen. */
|
|
179
180
|
readonly effectiveFeatures?: CreateKumikoServerOptions["effectiveFeatures"];
|
|
181
|
+
/** Print a first-run banner after the server starts (URL, admin login,
|
|
182
|
+
* hot-reload hint, docs link). Default: off — apps that already log
|
|
183
|
+
* their own startup shouldn't get double-printed. The scaffold template
|
|
184
|
+
* (create-kumiko-app) flips this on so the first `bun dev` ends on
|
|
185
|
+
* something the user can click. Pass an object to override the
|
|
186
|
+
* features-dir hint or the docs URL. */
|
|
187
|
+
readonly welcomeBanner?: boolean | { readonly featuresDir?: string; readonly docsUrl?: string };
|
|
180
188
|
};
|
|
181
189
|
|
|
182
190
|
export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle> {
|
|
@@ -304,7 +312,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
304
312
|
}
|
|
305
313
|
: {};
|
|
306
314
|
|
|
307
|
-
|
|
315
|
+
const handle = await createKumikoServer({
|
|
308
316
|
features,
|
|
309
317
|
...(options.clientEntry !== undefined && { clientEntry: options.clientEntry }),
|
|
310
318
|
...(options.clientEntries !== undefined && { clientEntries: options.clientEntries }),
|
|
@@ -407,6 +415,26 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
407
415
|
}
|
|
408
416
|
},
|
|
409
417
|
});
|
|
418
|
+
|
|
419
|
+
if (options.welcomeBanner) {
|
|
420
|
+
const overrides = typeof options.welcomeBanner === "object" ? options.welcomeBanner : {};
|
|
421
|
+
const port = handle.server?.port ?? options.port ?? 3000;
|
|
422
|
+
const banner = renderWelcomeBanner({
|
|
423
|
+
url: `http://localhost:${port}`,
|
|
424
|
+
...(options.auth?.admin && {
|
|
425
|
+
admin: {
|
|
426
|
+
email: options.auth.admin.email,
|
|
427
|
+
password: options.auth.admin.password,
|
|
428
|
+
},
|
|
429
|
+
}),
|
|
430
|
+
...(overrides.featuresDir !== undefined && { featuresDir: overrides.featuresDir }),
|
|
431
|
+
...(overrides.docsUrl !== undefined && { docsUrl: overrides.docsUrl }),
|
|
432
|
+
});
|
|
433
|
+
// biome-ignore lint/suspicious/noConsole: boot-time UX print, opt-in via welcomeBanner.
|
|
434
|
+
console.log(`\n${banner}\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return handle;
|
|
410
438
|
}
|
|
411
439
|
|
|
412
440
|
// Exported for the wiring-contract test (config-resolver-default.integration):
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// First-run welcome banner for `runDevApp({ welcomeBanner: true })`.
|
|
2
|
+
// Opt-in only — `bun dev` should not normalize this for apps that
|
|
3
|
+
// already log their own startup. Scaffolded apps (create-kumiko-app)
|
|
4
|
+
// flip it on so the first `bun dev` doesn't end on "Server läuft" with
|
|
5
|
+
// the user wondering where to click.
|
|
6
|
+
|
|
7
|
+
export type WelcomeBannerInput = {
|
|
8
|
+
/** Resolved listen-URL (`http://localhost:3000`). */
|
|
9
|
+
readonly url: string;
|
|
10
|
+
/** Login the seeded admin uses (from runDevApp options.auth.admin). */
|
|
11
|
+
readonly admin?: { readonly email: string; readonly password: string };
|
|
12
|
+
/** Hot-reload hint shown in the "add a feature" line. Defaults to
|
|
13
|
+
* `src/features/`. */
|
|
14
|
+
readonly featuresDir?: string;
|
|
15
|
+
/** Custom docs URL — defaults to the public docs site. */
|
|
16
|
+
readonly docsUrl?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function renderWelcomeBanner(input: WelcomeBannerInput): string {
|
|
20
|
+
const featuresDir = input.featuresDir ?? "src/features/";
|
|
21
|
+
const docsUrl = input.docsUrl ?? "https://docs.kumiko.rocks/quickstart";
|
|
22
|
+
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
lines.push(`✓ kumiko-app läuft`);
|
|
25
|
+
lines.push(`→ Browser: ${input.url}`);
|
|
26
|
+
if (input.admin) {
|
|
27
|
+
lines.push(`→ Login als: ${input.admin.email} / ${input.admin.password}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push(`→ Feature add: edit ${featuresDir}`);
|
|
30
|
+
lines.push(`→ Docs: ${docsUrl}`);
|
|
31
|
+
|
|
32
|
+
const innerWidth = Math.max(...lines.map((l) => stringWidth(l)));
|
|
33
|
+
const border = "─".repeat(innerWidth + 2);
|
|
34
|
+
const top = `┌${border}┐`;
|
|
35
|
+
const bottom = `└${border}┘`;
|
|
36
|
+
const padded = lines.map((l) => `│ ${l}${" ".repeat(innerWidth - stringWidth(l))} │`);
|
|
37
|
+
return [top, ...padded, bottom].join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Plain monospace-cell width — counts each codepoint as one cell. Good
|
|
41
|
+
// enough for ASCII + the small set of arrows/checkmarks used above; if a
|
|
42
|
+
// real wide-char ever lands in the banner the row alignment will drift,
|
|
43
|
+
// caught visually by the snapshot test.
|
|
44
|
+
function stringWidth(s: string): number {
|
|
45
|
+
return [...s].length;
|
|
46
|
+
}
|