@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.54.29",
3
+ "version": "0.54.30",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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,