@mandujs/core 0.54.29 → 0.54.30
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/build.test.ts +34 -0
- package/src/client/runtime-entry.ts +31 -0
package/package.json
CHANGED
|
@@ -362,6 +362,40 @@ describe("buildClientBundles vendor shims", () => {
|
|
|
362
362
|
expect((globalThis as typeof globalThis & { __MANDU_TEST_DEFAULT_PROPS__?: unknown }).__MANDU_TEST_DEFAULT_PROPS__).toBeUndefined();
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
+
test("surfaces an island hydration failure to the DevTools hook (#324)", async () => {
|
|
366
|
+
const modulePath = path.join(rootDir, ".mandu", "client", "runtime-failing-boundary.js");
|
|
367
|
+
// A module that throws at import time → loadAndHydrate's catch runs.
|
|
368
|
+
await writeFile(modulePath, `throw new Error("boom at import");`, "utf-8");
|
|
369
|
+
|
|
370
|
+
const emitted: Array<{ type?: string; data?: { message?: string; islandId?: string } }> = [];
|
|
371
|
+
(window as unknown as { __MANDU_DEVTOOLS_HOOK__?: { emit: (e: unknown) => void } }).__MANDU_DEVTOOLS_HOOK__ = {
|
|
372
|
+
emit: (e: unknown) => {
|
|
373
|
+
emitted.push(e as { type?: string });
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
document.body.innerHTML = `
|
|
378
|
+
<div
|
|
379
|
+
data-mandu-island="failing--0"
|
|
380
|
+
data-mandu-src="${pathToFileURL(modulePath).href}?t=${Date.now()}"
|
|
381
|
+
data-hydrate="load"
|
|
382
|
+
></div>
|
|
383
|
+
`;
|
|
384
|
+
(window as typeof window & { __MANDU_ROOTS__?: Map<string, unknown> }).__MANDU_ROOTS__ = new Map();
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const runtime = await evaluateGeneratedHydrationRuntime();
|
|
388
|
+
runtime.hydrateIslands();
|
|
389
|
+
|
|
390
|
+
await waitForRuntimeAssertion(() => emitted.some((e) => e.type === "error"));
|
|
391
|
+
const errorEvent = emitted.find((e) => e.type === "error");
|
|
392
|
+
expect(errorEvent?.data?.islandId).toBe("failing--0");
|
|
393
|
+
expect(errorEvent?.data?.message).toContain("boom at import");
|
|
394
|
+
} finally {
|
|
395
|
+
delete (window as unknown as { __MANDU_DEVTOOLS_HOOK__?: unknown }).__MANDU_DEVTOOLS_HOOK__;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
365
399
|
test("runtime hydrates default client boundary from boundary-local props", async () => {
|
|
366
400
|
const modulePath = path.join(rootDir, ".mandu", "client", "runtime-default-boundary.js");
|
|
367
401
|
await writeFile(
|
|
@@ -160,6 +160,7 @@ class IslandErrorBoundary extends Component<IslandErrorBoundaryProps, IslandErro
|
|
|
160
160
|
|
|
161
161
|
componentDidCatch(error: Error, errorInfo: unknown): void {
|
|
162
162
|
console.error("[Mandu] Island error:", this.props.islandId, error, errorInfo);
|
|
163
|
+
emitIslandHydrationError(this.props.islandId, error);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
reset = (): void => {
|
|
@@ -371,6 +372,35 @@ function isManduIslandExport(value: unknown): value is ManduIslandExport {
|
|
|
371
372
|
"definition" in value;
|
|
372
373
|
}
|
|
373
374
|
|
|
375
|
+
// #324: surface island hydration/render failures to the in-page DevTools so
|
|
376
|
+
// its health badge and Issues counter reflect them instead of staying
|
|
377
|
+
// "HEALTHY". Render-time errors are swallowed by IslandErrorBoundary and never
|
|
378
|
+
// reach the global error listener, so we emit them to the devtools hook here.
|
|
379
|
+
function emitIslandHydrationError(islandId: string, error: unknown): void {
|
|
380
|
+
const devtoolsHook = (window as Window & {
|
|
381
|
+
__MANDU_DEVTOOLS_HOOK__?: RuntimeDevtoolsHook;
|
|
382
|
+
}).__MANDU_DEVTOOLS_HOOK__;
|
|
383
|
+
if (!devtoolsHook) return;
|
|
384
|
+
try {
|
|
385
|
+
devtoolsHook.emit({
|
|
386
|
+
type: "error",
|
|
387
|
+
timestamp: Date.now(),
|
|
388
|
+
data: {
|
|
389
|
+
id: "island-" + islandId + "-" + Date.now(),
|
|
390
|
+
type: "runtime",
|
|
391
|
+
severity: "error",
|
|
392
|
+
message: error instanceof Error ? error.message : String(error),
|
|
393
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
url: location.href,
|
|
396
|
+
islandId,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
} catch {
|
|
400
|
+
// DevTools must never break hydration.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
374
404
|
async function loadAndHydrate(element: HTMLElement, src: string): Promise<void> {
|
|
375
405
|
const id = element.getAttribute("data-mandu-island");
|
|
376
406
|
if (!id) {
|
|
@@ -490,6 +520,7 @@ async function loadAndHydrate(element: HTMLElement, src: string): Promise<void>
|
|
|
490
520
|
} catch (error) {
|
|
491
521
|
console.error("[Mandu] Hydration failed for", islandId, error);
|
|
492
522
|
element.setAttribute("data-mandu-error", "true");
|
|
523
|
+
emitIslandHydrationError(islandId, error);
|
|
493
524
|
|
|
494
525
|
element.dispatchEvent(new CustomEvent("mandu:hydration-error", {
|
|
495
526
|
bubbles: true,
|