@mandujs/core 0.54.16 → 0.54.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/agent/__tests__/context.test.ts +54 -16
- package/src/agent/context.ts +17 -0
- package/src/agent/types.ts +32 -12
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -0
- package/src/bundler/__tests__/build-runner.ts +130 -17
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -0
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -33
- package/src/bundler/build.test.ts +440 -8
- package/src/bundler/build.ts +455 -132
- package/src/bundler/client-boundary-transform.ts +977 -0
- package/src/bundler/dev.ts +39 -112
- package/src/bundler/fast-refresh-preamble.ts +47 -0
- package/src/bundler/index.ts +3 -2
- package/src/bundler/manifest-schema.ts +10 -0
- package/src/bundler/types.ts +20 -2
- package/src/diagnose/__tests__/checks.test.ts +117 -17
- package/src/diagnose/checks.ts +184 -3
- package/src/diagnose/run.ts +10 -8
- package/src/generator/templates.test.ts +48 -5
- package/src/generator/templates.ts +10 -1
- package/src/internal/client-boundary.ts +266 -0
- package/src/internal/index.ts +2 -1
- package/src/router/client-entry.test.ts +43 -6
- package/src/router/client-entry.ts +33 -12
- package/src/router/fs-routes.test.ts +388 -1
- package/src/router/fs-routes.ts +16 -3
- package/src/router/fs-scanner.ts +166 -18
- package/src/router/fs-types.ts +4 -1
- package/src/runtime/__tests__/inline-client-hydration.test.ts +134 -0
- package/src/runtime/__tests__/page-render-response.test.ts +212 -0
- package/src/runtime/handlers.ts +50 -26
- package/src/runtime/page-render-response.ts +43 -3
- package/src/runtime/server.ts +42 -5
- package/src/runtime/ssr.ts +16 -5
- package/src/runtime/streaming-ssr.ts +119 -76
- 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.
|
|
3
|
+
"version": "0.54.18",
|
|
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.
|
|
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?.
|
|
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({
|
package/src/agent/context.ts
CHANGED
|
@@ -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
|
};
|
package/src/agent/types.ts
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|
|
@@ -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 === "
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|