@mandujs/core 0.25.0 → 0.25.1

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.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -62,14 +62,24 @@ afterAll(async () => {
62
62
  }
63
63
  });
64
64
 
65
- // Bun.build has observed cross-worker races under bun:test's parallel file
66
- // execution — when tests shuffle under --randomize, this file sometimes
67
- // collides with another bundler test in a sibling worker and fails with
68
- // "AggregateError: Bundle failed" + missing shim outputs. Gate on an env
69
- // var so CI can run randomize-mode across everything else, then run this
70
- // suite in a dedicated serial pass. Local `bun run test:core` (no env set)
71
- // still runs these tests fully. Tracked as Phase 0.6.
72
- describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")("buildClientBundles vendor shims", () => {
65
+ // Historical note `MANDU_SKIP_BUNDLER_TESTS` gate REMOVED.
66
+ //
67
+ // A previous revision gated this describe block behind
68
+ // `describe.skipIf(MANDU_SKIP_BUNDLER_TESTS === "1")` because running
69
+ // `bun test src/bundler/` without the gate hung indefinitely on Windows
70
+ // (see Phase 0.6 and `docs/qa/wave-R2-integration-report.md`). Root cause
71
+ // was NOT actually in THIS file it was a deadlock in `safe-build.test.ts`'s
72
+ // "slot handoff" regression test, which drove Bun's microtask queue with a
73
+ // `while (!stop) { await Promise.resolve() }` sampler. That starved libuv
74
+ // I/O callbacks, so the 7 parallel `safeBuild()` calls never completed, the
75
+ // whole test process hung, and downstream test files (including this one
76
+ // when run in the same invocation) looked flaky when they were simply
77
+ // never reached. The handoff sampler now yields via `setImmediate`, which
78
+ // unblocks Bun.build completion and makes `bun test src/bundler/` finish
79
+ // deterministically in ~35s on Windows. Confirmed green 3/3 runs without
80
+ // the gate on 2026-04-20. If you are tempted to re-introduce the skip here,
81
+ // first check whether a sibling test is starving the event loop.
82
+ describe("buildClientBundles vendor shims", () => {
73
83
  test("build succeeds", () => {
74
84
  if (!result.success) {
75
85
  console.error("[build.test] errors:", result.errors);
@@ -141,16 +141,34 @@ describe("safeBuild", () => {
141
141
 
142
142
  let peak = 0;
143
143
  let samples = 0;
144
- // Sample at microtask granularity more aggressive than the setInterval
145
- // sampler in the earlier test so the cap+1 window has a realistic
146
- // chance of being observed if the bug returned.
144
+ // Sample active-slot count while the build burst is in flight. An earlier
145
+ // revision of this test used a `while (!stop) { await Promise.resolve() }`
146
+ // microtask busy-loop to push sampling granularity below setInterval's
147
+ // Windows 4ms-ish clamp. That deadlocks under Bun 1.3.x on Windows:
148
+ // `await Promise.resolve()` stays on the microtask queue, which runs
149
+ // to exhaustion before Bun's libuv I/O phase — so Bun.build completion
150
+ // callbacks never fire, `releaseSlot()` never runs, and the promises
151
+ // returned by the 7 parallel `safeBuild()` calls hang indefinitely.
152
+ // Reproduction: `bun test src/bundler/safe-build.test.ts` times out with
153
+ // only the banner printed (confirmed with a standalone repro of the
154
+ // sampler + 7 safeBuild calls — hung at "start" past 60s).
155
+ //
156
+ // Fix: yield to the macrotask queue via `setImmediate`. This lets
157
+ // libuv I/O callbacks run between samples, so Bun.build completes and
158
+ // `releaseSlot()` advances the queue. Per-tick granularity on Node/Bun
159
+ // is still sub-millisecond and fires ~hundreds of times during a 7-
160
+ // build burst — more than enough to statistically catch the cap+1
161
+ // regression window if it ever returned (the window is microtask-sized,
162
+ // but any cross-tick sampling with high fan-out has a realistic chance
163
+ // of landing inside it). The strict assertion is still `peak <= max`.
147
164
  let stop = false;
148
165
  const sample = async () => {
149
166
  while (!stop) {
150
167
  const { active } = _getConcurrencyState();
151
168
  if (active > peak) peak = active;
152
169
  samples++;
153
- await Promise.resolve(); // yield to microtask queue
170
+ // Yield to libuv I/O phase so Bun.build callbacks can fire.
171
+ await new Promise<void>((resolve) => setImmediate(resolve));
154
172
  }
155
173
  };
156
174
  const sampler = sample();
@@ -1547,7 +1547,27 @@ async function loadPageData(
1547
1547
  const exportedObj = exported as Record<string, unknown> | null;
1548
1548
  const component = typeof exported === "function"
1549
1549
  ? (exported as RouteComponent)
1550
- : (exportedObj?.component ?? exported);
1550
+ : (exportedObj?.component ?? undefined);
1551
+ // DX-1: pageLoader 경로에서 malformed default export를 silent 404 로
1552
+ //보내지 않고 명시적 에러로 즉시 실패시킨다. 이전에는
1553
+ // `export default "hello"` / `export default undefined` / named-only
1554
+ // 같은 실수가 registerRouteComponent(undefined) → defaultCreateApp
1555
+ // 에서 404로 렌더되어 사용자가 원인을 추적하기 어려웠음. 여기서 throw
1556
+ // 하면 try/catch(아래) 의 createPageLoadErrorResponse 가 500 응답과
1557
+ // 함께 route.id + pattern 을 출력해주므로 개발자가 바로 인지한다.
1558
+ if (typeof component !== "function") {
1559
+ const defaultSummary =
1560
+ exported === undefined
1561
+ ? "undefined (missing `export default`)"
1562
+ : exported === null
1563
+ ? "null"
1564
+ : `type ${typeof exported}`;
1565
+ throw new Error(
1566
+ `[Mandu] Page module for '${route.id}' (pattern ${route.pattern}) has an invalid default export: ${defaultSummary}. ` +
1567
+ "Expected `export default function Page() {…}` or `export default { component, filling }`. " +
1568
+ "If the page file is empty or only has named exports, add a default-exported React component."
1569
+ );
1570
+ }
1551
1571
  registry.registerRouteComponent(route.id, component as RouteComponent);
1552
1572
 
1553
1573
  // #186: page 모듈에서 metadata / generateMetadata export 캐싱
@@ -2201,8 +2221,22 @@ async function handlePageRoute(
2201
2221
  const cache = settings.cacheStore;
2202
2222
  // Only call ensurePageRouteMetadata when a pageHandler exists;
2203
2223
  // routes registered via registerPageLoader are handled by loadPageData instead.
2224
+ // DX-1: if the pageHandler returns a malformed registration (component is
2225
+ // not a function), ensurePageRouteMetadata now throws with a descriptive
2226
+ // message. Catch it here so the request becomes a loud 500 instead of
2227
+ // bubbling up as an opaque "Internal Server Error".
2204
2228
  if (registry.pageHandlers.has(route.id)) {
2205
- await ensurePageRouteMetadata(route.id, registry);
2229
+ try {
2230
+ await ensurePageRouteMetadata(route.id, registry);
2231
+ } catch (error) {
2232
+ const pageError = createPageLoadErrorResponse(
2233
+ route.id,
2234
+ route.pattern,
2235
+ error instanceof Error ? error : new Error(String(error))
2236
+ );
2237
+ console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
2238
+ return err(pageError);
2239
+ }
2206
2240
  }
2207
2241
  const renderMode = getRenderModeForRoute(route.id, registry);
2208
2242
 
@@ -2467,6 +2501,19 @@ async function ensurePageRouteMetadata(
2467
2501
  }
2468
2502
 
2469
2503
  const registration = await handler();
2504
+ // DX-1: pageHandler가 malformed registration을 반환해도 silent 404 대신
2505
+ // 명시적 에러로 실패. handlers.ts 의 auto-promote 블록이 function 기본값을
2506
+ // { component } 로 감싸주지만, 직접 registerPageHandler 를 쓰는 경우나
2507
+ // 사용자 코드가 이상한 값을 반환하는 경우를 방어한다.
2508
+ if (typeof registration?.component !== "function") {
2509
+ const t = registration === null || registration === undefined
2510
+ ? String(registration)
2511
+ : typeof registration.component;
2512
+ throw new Error(
2513
+ `[Mandu] Page handler for '${routeId}' returned an invalid registration: component is ${t}. ` +
2514
+ "Expected `{ component: ReactComponent, filling? }` from the page module's default export."
2515
+ );
2516
+ }
2470
2517
  const component = registration.component as RouteComponent;
2471
2518
  registry.registerRouteComponent(routeId, component);
2472
2519