@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 +1 -1
- package/src/bundler/build.test.ts +18 -8
- package/src/bundler/safe-build.test.ts +22 -4
- package/src/runtime/server.ts +49 -2
package/package.json
CHANGED
|
@@ -62,14 +62,24 @@ afterAll(async () => {
|
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
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
|
|
145
|
-
//
|
|
146
|
-
//
|
|
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
|
-
|
|
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();
|
package/src/runtime/server.ts
CHANGED
|
@@ -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 ??
|
|
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
|
-
|
|
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
|
|