@mandujs/core 0.54.10 → 0.54.11

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.
Files changed (67) hide show
  1. package/package.json +200 -200
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +17 -65
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +10 -18
  11. package/src/agent/verify.ts +17 -115
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +81 -62
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/analyzer.ts +15 -15
  17. package/src/bundler/build.test.ts +120 -88
  18. package/src/bundler/build.ts +511 -511
  19. package/src/bundler/css.ts +42 -42
  20. package/src/bundler/manifest-schema.ts +21 -21
  21. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  22. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  23. package/src/bundler/types.ts +31 -31
  24. package/src/client/island.ts +79 -79
  25. package/src/config/validate.ts +1 -1
  26. package/src/deploy/inference/context.ts +82 -82
  27. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  28. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  29. package/src/error/formatter.ts +22 -31
  30. package/src/filling/context.ts +17 -17
  31. package/src/generator/generate.ts +30 -30
  32. package/src/generator/index.ts +3 -3
  33. package/src/generator/templates.test.ts +66 -65
  34. package/src/generator/templates.ts +210 -210
  35. package/src/guard/check.ts +9 -9
  36. package/src/guard/config-guard.ts +13 -13
  37. package/src/guard/fs-routes-policy.ts +51 -51
  38. package/src/guard/index.ts +11 -11
  39. package/src/index.ts +3 -3
  40. package/src/kitchen/api/file-api.ts +11 -11
  41. package/src/report/index.ts +1 -1
  42. package/src/resource/__tests__/generator.test.ts +6 -6
  43. package/src/resource/__tests__/schema.test.ts +14 -14
  44. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  45. package/src/resource/ddl/emit.ts +146 -146
  46. package/src/resource/generator-schema.ts +11 -11
  47. package/src/resource/generators/slot.ts +72 -72
  48. package/src/resource/schema.ts +21 -21
  49. package/src/router/client-entry.test.ts +192 -140
  50. package/src/router/client-entry.ts +516 -486
  51. package/src/router/fs-routes.ts +16 -16
  52. package/src/router/fs-scanner.ts +16 -28
  53. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  54. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  55. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  56. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  57. package/src/runtime/devtools-adapter.ts +68 -68
  58. package/src/runtime/escape.ts +34 -34
  59. package/src/runtime/observability-lifecycle.ts +290 -290
  60. package/src/runtime/page-render-response.ts +106 -106
  61. package/src/runtime/request-middleware.ts +31 -31
  62. package/src/runtime/server.ts +243 -243
  63. package/src/runtime/ssr.ts +59 -59
  64. package/src/runtime/static-files.ts +289 -289
  65. package/src/runtime/streaming-ssr.ts +22 -22
  66. package/src/watcher/__tests__/watcher.test.ts +59 -59
  67. package/src/watcher/watcher.ts +61 -61
@@ -10,23 +10,23 @@ import type {
10
10
  BundleResult,
11
11
  BundleOutput,
12
12
  BundleManifest,
13
- BundleStats,
14
- BundlerOptions,
15
- IslandFileEntry,
16
- PartialFileEntry,
17
- } from "./types";
13
+ BundleStats,
14
+ BundlerOptions,
15
+ IslandFileEntry,
16
+ PartialFileEntry,
17
+ } from "./types";
18
18
  import { HYDRATION } from "../constants";
19
19
  import { safeBuild } from "./safe-build";
20
20
  import { fastRefreshPlugin } from "./fast-refresh-plugin";
21
21
  import { defaultBundlerPlugins } from "./plugins";
22
22
  import type { BunPlugin } from "bun";
