@cosmicdrift/kumiko-server-runtime 0.328.1 → 0.330.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/README.md +15 -0
- package/dist/boot/apply-boot-seeds.d.ts +8 -0
- package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
- package/dist/boot/boot-crypto.d.ts +9 -0
- package/dist/boot/boot-crypto.js +39 -0
- package/dist/boot/job-run-logger.d.ts +19 -0
- package/dist/boot/job-run-logger.js +44 -0
- package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
- package/dist/boot/pii-event-backfill-on-boot.js +25 -0
- package/dist/build-prod-bundle.d.ts +103 -0
- package/dist/build-prod-bundle.js +751 -0
- package/dist/bun-serve-options.d.ts +32 -0
- package/dist/bun-serve-options.js +49 -0
- package/dist/compose-features.d.ts +31 -0
- package/dist/compose-features.js +124 -0
- package/dist/extra-routes-deps.d.ts +30 -0
- package/dist/extra-routes-deps.js +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/pii-boot-gate.d.ts +13 -0
- package/dist/pii-boot-gate.js +46 -0
- package/dist/renderer-web-fonts.d.ts +9 -0
- package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
- package/dist/resolve-tailwind-cli.d.ts +14 -0
- package/dist/resolve-tailwind-cli.js +36 -0
- package/dist/run-prod-app-boot-context.d.ts +43 -0
- package/dist/run-prod-app-boot-context.js +198 -0
- package/dist/run-prod-app-static-files.d.ts +32 -0
- package/dist/run-prod-app-static-files.js +280 -0
- package/dist/run-prod-app.d.ts +471 -0
- package/dist/run-prod-app.js +741 -0
- package/dist/run-worker-app.d.ts +92 -0
- package/dist/run-worker-app.js +220 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/security-headers.js +49 -0
- package/dist/session-boot-gate.d.ts +11 -0
- package/dist/session-boot-gate.js +22 -0
- package/dist/session-wiring.d.ts +11 -0
- package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
- package/dist/try-hono-first.d.ts +24 -0
- package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
- package/package.json +102 -36
- package/src/changes.json +6 -0
- package/src/__tests__/boot-extra-context.test.ts +0 -163
- package/src/__tests__/boot-probe-fixture.ts +0 -64
- package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
- package/src/__tests__/build-prod-bundle.test.ts +0 -724
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
- package/src/__tests__/compose-features.test.ts +0 -206
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/pii-boot-gate.test.ts +0 -112
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
- package/src/__tests__/require-env.test.ts +0 -34
- package/src/__tests__/resolve-auth-mail.test.ts +0 -144
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
- package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
- package/src/__tests__/run-prod-app-spec.test.ts +0 -167
- package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
- package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
- package/src/__tests__/run-worker-app-boot.test.ts +0 -183
- package/src/__tests__/run-worker-app.integration.test.ts +0 -218
- package/src/__tests__/security-headers.test.ts +0 -88
- package/src/__tests__/session-boot-gate.test.ts +0 -70
- package/src/__tests__/session-wiring.test.ts +0 -16
- package/src/__tests__/try-hono-first.test.ts +0 -108
- package/src/__tests__/websocket-route.integration.test.ts +0 -580
- package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/boot-crypto.ts +0 -67
- package/src/boot/job-run-logger.ts +0 -63
- package/src/boot/pii-event-backfill-on-boot.ts +0 -43
- package/src/build-prod-bundle.ts +0 -996
- package/src/bun-serve-options.ts +0 -110
- package/src/compose-features.ts +0 -197
- package/src/extra-routes-deps.ts +0 -38
- package/src/index.ts +0 -27
- package/src/pii-boot-gate.ts +0 -80
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -319
- package/src/run-prod-app-static-files.ts +0 -359
- package/src/run-prod-app.ts +0 -1420
- package/src/run-worker-app.ts +0 -366
- package/src/security-headers.ts +0 -67
- package/src/session-boot-gate.ts +0 -34
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
// Unit-Tests für die Pure-Logic-Helpers von build-prod-bundle.
|
|
2
|
-
//
|
|
3
|
-
// Bun.build und Tailwind-CLI brauchen einen Bun-Runtime, deshalb
|
|
4
|
-
// werden die hier nicht aufgerufen — nur Discovery + HTML-Injection
|
|
5
|
-
// die unter Node funktionieren. End-to-End-Tests (mit echtem Bun.build)
|
|
6
|
-
// laufen im CI als `bun run build` auf der Showcase-App; das ist der
|
|
7
|
-
// ehrlichere Smoke-Test.
|
|
8
|
-
|
|
9
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
buildMissingTemplateError,
|
|
16
|
-
type ClientEntry,
|
|
17
|
-
type ClientEntryDeclaration,
|
|
18
|
-
computeBuildId,
|
|
19
|
-
discoverHtmlTemplate,
|
|
20
|
-
formatBuildResult,
|
|
21
|
-
injectAssetTags,
|
|
22
|
-
readClientEntriesConfig,
|
|
23
|
-
resolveClientEntries,
|
|
24
|
-
} from "../build-prod-bundle";
|
|
25
|
-
|
|
26
|
-
// Synthetic single-entry: injectAssetTags only needs the shape, not a
|
|
27
|
-
// resolveClientEntries round-trip.
|
|
28
|
-
function clientEntry(): ClientEntry {
|
|
29
|
-
return {
|
|
30
|
-
name: "client",
|
|
31
|
-
sourceFile: "src/client.tsx",
|
|
32
|
-
manifestKey: "client.js",
|
|
33
|
-
htmlPath: "index.html",
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function namedEntry(name: string): ClientEntry {
|
|
38
|
-
return {
|
|
39
|
-
name,
|
|
40
|
-
sourceFile: `src/client-${name}.tsx`,
|
|
41
|
-
manifestKey: `client-${name}.js`,
|
|
42
|
-
htmlPath: name === "public" ? "index.html" : `${name}.html`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function writeJson(path: string, value: unknown): Promise<void> {
|
|
47
|
-
await writeFile(path, JSON.stringify(value));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe("build-prod-bundle/discovery (convention mode)", () => {
|
|
51
|
-
let workDir = "";
|
|
52
|
-
|
|
53
|
-
beforeEach(async () => {
|
|
54
|
-
workDir = await mkdtemp(join(tmpdir(), "kumiko-build-test-"));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterEach(async () => {
|
|
58
|
-
await rm(workDir, { recursive: true, force: true });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("resolveClientEntries findet single-mode src/client.tsx wenn nichts deklariert ist", async () => {
|
|
62
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
63
|
-
await writeFile(join(workDir, "src/client.tsx"), "// single");
|
|
64
|
-
|
|
65
|
-
const entries = resolveClientEntries(workDir, {});
|
|
66
|
-
|
|
67
|
-
expect(entries).toHaveLength(1);
|
|
68
|
-
expect(entries[0]?.name).toBe("client");
|
|
69
|
-
expect(entries[0]?.manifestKey).toBe("client.js");
|
|
70
|
-
expect(entries[0]?.htmlPath).toBe("index.html");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("resolveClientEntries gibt leeres Array zurück wenn nichts da und nichts deklariert ist", () => {
|
|
74
|
-
expect(resolveClientEntries(workDir, {})).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// #2305 regression: a plain module imported by the real entry, named
|
|
78
|
-
// like a legacy client-<suffix>.tsx file, must not turn into an error or
|
|
79
|
-
// a second entry once src/client.tsx exists (single mode wins outright).
|
|
80
|
-
test("src/client.tsx neben src/client-utils.tsx → genau ein Entry, kein Fehler", async () => {
|
|
81
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
82
|
-
await writeFile(join(workDir, "src/client.tsx"), "// real entry");
|
|
83
|
-
await writeFile(join(workDir, "src/client-utils.tsx"), "export const utils = [];");
|
|
84
|
-
|
|
85
|
-
const entries = resolveClientEntries(workDir, {});
|
|
86
|
-
|
|
87
|
-
expect(entries).toHaveLength(1);
|
|
88
|
-
expect(entries[0]?.name).toBe("client");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("legacy src/client-<suffix>.tsx ohne Deklaration → Migrations-Fehler mit package.json-Snippet", async () => {
|
|
92
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
93
|
-
await writeFile(join(workDir, "src/client-admin.tsx"), "// admin");
|
|
94
|
-
await writeFile(join(workDir, "src/client-public.tsx"), "// public");
|
|
95
|
-
|
|
96
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(/client-admin\.tsx/);
|
|
97
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(/client-public\.tsx/);
|
|
98
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(/kumiko\.clientEntries/);
|
|
99
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(
|
|
100
|
-
/"name":\s*"admin",\s*"sourceFile":\s*"\.\/src\/client-admin\.tsx"/,
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("legacy src/client-<suffix>.ts (kein x) → Migrations-Snippet nennt die exakte .ts-Extension", async () => {
|
|
105
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
106
|
-
await writeFile(join(workDir, "src/client-admin.ts"), "// admin");
|
|
107
|
-
|
|
108
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(/src\/client-admin\.ts\b/);
|
|
109
|
-
expect(() => resolveClientEntries(workDir, {})).toThrow(
|
|
110
|
-
/"sourceFile":\s*"\.\/src\/client-admin\.ts"/,
|
|
111
|
-
);
|
|
112
|
-
expect(() => resolveClientEntries(workDir, {})).not.toThrow(/client-admin\.tsx/);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("discoverHtmlTemplate findet index.html im cwd", async () => {
|
|
116
|
-
await writeFile(join(workDir, "index.html"), "<html></html>");
|
|
117
|
-
|
|
118
|
-
expect(discoverHtmlTemplate(workDir)).toBe(join(workDir, "index.html"));
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("discoverHtmlTemplate findet public/index.html als Fallback", async () => {
|
|
122
|
-
await mkdir(join(workDir, "public"), { recursive: true });
|
|
123
|
-
await writeFile(join(workDir, "public/index.html"), "<html></html>");
|
|
124
|
-
|
|
125
|
-
expect(discoverHtmlTemplate(workDir)).toBe(join(workDir, "public/index.html"));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("discoverHtmlTemplate bevorzugt cwd-index.html über public/index.html", async () => {
|
|
129
|
-
await mkdir(join(workDir, "public"), { recursive: true });
|
|
130
|
-
await writeFile(join(workDir, "index.html"), "<!-- root -->");
|
|
131
|
-
await writeFile(join(workDir, "public/index.html"), "<!-- public -->");
|
|
132
|
-
|
|
133
|
-
expect(discoverHtmlTemplate(workDir)).toBe(join(workDir, "index.html"));
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("discoverHtmlTemplate gibt undefined zurück wenn nichts da ist", () => {
|
|
137
|
-
expect(existsSync(workDir)).toBe(true);
|
|
138
|
-
expect(discoverHtmlTemplate(workDir)).toBeUndefined();
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("build-prod-bundle/resolveClientEntries (declared)", () => {
|
|
143
|
-
let workDir = "";
|
|
144
|
-
|
|
145
|
-
beforeEach(async () => {
|
|
146
|
-
workDir = await mkdtemp(join(tmpdir(), "kumiko-build-declared-"));
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
afterEach(async () => {
|
|
150
|
-
await rm(workDir, { recursive: true, force: true });
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
async function writeSourceFiles(names: readonly string[]): Promise<void> {
|
|
154
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
155
|
-
for (const name of names) {
|
|
156
|
-
await writeFile(join(workDir, `src/${name}`), `// ${name}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
test("clientEntry → single entry named 'client'", async () => {
|
|
161
|
-
await writeSourceFiles(["main.tsx"]);
|
|
162
|
-
|
|
163
|
-
const entries = resolveClientEntries(workDir, { clientEntry: "./src/main.tsx" });
|
|
164
|
-
|
|
165
|
-
expect(entries).toHaveLength(1);
|
|
166
|
-
expect(entries[0]?.name).toBe("client");
|
|
167
|
-
expect(entries[0]?.manifestKey).toBe("client.js");
|
|
168
|
-
expect(entries[0]?.sourceFile).toBe(join(workDir, "src/main.tsx"));
|
|
169
|
-
expect(entries[0]?.htmlPath).toBe("index.html");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("clientEntry: sourceFile fehlt → throws", () => {
|
|
173
|
-
expect(() => resolveClientEntries(workDir, { clientEntry: "./src/missing.tsx" })).toThrow(
|
|
174
|
-
/kumiko\.clientEntry.*does not exist/s,
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("clientEntry with non-source extension throws even when the file exists", async () => {
|
|
179
|
-
await writeSourceFiles(["app.json"]);
|
|
180
|
-
|
|
181
|
-
expect(() => resolveClientEntries(workDir, { clientEntry: "./src/app.json" })).toThrow(
|
|
182
|
-
/kumiko\.clientEntry "\.\/src\/app\.json" must point to a \.ts, \.tsx, \.js or \.jsx file/,
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("clientEntry und clientEntries gleichzeitig → mutual-exclusion error", async () => {
|
|
187
|
-
await writeSourceFiles(["main.tsx", "client-admin.tsx"]);
|
|
188
|
-
|
|
189
|
-
expect(() =>
|
|
190
|
-
resolveClientEntries(workDir, {
|
|
191
|
-
clientEntry: "./src/main.tsx",
|
|
192
|
-
clientEntries: [{ name: "admin", sourceFile: "./src/client-admin.tsx" }],
|
|
193
|
-
}),
|
|
194
|
-
).toThrow(/mutually exclusive/);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("clientEntries: leeres Array → throws", () => {
|
|
198
|
-
expect(() => resolveClientEntries(workDir, { clientEntries: [] })).toThrow(
|
|
199
|
-
/"kumiko\.clientEntries" is empty/,
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("clientEntries: ungültiger name (führender Großbuchstabe) → throws", async () => {
|
|
204
|
-
await writeSourceFiles(["client-admin.tsx"]);
|
|
205
|
-
|
|
206
|
-
expect(() =>
|
|
207
|
-
resolveClientEntries(workDir, {
|
|
208
|
-
clientEntries: [{ name: "Admin", sourceFile: "./src/client-admin.tsx" }],
|
|
209
|
-
}),
|
|
210
|
-
).toThrow(/invalid "kumiko\.clientEntries\[\]\.name"/);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("clientEntries: name mit '..' → throws", async () => {
|
|
214
|
-
await writeSourceFiles(["client-admin.tsx"]);
|
|
215
|
-
|
|
216
|
-
expect(() =>
|
|
217
|
-
resolveClientEntries(workDir, {
|
|
218
|
-
clientEntries: [{ name: "../x", sourceFile: "./src/client-admin.tsx" }],
|
|
219
|
-
}),
|
|
220
|
-
).toThrow(/invalid "kumiko\.clientEntries\[\]\.name"/);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test("clientEntries: sourceFile escaping cwd via '..' → throws", () => {
|
|
224
|
-
expect(() =>
|
|
225
|
-
resolveClientEntries(workDir, {
|
|
226
|
-
clientEntries: [{ name: "admin", sourceFile: "../outside.tsx" }],
|
|
227
|
-
}),
|
|
228
|
-
).toThrow(/resolves outside the app root/);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("clientEntries: absolute sourceFile outside cwd → throws", () => {
|
|
232
|
-
expect(() =>
|
|
233
|
-
resolveClientEntries(workDir, {
|
|
234
|
-
clientEntries: [{ name: "admin", sourceFile: "/etc/passwd" }],
|
|
235
|
-
}),
|
|
236
|
-
).toThrow(/resolves outside the app root/);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("clientEntries: htmlPath escaping cwd → throws", async () => {
|
|
240
|
-
await writeSourceFiles(["client-admin.tsx"]);
|
|
241
|
-
|
|
242
|
-
expect(() =>
|
|
243
|
-
resolveClientEntries(workDir, {
|
|
244
|
-
clientEntries: [
|
|
245
|
-
{ name: "admin", sourceFile: "./src/client-admin.tsx", htmlPath: "../outside.html" },
|
|
246
|
-
],
|
|
247
|
-
}),
|
|
248
|
-
).toThrow(/resolves outside the app root/);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("clientEntries: sourceFile existiert nicht → throws", () => {
|
|
252
|
-
expect(() =>
|
|
253
|
-
resolveClientEntries(workDir, {
|
|
254
|
-
clientEntries: [{ name: "admin", sourceFile: "./src/client-admin.tsx" }],
|
|
255
|
-
}),
|
|
256
|
-
).toThrow(/sourceFile ".*" does not exist/);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("clientEntries: sourceFile with non-source extension throws even when the file exists", async () => {
|
|
260
|
-
await writeSourceFiles(["client-admin.css"]);
|
|
261
|
-
|
|
262
|
-
expect(() =>
|
|
263
|
-
resolveClientEntries(workDir, {
|
|
264
|
-
clientEntries: [{ name: "admin", sourceFile: "./src/client-admin.css" }],
|
|
265
|
-
}),
|
|
266
|
-
).toThrow(
|
|
267
|
-
/kumiko\.clientEntries\["admin"\]\.sourceFile "\.\/src\/client-admin\.css" must point to a \.ts, \.tsx, \.js or \.jsx file/,
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
test("clientEntries: htmlPath with non-html extension throws", async () => {
|
|
272
|
-
await writeSourceFiles(["client-admin.tsx"]);
|
|
273
|
-
|
|
274
|
-
expect(() =>
|
|
275
|
-
resolveClientEntries(workDir, {
|
|
276
|
-
clientEntries: [
|
|
277
|
-
{
|
|
278
|
-
name: "admin",
|
|
279
|
-
sourceFile: "./src/client-admin.tsx",
|
|
280
|
-
htmlPath: "./public/manifest.json",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
}),
|
|
284
|
-
).toThrow(
|
|
285
|
-
/kumiko\.clientEntries\["admin"\]\.htmlPath "\.\/public\/manifest\.json" must point to an \.html file/,
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test("clientEntries: doppelter name → throws", async () => {
|
|
290
|
-
await writeSourceFiles(["client-admin.tsx", "client-admin2.tsx"]);
|
|
291
|
-
|
|
292
|
-
expect(() =>
|
|
293
|
-
resolveClientEntries(workDir, {
|
|
294
|
-
clientEntries: [
|
|
295
|
-
{ name: "admin", sourceFile: "./src/client-admin.tsx" },
|
|
296
|
-
{ name: "admin", sourceFile: "./src/client-admin2.tsx" },
|
|
297
|
-
],
|
|
298
|
-
}),
|
|
299
|
-
).toThrow(/duplicate "kumiko\.clientEntries\[\]\.name" "admin"/);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
test("clientEntries: zwei Entries mit gleichem html-basename (default 'public' vs. explizit) → throws", async () => {
|
|
303
|
-
await writeSourceFiles(["client-public.tsx", "client-legacy.tsx"]);
|
|
304
|
-
|
|
305
|
-
expect(() =>
|
|
306
|
-
resolveClientEntries(workDir, {
|
|
307
|
-
clientEntries: [
|
|
308
|
-
{ name: "public", sourceFile: "./src/client-public.tsx" },
|
|
309
|
-
{ name: "legacy", sourceFile: "./src/client-legacy.tsx", htmlPath: "./index.html" },
|
|
310
|
-
],
|
|
311
|
-
}),
|
|
312
|
-
).toThrow(/resolve to the same HTML output file "index\.html"/);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("clientEntries: zwei Entries mit identischem source-basename → throws", async () => {
|
|
316
|
-
await mkdir(join(workDir, "src/admin"), { recursive: true });
|
|
317
|
-
await mkdir(join(workDir, "src/public"), { recursive: true });
|
|
318
|
-
await writeFile(join(workDir, "src/admin/index.tsx"), "// admin");
|
|
319
|
-
await writeFile(join(workDir, "src/public/index.tsx"), "// public");
|
|
320
|
-
|
|
321
|
-
expect(() =>
|
|
322
|
-
resolveClientEntries(workDir, {
|
|
323
|
-
clientEntries: [
|
|
324
|
-
{ name: "admin", sourceFile: "./src/admin/index.tsx", htmlPath: "./admin.html" },
|
|
325
|
-
{ name: "public", sourceFile: "./src/public/index.tsx" },
|
|
326
|
-
],
|
|
327
|
-
}),
|
|
328
|
-
).toThrow(/share the source basename "index"/);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("publicstatus-Shape: public ohne htmlPath, admin/auth mit explizitem htmlPath", async () => {
|
|
332
|
-
await mkdir(join(workDir, "public"), { recursive: true });
|
|
333
|
-
await writeFile(join(workDir, "public/admin.html"), "<html></html>");
|
|
334
|
-
await writeFile(join(workDir, "public/auth.html"), "<html></html>");
|
|
335
|
-
await writeSourceFiles(["client-public.tsx", "client-admin.tsx", "client-auth.tsx"]);
|
|
336
|
-
|
|
337
|
-
const entries = resolveClientEntries(workDir, {
|
|
338
|
-
clientEntries: [
|
|
339
|
-
{ name: "public", sourceFile: "./src/client-public.tsx" },
|
|
340
|
-
{ name: "admin", sourceFile: "./src/client-admin.tsx", htmlPath: "./public/admin.html" },
|
|
341
|
-
{ name: "auth", sourceFile: "./src/client-auth.tsx", htmlPath: "./public/auth.html" },
|
|
342
|
-
],
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
expect(entries.map((e) => e.name)).toEqual(["admin", "auth", "public"]);
|
|
346
|
-
const publicEntry = entries.find((e) => e.name === "public");
|
|
347
|
-
expect(publicEntry?.manifestKey).toBe("client-public.js");
|
|
348
|
-
expect(publicEntry?.htmlPath).toBe("index.html");
|
|
349
|
-
const admin = entries.find((e) => e.name === "admin");
|
|
350
|
-
expect(admin?.manifestKey).toBe("client-admin.js");
|
|
351
|
-
expect(admin?.htmlPath).toBe(join(workDir, "public/admin.html"));
|
|
352
|
-
const auth = entries.find((e) => e.name === "auth");
|
|
353
|
-
expect(auth?.htmlPath).toBe(join(workDir, "public/auth.html"));
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test("show-pony-Shape: admin und public beide mit explizitem htmlPath", async () => {
|
|
357
|
-
await mkdir(join(workDir, "public"), { recursive: true });
|
|
358
|
-
await writeFile(join(workDir, "public/admin.html"), "<html></html>");
|
|
359
|
-
await writeFile(join(workDir, "public/index.html"), "<html></html>");
|
|
360
|
-
await writeSourceFiles(["client-admin.tsx", "client-public.tsx"]);
|
|
361
|
-
|
|
362
|
-
const entries = resolveClientEntries(workDir, {
|
|
363
|
-
clientEntries: [
|
|
364
|
-
{ name: "admin", sourceFile: "./src/client-admin.tsx", htmlPath: "./public/admin.html" },
|
|
365
|
-
{ name: "public", sourceFile: "./src/client-public.tsx", htmlPath: "./public/index.html" },
|
|
366
|
-
],
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
expect(entries.map((e) => e.manifestKey)).toEqual(["client-admin.js", "client-public.js"]);
|
|
370
|
-
expect(entries.map((e) => e.htmlPath)).toEqual([
|
|
371
|
-
join(workDir, "public/admin.html"),
|
|
372
|
-
join(workDir, "public/index.html"),
|
|
373
|
-
]);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("offlot-app-Shape: einziger Entry 'app' mit explizitem htmlPath", async () => {
|
|
377
|
-
await mkdir(join(workDir, "public"), { recursive: true });
|
|
378
|
-
await writeFile(join(workDir, "public/app.html"), "<html></html>");
|
|
379
|
-
await writeSourceFiles(["client-app.tsx"]);
|
|
380
|
-
|
|
381
|
-
const entries = resolveClientEntries(workDir, {
|
|
382
|
-
clientEntries: [
|
|
383
|
-
{ name: "app", sourceFile: "./src/client-app.tsx", htmlPath: "./public/app.html" },
|
|
384
|
-
],
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
expect(entries).toHaveLength(1);
|
|
388
|
-
expect(entries[0]?.manifestKey).toBe("client-app.js");
|
|
389
|
-
expect(entries[0]?.htmlPath).toBe(join(workDir, "public/app.html"));
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
describe("build-prod-bundle/readClientEntriesConfig", () => {
|
|
394
|
-
let workDir = "";
|
|
395
|
-
|
|
396
|
-
beforeEach(async () => {
|
|
397
|
-
workDir = await mkdtemp(join(tmpdir(), "kumiko-build-config-"));
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
afterEach(async () => {
|
|
401
|
-
await rm(workDir, { recursive: true, force: true });
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
test("kein package.json → {}", () => {
|
|
405
|
-
expect(readClientEntriesConfig(workDir)).toEqual({});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test("package.json ohne kumiko-Block → {}", async () => {
|
|
409
|
-
await writeJson(join(workDir, "package.json"), { name: "app" });
|
|
410
|
-
|
|
411
|
-
expect(readClientEntriesConfig(workDir)).toEqual({});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("valides kumiko.clientEntry", async () => {
|
|
415
|
-
await writeJson(join(workDir, "package.json"), {
|
|
416
|
-
name: "app",
|
|
417
|
-
kumiko: { clientEntry: "./src/client.tsx" },
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
expect(readClientEntriesConfig(workDir)).toEqual({ clientEntry: "./src/client.tsx" });
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("valides kumiko.clientEntries", async () => {
|
|
424
|
-
const declared: readonly ClientEntryDeclaration[] = [
|
|
425
|
-
{ name: "public", sourceFile: "./src/client-public.tsx" },
|
|
426
|
-
{ name: "admin", sourceFile: "./src/client-admin.tsx", htmlPath: "./public/admin.html" },
|
|
427
|
-
];
|
|
428
|
-
await writeJson(join(workDir, "package.json"), {
|
|
429
|
-
name: "app",
|
|
430
|
-
kumiko: { clientEntries: declared },
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
expect(readClientEntriesConfig(workDir)).toEqual({ clientEntries: declared });
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test("kumiko.clientEntry ist keine string → throws", async () => {
|
|
437
|
-
await writeJson(join(workDir, "package.json"), { name: "app", kumiko: { clientEntry: 42 } });
|
|
438
|
-
|
|
439
|
-
expect(() => readClientEntriesConfig(workDir)).toThrow(
|
|
440
|
-
/"kumiko\.clientEntry" must be a string/,
|
|
441
|
-
);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("kumiko.clientEntries ist kein Array → throws", async () => {
|
|
445
|
-
await writeJson(join(workDir, "package.json"), {
|
|
446
|
-
name: "app",
|
|
447
|
-
kumiko: { clientEntries: { name: "admin" } },
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
expect(() => readClientEntriesConfig(workDir)).toThrow(
|
|
451
|
-
/"kumiko\.clientEntries" must be an array/,
|
|
452
|
-
);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test("kumiko.clientEntries[] ohne sourceFile → throws", async () => {
|
|
456
|
-
await writeJson(join(workDir, "package.json"), {
|
|
457
|
-
name: "app",
|
|
458
|
-
kumiko: { clientEntries: [{ name: "admin" }] },
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
expect(() => readClientEntriesConfig(workDir)).toThrow(
|
|
462
|
-
/"kumiko\.clientEntries\[0\]\.sourceFile" must be a non-empty string/,
|
|
463
|
-
);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
test("kumiko.clientEntries[].htmlPath ist keine string → throws", async () => {
|
|
467
|
-
await writeJson(join(workDir, "package.json"), {
|
|
468
|
-
name: "app",
|
|
469
|
-
kumiko: {
|
|
470
|
-
clientEntries: [{ name: "admin", sourceFile: "./src/client-admin.tsx", htmlPath: 1 }],
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
expect(() => readClientEntriesConfig(workDir)).toThrow(
|
|
475
|
-
/"kumiko\.clientEntries\[0\]\.htmlPath" must be a string/,
|
|
476
|
-
);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test("kaputtes JSON → throws", async () => {
|
|
480
|
-
await writeFile(join(workDir, "package.json"), "{not json");
|
|
481
|
-
|
|
482
|
-
expect(() => readClientEntriesConfig(workDir)).toThrow(/Invalid JSON/);
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
describe("build-prod-bundle/injectAssetTags", () => {
|
|
487
|
-
test("ersetzt /client.js durch hashed URL im script-tag", () => {
|
|
488
|
-
const html = `<html><body><script type="module" src="/client.js"></script></body></html>`;
|
|
489
|
-
const result = injectAssetTags(
|
|
490
|
-
html,
|
|
491
|
-
{ "client.js": "/assets/client-abc123.js" },
|
|
492
|
-
clientEntry(),
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
expect(result).toContain('src="/assets/client-abc123.js"');
|
|
496
|
-
expect(result).not.toContain('src="/client.js"');
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
test("ersetzt /styles.css durch hashed URL im link-tag", () => {
|
|
500
|
-
const html = `<html><head><link rel="stylesheet" href="/styles.css" /></head><body></body></html>`;
|
|
501
|
-
const result = injectAssetTags(
|
|
502
|
-
html,
|
|
503
|
-
{ "styles.css": "/assets/styles-def456.css" },
|
|
504
|
-
clientEntry(),
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
expect(result).toContain('href="/assets/styles-def456.css"');
|
|
508
|
-
expect(result).not.toContain('href="/styles.css"');
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
test("wirft mit Anweisung wenn /client.js Placeholder fehlt", () => {
|
|
512
|
-
const html = `<html><body><div id="root"></div></body></html>`;
|
|
513
|
-
|
|
514
|
-
expect(() =>
|
|
515
|
-
injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry()),
|
|
516
|
-
).toThrow(/keinen Entry-Tag für \/client\.js/);
|
|
517
|
-
expect(() =>
|
|
518
|
-
injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry()),
|
|
519
|
-
).toThrow(/<script type="module" src="\/client\.js"><\/script>/);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
test("wirft mit Anweisung wenn /styles.css Placeholder fehlt", () => {
|
|
523
|
-
const html = `<html><head><title>App</title></head><body></body></html>`;
|
|
524
|
-
|
|
525
|
-
expect(() =>
|
|
526
|
-
injectAssetTags(html, { "styles.css": "/assets/styles-xyz.css" }, clientEntry()),
|
|
527
|
-
).toThrow(/keinen Entry-Tag für \/styles\.css/);
|
|
528
|
-
expect(() =>
|
|
529
|
-
injectAssetTags(html, { "styles.css": "/assets/styles-xyz.css" }, clientEntry()),
|
|
530
|
-
).toThrow(/<link rel="stylesheet" href="\/styles\.css" \/>/);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
test("ist idempotent — zweite Injection auf bereits ersetztem HTML ändert nichts", () => {
|
|
534
|
-
const html = `<html><body><script type="module" src="/client.js"></script></body></html>`;
|
|
535
|
-
const manifest = { "client.js": "/assets/client-abc.js" };
|
|
536
|
-
const first = injectAssetTags(html, manifest, clientEntry());
|
|
537
|
-
const second = injectAssetTags(first, manifest, clientEntry());
|
|
538
|
-
|
|
539
|
-
expect(first).toBe(second);
|
|
540
|
-
expect(first).toContain('src="/assets/client-abc.js"');
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test("ändert template ohne client/styles im manifest nicht", () => {
|
|
544
|
-
const html = `<html><body>Hello</body></html>`;
|
|
545
|
-
const result = injectAssetTags(html, {}, clientEntry());
|
|
546
|
-
|
|
547
|
-
expect(result).toBe(html);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
test("verträgt mehrere script-tags und ersetzt nur den /client.js", () => {
|
|
551
|
-
const html = `<html><body>
|
|
552
|
-
<script>console.log("inline");</script>
|
|
553
|
-
<script type="module" src="/client.js"></script>
|
|
554
|
-
</body></html>`;
|
|
555
|
-
const result = injectAssetTags(html, { "client.js": "/assets/client-x.js" }, clientEntry());
|
|
556
|
-
|
|
557
|
-
expect(result).toContain('console.log("inline")');
|
|
558
|
-
expect(result).toContain('src="/assets/client-x.js"');
|
|
559
|
-
expect(result).not.toContain('src="/client.js"');
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
test("multi-mode: admin-entry ersetzt nur /client-admin.js, lässt /client-public.js liegen", () => {
|
|
563
|
-
const html = `<html><body>
|
|
564
|
-
<script type="module" src="/client-admin.js"></script>
|
|
565
|
-
<script type="module" src="/client-public.js"></script>
|
|
566
|
-
</body></html>`;
|
|
567
|
-
const manifest = {
|
|
568
|
-
"client-admin.js": "/assets/client-admin-aaa.js",
|
|
569
|
-
"client-public.js": "/assets/client-public-bbb.js",
|
|
570
|
-
};
|
|
571
|
-
const result = injectAssetTags(html, manifest, namedEntry("admin"));
|
|
572
|
-
|
|
573
|
-
expect(result).toContain('src="/assets/client-admin-aaa.js"');
|
|
574
|
-
// public-bundle bleibt unangetastet — admin.html lädt nur sein eigenes Bundle.
|
|
575
|
-
expect(result).toContain('src="/client-public.js"');
|
|
576
|
-
expect(result).not.toContain('src="/client-admin.js"');
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
test("multi-mode: error-message nennt den richtigen Template-Namen", () => {
|
|
580
|
-
const html = `<html><body>no admin script</body></html>`;
|
|
581
|
-
const manifest = { "client-admin.js": "/assets/client-admin-x.js" };
|
|
582
|
-
|
|
583
|
-
expect(() => injectAssetTags(html, manifest, namedEntry("admin"))).toThrow(
|
|
584
|
-
/admin\.html hat keinen Entry-Tag für \/client-admin\.js/,
|
|
585
|
-
);
|
|
586
|
-
});
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
describe("build-prod-bundle/computeBuildId", () => {
|
|
590
|
-
test("identisches Manifest → gleiche id", () => {
|
|
591
|
-
const manifest = {
|
|
592
|
-
"client.js": "/assets/client-abc.js",
|
|
593
|
-
"styles.css": "/assets/styles-xyz.css",
|
|
594
|
-
};
|
|
595
|
-
expect(computeBuildId(manifest)).toBe(computeBuildId({ ...manifest }));
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
test("Key-Reihenfolge egal — nur die URL-Werte zählen", () => {
|
|
599
|
-
const a = { "client.js": "/assets/client-abc.js", "styles.css": "/assets/styles-xyz.css" };
|
|
600
|
-
const b = { "styles.css": "/assets/styles-xyz.css", "client.js": "/assets/client-abc.js" };
|
|
601
|
-
expect(computeBuildId(a)).toBe(computeBuildId(b));
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test("geändertes Asset (neuer Hash) → andere id", () => {
|
|
605
|
-
const before = { "client.js": "/assets/client-abc.js" };
|
|
606
|
-
const after = { "client.js": "/assets/client-def.js" };
|
|
607
|
-
expect(computeBuildId(before)).not.toBe(computeBuildId(after));
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
test("12 Hex-Zeichen", () => {
|
|
611
|
-
expect(computeBuildId({ "client.js": "/assets/client-abc.js" })).toMatch(/^[0-9a-f]{12}$/);
|
|
612
|
-
});
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
describe("build-prod-bundle/injectAssetTags build-info", () => {
|
|
616
|
-
test("bäckt window.__KUMIKO_BUILD__ vor </head> wenn buildInfo gesetzt", () => {
|
|
617
|
-
const html = `<html><head><title>x</title></head><body><script type="module" src="/client.js"></script></body></html>`;
|
|
618
|
-
const result = injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry(), {
|
|
619
|
-
id: "deadbeef0000",
|
|
620
|
-
builtAt: "2026-06-18T12:00:00.000Z",
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
expect(result).toContain(
|
|
624
|
-
'window.__KUMIKO_BUILD__={"id":"deadbeef0000","builtAt":"2026-06-18T12:00:00.000Z"}',
|
|
625
|
-
);
|
|
626
|
-
expect(result.indexOf("__KUMIKO_BUILD__")).toBeLessThan(result.indexOf("</head>"));
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
test("ohne buildInfo kein __KUMIKO_BUILD__-Script", () => {
|
|
630
|
-
const html = `<html><head></head><body><script type="module" src="/client.js"></script></body></html>`;
|
|
631
|
-
const result = injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry());
|
|
632
|
-
|
|
633
|
-
expect(result).not.toContain("__KUMIKO_BUILD__");
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
describe("build-prod-bundle/formatBuildResult", () => {
|
|
638
|
-
test("lists outDir, duration, and every manifest entry", () => {
|
|
639
|
-
const out = formatBuildResult(
|
|
640
|
-
{
|
|
641
|
-
outDir: "dist",
|
|
642
|
-
manifest: {
|
|
643
|
-
"client.js": "/assets/client-abc.js",
|
|
644
|
-
"styles.css": "/assets/styles-xyz.css",
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
42,
|
|
648
|
-
);
|
|
649
|
-
expect(out).toContain("dist");
|
|
650
|
-
expect(out).toContain("42ms");
|
|
651
|
-
expect(out).toContain("client.js");
|
|
652
|
-
expect(out).toContain("/assets/client-abc.js");
|
|
653
|
-
expect(out).toContain("styles.css");
|
|
654
|
-
expect(out).toContain("/assets/styles-xyz.css");
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
test("empty manifest still prints the success line", () => {
|
|
658
|
-
const out = formatBuildResult({ outDir: "dist-server", manifest: {} }, 1);
|
|
659
|
-
expect(out).toContain("dist-server");
|
|
660
|
-
expect(out).toContain("1ms");
|
|
661
|
-
});
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
describe("build-prod-bundle/discovery edges", () => {
|
|
665
|
-
let workDir = "";
|
|
666
|
-
|
|
667
|
-
beforeEach(async () => {
|
|
668
|
-
workDir = await mkdtemp(join(tmpdir(), "kumiko-build-edge-"));
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
afterEach(async () => {
|
|
672
|
-
await rm(workDir, { recursive: true, force: true });
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test("resolveClientEntries accepts src/client.ts (no x)", async () => {
|
|
676
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
677
|
-
await writeFile(join(workDir, "src/client.ts"), "// single ts");
|
|
678
|
-
const entries = resolveClientEntries(workDir, {});
|
|
679
|
-
expect(entries).toHaveLength(1);
|
|
680
|
-
expect(entries[0]?.name).toBe("client");
|
|
681
|
-
expect(entries[0]?.sourceFile.endsWith("src/client.ts")).toBe(true);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
test("resolveClientEntries ignores non-matching legacy client-* names (no migration error)", async () => {
|
|
685
|
-
await mkdir(join(workDir, "src"), { recursive: true });
|
|
686
|
-
// Uppercase / leading digit / underscore violate ^client-([a-z][a-z0-9-]*)\.tsx?$
|
|
687
|
-
await writeFile(join(workDir, "src/client-Admin.tsx"), "// bad");
|
|
688
|
-
await writeFile(join(workDir, "src/client-1bad.tsx"), "// bad");
|
|
689
|
-
await writeFile(join(workDir, "src/client_admin.tsx"), "// bad");
|
|
690
|
-
expect(resolveClientEntries(workDir, {})).toEqual([]);
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
describe("build-prod-bundle/buildMissingTemplateError", () => {
|
|
695
|
-
function multiEntry(): ClientEntry {
|
|
696
|
-
return {
|
|
697
|
-
name: "features",
|
|
698
|
-
sourceFile: "/Users/dev/offlot-app/src/client-features.tsx",
|
|
699
|
-
manifestKey: "client-features.js",
|
|
700
|
-
htmlPath: "features.html",
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
test("names the declaring entry name and its sourceFile", () => {
|
|
705
|
-
const message = buildMissingTemplateError(
|
|
706
|
-
{ "client-features.js": "/assets/client-features-abcd.js" },
|
|
707
|
-
multiEntry(),
|
|
708
|
-
);
|
|
709
|
-
|
|
710
|
-
expect(message).toContain("src/client-features.tsx");
|
|
711
|
-
expect(message).toContain("kumiko.clientEntries");
|
|
712
|
-
expect(message).toContain('Entry "features"');
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
test("single-mode entry gets no declaration hint", () => {
|
|
716
|
-
const message = buildMissingTemplateError(
|
|
717
|
-
{ "client.js": "/assets/client-abcd.js" },
|
|
718
|
-
clientEntry(),
|
|
719
|
-
);
|
|
720
|
-
|
|
721
|
-
expect(message).toContain("src/client.tsx");
|
|
722
|
-
expect(message).not.toContain("kumiko.clientEntries");
|
|
723
|
-
});
|
|
724
|
-
});
|