@mandujs/core 0.54.18 → 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.
- package/package.json +1 -1
- package/src/agent/__tests__/context.test.ts +40 -9
- package/src/agent/verify.ts +55 -24
- package/src/bundler/build.test.ts +40 -3
- package/src/bundler/build.ts +35 -680
- package/src/client/__tests__/props-serialization.test.ts +37 -0
- package/src/client/hydrate.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/props-serialization.ts +233 -0
- package/src/client/runtime-entry.ts +567 -0
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -404
- package/src/diagnose/__tests__/checks.test.ts +15 -0
- package/src/diagnose/checks.ts +1 -1
- package/src/router/client-entry.test.ts +111 -23
- package/src/router/client-entry.ts +78 -301
- package/src/router/fs-routes.test.ts +55 -0
- package/src/router/fs-scanner.ts +11 -40
- package/src/router/fs-types.ts +7 -1
- package/src/router/route-source-analyzer.ts +521 -0
- package/src/runtime/__tests__/inline-client-hydration.test.ts +104 -1
- package/src/runtime/__tests__/page-render-response.test.ts +6 -0
- package/src/runtime/page-render-response.ts +23 -1
- package/src/runtime/server.ts +14 -0
package/package.json
CHANGED
|
@@ -240,9 +240,9 @@ describe("agent context", () => {
|
|
|
240
240
|
expect(parsed.project.name).toBe("agent-app");
|
|
241
241
|
});
|
|
242
242
|
|
|
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;
|
|
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;
|
|
246
246
|
|
|
247
247
|
expect((await runGit(root, ["init"])).ok).toBe(true);
|
|
248
248
|
await runGit(root, ["config", "user.email", "agent@example.com"]);
|
|
@@ -264,12 +264,43 @@ describe("agent context", () => {
|
|
|
264
264
|
expect(reason?.internalApi).toBe(true);
|
|
265
265
|
expect(reason?.recommendedChecks).toContain("bun run check:public-api && bun run check:target-boundaries");
|
|
266
266
|
expect(report.diagnostics.some((diag) => diag.code === "MANDU_VERIFY_INTERNAL_API_EDIT")).toBe(true);
|
|
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("
|
|
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 () => {
|
|
273
304
|
await writeAgentVerifyReport(root, {
|
|
274
305
|
schemaVersion: 1,
|
|
275
306
|
framework: "mandu",
|
package/src/agent/verify.ts
CHANGED
|
@@ -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 (
|
|
252
|
-
add("bun run
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
}
|
|
@@ -29,11 +29,11 @@ async function evaluateGeneratedHydrationRuntime(): Promise<{ hydrateIslands: ()
|
|
|
29
29
|
const runtimeSource = await readFile(await Bun.file(sourcePath).exists() ? sourcePath : bundledPath, "utf-8");
|
|
30
30
|
const instrumented = runtimeSource
|
|
31
31
|
.replace(
|
|
32
|
-
/import\s+React,\s*\{
|
|
32
|
+
/import\s+React,\s*\{[^}]*\}\s+from\s+['"]react['"];?/,
|
|
33
33
|
"const React = globalThis.__MANDU_TEST_REACT__; const { useState, useEffect, Component } = React;",
|
|
34
34
|
)
|
|
35
35
|
.replace(
|
|
36
|
-
/import\s+\{
|
|
36
|
+
/import\s+\{[^}]*\}\s+from\s+['"]react-dom\/client['"];?/,
|
|
37
37
|
"const { hydrateRoot, createRoot } = globalThis.__MANDU_TEST_REACT_DOM_CLIENT__;",
|
|
38
38
|
)
|
|
39
39
|
.replace(
|
|
@@ -308,7 +308,7 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
308
308
|
expect(runtimeSource).toContain("data-mandu-props");
|
|
309
309
|
expect(runtimeSource).toContain("Missing boundary-local props for transformed client boundary");
|
|
310
310
|
expect(runtimeSource).toContain("warnedBoundaryPropFallbacks");
|
|
311
|
-
expect(runtimeSource).toContain("
|
|
311
|
+
expect(runtimeSource).toContain("deserializeProps");
|
|
312
312
|
expect(runtimeSource).toContain("new Date");
|
|
313
313
|
expect(runtimeSource).toContain("new Map");
|
|
314
314
|
});
|
|
@@ -437,6 +437,43 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
437
437
|
);
|
|
438
438
|
});
|
|
439
439
|
|
|
440
|
+
test("runtime falls back to route-level server data when boundary-local props are absent", async () => {
|
|
441
|
+
const modulePath = path.join(rootDir, ".mandu", "client", "runtime-route-data-fallback.js");
|
|
442
|
+
await writeFile(
|
|
443
|
+
modulePath,
|
|
444
|
+
`
|
|
445
|
+
export default function RouteFallbackBoundary(props) {
|
|
446
|
+
globalThis.__MANDU_TEST_ROUTE_DATA_FALLBACK_PROPS__ = props;
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
`,
|
|
450
|
+
"utf-8",
|
|
451
|
+
);
|
|
452
|
+
Object.assign(globalThis, {
|
|
453
|
+
__MANDU_TEST_ROUTE_DATA_FALLBACK_PROPS__: undefined,
|
|
454
|
+
});
|
|
455
|
+
document.body.innerHTML = `
|
|
456
|
+
<div
|
|
457
|
+
data-mandu-island="route-fallback--0"
|
|
458
|
+
data-mandu-boundary-id="route-fallback--0"
|
|
459
|
+
data-mandu-route-id="route-fallback"
|
|
460
|
+
data-mandu-src="${pathToFileURL(modulePath).href}?t=${Date.now()}"
|
|
461
|
+
data-hydrate="load"
|
|
462
|
+
></div>
|
|
463
|
+
`;
|
|
464
|
+
(window as typeof window & { __MANDU_DATA__?: unknown; __MANDU_ROOTS__?: Map<string, unknown> }).__MANDU_DATA__ = {
|
|
465
|
+
"route-fallback": { serverData: { label: "route-level" } },
|
|
466
|
+
};
|
|
467
|
+
(window as typeof window & { __MANDU_ROOTS__?: Map<string, unknown> }).__MANDU_ROOTS__ = new Map();
|
|
468
|
+
|
|
469
|
+
const runtime = await evaluateGeneratedHydrationRuntime();
|
|
470
|
+
runtime.hydrateIslands();
|
|
471
|
+
|
|
472
|
+
await waitForRuntimeAssertion(() =>
|
|
473
|
+
(globalThis as typeof globalThis & { __MANDU_TEST_ROUTE_DATA_FALLBACK_PROPS__?: { label?: string } }).__MANDU_TEST_ROUTE_DATA_FALLBACK_PROPS__?.label === "route-level"
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
440
477
|
test("does not bundle a server page when stale manifest marks page.tsx as clientModule", async () => {
|
|
441
478
|
const staleRoot = await mkRepoTempDir("stale-client-module-");
|
|
442
479
|
try {
|