23
- import { mark, measure } from "../perf";
24
- import { HMR_PERF } from "../perf/hmr-markers";
25
- import { runOnBundleComplete } from "../plugins/runner";
26
- import {
27
- describeMissingHydrationClientModule,
28
- validateClientModuleForBrowserBundle,
29
- } from "../router/client-entry";
23
+ import { mark, measure } from "../perf";
24
+ import { HMR_PERF } from "../perf/hmr-markers";
25
+ import { runOnBundleComplete } from "../plugins/runner";
26
+ import {
27
+ describeMissingHydrationClientModule,
28
+ validateClientModuleForBrowserBundle,
29
+ } from "../router/client-entry";
30
30
  import {
31
31
  readVendorCache,
32
32
  writeVendorCache,
@@ -191,46 +191,46 @@ async function scanIslandFiles(routes: RouteSpec[], rootDir: string): Promise<Is
191
191
  *
192
192
  * @internal
193
193
  */
194
- export const _testOnly_scanIslandFiles = scanIslandFiles;
195
-
196
- function normalizeClientEntryName(name: string): string {
197
- return name
198
- .trim()
199
- .replace(/[^A-Za-z0-9_-]/g, "-")
200
- .replace(/^-+|-+$/g, "") || "partial";
201
- }
202
-
203
- async function scanPartialFiles(rootDir: string): Promise<PartialFileEntry[]> {
204
- const entries: PartialFileEntry[] = [];
205
- const seen = new Set<string>();
206
-
207
- try {
208
- const glob = new Bun.Glob("**/*.partial.{ts,tsx}");
209
- for await (const rel of glob.scan({ cwd: rootDir, absolute: true })) {
210
- if (rel.includes(`${path.sep}node_modules${path.sep}`)) continue;
211
- if (rel.includes(`${path.sep}.mandu${path.sep}`)) continue;
212
-
213
- const filePath = path.resolve(rel);
214
- if (seen.has(filePath)) continue;
215
- seen.add(filePath);
216
-
217
- const base = path.basename(filePath).replace(/\.partial\.tsx?$/, "");
218
- entries.push({
219
- name: normalizeClientEntryName(base),
220
- filePath,
221
- priority: HYDRATION.DEFAULT_PRIORITY,
222
- });
223
- }
224
- } catch {
225
- // Bun.Glob unavailable or scan failed — a missing partial bundle should
226
- // not prevent route-level islands from building.
227
- }
228
-
229
- return entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
230
- }
231
-
232
- /** @internal test helper */
233
- export const _testOnly_scanPartialFiles = scanPartialFiles;
194
+ export const _testOnly_scanIslandFiles = scanIslandFiles;
195
+
196
+ function normalizeClientEntryName(name: string): string {
197
+ return name
198
+ .trim()
199
+ .replace(/[^A-Za-z0-9_-]/g, "-")
200
+ .replace(/^-+|-+$/g, "") || "partial";
201
+ }
202
+
203
+ async function scanPartialFiles(rootDir: string): Promise<PartialFileEntry[]> {
204
+ const entries: PartialFileEntry[] = [];
205
+ const seen = new Set<string>();
206
+
207
+ try {
208
+ const glob = new Bun.Glob("**/*.partial.{ts,tsx}");
209
+ for await (const rel of glob.scan({ cwd: rootDir, absolute: true })) {
210
+ if (rel.includes(`${path.sep}node_modules${path.sep}`)) continue;
211
+ if (rel.includes(`${path.sep}.mandu${path.sep}`)) continue;
212
+
213
+ const filePath = path.resolve(rel);
214
+ if (seen.has(filePath)) continue;
215
+ seen.add(filePath);
216
+
217
+ const base = path.basename(filePath).replace(/\.partial\.tsx?$/, "");
218
+ entries.push({
219
+ name: normalizeClientEntryName(base),
220
+ filePath,
221
+ priority: HYDRATION.DEFAULT_PRIORITY,
222
+ });
223
+ }
224
+ } catch {
225
+ // Bun.Glob unavailable or scan failed — a missing partial bundle should
226
+ // not prevent route-level islands from building.
227
+ }
228
+
229
+ return entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
230
+ }
231
+
232
+ /** @internal test helper */
233
+ export const _testOnly_scanPartialFiles = scanPartialFiles;
234
234
 
235
235
  /**
236
236
  * Test-only accessor for the hydrated-routes filter. Mirrors the rationale
@@ -239,9 +239,9 @@ export const _testOnly_scanPartialFiles = scanPartialFiles;
239
239
  *
240
240
  * @internal
241
241
  */
242
- export const _testOnly_getHydratedRoutes = getHydratedRoutes;
243
- export const _testOnly_getHydrationRoutesMissingClientModule =
244
- getHydrationRoutesMissingClientModule;
242
+ export const _testOnly_getHydratedRoutes = getHydratedRoutes;
243
+ export const _testOnly_getHydrationRoutesMissingClientModule =
244
+ getHydrationRoutesMissingClientModule;
245
245
 
246
246
  /**
247
247
  * Issue #240 Phase 2 — collect every file the React Compiler plugin
@@ -260,42 +260,42 @@ export const _testOnly_getHydrationRoutesMissingClientModule =
260
260
  * Paths are absolute and deduplicated. Returns `[]` when the manifest
261
261
  * has no hydrated routes.
262
262
  */
263
- export async function collectCompilerLintTargets(
264
- manifest: RoutesManifest,
265
- rootDir: string,
266
- ): Promise<string[]> {
267
- const hydratedRoutes = getHydratedRoutes(manifest);
268
-
269
- const out = new Set<string>();
270
-
271
- // 1) Islands (reuses the bundler's canonical scanner).
272
- if (hydratedRoutes.length > 0) {
273
- const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
274
- for (const entry of islandFiles) {
275
- out.add(path.resolve(entry.filePath));
276
- }
277
-
278
- // 2) `"use client"` page modules — the hydrated-route filter
279
- // already asserted `clientModule` exists.
280
- for (const route of hydratedRoutes) {
281
- const rel = route.clientModule ?? route.module;
282
- if (rel) out.add(path.resolve(rootDir, rel));
283
- }
284
- }
285
-
286
- // 3) Partials — glob the whole project once. Partials live anywhere
287
- // the user chooses, they're not sibling-scoped like islands.
288
- for (const entry of await scanPartialFiles(rootDir)) {
289
- out.add(path.resolve(entry.filePath));
290
- }
291
-
292
- return Array.from(out).sort();
293
- }
263
+ export async function collectCompilerLintTargets(
264
+ manifest: RoutesManifest,
265
+ rootDir: string,
266
+ ): Promise<string[]> {
267
+ const hydratedRoutes = getHydratedRoutes(manifest);
268
+
269
+ const out = new Set<string>();
270
+
271
+ // 1) Islands (reuses the bundler's canonical scanner).
272
+ if (hydratedRoutes.length > 0) {
273
+ const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
274
+ for (const entry of islandFiles) {
275
+ out.add(path.resolve(entry.filePath));
276
+ }
277
+
278
+ // 2) `"use client"` page modules — the hydrated-route filter
279
+ // already asserted `clientModule` exists.
280
+ for (const route of hydratedRoutes) {
281
+ const rel = route.clientModule ?? route.module;
282
+ if (rel) out.add(path.resolve(rootDir, rel));
283
+ }
284
+ }
285
+
286
+ // 3) Partials — glob the whole project once. Partials live anywhere
287
+ // the user chooses, they're not sibling-scoped like islands.
288
+ for (const entry of await scanPartialFiles(rootDir)) {
289
+ out.add(path.resolve(entry.filePath));
290
+ }
291
+
292
+ return Array.from(out).sort();
293
+ }
294
294
 
295
295
  /** Build a single per-island bundle. */
296
- async function buildPerIslandBundle(
297
- entry: IslandFileEntry, outDir: string, options: BundlerOptions
298
- ): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
296
+ async function buildPerIslandBundle(
297
+ entry: IslandFileEntry, outDir: string, options: BundlerOptions
298
+ ): Promise<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> {
299
299
  const entryPath = path.join(outDir, `_entry_island_${entry.name}.js`);
300
300
  const outputName = `${entry.name}.island.js`;
301
301
  // Phase 7.1 B-1/B-4: wire Bun's native React Fast Refresh transform +
@@ -324,66 +324,66 @@ async function buildPerIslandBundle(
324
324
  return { name: entry.name, js: `/.mandu/client/${outputName}`, route: entry.routeId, priority: entry.priority };
325
325
  } catch (error) {
326
326
  await fs.unlink(entryPath).catch(() => {});
327
- throw error;
328
- }
329
- }
330
-
331
- interface PartialBundleBuild {
332
- name: string;
333
- js: string;
334
- priority: PartialFileEntry["priority"];
335
- size: number;
336
- gzipSize: number;
337
- }
338
-
339
- /** Build a single inline partial bundle. */
340
- async function buildPartialBundle(
341
- entry: PartialFileEntry,
342
- outDir: string,
343
- options: BundlerOptions,
344
- ): Promise<PartialBundleBuild> {
345
- const entryPath = path.join(outDir, `_entry_partial_${entry.name}.js`);
346
- const outputName = `${entry.name}.partial.js`;
347
- const isDev = isDevelopmentBuild(options);
348
-
349
- try {
350
- await Bun.write(entryPath, generatePartialEntry(entry.name, entry.filePath));
351
- const result = await safeBuild({
352
- entrypoints: [entryPath],
353
- outdir: outDir,
354
- naming: outputName,
355
- minify: shouldMinify(options),
356
- sourcemap: options.sourcemap ? "external" : "none",
357
- target: "browser",
358
- ...(isDev ? { reactFastRefresh: true } : {}),
359
- plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
360
- external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
361
- define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
362
- });
363
- await fs.unlink(entryPath).catch(() => {});
364
-
365
- if (!result.success) {
366
- const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
367
- throw new Error(`Partial build failed for '${entry.name}' (source: ${entry.filePath}):\n${grouped}\n Hint: Export a Mandu partial from this file with \`partial({ component })\`.`);
368
- }
369
-
370
- const outputPath = path.join(outDir, outputName);
371
- const outputFile = Bun.file(outputPath);
372
- const content = await outputFile.text();
373
- const gzipped = Bun.gzipSync(Buffer.from(content));
374
-
375
- return {
376
- name: entry.name,
377
- js: `/.mandu/client/${outputName}`,
378
- priority: entry.priority,
379
- size: outputFile.size,
380
- gzipSize: gzipped.length,
381
- };
382
- } catch (error) {
383
- await fs.unlink(entryPath).catch(() => {});
384
- throw error;
385
- }
386
- }
327
+ throw error;
328
+ }
329
+ }
330
+
331
+ interface PartialBundleBuild {
332
+ name: string;
333
+ js: string;
334
+ priority: PartialFileEntry["priority"];
335
+ size: number;
336
+ gzipSize: number;
337
+ }
338
+
339
+ /** Build a single inline partial bundle. */
340
+ async function buildPartialBundle(
341
+ entry: PartialFileEntry,
342
+ outDir: string,
343
+ options: BundlerOptions,
344
+ ): Promise<PartialBundleBuild> {
345
+ const entryPath = path.join(outDir, `_entry_partial_${entry.name}.js`);
346
+ const outputName = `${entry.name}.partial.js`;
347
+ const isDev = isDevelopmentBuild(options);
348
+
349
+ try {
350
+ await Bun.write(entryPath, generatePartialEntry(entry.name, entry.filePath));
351
+ const result = await safeBuild({
352
+ entrypoints: [entryPath],
353
+ outdir: outDir,
354
+ naming: outputName,
355
+ minify: shouldMinify(options),
356
+ sourcemap: options.sourcemap ? "external" : "none",
357
+ target: "browser",
358
+ ...(isDev ? { reactFastRefresh: true } : {}),
359
+ plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
360
+ external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
361
+ define: { "process.env.NODE_ENV": nodeEnvDefine(options), ...options.define },
362
+ });
363
+ await fs.unlink(entryPath).catch(() => {});
364
+
365
+ if (!result.success) {
366
+ const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
367
+ throw new Error(`Partial build failed for '${entry.name}' (source: ${entry.filePath}):\n${grouped}\n Hint: Export a Mandu partial from this file with \`partial({ component })\`.`);
368
+ }
369
+
370
+ const outputPath = path.join(outDir, outputName);
371
+ const outputFile = Bun.file(outputPath);
372
+ const content = await outputFile.text();
373
+ const gzipped = Bun.gzipSync(Buffer.from(content));
374
+
375
+ return {
376
+ name: entry.name,
377
+ js: `/.mandu/client/${outputName}`,
378
+ priority: entry.priority,
379
+ size: outputFile.size,
380
+ gzipSize: gzipped.length,
381
+ };
382
+ } catch (error) {
383
+ await fs.unlink(entryPath).catch(() => {});
384
+ throw error;
385
+ }
386
+ }
387
387
 
