@mandujs/core 0.54.17 → 0.54.19

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.
Files changed (46) hide show
  1. package/package.json +3 -1
  2. package/src/agent/__tests__/context.test.ts +94 -25
  3. package/src/agent/context.ts +17 -0
  4. package/src/agent/types.ts +32 -12
  5. package/src/agent/verify.ts +55 -24
  6. package/src/bundler/__snapshots__/build.test.ts.snap +5 -0
  7. package/src/bundler/__tests__/build-runner.ts +130 -17
  8. package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -0
  9. package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -33
  10. package/src/bundler/build.test.ts +478 -9
  11. package/src/bundler/build.ts +424 -746
  12. package/src/bundler/client-boundary-transform.ts +977 -0
  13. package/src/bundler/dev.ts +39 -112
  14. package/src/bundler/fast-refresh-preamble.ts +47 -0
  15. package/src/bundler/index.ts +3 -2
  16. package/src/bundler/manifest-schema.ts +10 -0
  17. package/src/bundler/types.ts +20 -2
  18. package/src/client/__tests__/props-serialization.test.ts +37 -0
  19. package/src/client/hydrate.ts +2 -2
  20. package/src/client/index.ts +1 -1
  21. package/src/client/props-serialization.ts +233 -0
  22. package/src/client/runtime-entry.ts +567 -0
  23. package/src/client/runtime.ts +1 -1
  24. package/src/client/serialize.ts +50 -404
  25. package/src/diagnose/__tests__/checks.test.ts +132 -17
  26. package/src/diagnose/checks.ts +184 -3
  27. package/src/diagnose/run.ts +10 -8
  28. package/src/generator/templates.test.ts +48 -5
  29. package/src/generator/templates.ts +10 -1
  30. package/src/internal/client-boundary.ts +266 -0
  31. package/src/internal/index.ts +2 -1
  32. package/src/router/client-entry.test.ts +154 -29
  33. package/src/router/client-entry.ts +111 -313
  34. package/src/router/fs-routes.test.ts +443 -1
  35. package/src/router/fs-routes.ts +16 -3
  36. package/src/router/fs-scanner.ts +176 -57
  37. package/src/router/fs-types.ts +11 -2
  38. package/src/router/route-source-analyzer.ts +521 -0
  39. package/src/runtime/__tests__/inline-client-hydration.test.ts +104 -1
  40. package/src/runtime/__tests__/page-render-response.test.ts +218 -0
  41. package/src/runtime/handlers.ts +50 -26
  42. package/src/runtime/page-render-response.ts +24 -1
  43. package/src/runtime/server.ts +14 -0
  44. package/src/runtime/ssr.ts +16 -5
  45. package/src/runtime/streaming-ssr.ts +119 -76
  46. package/src/spec/schema.ts +31 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.54.17",
3
+ "version": "0.54.19",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -65,6 +65,7 @@
65
65
  "./i18n": "./src/i18n/index.ts",
66
66
  "./id": "./src/id/index.ts",
67
67
  "./internal": "./src/internal/index.ts",
68
+ "./internal/client-boundary": "./src/internal/client-boundary.ts",
68
69
  "./observability": "./src/observability/index.ts",
69
70
  "./openapi/generator": "./src/openapi/generator.ts",
70
71
  "./perf": "./src/perf/index.ts",
@@ -203,6 +204,7 @@
203
204
  "fast-glob": "^3.3.2",
204
205
  "glob": "^13.0.0",
205
206
  "minimatch": "^10.1.1",
207
+ "typescript": ">=6.0.0",
206
208
  "zod": "^3.23.8"
207
209
  }
208
210
  }
