@mandujs/core 0.54.10 → 0.54.11
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 +200 -200
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +17 -65
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +10 -18
- package/src/agent/verify.ts +17 -115
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +81 -62
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +120 -88
- package/src/bundler/build.ts +511 -511
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +22 -31
- package/src/filling/context.ts +17 -17
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.test.ts +66 -65
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +3 -3
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +192 -140
- package/src/router/client-entry.ts +516 -486
- package/src/router/fs-routes.ts +16 -16
- package/src/router/fs-scanner.ts +16 -28
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/server.ts +243 -243
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
|
@@ -54,68 +54,87 @@
|
|
|
54
54
|
import { buildClientBundles } from "../build";
|
|
55
55
|
import type { RoutesManifest } from "../../spec/schema";
|
|
56
56
|
|
|
57
|
-
const rootDir = process.argv[2];
|
|
58
|
-
if (!rootDir) {
|
|
59
|
-
console.error("usage: build-runner.ts <rootDir>");
|
|
60
|
-
process.exit(2);
|
|
61
|
-
}
|
|
62
|
-
const mode = process.argv[3] ?? "default";
|
|
63
|
-
|
|
64
|
-
const manifest: RoutesManifest = mode === "server-page-client-module"
|
|
65
|
-
? {
|
|
66
|
-
version: 1,
|
|
67
|
-
routes: [
|
|
68
|
-
{
|
|
69
|
-
id: "index",
|
|
70
|
-
kind: "page",
|
|
71
|
-
pattern: "/",
|
|
72
|
-
module: "app/page.tsx",
|
|
73
|
-
componentModule: "app/page.tsx",
|
|
74
|
-
clientModule: "app/page.tsx",
|
|
75
|
-
hydration: {
|
|
76
|
-
strategy: "island",
|
|
77
|
-
priority: "visible",
|
|
78
|
-
preload: false,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
}
|
|
83
|
-
: mode === "
|
|
84
|
-
? {
|
|
85
|
-
version: 1,
|
|
86
|
-
routes: [
|
|
87
|
-
{
|
|
88
|
-
id: "login",
|
|
89
|
-
kind: "page",
|
|
90
|
-
pattern: "/login",
|
|
91
|
-
module: "app/login/page.tsx",
|
|
92
|
-
componentModule: "app/login/page.tsx",
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
57
|
+
const rootDir = process.argv[2];
|
|
58
|
+
if (!rootDir) {
|
|
59
|
+
console.error("usage: build-runner.ts <rootDir>");
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
const mode = process.argv[3] ?? "default";
|
|
63
|
+
|
|
64
|
+
const manifest: RoutesManifest = mode === "server-page-client-module"
|
|
65
|
+
? {
|
|
66
|
+
version: 1,
|
|
67
|
+
routes: [
|
|
68
|
+
{
|
|
69
|
+
id: "index",
|
|
70
|
+
kind: "page",
|
|
71
|
+
pattern: "/",
|
|
72
|
+
module: "app/page.tsx",
|
|
73
|
+
componentModule: "app/page.tsx",
|
|
74
|
+
clientModule: "app/page.tsx",
|
|
75
|
+
hydration: {
|
|
76
|
+
strategy: "island",
|
|
77
|
+
priority: "visible",
|
|
78
|
+
preload: false,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
: mode === "server-page-route-client-import"
|
|
84
|
+
? {
|
|
85
|
+
version: 1,
|
|
86
|
+
routes: [
|
|
87
|
+
{
|
|
88
|
+
id: "login",
|
|
89
|
+
kind: "page",
|
|
90
|
+
pattern: "/login",
|
|
91
|
+
module: "app/login/page.tsx",
|
|
92
|
+
componentModule: "app/login/page.tsx",
|
|
93
|
+
clientModule: "app/login/page.tsx",
|
|
94
|
+
hydration: {
|
|
95
|
+
strategy: "island",
|
|
96
|
+
priority: "visible",
|
|
97
|
+
preload: false,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
: mode === "hydration-no-client-module"
|
|
103
|
+
? {
|
|
104
|
+
version: 1,
|
|
105
|
+
routes: [
|
|
106
|
+
{
|
|
107
|
+
id: "login",
|
|
108
|
+
kind: "page",
|
|
109
|
+
pattern: "/login",
|
|
110
|
+
module: "app/login/page.tsx",
|
|
111
|
+
componentModule: "app/login/page.tsx",
|
|
112
|
+
hydration: {
|
|
113
|
+
strategy: "full",
|
|
114
|
+
priority: "immediate",
|
|
115
|
+
preload: false,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
version: 1,
|
|
122
|
+
routes: [
|
|
123
|
+
{
|
|
124
|
+
id: "demo",
|
|
125
|
+
kind: "page",
|
|
126
|
+
pattern: "/",
|
|
127
|
+
module: "app/page.tsx",
|
|
128
|
+
componentModule: "app/page.tsx",
|
|
129
|
+
clientModule: "app/demo.client.tsx",
|
|
130
|
+
hydration: {
|
|
131
|
+
strategy: "island",
|
|
132
|
+
priority: "visible",
|
|
133
|
+
preload: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
119
138
|
|
|
120
139
|
try {
|
|
121
140
|
const result = await buildClientBundles(manifest, rootDir, {
|
|
@@ -28,13 +28,13 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
|
28
28
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
29
29
|
import { tmpdir } from "os";
|
|
30
30
|
import path from "path";
|
|
31
|
-
import type { RouteSpec, RoutesManifest } from "../../spec/schema";
|
|
32
|
-
import {
|
|
33
|
-
_testOnly_scanIslandFiles,
|
|
34
|
-
_testOnly_scanPartialFiles,
|
|
35
|
-
_testOnly_getHydratedRoutes,
|
|
36
|
-
_testOnly_getHydrationRoutesMissingClientModule,
|
|
37
|
-
} from "../build";
|
|
31
|
+
import type { RouteSpec, RoutesManifest } from "../../spec/schema";
|
|
32
|
+
import {
|
|
33
|
+
_testOnly_scanIslandFiles,
|
|
34
|
+
_testOnly_scanPartialFiles,
|
|
35
|
+
_testOnly_getHydratedRoutes,
|
|
36
|
+
_testOnly_getHydrationRoutesMissingClientModule,
|
|
37
|
+
} from "../build";
|
|
38
38
|
import { HMR_PERF } from "../../perf/hmr-markers";
|
|
39
39
|
import {
|
|
40
40
|
_resetCacheForTesting as _resetPerfCache,
|
|
@@ -374,7 +374,7 @@ describe("Phase 7.1 R1 Agent C — getHydratedRoutes filter", () => {
|
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
-
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
377
|
+
describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", () => {
|
|
378
378
|
let project: ReturnType<typeof createProject>;
|
|
379
379
|
|
|
380
380
|
beforeEach(() => {
|
|
@@ -502,55 +502,55 @@ describe("Phase 7.1 R1 Agent C — per-island scan skips non-hydrated routes", (
|
|
|
502
502
|
];
|
|
503
503
|
const result = await _testOnly_scanIslandFiles(routes, project.rootDir);
|
|
504
504
|
expect(result).toEqual([]);
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
describe("hydration route validation", () => {
|
|
509
|
-
it("finds pages that request hydration without a client module", () => {
|
|
510
|
-
const manifest = {
|
|
511
|
-
version: 1,
|
|
512
|
-
routes: [
|
|
513
|
-
{
|
|
514
|
-
id: "login",
|
|
515
|
-
pattern: "/login",
|
|
516
|
-
kind: "page",
|
|
517
|
-
module: "app/login/page.tsx",
|
|
518
|
-
componentModule: "app/login/page.tsx",
|
|
519
|
-
hydration: { strategy: "full", priority: "immediate", preload: false },
|
|
520
|
-
},
|
|
521
|
-
pureSsrRoute("about", "/about", "app/about"),
|
|
522
|
-
pageRoute("dashboard", "/dashboard", "app/dashboard"),
|
|
523
|
-
],
|
|
524
|
-
} as RoutesManifest;
|
|
525
|
-
|
|
526
|
-
const missing = _testOnly_getHydrationRoutesMissingClientModule(manifest);
|
|
527
|
-
expect(missing.map((route) => route.id)).toEqual(["login"]);
|
|
528
|
-
});
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
describe("partial bundle scan", () => {
|
|
532
|
-
let project: ReturnType<typeof createProject>;
|
|
533
|
-
|
|
534
|
-
beforeEach(() => {
|
|
535
|
-
project = createProject();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
afterEach(() => {
|
|
539
|
-
try {
|
|
540
|
-
rmSync(project.rootDir, { recursive: true, force: true });
|
|
541
|
-
} catch {
|
|
542
|
-
/* Windows lock tolerance */
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
|
|
547
|
-
project.writeIslandFile("app", "Home.partial.tsx");
|
|
548
|
-
project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
|
|
549
|
-
project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
|
|
550
|
-
|
|
551
|
-
const result = await _testOnly_scanPartialFiles(project.rootDir);
|
|
552
|
-
|
|
553
|
-
expect(result.map((entry) => entry.name)).toEqual(["Home"]);
|
|
554
|
-
expect(result[0].priority).toBe("visible");
|
|
555
|
-
});
|
|
556
|
-
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe("hydration route validation", () => {
|
|
509
|
+
it("finds pages that request hydration without a client module", () => {
|
|
510
|
+
const manifest = {
|
|
511
|
+
version: 1,
|
|
512
|
+
routes: [
|
|
513
|
+
{
|
|
514
|
+
id: "login",
|
|
515
|
+
pattern: "/login",
|
|
516
|
+
kind: "page",
|
|
517
|
+
module: "app/login/page.tsx",
|
|
518
|
+
componentModule: "app/login/page.tsx",
|
|
519
|
+
hydration: { strategy: "full", priority: "immediate", preload: false },
|
|
520
|
+
},
|
|
521
|
+
pureSsrRoute("about", "/about", "app/about"),
|
|
522
|
+
pageRoute("dashboard", "/dashboard", "app/dashboard"),
|
|
523
|
+
],
|
|
524
|
+
} as RoutesManifest;
|
|
525
|
+
|
|
526
|
+
const missing = _testOnly_getHydrationRoutesMissingClientModule(manifest);
|
|
527
|
+
expect(missing.map((route) => route.id)).toEqual(["login"]);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe("partial bundle scan", () => {
|
|
532
|
+
let project: ReturnType<typeof createProject>;
|
|
533
|
+
|
|
534
|
+
beforeEach(() => {
|
|
535
|
+
project = createProject();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
afterEach(() => {
|
|
539
|
+
try {
|
|
540
|
+
rmSync(project.rootDir, { recursive: true, force: true });
|
|
541
|
+
} catch {
|
|
542
|
+
/* Windows lock tolerance */
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("discovers *.partial.tsx files outside .mandu and node_modules", async () => {
|
|
547
|
+
project.writeIslandFile("app", "Home.partial.tsx");
|
|
548
|
+
project.writeIslandFile(".mandu/client", "Stale.partial.tsx");
|
|
549
|
+
project.writeIslandFile("node_modules/pkg", "Ignored.partial.tsx");
|
|
550
|
+
|
|
551
|
+
const result = await _testOnly_scanPartialFiles(project.rootDir);
|
|
552
|
+
|
|
553
|
+
expect(result.map((entry) => entry.name)).toEqual(["Home"]);
|
|
554
|
+
expect(result[0].priority).toBe("visible");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { __private } from "../css";
|
|
3
|
-
|
|
4
|
-
describe("CSS Tailwind command resolution", () => {
|
|
5
|
-
test("uses process.execPath when it is Bun", () => {
|
|
6
|
-
expect(
|
|
7
|
-
__private.getTailwindCommand(["@tailwindcss/cli"], "C:\\tools\\bun.exe"),
|
|
8
|
-
).toEqual(["C:\\tools\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("does not treat standalone mandu.exe as Bun", () => {
|
|
12
|
-
expect(
|
|
13
|
-
__private.getTailwindCommand(
|
|
14
|
-
["@tailwindcss/cli"],
|
|
15
|
-
"C:\\Users\\User\\AppData\\Local\\Mandu\\bin\\mandu.exe",
|
|
16
|
-
() => "C:\\Users\\User\\.bun\\bin\\bun.exe",
|
|
17
|
-
),
|
|
18
|
-
).toEqual(["C:\\Users\\User\\.bun\\bin\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { __private } from "../css";
|
|
3
|
+
|
|
4
|
+
describe("CSS Tailwind command resolution", () => {
|
|
5
|
+
test("uses process.execPath when it is Bun", () => {
|
|
6
|
+
expect(
|
|
7
|
+
__private.getTailwindCommand(["@tailwindcss/cli"], "C:\\tools\\bun.exe"),
|
|
8
|
+
).toEqual(["C:\\tools\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("does not treat standalone mandu.exe as Bun", () => {
|
|
12
|
+
expect(
|
|
13
|
+
__private.getTailwindCommand(
|
|
14
|
+
["@tailwindcss/cli"],
|
|
15
|
+
"C:\\Users\\User\\AppData\\Local\\Mandu\\bin\\mandu.exe",
|
|
16
|
+
() => "C:\\Users\\User\\.bun\\bin\\bun.exe",
|
|
17
|
+
),
|
|
18
|
+
).toEqual(["C:\\Users\\User\\.bun\\bin\\bun.exe", "x", "@tailwindcss/cli"]);
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/bundler/analyzer.ts
CHANGED
|
@@ -341,26 +341,26 @@ export async function analyzeBundle(
|
|
|
341
341
|
deps: entry.dependencies ?? [],
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
|
-
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
-
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
-
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
-
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
344
|
+
for (const [islandName, entry] of Object.entries(manifest.islands ?? {})) {
|
|
345
|
+
// Avoid double-counting: if a per-island chunk shares its route id with
|
|
346
|
+
// a route-level bundle, we prefer the island entry (finer granularity).
|
|
347
|
+
const existing = islandSources.findIndex((s) => s.name === islandName);
|
|
348
348
|
if (existing !== -1) islandSources.splice(existing, 1);
|
|
349
349
|
islandSources.push({
|
|
350
350
|
name: islandName,
|
|
351
351
|
url: entry.js,
|
|
352
352
|
priority: entry.priority,
|
|
353
|
-
deps: [],
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
for (const [partialName, entry] of Object.entries(manifest.partials ?? {})) {
|
|
357
|
-
islandSources.push({
|
|
358
|
-
name: `partial:${partialName}`,
|
|
359
|
-
url: entry.js,
|
|
360
|
-
priority: entry.priority,
|
|
361
|
-
deps: [],
|
|
362
|
-
});
|
|
363
|
-
}
|
|
353
|
+
deps: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
for (const [partialName, entry] of Object.entries(manifest.partials ?? {})) {
|
|
357
|
+
islandSources.push({
|
|
358
|
+
name: `partial:${partialName}`,
|
|
359
|
+
url: entry.js,
|
|
360
|
+
priority: entry.priority,
|
|
361
|
+
deps: [],
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
364
|
|
|
365
365
|
const islands: AnalyzeIsland[] = [];
|
|
366
366
|
for (const src of islandSources) {
|
|
@@ -24,19 +24,19 @@ async function importBuiltModule(relativePath: string): Promise<Record<string, u
|
|
|
24
24
|
* See `__tests__/build-runner.ts` for the subprocess entrypoint and more
|
|
25
25
|
* background.
|
|
26
26
|
*/
|
|
27
|
-
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
28
|
-
success: boolean;
|
|
29
|
-
errors: string[];
|
|
30
|
-
}> {
|
|
27
|
+
async function runBuildInSubprocess(root: string, mode?: string): Promise<{
|
|
28
|
+
success: boolean;
|
|
29
|
+
errors: string[];
|
|
30
|
+
}> {
|
|
31
31
|
const runner = path.join(
|
|
32
32
|
import.meta.dir,
|
|
33
33
|
"__tests__",
|
|
34
34
|
"build-runner.ts",
|
|
35
35
|
);
|
|
36
36
|
try {
|
|
37
|
-
const args = [process.execPath, "run", runner, root];
|
|
38
|
-
if (mode) args.push(mode);
|
|
39
|
-
const proc = Bun.spawn(args, {
|
|
37
|
+
const args = [process.execPath, "run", runner, root];
|
|
38
|
+
if (mode) args.push(mode);
|
|
39
|
+
const proc = Bun.spawn(args, {
|
|
40
40
|
cwd: path.resolve(import.meta.dir, "..", ".."),
|
|
41
41
|
stdin: "ignore",
|
|
42
42
|
stdout: "pipe",
|
|
@@ -173,84 +173,116 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
173
173
|
expect(runtimeSource).toContain("function hasHydratableMarkup");
|
|
174
174
|
expect(runtimeSource).toContain("function shouldHydrateCompiledIsland");
|
|
175
175
|
expect(runtimeSource).toContain("onRecoverableError");
|
|
176
|
-
expect(runtimeSource).toContain("data-mandu-hydrating");
|
|
177
|
-
expect(runtimeSource).toContain("data-mandu-render-mode");
|
|
178
|
-
expect(runtimeSource).toContain("data-mandu-recoverable-error");
|
|
179
|
-
expect(runtimeSource).toContain("pointerdown");
|
|
180
|
-
});
|
|
181
|
-
|
|
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("JSON.parse");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
190
|
-
const staleRoot = await mkdtemp(path.join(import.meta.dir, ".tmp-stale-client-module-"));
|
|
191
|
-
try {
|
|
192
|
-
await mkdir(path.join(staleRoot, "app"), { recursive: true });
|
|
193
|
-
await mkdir(path.join(staleRoot, "src", "shared", "contracts"), { recursive: true });
|
|
194
|
-
await writeFile(
|
|
195
|
-
path.join(staleRoot, "package.json"),
|
|
196
|
-
JSON.stringify({ name: "mandu-stale-client-module-test", type: "module" }, null, 2),
|
|
197
|
-
"utf-8",
|
|
198
|
-
);
|
|
199
|
-
await writeFile(
|
|
200
|
-
path.join(staleRoot, "src", "shared", "contracts", "api.ts"),
|
|
201
|
-
'export const INTERNAL_BASE = process.env.MANDU_INTERNAL_URL ?? "http://localhost:3333";\n',
|
|
202
|
-
"utf-8",
|
|
203
|
-
);
|
|
204
|
-
await writeFile(
|
|
205
|
-
path.join(staleRoot, "app", "page.tsx"),
|
|
206
|
-
'import { INTERNAL_BASE } from "../src/shared/contracts/api";\n' +
|
|
207
|
-
"export default async function HomePage() {\n" +
|
|
208
|
-
" return <main>{INTERNAL_BASE}</main>;\n" +
|
|
209
|
-
"}\n",
|
|
210
|
-
"utf-8",
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const staleResult = await runBuildInSubprocess(staleRoot, "server-page-client-module");
|
|
214
|
-
expect(staleResult.success).toBe(false);
|
|
215
|
-
expect(staleResult.errors.join("\n")).toContain("missing \"use client\"");
|
|
216
|
-
expect(await Bun.file(path.join(staleRoot, ".mandu", "client", "index.island.js")).exists()).toBe(false);
|
|
217
|
-
} finally {
|
|
218
|
-
await rm(staleRoot, { recursive: true, force: true });
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("
|
|
223
|
-
const
|
|
224
|
-
try {
|
|
225
|
-
await mkdir(path.join(
|
|
226
|
-
await mkdir(path.join(
|
|
227
|
-
await writeFile(
|
|
228
|
-
path.join(
|
|
229
|
-
JSON.stringify({ name: "mandu-
|
|
230
|
-
"utf-8",
|
|
231
|
-
);
|
|
232
|
-
await writeFile(
|
|
233
|
-
path.join(
|
|
234
|
-
'import
|
|
235
|
-
"export default function
|
|
236
|
-
" return <
|
|
237
|
-
"}\n",
|
|
238
|
-
"utf-8",
|
|
239
|
-
);
|
|
240
|
-
await writeFile(
|
|
241
|
-
path.join(
|
|
242
|
-
"
|
|
243
|
-
"utf-8",
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
expect(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
176
|
+
expect(runtimeSource).toContain("data-mandu-hydrating");
|
|
177
|
+
expect(runtimeSource).toContain("data-mandu-render-mode");
|
|
178
|
+
expect(runtimeSource).toContain("data-mandu-recoverable-error");
|
|
179
|
+
expect(runtimeSource).toContain("pointerdown");
|
|
180
|
+
});
|
|
181
|
+
|
|
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("JSON.parse");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
190
|
+
const staleRoot = await mkdtemp(path.join(import.meta.dir, ".tmp-stale-client-module-"));
|
|
191
|
+
try {
|
|
192
|
+
await mkdir(path.join(staleRoot, "app"), { recursive: true });
|
|
193
|
+
await mkdir(path.join(staleRoot, "src", "shared", "contracts"), { recursive: true });
|
|
194
|
+
await writeFile(
|
|
195
|
+
path.join(staleRoot, "package.json"),
|
|
196
|
+
JSON.stringify({ name: "mandu-stale-client-module-test", type: "module" }, null, 2),
|
|
197
|
+
"utf-8",
|
|
198
|
+
);
|
|
199
|
+
await writeFile(
|
|
200
|
+
path.join(staleRoot, "src", "shared", "contracts", "api.ts"),
|
|
201
|
+
'export const INTERNAL_BASE = process.env.MANDU_INTERNAL_URL ?? "http://localhost:3333";\n',
|
|
202
|
+
"utf-8",
|
|
203
|
+
);
|
|
204
|
+
await writeFile(
|
|
205
|
+
path.join(staleRoot, "app", "page.tsx"),
|
|
206
|
+
'import { INTERNAL_BASE } from "../src/shared/contracts/api";\n' +
|
|
207
|
+
"export default async function HomePage() {\n" +
|
|
208
|
+
" return <main>{INTERNAL_BASE}</main>;\n" +
|
|
209
|
+
"}\n",
|
|
210
|
+
"utf-8",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const staleResult = await runBuildInSubprocess(staleRoot, "server-page-client-module");
|
|
214
|
+
expect(staleResult.success).toBe(false);
|
|
215
|
+
expect(staleResult.errors.join("\n")).toContain("missing \"use client\"");
|
|
216
|
+
expect(await Bun.file(path.join(staleRoot, ".mandu", "client", "index.island.js")).exists()).toBe(false);
|
|
217
|
+
} finally {
|
|
218
|
+
await rm(staleRoot, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("rewrites route-component clientModule to the real client import before bundling", async () => {
|
|
223
|
+
const routeClientRoot = await mkdtemp(path.join(import.meta.dir, ".tmp-route-client-import-"));
|
|
224
|
+
try {
|
|
225
|
+
await mkdir(path.join(routeClientRoot, "app", "login"), { recursive: true });
|
|
226
|
+
await mkdir(path.join(routeClientRoot, "src", "client", "pages", "login"), { recursive: true });
|
|
227
|
+
await writeFile(
|
|
228
|
+
path.join(routeClientRoot, "package.json"),
|
|
229
|
+
JSON.stringify({ name: "mandu-route-client-import-test", type: "module" }, null, 2),
|
|
230
|
+
"utf-8",
|
|
231
|
+
);
|
|
232
|
+
await writeFile(
|
|
233
|
+
path.join(routeClientRoot, "app", "login", "page.tsx"),
|
|
234
|
+
'import LoginPage from "@/client/pages/login/LoginPage.client";\n' +
|
|
235
|
+
"export default function Page() {\n" +
|
|
236
|
+
" return <LoginPage />;\n" +
|
|
237
|
+
"}\n",
|
|
238
|
+
"utf-8",
|
|
239
|
+
);
|
|
240
|
+
await writeFile(
|
|
241
|
+
path.join(routeClientRoot, "src", "client", "pages", "login", "LoginPage.client.tsx"),
|
|
242
|
+
'"use client";\nexport default function LoginPage() { return <form />; }\n',
|
|
243
|
+
"utf-8",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const routeClientResult = await runBuildInSubprocess(routeClientRoot, "server-page-route-client-import");
|
|
247
|
+
expect(routeClientResult.success).toBe(true);
|
|
248
|
+
expect(await Bun.file(path.join(routeClientRoot, ".mandu", "client", "login.island.js")).exists()).toBe(true);
|
|
249
|
+
} finally {
|
|
250
|
+
await rm(routeClientRoot, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("fails when hydration is enabled but no clientModule can be resolved", async () => {
|
|
255
|
+
const missingRoot = await mkdtemp(path.join(import.meta.dir, ".tmp-hydration-no-client-"));
|
|
256
|
+
try {
|
|
257
|
+
await mkdir(path.join(missingRoot, "app", "login"), { recursive: true });
|
|
258
|
+
await mkdir(path.join(missingRoot, "src", "client", "widgets", "login-form"), { recursive: true });
|
|
259
|
+
await writeFile(
|
|
260
|
+
path.join(missingRoot, "package.json"),
|
|
261
|
+
JSON.stringify({ name: "mandu-hydration-no-client-test", type: "module" }, null, 2),
|
|
262
|
+
"utf-8",
|
|
263
|
+
);
|
|
264
|
+
await writeFile(
|
|
265
|
+
path.join(missingRoot, "app", "login", "page.tsx"),
|
|
266
|
+
'import { LoginForm } from "../../src/client/widgets/login-form/LoginForm.client";\n' +
|
|
267
|
+
"export default function LoginPage() {\n" +
|
|
268
|
+
" return <main><LoginForm /></main>;\n" +
|
|
269
|
+
"}\n",
|
|
270
|
+
"utf-8",
|
|
271
|
+
);
|
|
272
|
+
await writeFile(
|
|
273
|
+
path.join(missingRoot, "src", "client", "widgets", "login-form", "LoginForm.client.tsx"),
|
|
274
|
+
"export function LoginForm() { return <form />; }\n",
|
|
275
|
+
"utf-8",
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const noClientResult = await runBuildInSubprocess(missingRoot, "hydration-no-client-module");
|
|
279
|
+
const errors = noClientResult.errors.join("\n");
|
|
280
|
+
expect(noClientResult.success).toBe(false);
|
|
281
|
+
expect(errors).toContain("no clientModule could be resolved");
|
|
282
|
+
expect(errors).toContain("LoginForm.client");
|
|
283
|
+
expect(errors).toContain("run mandu generate");
|
|
284
|
+
} finally {
|
|
285
|
+
await rm(missingRoot, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|