388
388
  /**
389
389
  * 빈 매니페스트 생성
@@ -407,23 +407,23 @@ function createEmptyManifest(env: "development" | "production"): BundleManifest
407
407
  /**
408
408
  * Hydration이 필요한 라우트 필터링
409
409
  */
410
- function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
411
- return manifest.routes.filter(
412
- (route) =>
413
- route.kind === "page" &&
414
- route.clientModule &&
415
- needsHydration(route)
416
- );
417
- }
418
-
419
- function getHydrationRoutesMissingClientModule(manifest: RoutesManifest): RouteSpec[] {
420
- return manifest.routes.filter(
421
- (route) =>
422
- route.kind === "page" &&
423
- !route.clientModule &&
424
- needsHydration(route)
425
- );
426
- }
410
+ function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
411
+ return manifest.routes.filter(
412
+ (route) =>
413
+ route.kind === "page" &&
414
+ route.clientModule &&
415
+ needsHydration(route)
416
+ );
417
+ }
418
+
419
+ function getHydrationRoutesMissingClientModule(manifest: RoutesManifest): RouteSpec[] {
420
+ return manifest.routes.filter(
421
+ (route) =>
422
+ route.kind === "page" &&
423
+ !route.clientModule &&
424
+ needsHydration(route)
425
+ );
426
+ }
427
427
 