@@ -64,14 +64,35 @@ describe("agent context", () => {
64
64
  await writeFile(root, ".mandu/routes.manifest.json", JSON.stringify({
65
65
  version: 1,
66
66
  routes: [
67
- {
68
- id: "home",
69
- pattern: "/",
70
- kind: "page",
71
- module: "app/page.tsx",
72
- componentModule: "app/page.tsx",
73
- layoutChain: [],
74
- },
67
+ {
68
+ id: "home",
69
+ pattern: "/",
70
+ kind: "page",
71
+ module: "app/page.tsx",
72
+ componentModule: "app/page.tsx",
73
+ layoutChain: [],
74
+ hydration: { strategy: "island", priority: "visible", preload: false },
75
+ boundaries: [
76
+ {
77
+ id: "home--0",
78
+ routeId: "home",
79
+ module: "src/client/HomeWidget.client.tsx",
80
+ importSpecifier: "@/client/HomeWidget.client",
81
+ exportName: "HomeWidget",
82
+ localName: "HomeWidget",
83
+ hydrate: "visible",
84
+ ordinal: 0,
85
+ propsSource: "inline",
86
+ propsKeys: ["user"],
87
+ hasSpreadProps: false,
88
+ source: {
89
+ file: "app/page.tsx",
90
+ line: 5,
91
+ column: 12,
92
+ },
93
+ },
94
+ ],
95
+ },
75
96
  {
76
97
  id: "api-ping",
77
98
  pattern: "/api/ping",
@@ -102,10 +123,26 @@ describe("agent context", () => {
102
123
  expect(context.framework).toBe("mandu");
103
124
  expect(context.project.name).toBe("agent-app");
104
125
  expect(context.project.packageManager).toBe("bun@1.3.14");
105
- expect(context.routeSource).toBe("manifest");
106
- expect(context.routes).toHaveLength(2);
107
- expect(context.pages.map((r) => r.id)).toEqual(["home"]);
108
- expect(context.apis.map((r) => r.id)).toEqual(["api-ping"]);
126
+ expect(context.routeSource).toBe("manifest");
127
+ expect(context.routes).toHaveLength(2);
128
+ expect(context.pages.map((r) => r.id)).toEqual(["home"]);
129
+ expect(context.pages[0]?.boundaryCount).toBe(1);
130
+ expect(context.pages[0]?.boundaries?.[0]).toMatchObject({
131
+ id: "home--0",
132
+ module: "src/client/HomeWidget.client.tsx",
133
+ importSpecifier: "@/client/HomeWidget.client",
134
+ exportName: "HomeWidget",
135
+ localName: "HomeWidget",
136
+ ordinal: 0,
137
+ hydrate: "visible",
138
+ propsKeys: ["user"],
139
+ source: {
140
+ file: "app/page.tsx",
141
+ line: 5,
142
+ column: 12,
143
+ },
144
+ });
145
+ expect(context.apis.map((r) => r.id)).toEqual(["api-ping"]);
109
146
  expect(context.apis[0]?.hasContractModule).toBe(true);
110
147
  expect(context.partials.map((p) => p.path)).toEqual(["app/dashboard/counter.partial.tsx"]);
111
148
  expect(context.islands.map((p) => p.path)).toEqual(["app/dashboard/chart.island.tsx"]);
@@ -125,10 +162,11 @@ describe("agent context", () => {
125
162
 
126
163
  expect(result.path).toBe(agentManifestPath(root));
127
164
  const manifest = await readAgentManifest(root);
128
- expect(manifest?.schemaVersion).toBe(1);
129
- expect(manifest?.project.name).toBe("agent-app");
130
- expect(manifest?.agentWorkflow.canonical).toEqual(["context", "plan", "apply", "verify", "repair"]);
131
- });
165
+ expect(manifest?.schemaVersion).toBe(1);
166
+ expect(manifest?.project.name).toBe("agent-app");
167
+ expect(manifest?.routes[0]?.boundaries?.[0]?.id).toBe("home--0");
168
+ expect(manifest?.agentWorkflow.canonical).toEqual(["context", "plan", "apply", "verify", "repair"]);
169
+ });
132
170
 
133
171
  it("builds a deterministic agent plan from intent", () => {
134
172
  const plan = buildAgentPlan({
@@ -202,9 +240,9 @@ describe("agent context", () => {
202
240
  expect(parsed.project.name).toBe("agent-app");
203
241
  });
204
242
 
205
- it("records changed file reasons and warns on internal API edits", async () => {
206
- const version = await runGit(root, ["--version"]);
207
- if (!version.ok) return;
243
+ it("records changed file reasons and warns on internal API edits", async () => {
244
+ const version = await runGit(root, ["--version"]);
245
+ if (!version.ok) return;
208
246
 
209
247
  expect((await runGit(root, ["init"])).ok).toBe(true);
210
248
  await runGit(root, ["config", "user.email", "agent@example.com"]);
@@ -226,12 +264,43 @@ describe("agent context", () => {
226
264
  expect(reason?.internalApi).toBe(true);
227
265
  expect(reason?.recommendedChecks).toContain("bun run check:public-api && bun run check:target-boundaries");
228
266
  expect(report.diagnostics.some((diag) => diag.code === "MANDU_VERIFY_INTERNAL_API_EDIT")).toBe(true);
229
- expect(report.suggestedCommands.map((cmd) => cmd.command)).toContain(
230
- "bun run check:public-api && bun run check:target-boundaries",
231
- );
232
- });
233
-
234
- it("turns a verify report into repair actions", async () => {
267
+ expect(report.suggestedCommands.map((cmd) => cmd.command)).toContain(
268
+ "bun run check:public-api && bun run check:target-boundaries",
269
+ );
270
+ });
271
+
272
+ it("recommends hydration gates for hydration-sensitive runtime edits", async () => {
273
+ const version = await runGit(root, ["--version"]);
274
+ if (!version.ok) return;
275
+
276
+ expect((await runGit(root, ["init"])).ok).toBe(true);
277
+ await runGit(root, ["config", "user.email", "agent@example.com"]);
278
+ await runGit(root, ["config", "user.name", "Agent"]);
279
+ expect((await runGit(root, ["add", "."])).ok).toBe(true);
280
+ expect((await runGit(root, ["commit", "-m", "initial"])).ok).toBe(true);
281
+
282
+ await writeFile(root, "packages/core/src/bundler/build.ts", "export const changed = true;\n");
283
+
284
+ const report = await buildAgentVerifyReport(root, {
285
+ includeDiagnose: false,
286
+ includeGuard: false,
287
+ includeContract: false,
288
+ });
289
+
290
+ const reason = report.changedFileReasons.find(
291
+ (entry) => entry.file === "packages/core/src/bundler/build.ts",
292
+ );
293
+ expect(reason?.recommendedChecks).toContain("bun run test:hydration-boundary");
294
+ expect(reason?.recommendedChecks).toContain("bun run test:hydration-e2e");
295
+ expect(reason?.recommendedChecks).toContain("bun run check:publish");
296
+
297
+ const suggestedCommands = report.suggestedCommands.map((cmd) => cmd.command);
298
+ expect(suggestedCommands).toContain("bun run test:hydration-boundary");
299
+ expect(suggestedCommands).toContain("bun run test:hydration-e2e");
300
+ expect(suggestedCommands).toContain("bun run check:publish");
301
+ });
302
+
303
+ it("turns a verify report into repair actions", async () => {
235
304
  await writeAgentVerifyReport(root, {
236
305
  schemaVersion: 1,
237
306
  framework: "mandu",
@@ -119,6 +119,7 @@ function summarizeRoute(route: RouteSpec | {
119
119
  methods?: string[];
120
120
  hydration?: { strategy?: string; priority?: string };
121
121
  clientModule?: string;
122
+ boundaries?: RouteSpec["boundaries"];
122
123
  contractModule?: string;
123
124
  layoutChain?: string[];
124
125
  metadataKind?: string;
@@ -127,6 +128,21 @@ function summarizeRoute(route: RouteSpec | {
127
128
  "metadataKind" in route && typeof route.metadataKind === "string"
128
129
  ? route.metadataKind
129
130
  : undefined;
131
+ const boundaries = Array.isArray(route.boundaries)
132
+ ? route.boundaries.map((boundary) => ({
133
+ id: boundary.id,
134
+ module: boundary.module,
135
+ ...(boundary.importSpecifier ? { importSpecifier: boundary.importSpecifier } : {}),
136
+ exportName: boundary.exportName,
137
+ localName: boundary.localName,
138
+ ordinal: boundary.ordinal,
139
+ hydrate: boundary.hydrate,
140
+ propsSource: boundary.propsSource,
141
+ propsKeys: boundary.propsKeys ?? [],
142
+ hasSpreadProps: !!boundary.hasSpreadProps,
143
+ source: boundary.source,
144
+ }))
145
+ : [];
130
146
  return {
131
147
  id: route.id,
132
148
  pattern: route.pattern,
@@ -144,6 +160,7 @@ function summarizeRoute(route: RouteSpec | {
144
160
  hasClientModule: typeof route.clientModule === "string" && route.clientModule.length > 0,
145
161
  hasContractModule:
146
162
  typeof route.contractModule === "string" && route.contractModule.length > 0,
163
+ ...(boundaries.length > 0 ? { boundaryCount: boundaries.length, boundaries } : {}),
147
164
  layoutDepth: Array.isArray(route.layoutChain) ? route.layoutChain.length : 0,
148
165
  ...(metadataKind ? { metadataKind } : {}),
149
166
  };
@@ -32,23 +32,43 @@ export interface AgentProjectSummary {
32
32
  configFile: string | null;
33
33
  }
34
34
 
35
- export interface AgentRouteSummary {
36
- id: string;
37
- pattern: string;
38
- kind: "page" | "api" | "metadata" | string;
35
+ export interface AgentRouteSummary {
36
+ id: string;
37
+ pattern: string;
38
+ kind: "page" | "api" | "metadata" | string;
39
39
  module: string;
40
40
  methods?: string[];
41
41
  hydration?: {
42
42
  strategy?: string;
43
43
  priority?: string;
44
- };
45
- hasClientModule: boolean;
46
- hasContractModule: boolean;
47
- layoutDepth: number;
48
- metadataKind?: string;
49
- }
50
-
51
- export interface AgentArtifactSummary {
44
+ };
45
+ hasClientModule: boolean;
46
+ hasContractModule: boolean;
47
+ boundaryCount?: number;
48
+ boundaries?: AgentRouteBoundarySummary[];
49
+ layoutDepth: number;
50
+ metadataKind?: string;
51
+ }
52
+
53
+ export interface AgentRouteBoundarySummary {
54
+ id: string;
55
+ module: string;
56
+ importSpecifier?: string;
57
+ exportName: string;
58
+ localName: string;
59
+ ordinal: number;
60
+ hydrate: string;
61
+ propsSource: string;
62
+ propsKeys: string[];
63
+ hasSpreadProps: boolean;
64
+ source: {
65
+ file: string;
66
+ line: number;
67
+ column: number;
68
+ };
69
+ }
70
+
71
+ export interface AgentArtifactSummary {
52
72
  path: string;
53
73
  kind: "partial" | "island" | "slot" | "contract";
54
74
  }
@@ -245,12 +245,17 @@ function commandSuggestions(
245
245
  if (files.some((file) => file.startsWith("packages/mcp/"))) {
246
246
  add("bun test packages/mcp/tests", "MCP package files changed.", true);
247
247
  }
248
- if (files.some((file) => file.includes("package.json") || file === "bun.lock")) {
249
- add("bun run check:publish", "Package metadata or lockfile changed.", true);
250
- }
251
- if (changedFileReasons.some((entry) => entry.internalApi)) {
252
- add("bun run check:public-api && bun run check:target-boundaries", "Internal framework boundaries changed.", true);
253
- }
248
+ if (files.some((file) => file.includes("package.json") || file === "bun.lock")) {
249
+ add("bun run check:publish", "Package metadata or lockfile changed.", true);
250
+ }
251
+ if (files.some(isHydrationSensitiveEdit)) {
252
+ add("bun run test:hydration-boundary", "Hydration-sensitive runtime/router/bundler files changed.", true);
253
+ add("bun run test:hydration-e2e", "Hydration-sensitive changes need browser-level validation.", true);
254
+ add("bun run check:publish", "Pre-publish hydration gates should pass before release.", true);
255
+ }
256
+ if (changedFileReasons.some((entry) => entry.internalApi)) {
257
+ add("bun run check:public-api && bun run check:target-boundaries", "Internal framework boundaries changed.", true);
258
+ }
254
259
  if (diagnostics.some((d) => d.severity === "error" || d.severity === "fatal")) {
255
260
  add("mandu agent repair --from .mandu/agent-verify.json", "Verification produced repairable diagnostics.", false);
256
261
  }
@@ -258,22 +263,40 @@ function commandSuggestions(
258
263
  return out;
259
264
  }
260
265
 
261
- function isInternalApiEdit(file: string): boolean {
262
- return [
263
- "packages/core/src/runtime/",
264
- "packages/core/src/bundler/",
266
+ function isInternalApiEdit(file: string): boolean {
267
+ return [
268
+ "packages/core/src/runtime/",
269
+ "packages/core/src/bundler/",
265
270
  "packages/core/src/server/",
266
271
  "packages/core/src/guard/",
267
272
  "packages/core/src/spec/",
268
273
  "packages/core/src/router/",
269
274
  "packages/core/src/internal/",
270
- ].some((prefix) => file.startsWith(prefix));
271
- }
272
-
273
- function changedFileReason(file: string): AgentChangedFileReason {
274
- const normalized = normalizePath(file) ?? file;
275
- const reasons: string[] = [];
276
- const recommendedChecks: string[] = [];
275
+ ].some((prefix) => file.startsWith(prefix));
276
+ }
277
+
278
+ function isHydrationSensitiveEdit(file: string): boolean {
279
+ return [
280
+ "packages/core/src/runtime/page-render-response.ts",
281
+ "packages/core/src/runtime/server.ts",
282
+ "packages/core/src/runtime/ssr.ts",
283
+ "packages/core/src/runtime/streaming-ssr.ts",
284
+ "packages/core/src/internal/client-boundary.ts",
285
+ "packages/core/src/bundler/build.ts",
286
+ "packages/core/src/bundler/client-boundary-transform.ts",
287
+ "packages/core/src/router/client-entry.ts",
288
+ "packages/core/src/router/fs-routes.ts",
289
+ "packages/core/src/router/fs-scanner.ts",
290
+ "packages/core/src/client/serialize.ts",
291
+ "packages/core/src/client/props-serialization.ts",
292
+ "packages/core/src/client/runtime-entry.ts",
293
+ ].includes(file);
294
+ }
295
+
296
+ function changedFileReason(file: string): AgentChangedFileReason {
297
+ const normalized = normalizePath(file) ?? file;
298
+ const reasons: string[] = [];
299
+ const recommendedChecks: string[] = [];
277
300
 
278
301
  if (/\.(ts|tsx|js|jsx)$/.test(normalized)) {
279
302
  reasons.push("Source code changed.");
@@ -295,13 +318,21 @@ function changedFileReason(file: string): AgentChangedFileReason {
295
318
  reasons.push("User-facing documentation changed.");
296
319
  recommendedChecks.push("bun run check:docs-drift");
297
320
  }
298
- if (normalized === "package.json" || normalized === "bun.lock" || normalized.endsWith("/package.json")) {
299
- reasons.push("Package metadata or dependency graph changed.");
300
- recommendedChecks.push("bun run check:publish");
301
- }
302
-
303
- const internalApi = isInternalApiEdit(normalized);
304
- if (internalApi) {
321
+ if (normalized === "package.json" || normalized === "bun.lock" || normalized.endsWith("/package.json")) {
322
+ reasons.push("Package metadata or dependency graph changed.");
323
+ recommendedChecks.push("bun run check:publish");
324
+ }
325
+ if (isHydrationSensitiveEdit(normalized)) {
326
+ reasons.push("Hydration/runtime/client boundary behavior changed.");
327
+ recommendedChecks.push(
328
+ "bun run test:hydration-boundary",
329
+ "bun run test:hydration-e2e",
330
+ "bun run check:publish",
331
+ );
332
+ }
333
+
334
+ const internalApi = isInternalApiEdit(normalized);
335
+ if (internalApi) {
305
336
  reasons.push("Framework internal API changed.");
306
337
  recommendedChecks.push("bun run check:public-api && bun run check:target-boundaries");
307
338
  }
@@ -0,0 +1,5 @@
1
+ {
2
+ "snapshots": {
3
+ "client boundary manifest metadata": "{\n \"bundleBoundaries\": {\n \"boundary-demo--0\": {\n \"exportName\": \"BoundaryWidget\",\n \"hydrate\": \"visible\",\n \"js\": \"/.mandu/client/boundary-demo--0.boundary.js\",\n \"module\": \"src/client/BoundaryWidget.client.tsx\",\n \"priority\": \"visible\",\n \"route\": \"boundary-demo\"\n }\n },\n \"routes\": [\n {\n \"boundaries\": [\n {\n \"exportName\": \"BoundaryWidget\",\n \"hasSpreadProps\": false,\n \"hydrate\": \"visible\",\n \"id\": \"boundary-demo--0\",\n \"importSpecifier\": \"@/client/BoundaryWidget.client\",\n \"localName\": \"BoundaryWidget\",\n \"module\": \"src/client/BoundaryWidget.client.tsx\",\n \"ordinal\": 0,\n \"propsKeys\": [\n \"label\"\n ],\n \"propsSource\": \"inline\",\n \"routeId\": \"boundary-demo\",\n \"source\": {\n \"column\": 12,\n \"file\": \"app/boundary/page.tsx\",\n \"line\": 5\n }\n }\n ],\n \"componentModule\": \"app/boundary/page.tsx\",\n \"id\": \"boundary-demo\",\n \"module\": \"app/boundary/page.tsx\"\n }\n ]\n}\n"
4
+ }
5
+ }
@@ -51,8 +51,11 @@
51
51
  * clean tmpdir with no prior `.mandu/vendor-cache/`.
52
52
  */
53
53
 
54
- import { buildClientBundles } from "../build";
55
- import type { RoutesManifest } from "../../spec/schema";
54
+ import { buildClientBundles } from "../build";
55
+ import type { RoutesManifest } from "../../spec/schema";
56
+ import { mkdir } from "fs/promises";
57
+ import path from "path";
58
+ import { generateManifest } from "../../router/fs-routes";
56
59
 
57
60
  const rootDir = process.argv[2];
58
61
  if (!rootDir) {
@@ -61,7 +64,9 @@ if (!rootDir) {
61
64
  }
62
65
  const mode = process.argv[3] ?? "default";
63
66
 
64
- const manifest: RoutesManifest = mode === "server-page-client-module"
67
+ const manifest: RoutesManifest = mode === "client-boundary-generated-transitive"
68
+ ? (await generateManifest(rootDir)).manifest
69
+ : mode === "server-page-client-module"
65
70
  ? {
66
71
  version: 1,
67
72
  routes: [
@@ -118,6 +123,83 @@ const manifest: RoutesManifest = mode === "server-page-client-module"
118
123
  },
119
124
  ],
120
125
  }
126
+ : mode === "client-boundary"
127
+ || mode === "client-boundary-target"
128
+ || mode === "client-boundary-skip-framework"
129
+ || mode === "client-boundary-duplicate-id"
130
+ ? {
131
+ version: 1,
132
+ routes: [
133
+ {
134
+ id: "boundary-demo",
135
+ kind: "page",
136
+ pattern: "/boundary",
137
+ module: "app/boundary/page.tsx",
138
+ componentModule: "app/boundary/page.tsx",
139
+ hydration: {
140
+ strategy: "island",
141
+ priority: "visible",
142
+ preload: false,
143
+ },
144
+ boundaries: [
145
+ {
146
+ id: "boundary-demo--0",
147
+ routeId: "boundary-demo",
148
+ module: "src/client/BoundaryWidget.client.tsx",
149
+ importSpecifier: "@/client/BoundaryWidget.client",
150
+ exportName: "BoundaryWidget",
151
+ localName: "BoundaryWidget",
152
+ hydrate: "visible",
153
+ ordinal: 0,
154
+ propsSource: "inline",
155
+ propsKeys: ["label"],
156
+ hasSpreadProps: false,
157
+ source: {
158
+ file: "app/boundary/page.tsx",
159
+ line: 5,
160
+ column: 12,
161
+ },
162
+ },
163
+ ],
164
+ },
165
+ ...(mode === "client-boundary-duplicate-id"
166
+ ? [
167
+ {
168
+ id: "boundary-copy",
169
+ kind: "page" as const,
170
+ pattern: "/boundary-copy",
171
+ module: "app/boundary/page.tsx",
172
+ componentModule: "app/boundary/page.tsx",
173
+ hydration: {
174
+ strategy: "island" as const,
175
+ priority: "visible" as const,
176
+ preload: false,
177
+ },
178
+ boundaries: [
179
+ {
180
+ id: "boundary-demo--0",
181
+ routeId: "boundary-copy",
182
+ module: "src/client/BoundaryWidget.client.tsx",
183
+ importSpecifier: "@/client/BoundaryWidget.client",
184
+ exportName: "BoundaryWidget",
185
+ localName: "BoundaryWidget",
186
+ hydrate: "visible" as const,
187
+ ordinal: 0,
188
+ propsSource: "inline" as const,
189
+ propsKeys: ["label"],
190
+ hasSpreadProps: false,
191
+ source: {
192
+ file: "app/boundary/page.tsx",
193
+ line: 8,
194
+ column: 12,
195
+ },
196
+ },
197
+ ],
198
+ },
199
+ ]
200
+ : []),
201
+ ],
202
+ }
121
203
  : mode === "i18n-locale-route-id"
122
204
  ? {
123
205
  version: 1,
@@ -156,27 +238,58 @@ const manifest: RoutesManifest = mode === "server-page-client-module"
156
238
  ],
157
239
  };
158
240
 
159
- try {
160
- const result = await buildClientBundles(manifest, rootDir, {
161
- minify: false,
162
- sourcemap: false,
163
- splitting: false,
164
- });
241
+ try {
242
+ if (mode === "client-boundary-target" || mode === "client-boundary-skip-framework") {
243
+ await mkdir(path.join(rootDir, ".mandu"), { recursive: true });
244
+ await Bun.write(
245
+ path.join(rootDir, ".mandu", "manifest.json"),
246
+ JSON.stringify(
247
+ {
248
+ version: 1,
249
+ buildTime: "2026-05-23T00:00:00.000Z",
250
+ env: "development",
251
+ bundles: {},
252
+ shared: {
253
+ runtime: "/.mandu/client/_runtime.js",
254
+ vendor: "/.mandu/client/_react.js",
255
+ router: "/.mandu/client/_router.js",
256
+ },
257
+ },
258
+ null,
259
+ 2,
260
+ ),
261
+ );
262
+ }
263
+
264
+ const result = await buildClientBundles(manifest, rootDir, {
265
+ minify: false,
266
+ sourcemap: false,
267
+ splitting: false,
268
+ ...(mode === "client-boundary-target" ? { targetRouteIds: ["boundary-demo"] } : {}),
269
+ ...(mode === "client-boundary-skip-framework" ? { skipFrameworkBundles: true } : {}),
270
+ });
165
271
  process.stdout.write(
166
272
  JSON.stringify({
167
- success: result.success,
168
- errors: result.errors,
169
- manifest: {
170
- shared: {
273
+ success: result.success,
274
+ errors: result.errors,
275
+ manifest: {
276
+ routes: manifest.routes.map((route) => ({
277
+ id: route.id,
278
+ module: route.module,
279
+ componentModule: route.componentModule,
280
+ boundaries: route.kind === "page" ? route.boundaries ?? [] : [],
281
+ })),
282
+ shared: {
171
283
  runtime: result.manifest.shared?.runtime ?? null,
172
284
  vendor: result.manifest.shared?.vendor ?? null,
173
285
  router: result.manifest.shared?.router ?? null,
174
286
  fastRefresh: result.manifest.shared?.fastRefresh ?? null,
175
287
  },
176
- bundles: result.manifest.bundles ?? {},
177
- },
178
- }) + "\n",
179
- );
288
+ bundles: result.manifest.bundles ?? {},
289
+ boundaries: result.manifest.boundaries ?? {},
290
+ },
291
+ }) + "\n",
292
+ );
180
293
  process.exit(result.success ? 0 : 1);
181
294
  } catch (err) {
182
295
  process.stdout.write(