@mandujs/core 0.54.13 → 0.54.15
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 +1 -1
- package/src/bundler/__tests__/build-runner.ts +31 -12
- package/src/bundler/__tests__/fast-refresh.test.ts +24 -17
- package/src/bundler/__tests__/hdr.test.ts +10 -3
- package/src/bundler/__tests__/hmr-client.test.ts +16 -11
- package/src/bundler/build.test.ts +101 -49
- package/src/bundler/build.ts +244 -115
- package/src/client/router.ts +47 -40
- package/src/router/client-entry.test.ts +15 -8
- package/src/router/client-entry.ts +22 -15
- package/src/router/fs-routes.test.ts +90 -0
- package/src/router/fs-routes.ts +13 -7
- package/src/router/fs-scanner.ts +60 -16
- package/src/runtime/__tests__/page-render-response.test.ts +194 -0
- package/src/runtime/page-render-response.ts +106 -4
- package/src/runtime/router.test.ts +4 -4
- package/src/runtime/router.ts +10 -12
- package/src/runtime/server.ts +121 -58
- package/src/runtime/streaming-ssr.ts +26 -18
package/package.json
CHANGED
|
@@ -100,11 +100,11 @@ const manifest: RoutesManifest = mode === "server-page-client-module"
|
|
|
100
100
|
},
|
|
101
101
|
],
|
|
102
102
|
}
|
|
103
|
-
: mode === "hydration-no-client-module"
|
|
104
|
-
? {
|
|
105
|
-
version: 1,
|
|
106
|
-
routes: [
|
|
107
|
-
{
|
|
103
|
+
: mode === "hydration-no-client-module"
|
|
104
|
+
? {
|
|
105
|
+
version: 1,
|
|
106
|
+
routes: [
|
|
107
|
+
{
|
|
108
108
|
id: "login",
|
|
109
109
|
kind: "page",
|
|
110
110
|
pattern: "/login",
|
|
@@ -115,13 +115,32 @@ const manifest: RoutesManifest = mode === "server-page-client-module"
|
|
|
115
115
|
priority: "immediate",
|
|
116
116
|
preload: false,
|
|
117
117
|
},
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
: mode === "i18n-locale-route-id"
|
|
122
|
+
? {
|
|
123
|
+
version: 1,
|
|
124
|
+
routes: [
|
|
125
|
+
{
|
|
126
|
+
id: "ko::demo",
|
|
127
|
+
kind: "page",
|
|
128
|
+
pattern: "/ko",
|
|
129
|
+
module: "app/page.tsx",
|
|
130
|
+
componentModule: "app/page.tsx",
|
|
131
|
+
clientModule: "app/demo.client.tsx",
|
|
132
|
+
hydration: {
|
|
133
|
+
strategy: "island",
|
|
134
|
+
priority: "visible",
|
|
135
|
+
preload: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
version: 1,
|
|
142
|
+
routes: [
|
|
143
|
+
{
|
|
125
144
|
id: "demo",
|
|
126
145
|
kind: "page",
|
|
127
146
|
pattern: "/",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* packages/core/src/runtime/fast-refresh-runtime.ts
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
-
import { mkdtemp, mkdir, rm, writeFile, readFile } from "fs/promises";
|
|
24
|
-
import path from "path";
|
|
22
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
23
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile } from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
27
|
appendBoundary,
|
|
@@ -40,12 +40,19 @@ import {
|
|
|
40
40
|
_isRefreshScheduledForTests,
|
|
41
41
|
type ReactRefreshRuntime,
|
|
42
42
|
} from "../../runtime/fast-refresh-runtime";
|
|
43
|
-
import {
|
|
44
|
-
createManduHot,
|
|
45
|
-
dispatchReplacement,
|
|
46
|
-
_resetRegistryForTests,
|
|
47
|
-
} from "../../runtime/hmr-client";
|
|
48
|
-
import { generateFastRefreshPreamble } from "../dev";
|
|
43
|
+
import {
|
|
44
|
+
createManduHot,
|
|
45
|
+
dispatchReplacement,
|
|
46
|
+
_resetRegistryForTests,
|
|
47
|
+
} from "../../runtime/hmr-client";
|
|
48
|
+
import { generateFastRefreshPreamble } from "../dev";
|
|
49
|
+
|
|
50
|
+
const repoTempRoot = path.resolve(import.meta.dir, "../../../../..", ".tmp-test-artifacts");
|
|
51
|
+
|
|
52
|
+
async function mkRepoTempDir(prefix: string): Promise<string> {
|
|
53
|
+
await mkdir(repoTempRoot, { recursive: true });
|
|
54
|
+
return mkdtemp(path.join(repoTempRoot, prefix));
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
// ═══════════════════════════════════════════════════════════════════
|
|
51
58
|
// Section A — plugin pure unit tests
|
|
@@ -263,10 +270,10 @@ describe("manduHMR — __MANDU_HMR__ global behavior", () => {
|
|
|
263
270
|
describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
264
271
|
"Bun.build({ reactFastRefresh: true }) + fastRefreshPlugin()",
|
|
265
272
|
() => {
|
|
266
|
-
let rootDir: string;
|
|
267
|
-
|
|
268
|
-
beforeAll(async () => {
|
|
269
|
-
rootDir = await
|
|
273
|
+
let rootDir: string;
|
|
274
|
+
|
|
275
|
+
beforeAll(async () => {
|
|
276
|
+
rootDir = await mkRepoTempDir("fr-build-");
|
|
270
277
|
// Three source files — one boundary, one plain, one .island.tsx —
|
|
271
278
|
// give us the minimal matrix for the plugin's include filter.
|
|
272
279
|
await writeFile(
|
|
@@ -512,10 +519,10 @@ describe("dispatchReplacement + __MANDU_HMR__ integration", () => {
|
|
|
512
519
|
describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
|
|
513
520
|
"buildVendorShims emits fast-refresh shims in dev mode",
|
|
514
521
|
() => {
|
|
515
|
-
let rootDir: string;
|
|
516
|
-
|
|
517
|
-
beforeAll(async () => {
|
|
518
|
-
rootDir = await
|
|
522
|
+
let rootDir: string;
|
|
523
|
+
|
|
524
|
+
beforeAll(async () => {
|
|
525
|
+
rootDir = await mkRepoTempDir("fr-vendor-");
|
|
519
526
|
await mkdir(path.join(rootDir, "app"), { recursive: true });
|
|
520
527
|
await writeFile(
|
|
521
528
|
path.join(rootDir, "package.json"),
|
|
@@ -204,9 +204,16 @@ describe("Phase 7.2 Agent B — findRouteIdForSlot", () => {
|
|
|
204
204
|
// Section B — HMR server broadcast of slot-refetch
|
|
205
205
|
// -----------------------------------------------------------------------------
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
const HMR_TEST_PORT_STATE = "__MANDU_HMR_TEST_PORT_STATE__";
|
|
208
|
+
|
|
209
|
+
function pickPort(): number {
|
|
210
|
+
const stateGlobal = globalThis as typeof globalThis & {
|
|
211
|
+
__MANDU_HMR_TEST_PORT_STATE__?: { next: number };
|
|
212
|
+
};
|
|
213
|
+
stateGlobal.__MANDU_HMR_TEST_PORT_STATE__ ??= { next: 0 };
|
|
214
|
+
const index = stateGlobal.__MANDU_HMR_TEST_PORT_STATE__.next++;
|
|
215
|
+
return 41000 + (((process.pid % 3500) * 2 + index * 2) % 7000);
|
|
216
|
+
}
|
|
210
217
|
|
|
211
218
|
describe("Phase 7.2 Agent B — slot-refetch broadcast", () => {
|
|
212
219
|
let server: HMRServer | null = null;
|
|
@@ -172,17 +172,22 @@ describe("createManduHot — Vite-compat import.meta.hot runtime", () => {
|
|
|
172
172
|
* Utility: spin up an HMR server and return it plus the public port the
|
|
173
173
|
* client should dial. The caller owns teardown via `afterEach`.
|
|
174
174
|
*
|
|
175
|
-
* We pass `port: 0` ... almost. `createHMRServer` computes
|
|
176
|
-
* `port + PORTS.HMR_OFFSET` internally, so if we want an ephemeral
|
|
177
|
-
* listener we'd need to bind ahead of time.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
175
|
+
* We pass `port: 0` ... almost. `createHMRServer` computes
|
|
176
|
+
* `port + PORTS.HMR_OFFSET` internally, so if we want an ephemeral
|
|
177
|
+
* listener we'd need to bind ahead of time. These tests instead use a
|
|
178
|
+
* process-local monotonic port allocator to avoid random collisions
|
|
179
|
+
* during the full parallel core suite.
|
|
180
|
+
*/
|
|
181
|
+
const HMR_TEST_PORT_STATE = "__MANDU_HMR_TEST_PORT_STATE__";
|
|
182
|
+
|
|
183
|
+
function pickPort(): number {
|
|
184
|
+
const stateGlobal = globalThis as typeof globalThis & {
|
|
185
|
+
__MANDU_HMR_TEST_PORT_STATE__?: { next: number };
|
|
186
|
+
};
|
|
187
|
+
stateGlobal.__MANDU_HMR_TEST_PORT_STATE__ ??= { next: 0 };
|
|
188
|
+
const index = stateGlobal.__MANDU_HMR_TEST_PORT_STATE__.next++;
|
|
189
|
+
return 41000 + (((process.pid % 3500) * 2 + index * 2) % 7000);
|
|
190
|
+
}
|
|
186
191
|
|
|
187
192
|
/**
|
|
188
193
|
* A wrapper around WebSocket that stashes every incoming message in an
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import { pathToFileURL } from "url";
|
|
5
|
-
|
|
6
|
-
// 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
|
|
7
|
-
let rootDir: string;
|
|
8
|
-
let result: { success: boolean; errors: string[] };
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
|
|
6
|
+
// 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
|
|
7
|
+
let rootDir: string;
|
|
8
|
+
let result: { success: boolean; errors: string[] };
|
|
9
|
+
const repoTempRoot = path.resolve(import.meta.dir, "../../../..", ".tmp-test-artifacts");
|
|
10
|
+
|
|
11
|
+
async function mkRepoTempDir(prefix: string): Promise<string> {
|
|
12
|
+
await mkdir(repoTempRoot, { recursive: true });
|
|
13
|
+
return mkdtemp(path.join(repoTempRoot, prefix));
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
async function importBuiltModule(relativePath: string): Promise<Record<string, unknown>> {
|
|
11
17
|
const fileUrl = pathToFileURL(path.join(rootDir, relativePath)).href;
|
|
@@ -24,10 +30,13 @@ async function importBuiltModule(relativePath: string): Promise<Record<string, u
|
|
|
24
30
|
* See `__tests__/build-runner.ts` for the subprocess entrypoint and more
|
|
25
31
|
* background.
|
|
26
32
|
*/
|
|
27
|
-
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
28
|
-
success: boolean;
|
|
29
|
-
errors: string[];
|
|
30
|
-
|
|
33
|
+
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
errors: string[];
|
|
36
|
+
manifest?: {
|
|
37
|
+
bundles?: Record<string, { js?: string }>;
|
|
38
|
+
} | null;
|
|
39
|
+
}> {
|
|
31
40
|
const runner = path.join(
|
|
32
41
|
import.meta.dir,
|
|
33
42
|
"__tests__",
|
|
@@ -52,25 +61,27 @@ async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
|
52
61
|
const last = lines[lines.length - 1] ?? "";
|
|
53
62
|
try {
|
|
54
63
|
const parsed = JSON.parse(last);
|
|
55
|
-
return {
|
|
56
|
-
success: parsed.success === true,
|
|
57
|
-
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
|
58
|
-
|
|
64
|
+
return {
|
|
65
|
+
success: parsed.success === true,
|
|
66
|
+
errors: Array.isArray(parsed.errors) ? parsed.errors : [],
|
|
67
|
+
manifest: parsed.manifest ?? null,
|
|
68
|
+
};
|
|
59
69
|
} catch (e) {
|
|
60
|
-
return {
|
|
61
|
-
success: false,
|
|
62
|
-
errors: [
|
|
63
|
-
`build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
|
|
64
|
-
],
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
errors: [
|
|
73
|
+
`build-runner output could not be parsed as JSON: ${String(e)}\nLast stdout line: ${last}`,
|
|
74
|
+
],
|
|
75
|
+
manifest: null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { success: false, errors: [`spawn failed: ${String(err)}`], manifest: null };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
71
82
|
|
|
72
|
-
beforeAll(async () => {
|
|
73
|
-
rootDir = await
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
rootDir = await mkRepoTempDir("bundler-");
|
|
74
85
|
|
|
75
86
|
await mkdir(path.join(rootDir, "app"), { recursive: true });
|
|
76
87
|
await writeFile(
|
|
@@ -117,14 +128,26 @@ afterAll(async () => {
|
|
|
117
128
|
// subprocess via `__tests__/build-runner.ts`. In-process retry does not
|
|
118
129
|
// recover from that one; a fresh module graph does.
|
|
119
130
|
describe("buildClientBundles vendor shims", () => {
|
|
120
|
-
test("build succeeds", () => {
|
|
121
|
-
if (!result.success) {
|
|
122
|
-
console.error("[build.test] errors:", result.errors);
|
|
123
|
-
}
|
|
124
|
-
expect(result.success).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("
|
|
131
|
+
test("build succeeds", () => {
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
console.error("[build.test] errors:", result.errors);
|
|
134
|
+
}
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("runtime reads canonical data-hydrate strategies", async () => {
|
|
139
|
+
const runtimePath = path.join(rootDir, ".mandu", "client", "_runtime.js");
|
|
140
|
+
const runtimeSource = await readFile(runtimePath, "utf-8");
|
|
141
|
+
|
|
142
|
+
expect(runtimeSource).toContain("data-hydrate");
|
|
143
|
+
expect(runtimeSource).toContain("matchMedia");
|
|
144
|
+
expect(runtimeSource).toContain("200px");
|
|
145
|
+
expect(runtimeSource).toContain('"click"');
|
|
146
|
+
expect(runtimeSource).not.toContain("mouseenter");
|
|
147
|
+
expect(runtimeSource).not.toContain("pointerdown");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("re-exports modern React 19 APIs used by islands", async () => {
|
|
128
151
|
const reactShim = await importBuiltModule(".mandu/client/_react.js");
|
|
129
152
|
const requiredExports = [
|
|
130
153
|
"Activity",
|
|
@@ -176,18 +199,21 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
176
199
|
expect(runtimeSource).toContain("data-mandu-hydrating");
|
|
177
200
|
expect(runtimeSource).toContain("data-mandu-render-mode");
|
|
178
201
|
expect(runtimeSource).toContain("data-mandu-recoverable-error");
|
|
179
|
-
expect(runtimeSource).toContain("
|
|
202
|
+
expect(runtimeSource).toContain('"click"');
|
|
203
|
+
expect(runtimeSource).not.toContain("pointerdown");
|
|
180
204
|
});
|
|
181
205
|
|
|
182
|
-
test("runtime parses SSR data script before island setup", async () => {
|
|
183
|
-
const runtimeSource = await readFile(path.join(rootDir, ".mandu", "client", "_runtime.js"), "utf-8");
|
|
184
|
-
expect(runtimeSource).toContain("function readManduData");
|
|
185
|
-
expect(runtimeSource).toContain("document.getElementById(\"__MANDU_DATA__\")");
|
|
186
|
-
expect(runtimeSource).toContain("
|
|
187
|
-
|
|
206
|
+
test("runtime parses SSR data script before island setup", async () => {
|
|
207
|
+
const runtimeSource = await readFile(path.join(rootDir, ".mandu", "client", "_runtime.js"), "utf-8");
|
|
208
|
+
expect(runtimeSource).toContain("function readManduData");
|
|
209
|
+
expect(runtimeSource).toContain("document.getElementById(\"__MANDU_DATA__\")");
|
|
210
|
+
expect(runtimeSource).toContain("deserializeManduProps");
|
|
211
|
+
expect(runtimeSource).toContain("new Date");
|
|
212
|
+
expect(runtimeSource).toContain("new Map");
|
|
213
|
+
});
|
|
188
214
|
|
|
189
|
-
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
190
|
-
const staleRoot = await
|
|
215
|
+
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
216
|
+
const staleRoot = await mkRepoTempDir("stale-client-module-");
|
|
191
217
|
try {
|
|
192
218
|
await mkdir(path.join(staleRoot, "app"), { recursive: true });
|
|
193
219
|
await mkdir(path.join(staleRoot, "src", "shared", "contracts"), { recursive: true });
|
|
@@ -220,7 +246,7 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
220
246
|
});
|
|
221
247
|
|
|
222
248
|
test("rewrites route-component clientModule to the real client import before bundling", async () => {
|
|
223
|
-
const routeClientRoot = await
|
|
249
|
+
const routeClientRoot = await mkRepoTempDir("route-client-import-");
|
|
224
250
|
try {
|
|
225
251
|
await mkdir(path.join(routeClientRoot, "app", "login"), { recursive: true });
|
|
226
252
|
await mkdir(path.join(routeClientRoot, "src", "client", "pages", "login"), { recursive: true });
|
|
@@ -267,7 +293,7 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
267
293
|
});
|
|
268
294
|
|
|
269
295
|
test("bundles route-level named client exports without requiring a default export", async () => {
|
|
270
|
-
const routeClientRoot = await
|
|
296
|
+
const routeClientRoot = await mkRepoTempDir("route-named-client-import-");
|
|
271
297
|
try {
|
|
272
298
|
await mkdir(path.join(routeClientRoot, "app", "login"), { recursive: true });
|
|
273
299
|
await mkdir(path.join(routeClientRoot, "src", "client", "pages", "login"), { recursive: true });
|
|
@@ -317,8 +343,34 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
317
343
|
}
|
|
318
344
|
});
|
|
319
345
|
|
|
346
|
+
test("uses Windows-safe asset filenames for locale-prefixed route ids", async () => {
|
|
347
|
+
const localeRoot = await mkRepoTempDir("i18n-locale-route-id-");
|
|
348
|
+
try {
|
|
349
|
+
await mkdir(path.join(localeRoot, "app"), { recursive: true });
|
|
350
|
+
await writeFile(
|
|
351
|
+
path.join(localeRoot, "package.json"),
|
|
352
|
+
JSON.stringify({ name: "mandu-i18n-route-id-test", type: "module" }, null, 2),
|
|
353
|
+
"utf-8",
|
|
354
|
+
);
|
|
355
|
+
await writeFile(
|
|
356
|
+
path.join(localeRoot, "app", "demo.client.tsx"),
|
|
357
|
+
"export default function DemoIsland() { return null; }\n",
|
|
358
|
+
"utf-8",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const localeResult = await runBuildInSubprocess(localeRoot, "i18n-locale-route-id");
|
|
362
|
+
expect(localeResult.success).toBe(true);
|
|
363
|
+
const bundle = localeResult.manifest?.bundles?.["ko::demo"];
|
|
364
|
+
expect(bundle?.js).toBe("/.mandu/client/ko_3a__3a_demo.island.js");
|
|
365
|
+
expect(bundle?.js).not.toContain(":");
|
|
366
|
+
expect(await Bun.file(path.join(localeRoot, ".mandu", "client", "ko_3a__3a_demo.island.js")).exists()).toBe(true);
|
|
367
|
+
} finally {
|
|
368
|
+
await rm(localeRoot, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
320
372
|
test("fails when hydration is enabled but no clientModule can be resolved", async () => {
|
|
321
|
-
const missingRoot = await
|
|
373
|
+
const missingRoot = await mkRepoTempDir("hydration-no-client-");
|
|
322
374
|
try {
|
|
323
375
|
await mkdir(path.join(missingRoot, "app", "login"), { recursive: true });
|
|
324
376
|
await mkdir(path.join(missingRoot, "src", "client", "widgets", "login-form"), { recursive: true });
|