428
428
  const REACT_SHIM_EXPORTS = [
429
429
  "Activity",
@@ -518,31 +518,31 @@ function generateRuntimeSource(): string {
518
518
  import React, { useState, useEffect, Component } from 'react';
519
519
  import { hydrateRoot, createRoot } from 'react-dom/client';
520
520
 
521
- // Hydrated roots 추적 (unmount용) - 전역 초기화
522
- window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
523
- const hydratedRoots = window.__MANDU_ROOTS__;
524
-
525
- // 서버 데이터
526
- function readManduData() {
527
- if (window.__MANDU_DATA__) return window.__MANDU_DATA__;
528
-
529
- const raw = window.__MANDU_DATA_RAW__ || document.getElementById('__MANDU_DATA__')?.textContent;
530
- if (!raw) {
531
- window.__MANDU_DATA__ = {};
532
- return window.__MANDU_DATA__;
533
- }
534
-
535
- try {
536
- window.__MANDU_DATA__ = JSON.parse(raw);
537
- } catch (error) {
538
- console.warn('[Mandu] Failed to parse server data:', error);
539
- window.__MANDU_DATA__ = {};
540
- }
541
-
542
- return window.__MANDU_DATA__;
543
- }
544
-
545
- const getServerData = (id) => readManduData()[id]?.serverData || {};
521
+ // Hydrated roots 추적 (unmount용) - 전역 초기화
522
+ window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
523
+ const hydratedRoots = window.__MANDU_ROOTS__;
524
+
525
+ // 서버 데이터
526
+ function readManduData() {
527
+ if (window.__MANDU_DATA__) return window.__MANDU_DATA__;
528
+
529
+ const raw = window.__MANDU_DATA_RAW__ || document.getElementById('__MANDU_DATA__')?.textContent;
530
+ if (!raw) {
531
+ window.__MANDU_DATA__ = {};
532
+ return window.__MANDU_DATA__;
533
+ }
534
+
535
+ try {
536
+ window.__MANDU_DATA__ = JSON.parse(raw);
537
+ } catch (error) {
538
+ console.warn('[Mandu] Failed to parse server data:', error);
539
+ window.__MANDU_DATA__ = {};
540
+ }
541
+
542
+ return window.__MANDU_DATA__;
543
+ }
544
+
545
+ const getServerData = (id) => readManduData()[id]?.serverData || {};
546
546
 
547
547
  /**
548
548
  * Error Boundary 컴포넌트 (Class Component)
@@ -756,16 +756,16 @@ async function loadAndHydrate(element, src) {
756
756
  const island = module.default;
757
757
  let data = getServerData(id);
758
758
 
759
- // Fallback: read data-props from the island root or a child element if
760
- // __MANDU_DATA__ is empty. Inline partials put their serialized props on
761
- // the root marker itself.
762
- if (!data || Object.keys(data).length === 0) {
763
- const propsEl = element.hasAttribute('data-props')
764
- ? element
765
- : element.querySelector('[data-props]');
766
- if (propsEl) {
767
- try {
768
- data = JSON.parse(propsEl.getAttribute('data-props'));
759
+ // Fallback: read data-props from the island root or a child element if
760
+ // __MANDU_DATA__ is empty. Inline partials put their serialized props on
761
+ // the root marker itself.
762
+ if (!data || Object.keys(data).length === 0) {
763
+ const propsEl = element.hasAttribute('data-props')
764
+ ? element
765
+ : element.querySelector('[data-props]');
766
+ if (propsEl) {
767
+ try {
768
+ data = JSON.parse(propsEl.getAttribute('data-props'));
769
769
  } catch (e) {
770
770
  console.warn('[Mandu] Failed to parse data-props fallback:', e);
771
771
  }
@@ -1439,63 +1439,63 @@ async function buildRouterRuntime(
1439
1439
  * - Runtime이 dynamic import로 로드
1440
1440
  * - 등록/초기화 코드 없음
1441
1441
  */
1442
- function generateIslandEntry(routeId: string, clientModulePath: string): string {
1443
- // Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
1444
- const normalizedPath = clientModulePath.replace(/\\/g, "/");
1445
- return `
1442
+ function generateIslandEntry(routeId: string, clientModulePath: string): string {
1443
+ // Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
1444
+ const normalizedPath = clientModulePath.replace(/\\/g, "/");
1445
+ return `
1446
1446
  /**
1447
1447
  * Mandu Island: ${routeId} (Generated)
1448
1448
  * Pure export - no side effects
1449
1449
  */
1450
1450
  import island from "${normalizedPath}";
1451
- export default island;
1452
- `;
1453
- }
1454
-
1455
- function generatePartialEntry(partialId: string, partialModulePath: string): string {
1456
- const normalizedPath = partialModulePath.replace(/\\/g, "/");
1457
- return `
1458
- /**
1459
- * Mandu Partial: ${partialId} (Generated)
1460
- * Exports a runtime-compatible island wrapper around a compiled partial.
1461
- */
1462
- import React from "react";
1463
- import * as partialModule from "${normalizedPath}";
1464
-
1465
- function findPartial(mod) {
1466
- if (mod.default && mod.default.__mandu_partial === true) return mod.default;
1467
- for (const value of Object.values(mod)) {
1468
- if (value && value.__mandu_partial === true) return value;
1469
- }
1470
- throw new Error("[Mandu Partial] ${partialId} must export a value returned by partial({ component })");
1471
- }
1472
-
1473
- const partial = findPartial(partialModule);
1474
- const definition = partial.definition;
1475
- const component = definition.component;
1476
-
1477
- export default {
1478
- __mandu_island: true,
1479
- definition: {
1480
- setup(serverData) {
1481
- if (serverData && typeof serverData === "object" && Object.keys(serverData).length > 0) {
1482
- return serverData;
1483
- }
1484
- return definition.initialProps || {};
1485
- },
1486
- render(props) {
1487
- return React.createElement(component, props);
1488
- },
1489
- errorBoundary: definition.errorBoundary,
1490
- loading: definition.loading,
1491
- },
1492
- };
1493
- `;
1494
- }
1495
-
1496
- /**
1497
- * Runtime 번들 빌드
1498
- */
1451
+ export default island;
1452
+ `;
1453
+ }
1454
+
1455
+ function generatePartialEntry(partialId: string, partialModulePath: string): string {
1456
+ const normalizedPath = partialModulePath.replace(/\\/g, "/");
1457
+ return `
1458
+ /**
1459
+ * Mandu Partial: ${partialId} (Generated)
1460
+ * Exports a runtime-compatible island wrapper around a compiled partial.
1461
+ */
1462
+ import React from "react";
1463
+ import * as partialModule from "${normalizedPath}";
1464
+
1465
+ function findPartial(mod) {
1466
+ if (mod.default && mod.default.__mandu_partial === true) return mod.default;
1467
+ for (const value of Object.values(mod)) {
1468
+ if (value && value.__mandu_partial === true) return value;
1469
+ }
1470
+ throw new Error("[Mandu Partial] ${partialId} must export a value returned by partial({ component })");
1471
+ }
1472
+
1473
+ const partial = findPartial(partialModule);
1474
+ const definition = partial.definition;
1475
+ const component = definition.component;
1476
+
1477
+ export default {
1478
+ __mandu_island: true,
1479
+ definition: {
1480
+ setup(serverData) {
1481
+ if (serverData && typeof serverData === "object" && Object.keys(serverData).length > 0) {
1482
+ return serverData;
1483
+ }
1484
+ return definition.initialProps || {};
1485
+ },
1486
+ render(props) {
1487
+ return React.createElement(component, props);
1488
+ },
1489
+ errorBoundary: definition.errorBoundary,
1490
+ loading: definition.loading,
1491
+ },
1492
+ };
1493
+ `;
1494
+ }
1495
+
1496
+ /**
1497
+ * Runtime 번들 빌드
1498
+ */
1499
1499
  async function buildRuntime(
1500
1500
  outDir: string,
1501
1501
  options: BundlerOptions
@@ -1776,9 +1776,9 @@ async function buildVendorShims(
1776
1776
  }
1777
1777
  }
1778
1778
 
1779
- const buildShim = async (
1780
- shim: { name: string; source: string; key: VendorShimKey; cacheId: string }
1781
- ): Promise<{ key: VendorShimKey; cacheId: string; outputName?: string; outputPath?: string; error?: string }> => {
1779
+ const buildShim = async (
1780
+ shim: { name: string; source: string; key: VendorShimKey; cacheId: string }
1781
+ ): Promise<{ key: VendorShimKey; cacheId: string; outputName?: string; outputPath?: string; error?: string }> => {
1782
1782
  const srcPath = path.join(outDir, `${shim.name}.src.js`);
1783
1783
  const outputName = `${shim.name}.js`;
1784
1784
 
@@ -1814,14 +1814,14 @@ async function buildVendorShims(
1814
1814
 
1815
1815
  await fs.unlink(srcPath).catch(() => {});
1816
1816
 
1817
- if (!result.success) {
1818
- const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
1819
- return {
1820
- key: shim.key,
1821
- cacheId: shim.cacheId,
1822
- error: `Vendor shim '${shim.name}' build failed (source: ${srcPath}):\n${grouped}\n ${vendorShimFailureHint(shim.name)}`,
1823
- };
1824
- }
1817
+ if (!result.success) {
1818
+ const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
1819
+ return {
1820
+ key: shim.key,
1821
+ cacheId: shim.cacheId,
1822
+ error: `Vendor shim '${shim.name}' build failed (source: ${srcPath}):\n${grouped}\n ${vendorShimFailureHint(shim.name)}`,
1823
+ };
1824
+ }
1825
1825
 
1826
1826
  return {
1827
1827
  key: shim.key,
@@ -1831,13 +1831,13 @@ async function buildVendorShims(
1831
1831
  };
1832
1832
  } catch (error) {
1833
1833
  await fs.unlink(srcPath).catch(() => {});
1834
- return {
1835
- key: shim.key,
1836
- cacheId: shim.cacheId,
1837
- error: `[${shim.name}] ${String(error)}\n ${vendorShimFailureHint(shim.name)}`,
1838
- };
1839
- }
1840
- };
1834
+ return {
1835
+ key: shim.key,
1836
+ cacheId: shim.cacheId,
1837
+ error: `[${shim.name}] ${String(error)}\n ${vendorShimFailureHint(shim.name)}`,
1838
+ };
1839
+ }
1840
+ };
1841
1841
 
1842
1842
  const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
1843
1843
  const writeEntries: VendorCacheWriteEntry[] = [];
@@ -1880,14 +1880,14 @@ async function buildVendorShims(
1880
1880
  fastRefreshRuntime: results.fastRefreshRuntime,
1881
1881
  errors,
1882
1882
  };
1883
- }
1884
-
1885
- function vendorShimFailureHint(shimName: string): string {
1886
- if (shimName.includes("react-refresh")) {
1887
- return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
1888
- }
1889
- return "Hint: check the import paths and ensure the vendor package is installed.";
1890
- }
1883
+ }
1884
+
1885
+ function vendorShimFailureHint(shimName: string): string {
1886
+ if (shimName.includes("react-refresh")) {
1887
+ return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
1888
+ }
1889
+ return "Hint: check the import paths and ensure the vendor package is installed.";
1890
+ }
1891
1891
 
1892
1892
  /**
1893
1893
  * 단일 Island 번들 빌드
@@ -1979,16 +1979,16 @@ async function buildIsland(
1979
1979
  /**
1980
1980
  * 번들 매니페스트 생성
1981
1981
  */
1982
- function createBundleManifest(
1983
- outputs: BundleOutput[],
1984
- routes: RouteSpec[],
1985
- runtimePath: string,
1986
- vendorResult: VendorBuildResult,
1987
- routerPath: string,
1988
- env: "development" | "production",
1989
- islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
1990
- partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
1991
- ): BundleManifest {
1982
+ function createBundleManifest(
1983
+ outputs: BundleOutput[],
1984
+ routes: RouteSpec[],
1985
+ runtimePath: string,
1986
+ vendorResult: VendorBuildResult,
1987
+ routerPath: string,
1988
+ env: "development" | "production",
1989
+ islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
1990
+ partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
1991
+ ): BundleManifest {
1992
1992
  const bundles: BundleManifest["bundles"] = {};
1993
1993
 
1994
1994
  for (const output of outputs) {
@@ -2004,27 +2004,27 @@ function createBundleManifest(
2004
2004
 
2005
2005
  // Per-island bundles (code splitting)
2006
2006
  let islands: BundleManifest["islands"];
2007
- if (islandBundles && islandBundles.length > 0) {
2008
- islands = {};
2009
- for (const ib of islandBundles) {
2010
- islands[ib.name] = {
2011
- js: ib.js,
2007
+ if (islandBundles && islandBundles.length > 0) {
2008
+ islands = {};
2009
+ for (const ib of islandBundles) {
2010
+ islands[ib.name] = {
2011
+ js: ib.js,
2012
2012
  route: ib.route,
2013
2013
  priority: ib.priority,
2014
- };
2015
- }
2016
- }
2017
-
2018
- let partials: BundleManifest["partials"];
2019
- if (partialBundles && partialBundles.length > 0) {
2020
- partials = {};
2021
- for (const partial of partialBundles) {
2022
- partials[partial.name] = {
2023
- js: partial.js,
2024
- priority: partial.priority,
2025
- };
2026
- }
2027
- }
2014
+ };
2015
+ }
2016
+ }
2017
+
2018
+ let partials: BundleManifest["partials"];
2019
+ if (partialBundles && partialBundles.length > 0) {
2020
+ partials = {};
2021
+ for (const partial of partialBundles) {
2022
+ partials[partial.name] = {
2023
+ js: partial.js,
2024
+ priority: partial.priority,
2025
+ };
2026
+ }
2027
+ }
2028
2028
 
2029
2029
  // Phase 7.1 B-2: expose Fast Refresh dev bundles so the HTML
2030
2030
  // preamble can inject a dynamic import pointing at them. Only
@@ -2040,11 +2040,11 @@ function createBundleManifest(
2040
2040
  return {
2041
2041
  version: 1,
2042
2042
  buildTime: new Date().toISOString(),
2043
- env,
2044
- bundles,
2045
- ...(islands ? { islands } : {}),
2046
- ...(partials ? { partials } : {}),
2047
- shared: {
2043
+ env,
2044
+ bundles,
2045
+ ...(islands ? { islands } : {}),
2046
+ ...(partials ? { partials } : {}),
2047
+ shared: {
2048
2048
  runtime: runtimePath,
2049
2049
  vendor: vendorResult.react, // primary vendor for backwards compatibility
2050
2050
  router: routerPath, // Client-side Router
@@ -2065,32 +2065,32 @@ function createBundleManifest(
2065
2065
  /**
2066
2066
  * 번들 통계 계산
2067
2067
  */
2068
- function calculateStats(
2069
- outputs: BundleOutput[],
2070
- startTime: number,
2071
- extraOutputs: Array<{ routeId: string; size: number; gzipSize: number }> = [],
2072
- ): BundleStats {
2073
- let totalSize = 0;
2074
- let totalGzipSize = 0;
2075
- let largestBundle = { routeId: "", size: 0 };
2076
-
2077
- for (const output of [...outputs, ...extraOutputs]) {
2078
- totalSize += output.size;
2079
- totalGzipSize += output.gzipSize;
2080
-
2081
- if (output.size > largestBundle.size) {
2082
- largestBundle = { routeId: output.routeId, size: output.size };
2068
+ function calculateStats(
2069
+ outputs: BundleOutput[],
2070
+ startTime: number,
2071
+ extraOutputs: Array<{ routeId: string; size: number; gzipSize: number }> = [],
2072
+ ): BundleStats {
2073
+ let totalSize = 0;
2074
+ let totalGzipSize = 0;
2075
+ let largestBundle = { routeId: "", size: 0 };
2076
+
2077
+ for (const output of [...outputs, ...extraOutputs]) {
2078
+ totalSize += output.size;
2079
+ totalGzipSize += output.gzipSize;
2080
+
2081
+ if (output.size > largestBundle.size) {
2082
+ largestBundle = { routeId: output.routeId, size: output.size };
2083
2083
  }
2084
2084
  }
2085
2085
 
2086
2086
  return {
2087
2087
  totalSize,
2088
- totalGzipSize,
2089
- largestBundle,
2090
- buildTime: performance.now() - startTime,
2091
- bundleCount: outputs.length + extraOutputs.length,
2092
- };
2093
- }
2088
+ totalGzipSize,
2089
+ largestBundle,
2090
+ buildTime: performance.now() - startTime,
2091
+ bundleCount: outputs.length + extraOutputs.length,
2092
+ };
2093
+ }
2094
2094
 
2095
2095
  /**
2096
2096
  * 클라이언트 번들 빌드
@@ -2134,47 +2134,47 @@ export async function buildClientBundles(
2134
2134
  errors.push(`onBundleComplete[${e.source}]: ${e.error.message}`);
2135
2135
  }
2136
2136
  };
2137
- const env = resolveBundlerMode(options);
2138
-
2139
- // 1. Hydration이 필요한 라우트 필터링
2140
- const invalidClientRouteIds = new Set<string>();
2141
- const runtimeRoutes = manifest.routes.filter((route) => route.kind === "page" && needsHydration(route));
2142
- const partialFiles = runtimeRoutes.length > 0 ? await scanPartialFiles(rootDir) : [];
2143
- const routesMissingClientModule = getHydrationRoutesMissingClientModule(manifest);
2144
- if (routesMissingClientModule.length > 0) {
2145
- const missingClientErrors = await Promise.all(
2146
- routesMissingClientModule.map((route) =>
2147
- describeMissingHydrationClientModule(route, rootDir, {
2148
- allowPartialOnly: partialFiles.length > 0,
2149
- })
2150
- )
2151
- );
2152
- errors.push(...missingClientErrors.filter((error): error is string => error !== null));
2153
- }
2154
-
2155
- let hydratedRoutes = getHydratedRoutes(manifest);
2156
- if (hydratedRoutes.length > 0) {
2157
- const validRoutes: RouteSpec[] = [];
2158
- for (const route of hydratedRoutes) {
2159
- const validationError = await validateClientModuleForBrowserBundle(route, rootDir);
2160
- if (validationError) {
2161
- invalidClientRouteIds.add(route.id);
2162
- errors.push(validationError);
2163
- continue;
2164
- }
2165
- validRoutes.push(route);
2166
- }
2167
- hydratedRoutes = validRoutes;
2168
- }
2169
- // 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
2137
+ const env = resolveBundlerMode(options);
2138
+
2139
+ // 1. Hydration이 필요한 라우트 필터링
2140
+ const invalidClientRouteIds = new Set<string>();
2141
+ const runtimeRoutes = manifest.routes.filter((route) => route.kind === "page" && needsHydration(route));
2142
+ const partialFiles = runtimeRoutes.length > 0 ? await scanPartialFiles(rootDir) : [];
2143
+ const routesMissingClientModule = getHydrationRoutesMissingClientModule(manifest);
2144
+ if (routesMissingClientModule.length > 0) {
2145
+ const missingClientErrors = await Promise.all(
2146
+ routesMissingClientModule.map((route) =>
2147
+ describeMissingHydrationClientModule(route, rootDir, {
2148
+ allowPartialOnly: partialFiles.length > 0,
2149
+ })
2150
+ )
2151
+ );
2152
+ errors.push(...missingClientErrors.filter((error): error is string => error !== null));
2153
+ }
2154
+
2155
+ let hydratedRoutes = getHydratedRoutes(manifest);
2156
+ if (hydratedRoutes.length > 0) {
2157
+ const validRoutes: RouteSpec[] = [];
2158
+ for (const route of hydratedRoutes) {
2159
+ const validationError = await validateClientModuleForBrowserBundle(route, rootDir);
2160
+ if (validationError) {
2161
+ invalidClientRouteIds.add(route.id);
2162
+ errors.push(validationError);
2163
+ continue;
2164
+ }
2165
+ validRoutes.push(route);
2166
+ }
2167
+ hydratedRoutes = validRoutes;
2168
+ }
2169
+ // 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
2170
2170
  const outDir = resolveClientOutDir(rootDir, options.outDir);
2171
2171
  await fs.mkdir(outDir, { recursive: true });
2172
2172
 
2173
2173
  // Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
2174
2174
  // (이전 빌드의 stale 매니페스트 참조 방지)
2175
- if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
2175
+ if (hydratedRoutes.length === 0 && partialFiles.length === 0) {
2176
2176
  // #185: skipFrameworkBundles 모드에서는 기존 manifest를 그대로 유지 (devtools 재빌드도 스킵)
2177
- if (options.skipFrameworkBundles && errors.length === 0) {
2177
+ if (options.skipFrameworkBundles && errors.length === 0) {
2178
2178
  const manifestPath = path.join(rootDir, ".mandu/manifest.json");
2179
2179
  try {
2180
2180
  const manifestRaw = await fs.readFile(manifestPath, "utf-8");
@@ -2225,11 +2225,11 @@ export async function buildClientBundles(
2225
2225
  path.join(rootDir, ".mandu/manifest.json"),
2226
2226
  JSON.stringify(emptyManifest, null, 2)
2227
2227
  );
2228
- return {
2229
- success: errors.length === 0,
2230
- outputs: [],
2231
- errors,
2232
- manifest: emptyManifest,
2228
+ return {
2229
+ success: errors.length === 0,
2230
+ outputs: [],
2231
+ errors,
2232
+ manifest: emptyManifest,
2233
2233
  stats: {
2234
2234
  totalSize: 0,
2235
2235
  totalGzipSize: 0,
@@ -2268,14 +2268,14 @@ export async function buildClientBundles(
2268
2268
  return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
2269
2269
  }
2270
2270
 
2271
- // Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
2272
- for (const routeId of invalidClientRouteIds) {
2273
- delete existingManifest.bundles[routeId];
2274
- }
2275
- if (outputs.length > 0 || invalidClientRouteIds.size > 0) {
2276
- for (const output of outputs) {
2277
- if (existingManifest.bundles[output.routeId]) {
2278
- existingManifest.bundles[output.routeId].js = output.outputPath;
2271
+ // Only update manifest with successfully built outputs (#10: preserve previous good manifest on failure)
2272
+ for (const routeId of invalidClientRouteIds) {
2273
+ delete existingManifest.bundles[routeId];
2274
+ }
2275
+ if (outputs.length > 0 || invalidClientRouteIds.size > 0) {
2276
+ for (const output of outputs) {
2277
+ if (existingManifest.bundles[output.routeId]) {
2278
+ existingManifest.bundles[output.routeId].js = output.outputPath;
2279
2279
  } else {
2280
2280
  const route = targetRoutes.find((r) => r.id === output.routeId);
2281
2281
  const hydration = route ? getRouteHydration(route) : null;
@@ -2287,11 +2287,11 @@ export async function buildClientBundles(
2287
2287
  }
2288
2288
  }
2289
2289
 
2290
- await fs.writeFile(
2291
- path.join(rootDir, ".mandu/manifest.json"),
2292
- JSON.stringify(existingManifest, null, 2)
2293
- );
2294
- }
2290
+ await fs.writeFile(
2291
+ path.join(rootDir, ".mandu/manifest.json"),
2292
+ JSON.stringify(existingManifest, null, 2)
2293
+ );
2294
+ }
2295
2295
  // When all builds failed, do NOT overwrite manifest — keep previous good state
2296
2296
 
2297
2297
  const stats = calculateStats(outputs, startTime);
@@ -2327,13 +2327,13 @@ export async function buildClientBundles(
2327
2327
  "[Mandu] Existing manifest missing required fields (shared/bundles), falling back to full build",
2328
2328
  );
2329
2329
  return buildClientBundles(manifest, rootDir, { ...options, skipFrameworkBundles: false });
2330
- }
2331
-
2332
- for (const routeId of invalidClientRouteIds) {
2333
- delete existingManifest.bundles[routeId];
2334
- }
2335
-
2336
- // Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
2330
+ }
2331
+
2332
+ for (const routeId of invalidClientRouteIds) {
2333
+ delete existingManifest.bundles[routeId];
2334
+ }
2335
+
2336
+ // Pre-build validation + 병렬 island 빌드 (framework 번들은 스킵)
2337
2337
  for (const route of hydratedRoutes) {
2338
2338
  if (!route.clientModule) continue;
2339
2339
  const clientModulePath = path.join(rootDir, route.clientModule);
@@ -2398,62 +2398,62 @@ export async function buildClientBundles(
2398
2398
  };
2399
2399
  }
2400
2400
  }
2401
- if (perIslandBundles.length > 0) {
2402
- existingManifest.islands = existingManifest.islands || {};
2403
- for (const ib of perIslandBundles) {
2404
- existingManifest.islands[ib.name] = {
2405
- js: ib.js,
2401
+ if (perIslandBundles.length > 0) {
2402
+ existingManifest.islands = existingManifest.islands || {};
2403
+ for (const ib of perIslandBundles) {
2404
+ existingManifest.islands[ib.name] = {
2405
+ js: ib.js,
2406
2406
  route: ib.route,
2407
2407
  priority: ib.priority,
2408
- };
2409
- }
2410
- }
2411
-
2412
- const partialBundles: PartialBundleBuild[] = [];
2413
- if (partialFiles.length > 0) {
2414
- const partialResults = await Promise.all(
2415
- partialFiles.map(async (entry) => {
2416
- try {
2417
- return await buildPartialBundle(entry, outDir, options);
2418
- } catch (error) {
2419
- errors.push(`[partial:${entry.name}] ${String(error)}`);
2420
- return null;
2421
- }
2422
- }),
2423
- );
2424
- for (const result of partialResults) {
2425
- if (result) partialBundles.push(result);
2426
- }
2427
- }
2428
- if (partialFiles.length > 0 || existingManifest.partials) {
2429
- existingManifest.partials = {};
2430
- for (const partial of partialBundles) {
2431
- existingManifest.partials[partial.name] = {
2432
- js: partial.js,
2433
- priority: partial.priority,
2434
- };
2435
- }
2436
- if (Object.keys(existingManifest.partials).length === 0) {
2437
- delete existingManifest.partials;
2438
- }
2439
- }
2440
-
2441
- await fs.writeFile(
2442
- path.join(rootDir, ".mandu/manifest.json"),
2443
- JSON.stringify(existingManifest, null, 2),
2444
- );
2445
-
2446
- const stats = calculateStats(
2447
- outputs,
2448
- startTime,
2449
- partialBundles.map((partial) => ({
2450
- routeId: `partial:${partial.name}`,
2451
- size: partial.size,
2452
- gzipSize: partial.gzipSize,
2453
- })),
2454
- );
2455
- return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
2456
- }
2408
+ };
2409
+ }
2410
+ }
2411
+
2412
+ const partialBundles: PartialBundleBuild[] = [];
2413
+ if (partialFiles.length > 0) {
2414
+ const partialResults = await Promise.all(
2415
+ partialFiles.map(async (entry) => {
2416
+ try {
2417
+ return await buildPartialBundle(entry, outDir, options);
2418
+ } catch (error) {
2419
+ errors.push(`[partial:${entry.name}] ${String(error)}`);
2420
+ return null;
2421
+ }
2422
+ }),
2423
+ );
2424
+ for (const result of partialResults) {
2425
+ if (result) partialBundles.push(result);
2426
+ }
2427
+ }
2428
+ if (partialFiles.length > 0 || existingManifest.partials) {
2429
+ existingManifest.partials = {};
2430
+ for (const partial of partialBundles) {
2431
+ existingManifest.partials[partial.name] = {
2432
+ js: partial.js,
2433
+ priority: partial.priority,
2434
+ };
2435
+ }
2436
+ if (Object.keys(existingManifest.partials).length === 0) {
2437
+ delete existingManifest.partials;
2438
+ }
2439
+ }
2440
+
2441
+ await fs.writeFile(
2442
+ path.join(rootDir, ".mandu/manifest.json"),
2443
+ JSON.stringify(existingManifest, null, 2),
2444
+ );
2445
+
2446
+ const stats = calculateStats(
2447
+ outputs,
2448
+ startTime,
2449
+ partialBundles.map((partial) => ({
2450
+ routeId: `partial:${partial.name}`,
2451
+ size: partial.size,
2452
+ gzipSize: partial.gzipSize,
2453
+ })),
2454
+ );
2455
+ return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
2456
+ }
2457
2457
 
2458
2458
  // 3-4. Runtime, Router, Vendor, DevTools 번들 병렬 빌드 (서로 독립적)
2459
2459
  const isDev = env === "development";
@@ -2539,11 +2539,11 @@ export async function buildClientBundles(
2539
2539
  const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
2540
2540
  const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
2541
2541
 
2542
- if (islandFiles.length > 0) {
2543
- const islandResults = await Promise.all(
2544
- islandFiles.map(async (entry) => {
2545
- try {
2546
- return await buildPerIslandBundle(entry, outDir, options);
2542
+ if (islandFiles.length > 0) {
2543
+ const islandResults = await Promise.all(
2544
+ islandFiles.map(async (entry) => {
2545
+ try {
2546
+ return await buildPerIslandBundle(entry, outDir, options);
2547
2547
  } catch (error) {
2548
2548
  errors.push(`[island:${entry.name}] ${String(error)}`);
2549
2549
  return null;
@@ -2551,38 +2551,38 @@ export async function buildClientBundles(
2551
2551
  })
2552
2552
  );
2553
2553
  for (const result of islandResults) {
2554
- if (result) islandBundles.push(result);
2555
- }
2556
- }
2557
-
2558
- const partialBundles: PartialBundleBuild[] = [];
2559
- if (partialFiles.length > 0) {
2560
- const partialResults = await Promise.all(
2561
- partialFiles.map(async (entry) => {
2562
- try {
2563
- return await buildPartialBundle(entry, outDir, options);
2564
- } catch (error) {
2565
- errors.push(`[partial:${entry.name}] ${String(error)}`);
2566
- return null;
2567
- }
2568
- }),
2569
- );
2570
- for (const result of partialResults) {
2571
- if (result) partialBundles.push(result);
2572
- }
2573
- }
2574
-
2575
- // 6. 번들 매니페스트 생성
2576
- const bundleManifest = createBundleManifest(
2577
- outputs,
2578
- hydratedRoutes,
2579
- runtimeResult.outputPath,
2580
- vendorResult,
2581
- routerResult.outputPath,
2582
- env,
2583
- islandBundles,
2584
- partialBundles,
2585
- );
2554
+ if (result) islandBundles.push(result);
2555
+ }
2556
+ }
2557
+
2558
+ const partialBundles: PartialBundleBuild[] = [];
2559
+ if (partialFiles.length > 0) {
2560
+ const partialResults = await Promise.all(
2561
+ partialFiles.map(async (entry) => {
2562
+ try {
2563
+ return await buildPartialBundle(entry, outDir, options);
2564
+ } catch (error) {
2565
+ errors.push(`[partial:${entry.name}] ${String(error)}`);
2566
+ return null;
2567
+ }
2568
+ }),
2569
+ );
2570
+ for (const result of partialResults) {
2571
+ if (result) partialBundles.push(result);
2572
+ }
2573
+ }
2574
+
2575
+ // 6. 번들 매니페스트 생성
2576
+ const bundleManifest = createBundleManifest(
2577
+ outputs,
2578
+ hydratedRoutes,
2579
+ runtimeResult.outputPath,
2580
+ vendorResult,
2581
+ routerResult.outputPath,
2582
+ env,
2583
+ islandBundles,
2584
+ partialBundles,
2585
+ );
2586
2586
 
2587
2587
  await fs.writeFile(
2588
2588
  path.join(rootDir, ".mandu/manifest.json"),
@@ -2590,15 +2590,15 @@ export async function buildClientBundles(
2590
2590
  );
2591
2591
 
2592
2592
  // 7. 통계 계산
2593
- const stats = calculateStats(
2594
- outputs,
2595
- startTime,
2596
- partialBundles.map((partial) => ({
2597
- routeId: `partial:${partial.name}`,
2598
- size: partial.size,
2599
- gzipSize: partial.gzipSize,
2600
- })),
2601
- );
2593
+ const stats = calculateStats(
2594
+ outputs,
2595
+ startTime,
2596
+ partialBundles.map((partial) => ({
2597
+ routeId: `partial:${partial.name}`,
2598
+ size: partial.size,
2599
+ gzipSize: partial.gzipSize,
2600
+ })),
2601
+ );
2602
2602
 
2603
2603
  // Phase 18.τ — fire onBundleComplete(stats) before return.
2604
2604
  await fireOnBundleComplete(stats);
@@ -2626,14 +2626,14 @@ export function formatSize(bytes: number): string {
2626
2626
  * 번들 결과 요약 출력
2627
2627
  */
2628
2628
  export function printBundleStats(result: BundleResult): void {
2629
- console.log("\n📦 Mandu Client Bundles");
2630
- console.log("=".repeat(50));
2631
-
2632
- const partialCount = Object.keys(result.manifest.partials ?? {}).length;
2633
- if (result.outputs.length === 0 && partialCount === 0) {
2634
- console.log("No islands or partials to bundle (hydration: none or no client entry)");
2635
- return;
2636
- }
2629
+ console.log("\n📦 Mandu Client Bundles");
2630
+ console.log("=".repeat(50));
2631
+
2632
+ const partialCount = Object.keys(result.manifest.partials ?? {}).length;
2633
+ if (result.outputs.length === 0 && partialCount === 0) {
2634
+ console.log("No islands or partials to bundle (hydration: none or no client entry)");
2635
+ return;
2636
+ }
2637
2637
 
2638
2638
  console.log(`Environment: ${result.manifest.env}`);
2639
2639
  console.log(`Bundles: ${result.stats.bundleCount}`);
@@ -2642,15 +2642,15 @@ export function printBundleStats(result: BundleResult): void {
2642
2642
  console.log(`Build Time: ${result.stats.buildTime.toFixed(0)}ms`);
2643
2643
  console.log("");
2644
2644
 
2645
- // 각 번들 정보
2646
- for (const output of result.outputs) {
2647
- console.log(
2648
- ` ${output.routeId}: ${formatSize(output.size)} (gzip: ${formatSize(output.gzipSize)})`
2649
- );
2650
- }
2651
- if (partialCount > 0) {
2652
- console.log(` Partials: ${partialCount}`);
2653
- }
2645
+ // 각 번들 정보
2646
+ for (const output of result.outputs) {
2647
+ console.log(
2648
+ ` ${output.routeId}: ${formatSize(output.size)} (gzip: ${formatSize(output.gzipSize)})`
2649
+ );
2650
+ }
2651
+ if (partialCount > 0) {
2652
+ console.log(` Partials: ${partialCount}`);
2653
+ }
2654
2654
 
2655
2655
  if (result.errors.length > 0) {
2656
2656
  console.log("\n⚠️ Errors:");