@mandujs/core 0.54.23 → 0.54.25
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/src/bundler/build.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type * as __ManduPluginsReactCompilerTypes0 from "./plugins/react-compile
|
|
|
4
4
|
* Bun.build 기반 클라이언트 번들 빌드
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { RouteClientBoundary, RoutesManifest, RouteSpec } from "../spec/schema";
|
|
7
|
+
import type { RouteClientBoundary, RoutesManifest, RouteSpec } from "../spec/schema";
|
|
8
8
|
import { needsHydration, getRouteHydration } from "../spec/schema";
|
|
9
9
|
import type {
|
|
10
10
|
BundleResult,
|
|
@@ -36,19 +36,19 @@ import {
|
|
|
36
36
|
type VendorCacheWriteEntry,
|
|
37
37
|
} from "./vendor-cache";
|
|
38
38
|
import path from "path";
|
|
39
|
-
import fs from "fs/promises";
|
|
40
|
-
|
|
41
|
-
interface BoundaryBundleBuild {
|
|
42
|
-
id: string;
|
|
43
|
-
route: string;
|
|
44
|
-
js: string;
|
|
45
|
-
module: string;
|
|
46
|
-
exportName: string;
|
|
47
|
-
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
48
|
-
hydrate: string;
|
|
49
|
-
size: number;
|
|
50
|
-
gzipSize: number;
|
|
51
|
-
}
|
|
39
|
+
import fs from "fs/promises";
|
|
40
|
+
|
|
41
|
+
interface BoundaryBundleBuild {
|
|
42
|
+
id: string;
|
|
43
|
+
route: string;
|
|
44
|
+
js: string;
|
|
45
|
+
module: string;
|
|
46
|
+
exportName: string;
|
|
47
|
+
priority: "immediate" | "visible" | "idle" | "interaction";
|
|
48
|
+
hydrate: string;
|
|
49
|
+
size: number;
|
|
50
|
+
gzipSize: number;
|
|
51
|
+
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Resolve Mandu's default bundler plugin set from a `BundlerOptions`
|
|
@@ -423,24 +423,24 @@ function createEmptyManifest(env: "development" | "production"): BundleManifest
|
|
|
423
423
|
/**
|
|
424
424
|
* Hydration이 필요한 라우트 필터링
|
|
425
425
|
*/
|
|
426
|
-
function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
|
|
427
|
-
return manifest.routes.filter(
|
|
428
|
-
(route) =>
|
|
429
|
-
route.kind === "page" &&
|
|
430
|
-
(!!route.clientModule || !!route.boundaries?.length) &&
|
|
431
|
-
needsHydration(route)
|
|
432
|
-
);
|
|
433
|
-
}
|
|
426
|
+
function getHydratedRoutes(manifest: RoutesManifest): RouteSpec[] {
|
|
427
|
+
return manifest.routes.filter(
|
|
428
|
+
(route) =>
|
|
429
|
+
route.kind === "page" &&
|
|
430
|
+
(!!route.clientModule || !!route.boundaries?.length) &&
|
|
431
|
+
needsHydration(route)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
434
|
|
|
435
435
|
function getHydrationRoutesMissingClientModule(manifest: RoutesManifest): RouteSpec[] {
|
|
436
436
|
return manifest.routes.filter(
|
|
437
|
-
(route) =>
|
|
438
|
-
route.kind === "page" &&
|
|
439
|
-
!route.clientModule &&
|
|
440
|
-
!route.boundaries?.length &&
|
|
441
|
-
needsHydration(route)
|
|
442
|
-
);
|
|
443
|
-
}
|
|
437
|
+
(route) =>
|
|
438
|
+
route.kind === "page" &&
|
|
439
|
+
!route.clientModule &&
|
|
440
|
+
!route.boundaries?.length &&
|
|
441
|
+
needsHydration(route)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
444
|
|
|
445
445
|
const REACT_SHIM_EXPORTS = [
|
|
446
446
|
"Activity",
|
|
@@ -514,9 +514,9 @@ function formatShimBindings(names: readonly string[], indent = " "): string {
|
|
|
514
514
|
return names.map((name) => `${indent}${name},`).join("\n");
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
/**
|
|
518
|
-
* Runtime bundle entry point.
|
|
519
|
-
*
|
|
517
|
+
/**
|
|
518
|
+
* Runtime bundle entry point.
|
|
519
|
+
*
|
|
520
520
|
* The browser runtime lives in client/runtime-entry.ts so it is typechecked
|
|
521
521
|
* with the rest of core and imports the shared props deserializer directly.
|
|
522
522
|
*/
|
|
@@ -633,9 +633,11 @@ function generateJsxRuntimeShimSource(): string {
|
|
|
633
633
|
/**
|
|
634
634
|
* Mandu JSX Runtime Shim (Generated)
|
|
635
635
|
* Production JSX 변환용
|
|
636
|
-
*
|
|
636
|
+
* jsx/jsxs/Fragment는 'react/jsx-runtime' 원본에서 직접 가져온다.
|
|
637
|
+
* import map의 'react/jsx-runtime'이 다시 이 셰임을 가리키므로,
|
|
638
|
+
* 셰임 내부는 실제 원본 경로를 직접 참조해야 순환/누락이 생기지 않는다.
|
|
637
639
|
*/
|
|
638
|
-
import { jsx, jsxs, Fragment } from 'react';
|
|
640
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
639
641
|
|
|
640
642
|
// Named exports
|
|
641
643
|
export { jsx, jsxs, Fragment };
|
|
@@ -654,9 +656,11 @@ function generateJsxDevRuntimeShimSource(): string {
|
|
|
654
656
|
/**
|
|
655
657
|
* Mandu JSX Dev Runtime Shim (Generated)
|
|
656
658
|
* Development JSX 변환용
|
|
657
|
-
*
|
|
659
|
+
* jsxDEV/Fragment는 'react'가 아니라 'react/jsx-dev-runtime'에서만 export된다.
|
|
660
|
+
* import map의 'react/jsx-dev-runtime'이 다시 이 셰임을 가리키므로,
|
|
661
|
+
* 셰임 내부는 실제 원본 경로를 직접 참조해야 순환/누락(jsxDEV undefined)이 생기지 않는다.
|
|
658
662
|
*/
|
|
659
|
-
import { jsxDEV, Fragment } from 'react';
|
|
663
|
+
import { jsxDEV, Fragment } from 'react/jsx-dev-runtime';
|
|
660
664
|
|
|
661
665
|
// Named exports
|
|
662
666
|
export { jsxDEV, Fragment };
|
|
@@ -666,6 +670,18 @@ export default { jsxDEV, Fragment };
|
|
|
666
670
|
`;
|
|
667
671
|
}
|
|
668
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Test-only access to the JSX runtime shim generators so tests can assert
|
|
675
|
+
* that jsx/jsxDEV/Fragment are sourced from the correct React subpaths
|
|
676
|
+
* (regression guard for #322: jsxDEV must come from 'react/jsx-dev-runtime',
|
|
677
|
+
* never from bare 'react' which does not export it).
|
|
678
|
+
*
|
|
679
|
+
* @internal
|
|
680
|
+
*/
|
|
681
|
+
export const _testOnly_generateJsxRuntimeShimSource = generateJsxRuntimeShimSource;
|
|
682
|
+
/** @internal */
|
|
683
|
+
export const _testOnly_generateJsxDevRuntimeShimSource = generateJsxDevRuntimeShimSource;
|
|
684
|
+
|
|
669
685
|
/**
|
|
670
686
|
* Client-side Router 런타임 소스 생성
|
|
671
687
|
*/
|
|
@@ -730,44 +746,44 @@ function patternCacheSet(key, value) {
|
|
|
730
746
|
patternCache.set(key, value);
|
|
731
747
|
}
|
|
732
748
|
|
|
733
|
-
function compilePattern(pattern) {
|
|
734
|
-
var cached = patternCacheGet(pattern);
|
|
735
|
-
if (cached) return cached;
|
|
736
|
-
|
|
737
|
-
const paramNames = [];
|
|
738
|
-
const normalized = pattern === '/' ? '/' : pattern.replace(/\\/+$/, '') || '/';
|
|
739
|
-
const segments = normalized.split('/').filter(Boolean);
|
|
740
|
-
const regexStr = segments.length === 0
|
|
741
|
-
? '/'
|
|
742
|
-
: segments.map((segment) => {
|
|
743
|
-
if (segment === '*') return '/.+';
|
|
744
|
-
const wildcardMatch = segment.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)\\*(\\?)?$/);
|
|
745
|
-
if (wildcardMatch) {
|
|
746
|
-
paramNames.push(wildcardMatch[1]);
|
|
747
|
-
return wildcardMatch[2] === '?' ? '(?:/(.*))?' : '/(.+)';
|
|
748
|
-
}
|
|
749
|
-
const paramMatch = segment.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
750
|
-
if (paramMatch) {
|
|
751
|
-
paramNames.push(paramMatch[1]);
|
|
752
|
-
return '/([^/]+)';
|
|
753
|
-
}
|
|
754
|
-
return '/' + segment.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
755
|
-
}).join('');
|
|
756
|
-
|
|
757
|
-
const compiled = { regex: new RegExp('^' + regexStr + '$'), paramNames };
|
|
758
|
-
patternCacheSet(pattern, compiled);
|
|
759
|
-
return compiled;
|
|
760
|
-
}
|
|
749
|
+
function compilePattern(pattern) {
|
|
750
|
+
var cached = patternCacheGet(pattern);
|
|
751
|
+
if (cached) return cached;
|
|
752
|
+
|
|
753
|
+
const paramNames = [];
|
|
754
|
+
const normalized = pattern === '/' ? '/' : pattern.replace(/\\/+$/, '') || '/';
|
|
755
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
756
|
+
const regexStr = segments.length === 0
|
|
757
|
+
? '/'
|
|
758
|
+
: segments.map((segment) => {
|
|
759
|
+
if (segment === '*') return '/.+';
|
|
760
|
+
const wildcardMatch = segment.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)\\*(\\?)?$/);
|
|
761
|
+
if (wildcardMatch) {
|
|
762
|
+
paramNames.push(wildcardMatch[1]);
|
|
763
|
+
return wildcardMatch[2] === '?' ? '(?:/(.*))?' : '/(.+)';
|
|
764
|
+
}
|
|
765
|
+
const paramMatch = segment.match(/^:([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
766
|
+
if (paramMatch) {
|
|
767
|
+
paramNames.push(paramMatch[1]);
|
|
768
|
+
return '/([^/]+)';
|
|
769
|
+
}
|
|
770
|
+
return '/' + segment.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
771
|
+
}).join('');
|
|
772
|
+
|
|
773
|
+
const compiled = { regex: new RegExp('^' + regexStr + '$'), paramNames };
|
|
774
|
+
patternCacheSet(pattern, compiled);
|
|
775
|
+
return compiled;
|
|
776
|
+
}
|
|
761
777
|
|
|
762
778
|
function extractParams(pattern, pathname) {
|
|
763
779
|
const compiled = compilePattern(pattern);
|
|
764
780
|
const match = pathname.match(compiled.regex);
|
|
765
781
|
if (!match) return {};
|
|
766
|
-
|
|
767
|
-
const params = {};
|
|
768
|
-
compiled.paramNames.forEach((name, i) => { params[name] = match[i + 1] || ''; });
|
|
769
|
-
return params;
|
|
770
|
-
}
|
|
782
|
+
|
|
783
|
+
const params = {};
|
|
784
|
+
compiled.paramNames.forEach((name, i) => { params[name] = match[i + 1] || ''; });
|
|
785
|
+
return params;
|
|
786
|
+
}
|
|
771
787
|
|
|
772
788
|
function notifyListeners() {
|
|
773
789
|
const state = getGlobalState();
|
|
@@ -1010,74 +1026,74 @@ async function buildRouterRuntime(
|
|
|
1010
1026
|
* - Runtime이 dynamic import로 로드
|
|
1011
1027
|
* - 등록/초기화 코드 없음
|
|
1012
1028
|
*/
|
|
1013
|
-
function generateIslandEntry(routeId: string, clientModulePath: string, exportName?: string): string {
|
|
1014
|
-
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1015
|
-
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1016
|
-
const normalizedExportName = exportName && exportName !== "default" ? exportName : undefined;
|
|
1017
|
-
const namedExport = normalizedExportName && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalizedExportName)
|
|
1018
|
-
? `export const ${normalizedExportName} = exportedIsland;`
|
|
1019
|
-
: "";
|
|
1020
|
-
const candidates = [
|
|
1021
|
-
normalizedExportName,
|
|
1022
|
-
inferClientExportNameFromPath(clientModulePath),
|
|
1023
|
-
inferClientExportNameFromRouteId(routeId),
|
|
1024
|
-
].filter((candidate, index, values): candidate is string =>
|
|
1025
|
-
!!candidate && values.indexOf(candidate) === index
|
|
1026
|
-
);
|
|
1027
|
-
const importSpecifier = JSON.stringify(normalizedPath);
|
|
1028
|
-
const routeLabel = JSON.stringify(routeId);
|
|
1029
|
-
const commentRouteId = routeId.replace(/\*\//g, "* /");
|
|
1030
|
-
return `
|
|
1031
|
-
/**
|
|
1032
|
-
* Mandu Island: ${commentRouteId} (Generated)
|
|
1033
|
-
* Pure export - no side effects
|
|
1034
|
-
*/
|
|
1035
|
-
import React from "react";
|
|
1036
|
-
import * as islandModule from ${importSpecifier};
|
|
1037
|
-
|
|
1038
|
-
const candidateExportNames = ${JSON.stringify(candidates)};
|
|
1039
|
-
const explicitExportName = ${JSON.stringify(normalizedExportName ?? null)};
|
|
1040
|
-
|
|
1041
|
-
function resolveIslandExport(mod) {
|
|
1042
|
-
if (explicitExportName && mod[explicitExportName]) return mod[explicitExportName];
|
|
1043
|
-
if (mod.default) return mod.default;
|
|
1044
|
-
for (const name of candidateExportNames) {
|
|
1045
|
-
if (mod[name]) return mod[name];
|
|
1046
|
-
}
|
|
1047
|
-
const runtimeExports = Object.keys(mod).filter((name) => name !== "__esModule");
|
|
1048
|
-
if (runtimeExports.length === 1) return mod[runtimeExports[0]];
|
|
1049
|
-
throw new Error(
|
|
1050
|
-
"[Mandu Island] " + ${routeLabel} + " must export a default component" +
|
|
1051
|
-
(candidateExportNames.length > 0 ? " or one of: " + candidateExportNames.join(", ") : "")
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
const island = resolveIslandExport(islandModule);
|
|
1056
|
-
const exportedIsland = island && island.__mandu_island === true
|
|
1057
|
-
? island
|
|
1058
|
-
: function ManduGeneratedIsland(props) {
|
|
1059
|
-
return React.createElement(island, props || {});
|
|
1060
|
-
};
|
|
1061
|
-
|
|
1062
|
-
export default exportedIsland;
|
|
1063
|
-
${namedExport}
|
|
1064
|
-
`;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function inferClientExportNameFromPath(clientModulePath: string): string | null {
|
|
1068
|
-
const basename = path.basename(clientModulePath).replace(/\.[cm]?[jt]sx?$/, "");
|
|
1069
|
-
const withoutClientSuffix = basename.replace(/\.(client|island)$/, "");
|
|
1070
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(withoutClientSuffix) ? withoutClientSuffix : null;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
function inferClientExportNameFromRouteId(routeId: string): string | null {
|
|
1074
|
-
const pascal = routeId
|
|
1075
|
-
.split(/[^A-Za-z0-9]+/)
|
|
1076
|
-
.filter(Boolean)
|
|
1077
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1078
|
-
.join("");
|
|
1079
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(pascal) ? pascal : null;
|
|
1080
|
-
}
|
|
1029
|
+
function generateIslandEntry(routeId: string, clientModulePath: string, exportName?: string): string {
|
|
1030
|
+
// Windows 경로의 백슬래시를 슬래시로 변환 (JS escape 문제 방지)
|
|
1031
|
+
const normalizedPath = clientModulePath.replace(/\\/g, "/");
|
|
1032
|
+
const normalizedExportName = exportName && exportName !== "default" ? exportName : undefined;
|
|
1033
|
+
const namedExport = normalizedExportName && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalizedExportName)
|
|
1034
|
+
? `export const ${normalizedExportName} = exportedIsland;`
|
|
1035
|
+
: "";
|
|
1036
|
+
const candidates = [
|
|
1037
|
+
normalizedExportName,
|
|
1038
|
+
inferClientExportNameFromPath(clientModulePath),
|
|
1039
|
+
inferClientExportNameFromRouteId(routeId),
|
|
1040
|
+
].filter((candidate, index, values): candidate is string =>
|
|
1041
|
+
!!candidate && values.indexOf(candidate) === index
|
|
1042
|
+
);
|
|
1043
|
+
const importSpecifier = JSON.stringify(normalizedPath);
|
|
1044
|
+
const routeLabel = JSON.stringify(routeId);
|
|
1045
|
+
const commentRouteId = routeId.replace(/\*\//g, "* /");
|
|
1046
|
+
return `
|
|
1047
|
+
/**
|
|
1048
|
+
* Mandu Island: ${commentRouteId} (Generated)
|
|
1049
|
+
* Pure export - no side effects
|
|
1050
|
+
*/
|
|
1051
|
+
import React from "react";
|
|
1052
|
+
import * as islandModule from ${importSpecifier};
|
|
1053
|
+
|
|
1054
|
+
const candidateExportNames = ${JSON.stringify(candidates)};
|
|
1055
|
+
const explicitExportName = ${JSON.stringify(normalizedExportName ?? null)};
|
|
1056
|
+
|
|
1057
|
+
function resolveIslandExport(mod) {
|
|
1058
|
+
if (explicitExportName && mod[explicitExportName]) return mod[explicitExportName];
|
|
1059
|
+
if (mod.default) return mod.default;
|
|
1060
|
+
for (const name of candidateExportNames) {
|
|
1061
|
+
if (mod[name]) return mod[name];
|
|
1062
|
+
}
|
|
1063
|
+
const runtimeExports = Object.keys(mod).filter((name) => name !== "__esModule");
|
|
1064
|
+
if (runtimeExports.length === 1) return mod[runtimeExports[0]];
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
"[Mandu Island] " + ${routeLabel} + " must export a default component" +
|
|
1067
|
+
(candidateExportNames.length > 0 ? " or one of: " + candidateExportNames.join(", ") : "")
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const island = resolveIslandExport(islandModule);
|
|
1072
|
+
const exportedIsland = island && island.__mandu_island === true
|
|
1073
|
+
? island
|
|
1074
|
+
: function ManduGeneratedIsland(props) {
|
|
1075
|
+
return React.createElement(island, props || {});
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
export default exportedIsland;
|
|
1079
|
+
${namedExport}
|
|
1080
|
+
`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function inferClientExportNameFromPath(clientModulePath: string): string | null {
|
|
1084
|
+
const basename = path.basename(clientModulePath).replace(/\.[cm]?[jt]sx?$/, "");
|
|
1085
|
+
const withoutClientSuffix = basename.replace(/\.(client|island)$/, "");
|
|
1086
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(withoutClientSuffix) ? withoutClientSuffix : null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function inferClientExportNameFromRouteId(routeId: string): string | null {
|
|
1090
|
+
const pascal = routeId
|
|
1091
|
+
.split(/[^A-Za-z0-9]+/)
|
|
1092
|
+
.filter(Boolean)
|
|
1093
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1094
|
+
.join("");
|
|
1095
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(pascal) ? pascal : null;
|
|
1096
|
+
}
|
|
1081
1097
|
|
|
1082
1098
|
function generatePartialEntry(partialId: string, partialModulePath: string): string {
|
|
1083
1099
|
const normalizedPath = partialModulePath.replace(/\\/g, "/");
|
|
@@ -1123,17 +1139,17 @@ export default {
|
|
|
1123
1139
|
/**
|
|
1124
1140
|
* Runtime 번들 빌드
|
|
1125
1141
|
*/
|
|
1126
|
-
async function buildRuntime(
|
|
1127
|
-
outDir: string,
|
|
1128
|
-
options: BundlerOptions
|
|
1129
|
-
): Promise<{ success: boolean; outputPath: string; errors: string[] }> {
|
|
1130
|
-
const runtimePath = getRuntimeEntryPath();
|
|
1131
|
-
const outputName = "_runtime.js";
|
|
1132
|
-
|
|
1133
|
-
try {
|
|
1134
|
-
const result = await safeBuild({
|
|
1135
|
-
entrypoints: [runtimePath],
|
|
1136
|
-
outdir: outDir,
|
|
1142
|
+
async function buildRuntime(
|
|
1143
|
+
outDir: string,
|
|
1144
|
+
options: BundlerOptions
|
|
1145
|
+
): Promise<{ success: boolean; outputPath: string; errors: string[] }> {
|
|
1146
|
+
const runtimePath = getRuntimeEntryPath();
|
|
1147
|
+
const outputName = "_runtime.js";
|
|
1148
|
+
|
|
1149
|
+
try {
|
|
1150
|
+
const result = await safeBuild({
|
|
1151
|
+
entrypoints: [runtimePath],
|
|
1152
|
+
outdir: outDir,
|
|
1137
1153
|
naming: outputName,
|
|
1138
1154
|
minify: shouldMinify(options),
|
|
1139
1155
|
sourcemap: options.sourcemap ? "external" : "none",
|
|
@@ -1144,24 +1160,24 @@ async function buildRuntime(
|
|
|
1144
1160
|
"process.env.NODE_ENV": nodeEnvDefine(options),
|
|
1145
1161
|
...options.define,
|
|
1146
1162
|
},
|
|
1147
|
-
});
|
|
1148
|
-
|
|
1149
|
-
if (!result.success) {
|
|
1150
|
-
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1151
|
-
return {
|
|
1152
|
-
success: false,
|
|
1153
|
-
outputPath: "",
|
|
1154
|
-
errors: [`Runtime bundle build failed (source: ${runtimePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types.`],
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
return {
|
|
1159
|
-
success: true,
|
|
1160
|
-
outputPath: `/.mandu/client/${outputName}`,
|
|
1161
|
-
errors: [],
|
|
1162
|
-
};
|
|
1163
|
-
} catch (error: unknown) {
|
|
1164
|
-
const extra: string[] = [];
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
if (!result.success) {
|
|
1166
|
+
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1167
|
+
return {
|
|
1168
|
+
success: false,
|
|
1169
|
+
outputPath: "",
|
|
1170
|
+
errors: [`Runtime bundle build failed (source: ${runtimePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types.`],
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return {
|
|
1175
|
+
success: true,
|
|
1176
|
+
outputPath: `/.mandu/client/${outputName}`,
|
|
1177
|
+
errors: [],
|
|
1178
|
+
};
|
|
1179
|
+
} catch (error: unknown) {
|
|
1180
|
+
const extra: string[] = [];
|
|
1165
1181
|
const errObj = error as Record<string, unknown> | null;
|
|
1166
1182
|
if (errObj && Array.isArray(errObj.errors)) {
|
|
1167
1183
|
extra.push(...errObj.errors.map((e: unknown) => String((e as Record<string, unknown>)?.message || e)));
|
|
@@ -1500,41 +1516,41 @@ async function buildVendorShims(
|
|
|
1500
1516
|
};
|
|
1501
1517
|
}
|
|
1502
1518
|
|
|
1503
|
-
function vendorShimFailureHint(shimName: string): string {
|
|
1504
|
-
if (shimName.includes("react-refresh")) {
|
|
1505
|
-
return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
|
|
1506
|
-
}
|
|
1507
|
-
return "Hint: check the import paths and ensure the vendor package is installed.";
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
function routeIdToAssetStem(routeId: string): string {
|
|
1511
|
-
const safe = routeId.replace(/[<>:"/\\|?*\x00-\x1F]/g, (ch) =>
|
|
1512
|
-
`_${ch.codePointAt(0)!.toString(16)}_`
|
|
1513
|
-
);
|
|
1514
|
-
return safe.replace(/[. ]+$/g, "") || "route";
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
/**
|
|
1518
|
-
* 단일 Island 번들 빌드
|
|
1519
|
-
*/
|
|
1520
|
-
async function buildIsland(
|
|
1519
|
+
function vendorShimFailureHint(shimName: string): string {
|
|
1520
|
+
if (shimName.includes("react-refresh")) {
|
|
1521
|
+
return "Hint: install the optional dev peer dependency with `bun add -d react-refresh`.";
|
|
1522
|
+
}
|
|
1523
|
+
return "Hint: check the import paths and ensure the vendor package is installed.";
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function routeIdToAssetStem(routeId: string): string {
|
|
1527
|
+
const safe = routeId.replace(/[<>:"/\\|?*\x00-\x1F]/g, (ch) =>
|
|
1528
|
+
`_${ch.codePointAt(0)!.toString(16)}_`
|
|
1529
|
+
);
|
|
1530
|
+
return safe.replace(/[. ]+$/g, "") || "route";
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* 단일 Island 번들 빌드
|
|
1535
|
+
*/
|
|
1536
|
+
async function buildIsland(
|
|
1521
1537
|
route: RouteSpec,
|
|
1522
1538
|
rootDir: string,
|
|
1523
1539
|
outDir: string,
|
|
1524
1540
|
options: BundlerOptions
|
|
1525
|
-
): Promise<BundleOutput> {
|
|
1526
|
-
const clientModulePath = path.join(rootDir, route.clientModule!);
|
|
1527
|
-
const assetStem = routeIdToAssetStem(route.id);
|
|
1528
|
-
const entryStem = `_entry_${assetStem}`;
|
|
1529
|
-
const entryPath = path.join(outDir, `${entryStem}.js`);
|
|
1530
|
-
const outputName = `${assetStem}.island.js`;
|
|
1541
|
+
): Promise<BundleOutput> {
|
|
1542
|
+
const clientModulePath = path.join(rootDir, route.clientModule!);
|
|
1543
|
+
const assetStem = routeIdToAssetStem(route.id);
|
|
1544
|
+
const entryStem = `_entry_${assetStem}`;
|
|
1545
|
+
const entryPath = path.join(outDir, `${entryStem}.js`);
|
|
1546
|
+
const outputName = `${assetStem}.island.js`;
|
|
1531
1547
|
|
|
1532
1548
|
// Phase 7.1 B-1/B-4: wire native Fast Refresh transform + Mandu's
|
|
1533
1549
|
// boundary injection plugin. Dev-only; prod bundles remain clean.
|
|
1534
1550
|
const isDev = isDevelopmentBuild(options);
|
|
1535
1551
|
try {
|
|
1536
1552
|
// 엔트리 래퍼 생성
|
|
1537
|
-
await Bun.write(entryPath, generateIslandEntry(route.id, clientModulePath, route.clientExportName));
|
|
1553
|
+
await Bun.write(entryPath, generateIslandEntry(route.id, clientModulePath, route.clientExportName));
|
|
1538
1554
|
|
|
1539
1555
|
// 빌드
|
|
1540
1556
|
// splitting 옵션: true면 공통 코드를 별도 청크로 추출
|
|
@@ -1568,11 +1584,11 @@ async function buildIsland(
|
|
|
1568
1584
|
let actualOutputPath: string;
|
|
1569
1585
|
let actualOutputName: string;
|
|
1570
1586
|
|
|
1571
|
-
if (options.splitting && result.outputs.length > 0) {
|
|
1572
|
-
// splitting 모드: 결과에서 엔트리 파일 찾기
|
|
1573
|
-
const entryOutput = result.outputs.find(
|
|
1574
|
-
(o) => o.kind === "entry-point" || o.path.includes(entryStem) || o.path.includes(assetStem)
|
|
1575
|
-
);
|
|
1587
|
+
if (options.splitting && result.outputs.length > 0) {
|
|
1588
|
+
// splitting 모드: 결과에서 엔트리 파일 찾기
|
|
1589
|
+
const entryOutput = result.outputs.find(
|
|
1590
|
+
(o) => o.kind === "entry-point" || o.path.includes(entryStem) || o.path.includes(assetStem)
|
|
1591
|
+
);
|
|
1576
1592
|
if (entryOutput) {
|
|
1577
1593
|
actualOutputPath = entryOutput.path;
|
|
1578
1594
|
actualOutputName = path.basename(entryOutput.path);
|
|
@@ -1600,171 +1616,171 @@ async function buildIsland(
|
|
|
1600
1616
|
} catch (error) {
|
|
1601
1617
|
await fs.unlink(entryPath).catch(() => {});
|
|
1602
1618
|
throw error;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
async function buildBoundaryBundle(
|
|
1607
|
-
boundary: RouteClientBoundary,
|
|
1608
|
-
rootDir: string,
|
|
1609
|
-
outDir: string,
|
|
1610
|
-
options: BundlerOptions,
|
|
1611
|
-
): Promise<BoundaryBundleBuild> {
|
|
1612
|
-
const clientModulePath = path.join(rootDir, boundary.module);
|
|
1613
|
-
const assetStem = routeIdToAssetStem(boundary.id);
|
|
1614
|
-
const entryStem = `_entry_boundary_${assetStem}`;
|
|
1615
|
-
const entryPath = path.join(outDir, `${entryStem}.js`);
|
|
1616
|
-
const outputName = `${assetStem}.boundary.js`;
|
|
1617
|
-
const isDev = isDevelopmentBuild(options);
|
|
1618
|
-
|
|
1619
|
-
try {
|
|
1620
|
-
await Bun.write(entryPath, generateIslandEntry(boundary.id, clientModulePath, boundary.exportName));
|
|
1621
|
-
|
|
1622
|
-
const result = await safeBuild({
|
|
1623
|
-
entrypoints: [entryPath],
|
|
1624
|
-
outdir: outDir,
|
|
1625
|
-
naming: options.splitting ? "[name]-[hash].js" : outputName,
|
|
1626
|
-
minify: shouldMinify(options),
|
|
1627
|
-
sourcemap: options.sourcemap ? "external" : "none",
|
|
1628
|
-
target: "browser",
|
|
1629
|
-
splitting: shouldSplitChunks(options),
|
|
1630
|
-
...(isDev ? { reactFastRefresh: true } : {}),
|
|
1631
|
-
plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
|
|
1632
|
-
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
1633
|
-
define: {
|
|
1634
|
-
"process.env.NODE_ENV": nodeEnvDefine(options),
|
|
1635
|
-
...options.define,
|
|
1636
|
-
},
|
|
1637
|
-
});
|
|
1638
|
-
|
|
1639
|
-
await fs.unlink(entryPath).catch(() => {});
|
|
1640
|
-
|
|
1641
|
-
if (!result.success) {
|
|
1642
|
-
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1643
|
-
throw new Error(`Boundary build failed for '${boundary.id}' (source: ${clientModulePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types in this client boundary file.`);
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
let actualOutputPath: string;
|
|
1647
|
-
let actualOutputName: string;
|
|
1648
|
-
if (options.splitting && result.outputs.length > 0) {
|
|
1649
|
-
const entryOutput = result.outputs.find(
|
|
1650
|
-
(o) => o.kind === "entry-point" || o.path.includes(entryStem) || o.path.includes(assetStem),
|
|
1651
|
-
);
|
|
1652
|
-
actualOutputPath = entryOutput?.path ?? result.outputs[0].path;
|
|
1653
|
-
actualOutputName = path.basename(actualOutputPath);
|
|
1654
|
-
} else {
|
|
1655
|
-
actualOutputPath = path.join(outDir, outputName);
|
|
1656
|
-
actualOutputName = outputName;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
const outputFile = Bun.file(actualOutputPath);
|
|
1660
|
-
const content = await sanitizeGeneratedClientBundle(actualOutputPath, isDev);
|
|
1661
|
-
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
1662
|
-
const priority = boundaryPriorityToLegacyPriority(boundary.hydrate);
|
|
1663
|
-
|
|
1664
|
-
return {
|
|
1665
|
-
id: boundary.id,
|
|
1666
|
-
route: boundary.routeId,
|
|
1667
|
-
js: `/.mandu/client/${actualOutputName}`,
|
|
1668
|
-
module: boundary.module,
|
|
1669
|
-
exportName: boundary.exportName,
|
|
1670
|
-
priority,
|
|
1671
|
-
hydrate: boundary.hydrate,
|
|
1672
|
-
size: outputFile.size,
|
|
1673
|
-
gzipSize: gzipped.length,
|
|
1674
|
-
};
|
|
1675
|
-
} finally {
|
|
1676
|
-
await fs.unlink(entryPath).catch(() => {});
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
async function buildBoundaryBundlesForRecords(
|
|
1681
|
-
boundaries: RouteClientBoundary[],
|
|
1682
|
-
rootDir: string,
|
|
1683
|
-
outDir: string,
|
|
1684
|
-
options: BundlerOptions,
|
|
1685
|
-
errors: string[],
|
|
1686
|
-
): Promise<BoundaryBundleBuild[]> {
|
|
1687
|
-
if (boundaries.length === 0) return [];
|
|
1688
|
-
if (pushDuplicateBoundaryIdErrors(boundaries, errors)) return [];
|
|
1689
|
-
|
|
1690
|
-
const results = await Promise.all(
|
|
1691
|
-
boundaries.map(async (boundary) => {
|
|
1692
|
-
try {
|
|
1693
|
-
return await buildBoundaryBundle(boundary, rootDir, outDir, options);
|
|
1694
|
-
} catch (error) {
|
|
1695
|
-
errors.push(`[boundary:${boundary.id}] ${String(error)}`);
|
|
1696
|
-
return null;
|
|
1697
|
-
}
|
|
1698
|
-
}),
|
|
1699
|
-
);
|
|
1700
|
-
|
|
1701
|
-
return results.filter((result): result is BoundaryBundleBuild => result !== null);
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
function pushDuplicateBoundaryIdErrors(boundaries: RouteClientBoundary[], errors: string[]): boolean {
|
|
1705
|
-
const firstById = new Map<string, RouteClientBoundary>();
|
|
1706
|
-
let hasDuplicate = false;
|
|
1707
|
-
|
|
1708
|
-
for (const boundary of boundaries) {
|
|
1709
|
-
const first = firstById.get(boundary.id);
|
|
1710
|
-
if (!first) {
|
|
1711
|
-
firstById.set(boundary.id, boundary);
|
|
1712
|
-
continue;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
hasDuplicate = true;
|
|
1716
|
-
errors.push(
|
|
1717
|
-
`[boundary:${boundary.id}] MANDU_BOUNDARY_DUPLICATE_ID Duplicate client boundary id. ` +
|
|
1718
|
-
`First route="${first.routeId}" source="${first.source.file}", duplicate route="${boundary.routeId}" source="${boundary.source.file}". ` +
|
|
1719
|
-
"Boundary ids must be unique before bundle manifest generation.",
|
|
1720
|
-
);
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
return hasDuplicate;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function mergeBoundaryBundlesIntoManifest(
|
|
1727
|
-
manifest: BundleManifest,
|
|
1728
|
-
routeIds: Iterable<string>,
|
|
1729
|
-
boundaryBundles: BoundaryBundleBuild[],
|
|
1730
|
-
): void {
|
|
1731
|
-
const rebuiltRouteIds = new Set(routeIds);
|
|
1732
|
-
if (rebuiltRouteIds.size === 0 && boundaryBundles.length === 0) return;
|
|
1733
|
-
|
|
1734
|
-
if (manifest.boundaries) {
|
|
1735
|
-
for (const [id, boundary] of Object.entries(manifest.boundaries)) {
|
|
1736
|
-
if (rebuiltRouteIds.has(boundary.route)) {
|
|
1737
|
-
delete manifest.boundaries[id];
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
if (boundaryBundles.length > 0) {
|
|
1743
|
-
manifest.boundaries = manifest.boundaries || {};
|
|
1744
|
-
for (const boundary of boundaryBundles) {
|
|
1745
|
-
manifest.boundaries[boundary.id] = {
|
|
1746
|
-
route: boundary.route,
|
|
1747
|
-
js: boundary.js,
|
|
1748
|
-
module: boundary.module,
|
|
1749
|
-
exportName: boundary.exportName,
|
|
1750
|
-
priority: boundary.priority,
|
|
1751
|
-
hydrate: boundary.hydrate,
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
if (manifest.boundaries && Object.keys(manifest.boundaries).length === 0) {
|
|
1757
|
-
delete manifest.boundaries;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
function boundaryPriorityToLegacyPriority(value: string): BoundaryBundleBuild["priority"] {
|
|
1762
|
-
if (value === "load") return "immediate";
|
|
1763
|
-
if (value === "immediate" || value === "visible" || value === "idle" || value === "interaction") {
|
|
1764
|
-
return value;
|
|
1765
|
-
}
|
|
1766
|
-
return "visible";
|
|
1767
|
-
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async function buildBoundaryBundle(
|
|
1623
|
+
boundary: RouteClientBoundary,
|
|
1624
|
+
rootDir: string,
|
|
1625
|
+
outDir: string,
|
|
1626
|
+
options: BundlerOptions,
|
|
1627
|
+
): Promise<BoundaryBundleBuild> {
|
|
1628
|
+
const clientModulePath = path.join(rootDir, boundary.module);
|
|
1629
|
+
const assetStem = routeIdToAssetStem(boundary.id);
|
|
1630
|
+
const entryStem = `_entry_boundary_${assetStem}`;
|
|
1631
|
+
const entryPath = path.join(outDir, `${entryStem}.js`);
|
|
1632
|
+
const outputName = `${assetStem}.boundary.js`;
|
|
1633
|
+
const isDev = isDevelopmentBuild(options);
|
|
1634
|
+
|
|
1635
|
+
try {
|
|
1636
|
+
await Bun.write(entryPath, generateIslandEntry(boundary.id, clientModulePath, boundary.exportName));
|
|
1637
|
+
|
|
1638
|
+
const result = await safeBuild({
|
|
1639
|
+
entrypoints: [entryPath],
|
|
1640
|
+
outdir: outDir,
|
|
1641
|
+
naming: options.splitting ? "[name]-[hash].js" : outputName,
|
|
1642
|
+
minify: shouldMinify(options),
|
|
1643
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
1644
|
+
target: "browser",
|
|
1645
|
+
splitting: shouldSplitChunks(options),
|
|
1646
|
+
...(isDev ? { reactFastRefresh: true } : {}),
|
|
1647
|
+
plugins: [...manduClientPlugins(options), ...(isDev ? [fastRefreshPlugin()] : [])],
|
|
1648
|
+
external: ["react", "react-dom", "react-dom/client", ...(options.external || [])],
|
|
1649
|
+
define: {
|
|
1650
|
+
"process.env.NODE_ENV": nodeEnvDefine(options),
|
|
1651
|
+
...options.define,
|
|
1652
|
+
},
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
1656
|
+
|
|
1657
|
+
if (!result.success) {
|
|
1658
|
+
const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
|
|
1659
|
+
throw new Error(`Boundary build failed for '${boundary.id}' (source: ${clientModulePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types in this client boundary file.`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
let actualOutputPath: string;
|
|
1663
|
+
let actualOutputName: string;
|
|
1664
|
+
if (options.splitting && result.outputs.length > 0) {
|
|
1665
|
+
const entryOutput = result.outputs.find(
|
|
1666
|
+
(o) => o.kind === "entry-point" || o.path.includes(entryStem) || o.path.includes(assetStem),
|
|
1667
|
+
);
|
|
1668
|
+
actualOutputPath = entryOutput?.path ?? result.outputs[0].path;
|
|
1669
|
+
actualOutputName = path.basename(actualOutputPath);
|
|
1670
|
+
} else {
|
|
1671
|
+
actualOutputPath = path.join(outDir, outputName);
|
|
1672
|
+
actualOutputName = outputName;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
const outputFile = Bun.file(actualOutputPath);
|
|
1676
|
+
const content = await sanitizeGeneratedClientBundle(actualOutputPath, isDev);
|
|
1677
|
+
const gzipped = Bun.gzipSync(Buffer.from(content));
|
|
1678
|
+
const priority = boundaryPriorityToLegacyPriority(boundary.hydrate);
|
|
1679
|
+
|
|
1680
|
+
return {
|
|
1681
|
+
id: boundary.id,
|
|
1682
|
+
route: boundary.routeId,
|
|
1683
|
+
js: `/.mandu/client/${actualOutputName}`,
|
|
1684
|
+
module: boundary.module,
|
|
1685
|
+
exportName: boundary.exportName,
|
|
1686
|
+
priority,
|
|
1687
|
+
hydrate: boundary.hydrate,
|
|
1688
|
+
size: outputFile.size,
|
|
1689
|
+
gzipSize: gzipped.length,
|
|
1690
|
+
};
|
|
1691
|
+
} finally {
|
|
1692
|
+
await fs.unlink(entryPath).catch(() => {});
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function buildBoundaryBundlesForRecords(
|
|
1697
|
+
boundaries: RouteClientBoundary[],
|
|
1698
|
+
rootDir: string,
|
|
1699
|
+
outDir: string,
|
|
1700
|
+
options: BundlerOptions,
|
|
1701
|
+
errors: string[],
|
|
1702
|
+
): Promise<BoundaryBundleBuild[]> {
|
|
1703
|
+
if (boundaries.length === 0) return [];
|
|
1704
|
+
if (pushDuplicateBoundaryIdErrors(boundaries, errors)) return [];
|
|
1705
|
+
|
|
1706
|
+
const results = await Promise.all(
|
|
1707
|
+
boundaries.map(async (boundary) => {
|
|
1708
|
+
try {
|
|
1709
|
+
return await buildBoundaryBundle(boundary, rootDir, outDir, options);
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
errors.push(`[boundary:${boundary.id}] ${String(error)}`);
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
}),
|
|
1715
|
+
);
|
|
1716
|
+
|
|
1717
|
+
return results.filter((result): result is BoundaryBundleBuild => result !== null);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function pushDuplicateBoundaryIdErrors(boundaries: RouteClientBoundary[], errors: string[]): boolean {
|
|
1721
|
+
const firstById = new Map<string, RouteClientBoundary>();
|
|
1722
|
+
let hasDuplicate = false;
|
|
1723
|
+
|
|
1724
|
+
for (const boundary of boundaries) {
|
|
1725
|
+
const first = firstById.get(boundary.id);
|
|
1726
|
+
if (!first) {
|
|
1727
|
+
firstById.set(boundary.id, boundary);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
hasDuplicate = true;
|
|
1732
|
+
errors.push(
|
|
1733
|
+
`[boundary:${boundary.id}] MANDU_BOUNDARY_DUPLICATE_ID Duplicate client boundary id. ` +
|
|
1734
|
+
`First route="${first.routeId}" source="${first.source.file}", duplicate route="${boundary.routeId}" source="${boundary.source.file}". ` +
|
|
1735
|
+
"Boundary ids must be unique before bundle manifest generation.",
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
return hasDuplicate;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function mergeBoundaryBundlesIntoManifest(
|
|
1743
|
+
manifest: BundleManifest,
|
|
1744
|
+
routeIds: Iterable<string>,
|
|
1745
|
+
boundaryBundles: BoundaryBundleBuild[],
|
|
1746
|
+
): void {
|
|
1747
|
+
const rebuiltRouteIds = new Set(routeIds);
|
|
1748
|
+
if (rebuiltRouteIds.size === 0 && boundaryBundles.length === 0) return;
|
|
1749
|
+
|
|
1750
|
+
if (manifest.boundaries) {
|
|
1751
|
+
for (const [id, boundary] of Object.entries(manifest.boundaries)) {
|
|
1752
|
+
if (rebuiltRouteIds.has(boundary.route)) {
|
|
1753
|
+
delete manifest.boundaries[id];
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (boundaryBundles.length > 0) {
|
|
1759
|
+
manifest.boundaries = manifest.boundaries || {};
|
|
1760
|
+
for (const boundary of boundaryBundles) {
|
|
1761
|
+
manifest.boundaries[boundary.id] = {
|
|
1762
|
+
route: boundary.route,
|
|
1763
|
+
js: boundary.js,
|
|
1764
|
+
module: boundary.module,
|
|
1765
|
+
exportName: boundary.exportName,
|
|
1766
|
+
priority: boundary.priority,
|
|
1767
|
+
hydrate: boundary.hydrate,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
if (manifest.boundaries && Object.keys(manifest.boundaries).length === 0) {
|
|
1773
|
+
delete manifest.boundaries;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function boundaryPriorityToLegacyPriority(value: string): BoundaryBundleBuild["priority"] {
|
|
1778
|
+
if (value === "load") return "immediate";
|
|
1779
|
+
if (value === "immediate" || value === "visible" || value === "idle" || value === "interaction") {
|
|
1780
|
+
return value;
|
|
1781
|
+
}
|
|
1782
|
+
return "visible";
|
|
1783
|
+
}
|
|
1768
1784
|
|
|
1769
1785
|
async function sanitizeGeneratedClientBundle(outputPath: string, isDev: boolean): Promise<string> {
|
|
1770
1786
|
const source = await Bun.file(outputPath).text();
|
|
@@ -1817,11 +1833,11 @@ function createBundleManifest(
|
|
|
1817
1833
|
runtimePath: string,
|
|
1818
1834
|
vendorResult: VendorBuildResult,
|
|
1819
1835
|
routerPath: string,
|
|
1820
|
-
env: "development" | "production",
|
|
1821
|
-
islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
|
|
1822
|
-
partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
|
|
1823
|
-
boundaryBundles?: BoundaryBundleBuild[],
|
|
1824
|
-
): BundleManifest {
|
|
1836
|
+
env: "development" | "production",
|
|
1837
|
+
islandBundles?: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }>,
|
|
1838
|
+
partialBundles?: Array<{ name: string; js: string; priority: PartialFileEntry["priority"] }>,
|
|
1839
|
+
boundaryBundles?: BoundaryBundleBuild[],
|
|
1840
|
+
): BundleManifest {
|
|
1825
1841
|
const bundles: BundleManifest["bundles"] = {};
|
|
1826
1842
|
|
|
1827
1843
|
for (const output of outputs) {
|
|
@@ -1848,7 +1864,7 @@ function createBundleManifest(
|
|
|
1848
1864
|
}
|
|
1849
1865
|
}
|
|
1850
1866
|
|
|
1851
|
-
let partials: BundleManifest["partials"];
|
|
1867
|
+
let partials: BundleManifest["partials"];
|
|
1852
1868
|
if (partialBundles && partialBundles.length > 0) {
|
|
1853
1869
|
partials = {};
|
|
1854
1870
|
for (const partial of partialBundles) {
|
|
@@ -1857,22 +1873,22 @@ function createBundleManifest(
|
|
|
1857
1873
|
priority: partial.priority,
|
|
1858
1874
|
};
|
|
1859
1875
|
}
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
let boundaries: BundleManifest["boundaries"];
|
|
1863
|
-
if (boundaryBundles && boundaryBundles.length > 0) {
|
|
1864
|
-
boundaries = {};
|
|
1865
|
-
for (const boundary of boundaryBundles) {
|
|
1866
|
-
boundaries[boundary.id] = {
|
|
1867
|
-
route: boundary.route,
|
|
1868
|
-
js: boundary.js,
|
|
1869
|
-
module: boundary.module,
|
|
1870
|
-
exportName: boundary.exportName,
|
|
1871
|
-
priority: boundary.priority,
|
|
1872
|
-
hydrate: boundary.hydrate,
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
let boundaries: BundleManifest["boundaries"];
|
|
1879
|
+
if (boundaryBundles && boundaryBundles.length > 0) {
|
|
1880
|
+
boundaries = {};
|
|
1881
|
+
for (const boundary of boundaryBundles) {
|
|
1882
|
+
boundaries[boundary.id] = {
|
|
1883
|
+
route: boundary.route,
|
|
1884
|
+
js: boundary.js,
|
|
1885
|
+
module: boundary.module,
|
|
1886
|
+
exportName: boundary.exportName,
|
|
1887
|
+
priority: boundary.priority,
|
|
1888
|
+
hydrate: boundary.hydrate,
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1876
1892
|
|
|
1877
1893
|
// Phase 7.1 B-2: expose Fast Refresh dev bundles so the HTML
|
|
1878
1894
|
// preamble can inject a dynamic import pointing at them. Only
|
|
@@ -1889,11 +1905,11 @@ function createBundleManifest(
|
|
|
1889
1905
|
version: 1,
|
|
1890
1906
|
buildTime: new Date().toISOString(),
|
|
1891
1907
|
env,
|
|
1892
|
-
bundles,
|
|
1893
|
-
...(islands ? { islands } : {}),
|
|
1894
|
-
...(partials ? { partials } : {}),
|
|
1895
|
-
...(boundaries ? { boundaries } : {}),
|
|
1896
|
-
shared: {
|
|
1908
|
+
bundles,
|
|
1909
|
+
...(islands ? { islands } : {}),
|
|
1910
|
+
...(partials ? { partials } : {}),
|
|
1911
|
+
...(boundaries ? { boundaries } : {}),
|
|
1912
|
+
shared: {
|
|
1897
1913
|
runtime: runtimePath,
|
|
1898
1914
|
vendor: vendorResult.react, // primary vendor for backwards compatibility
|
|
1899
1915
|
router: routerPath, // Client-side Router
|
|
@@ -2089,37 +2105,37 @@ export async function buildClientBundles(
|
|
|
2089
2105
|
};
|
|
2090
2106
|
}
|
|
2091
2107
|
|
|
2092
|
-
// 부분 빌드 모드: targetRouteIds가 지정되면 해당 Island만 재빌드 (#122)
|
|
2093
|
-
if (options.targetRouteIds && options.targetRouteIds.length > 0) {
|
|
2094
|
-
const targetRouteIds = new Set(options.targetRouteIds);
|
|
2095
|
-
const targetRoutes = hydratedRoutes.filter((r) => targetRouteIds.has(r.id));
|
|
2096
|
-
const targetIslandRoutes = targetRoutes.filter((route) => !!route.clientModule);
|
|
2097
|
-
|
|
2098
|
-
const targetResults = await Promise.all(
|
|
2099
|
-
targetIslandRoutes.map(async (route) => {
|
|
2100
|
-
try {
|
|
2101
|
-
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
2102
|
-
} catch (error) {
|
|
2108
|
+
// 부분 빌드 모드: targetRouteIds가 지정되면 해당 Island만 재빌드 (#122)
|
|
2109
|
+
if (options.targetRouteIds && options.targetRouteIds.length > 0) {
|
|
2110
|
+
const targetRouteIds = new Set(options.targetRouteIds);
|
|
2111
|
+
const targetRoutes = hydratedRoutes.filter((r) => targetRouteIds.has(r.id));
|
|
2112
|
+
const targetIslandRoutes = targetRoutes.filter((route) => !!route.clientModule);
|
|
2113
|
+
|
|
2114
|
+
const targetResults = await Promise.all(
|
|
2115
|
+
targetIslandRoutes.map(async (route) => {
|
|
2116
|
+
try {
|
|
2117
|
+
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
2118
|
+
} catch (error) {
|
|
2103
2119
|
return { ok: false as const, routeId: route.id, error: String(error) };
|
|
2104
2120
|
}
|
|
2105
2121
|
}),
|
|
2106
2122
|
);
|
|
2107
2123
|
for (const r of targetResults) {
|
|
2108
2124
|
if (r.ok) outputs.push(r.result);
|
|
2109
|
-
else errors.push(`[${r.routeId}] ${r.error}`);
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
const boundaryRecords = targetRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2113
|
-
const boundaryBundles = await buildBoundaryBundlesForRecords(
|
|
2114
|
-
boundaryRecords,
|
|
2115
|
-
rootDir,
|
|
2116
|
-
outDir,
|
|
2117
|
-
options,
|
|
2118
|
-
errors,
|
|
2119
|
-
);
|
|
2120
|
-
|
|
2121
|
-
// 기존 매니페스트를 읽어 변경된 Island만 갱신
|
|
2122
|
-
let existingManifest: BundleManifest;
|
|
2125
|
+
else errors.push(`[${r.routeId}] ${r.error}`);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const boundaryRecords = targetRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2129
|
+
const boundaryBundles = await buildBoundaryBundlesForRecords(
|
|
2130
|
+
boundaryRecords,
|
|
2131
|
+
rootDir,
|
|
2132
|
+
outDir,
|
|
2133
|
+
options,
|
|
2134
|
+
errors,
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
// 기존 매니페스트를 읽어 변경된 Island만 갱신
|
|
2138
|
+
let existingManifest: BundleManifest;
|
|
2123
2139
|
try {
|
|
2124
2140
|
const manifestData = await fs.readFile(path.join(rootDir, ".mandu/manifest.json"), "utf-8");
|
|
2125
2141
|
existingManifest = JSON.parse(manifestData) as BundleManifest;
|
|
@@ -2132,45 +2148,45 @@ export async function buildClientBundles(
|
|
|
2132
2148
|
for (const routeId of invalidClientRouteIds) {
|
|
2133
2149
|
delete existingManifest.bundles[routeId];
|
|
2134
2150
|
}
|
|
2135
|
-
if (outputs.length > 0 || invalidClientRouteIds.size > 0 || boundaryRecords.length > 0) {
|
|
2136
|
-
for (const output of outputs) {
|
|
2137
|
-
if (existingManifest.bundles[output.routeId]) {
|
|
2138
|
-
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
2139
|
-
} else {
|
|
2140
|
-
const route = targetIslandRoutes.find((r) => r.id === output.routeId);
|
|
2141
|
-
const hydration = route ? getRouteHydration(route) : null;
|
|
2142
|
-
existingManifest.bundles[output.routeId] = {
|
|
2143
|
-
js: output.outputPath,
|
|
2151
|
+
if (outputs.length > 0 || invalidClientRouteIds.size > 0 || boundaryRecords.length > 0) {
|
|
2152
|
+
for (const output of outputs) {
|
|
2153
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
2154
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
2155
|
+
} else {
|
|
2156
|
+
const route = targetIslandRoutes.find((r) => r.id === output.routeId);
|
|
2157
|
+
const hydration = route ? getRouteHydration(route) : null;
|
|
2158
|
+
existingManifest.bundles[output.routeId] = {
|
|
2159
|
+
js: output.outputPath,
|
|
2144
2160
|
dependencies: ["_runtime", "_react"],
|
|
2145
2161
|
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
mergeBoundaryBundlesIntoManifest(
|
|
2151
|
-
existingManifest,
|
|
2152
|
-
targetRoutes.map((route) => route.id),
|
|
2153
|
-
boundaryBundles,
|
|
2154
|
-
);
|
|
2155
|
-
|
|
2156
|
-
await fs.writeFile(
|
|
2157
|
-
path.join(rootDir, ".mandu/manifest.json"),
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
mergeBoundaryBundlesIntoManifest(
|
|
2167
|
+
existingManifest,
|
|
2168
|
+
targetRoutes.map((route) => route.id),
|
|
2169
|
+
boundaryBundles,
|
|
2170
|
+
);
|
|
2171
|
+
|
|
2172
|
+
await fs.writeFile(
|
|
2173
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
2158
2174
|
JSON.stringify(existingManifest, null, 2)
|
|
2159
2175
|
);
|
|
2160
2176
|
}
|
|
2161
|
-
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
2162
|
-
|
|
2163
|
-
const stats = calculateStats(
|
|
2164
|
-
outputs,
|
|
2165
|
-
startTime,
|
|
2166
|
-
boundaryBundles.map((boundary) => ({
|
|
2167
|
-
routeId: `boundary:${boundary.id}`,
|
|
2168
|
-
size: boundary.size,
|
|
2169
|
-
gzipSize: boundary.gzipSize,
|
|
2170
|
-
})),
|
|
2171
|
-
);
|
|
2172
|
-
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2173
|
-
}
|
|
2177
|
+
// When all builds failed, do NOT overwrite manifest — keep previous good state
|
|
2178
|
+
|
|
2179
|
+
const stats = calculateStats(
|
|
2180
|
+
outputs,
|
|
2181
|
+
startTime,
|
|
2182
|
+
boundaryBundles.map((boundary) => ({
|
|
2183
|
+
routeId: `boundary:${boundary.id}`,
|
|
2184
|
+
size: boundary.size,
|
|
2185
|
+
gzipSize: boundary.gzipSize,
|
|
2186
|
+
})),
|
|
2187
|
+
);
|
|
2188
|
+
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2189
|
+
}
|
|
2174
2190
|
|
|
2175
2191
|
// #185: Framework-internal 번들 스킵 모드
|
|
2176
2192
|
// 사용자 코드(src/shared 등) 변경 시 runtime/router/vendor/devtools 재빌드는 낭비.
|
|
@@ -2226,7 +2242,7 @@ export async function buildClientBundles(
|
|
|
2226
2242
|
}
|
|
2227
2243
|
|
|
2228
2244
|
const islandResults = await Promise.all(
|
|
2229
|
-
hydratedRoutes.filter((route) => !!route.clientModule).map(async (route) => {
|
|
2245
|
+
hydratedRoutes.filter((route) => !!route.clientModule).map(async (route) => {
|
|
2230
2246
|
try {
|
|
2231
2247
|
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
2232
2248
|
} catch (error) {
|
|
@@ -2272,32 +2288,32 @@ export async function buildClientBundles(
|
|
|
2272
2288
|
};
|
|
2273
2289
|
}
|
|
2274
2290
|
}
|
|
2275
|
-
if (perIslandBundles.length > 0) {
|
|
2276
|
-
existingManifest.islands = existingManifest.islands || {};
|
|
2277
|
-
for (const ib of perIslandBundles) {
|
|
2278
|
-
existingManifest.islands[ib.name] = {
|
|
2291
|
+
if (perIslandBundles.length > 0) {
|
|
2292
|
+
existingManifest.islands = existingManifest.islands || {};
|
|
2293
|
+
for (const ib of perIslandBundles) {
|
|
2294
|
+
existingManifest.islands[ib.name] = {
|
|
2279
2295
|
js: ib.js,
|
|
2280
2296
|
route: ib.route,
|
|
2281
2297
|
priority: ib.priority,
|
|
2282
|
-
};
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
const boundaryRecords = hydratedRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2287
|
-
const boundaryBundles = await buildBoundaryBundlesForRecords(
|
|
2288
|
-
boundaryRecords,
|
|
2289
|
-
rootDir,
|
|
2290
|
-
outDir,
|
|
2291
|
-
options,
|
|
2292
|
-
errors,
|
|
2293
|
-
);
|
|
2294
|
-
mergeBoundaryBundlesIntoManifest(
|
|
2295
|
-
existingManifest,
|
|
2296
|
-
hydratedRoutes.map((route) => route.id),
|
|
2297
|
-
boundaryBundles,
|
|
2298
|
-
);
|
|
2299
|
-
|
|
2300
|
-
const partialBundles: PartialBundleBuild[] = [];
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
const boundaryRecords = hydratedRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2303
|
+
const boundaryBundles = await buildBoundaryBundlesForRecords(
|
|
2304
|
+
boundaryRecords,
|
|
2305
|
+
rootDir,
|
|
2306
|
+
outDir,
|
|
2307
|
+
options,
|
|
2308
|
+
errors,
|
|
2309
|
+
);
|
|
2310
|
+
mergeBoundaryBundlesIntoManifest(
|
|
2311
|
+
existingManifest,
|
|
2312
|
+
hydratedRoutes.map((route) => route.id),
|
|
2313
|
+
boundaryBundles,
|
|
2314
|
+
);
|
|
2315
|
+
|
|
2316
|
+
const partialBundles: PartialBundleBuild[] = [];
|
|
2301
2317
|
if (partialFiles.length > 0) {
|
|
2302
2318
|
const partialResults = await Promise.all(
|
|
2303
2319
|
partialFiles.map(async (entry) => {
|
|
@@ -2331,22 +2347,22 @@ export async function buildClientBundles(
|
|
|
2331
2347
|
JSON.stringify(existingManifest, null, 2),
|
|
2332
2348
|
);
|
|
2333
2349
|
|
|
2334
|
-
const stats = calculateStats(
|
|
2335
|
-
outputs,
|
|
2336
|
-
startTime,
|
|
2337
|
-
[
|
|
2338
|
-
...partialBundles.map((partial) => ({
|
|
2339
|
-
routeId: `partial:${partial.name}`,
|
|
2340
|
-
size: partial.size,
|
|
2341
|
-
gzipSize: partial.gzipSize,
|
|
2342
|
-
})),
|
|
2343
|
-
...boundaryBundles.map((boundary) => ({
|
|
2344
|
-
routeId: `boundary:${boundary.id}`,
|
|
2345
|
-
size: boundary.size,
|
|
2346
|
-
gzipSize: boundary.gzipSize,
|
|
2347
|
-
})),
|
|
2348
|
-
],
|
|
2349
|
-
);
|
|
2350
|
+
const stats = calculateStats(
|
|
2351
|
+
outputs,
|
|
2352
|
+
startTime,
|
|
2353
|
+
[
|
|
2354
|
+
...partialBundles.map((partial) => ({
|
|
2355
|
+
routeId: `partial:${partial.name}`,
|
|
2356
|
+
size: partial.size,
|
|
2357
|
+
gzipSize: partial.gzipSize,
|
|
2358
|
+
})),
|
|
2359
|
+
...boundaryBundles.map((boundary) => ({
|
|
2360
|
+
routeId: `boundary:${boundary.id}`,
|
|
2361
|
+
size: boundary.size,
|
|
2362
|
+
gzipSize: boundary.gzipSize,
|
|
2363
|
+
})),
|
|
2364
|
+
],
|
|
2365
|
+
);
|
|
2350
2366
|
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
2351
2367
|
}
|
|
2352
2368
|
|
|
@@ -2404,7 +2420,7 @@ export async function buildClientBundles(
|
|
|
2404
2420
|
|
|
2405
2421
|
// 5. 각 Island 번들 병렬 빌드 (#185: L1631의 per-island와 일관성 확보)
|
|
2406
2422
|
const fullIslandResults = await Promise.all(
|
|
2407
|
-
hydratedRoutes.filter((route) => !!route.clientModule).map(async (route) => {
|
|
2423
|
+
hydratedRoutes.filter((route) => !!route.clientModule).map(async (route) => {
|
|
2408
2424
|
try {
|
|
2409
2425
|
return { ok: true as const, result: await buildIsland(route, rootDir, outDir, options) };
|
|
2410
2426
|
} catch (error) {
|
|
@@ -2413,27 +2429,27 @@ export async function buildClientBundles(
|
|
|
2413
2429
|
}),
|
|
2414
2430
|
);
|
|
2415
2431
|
for (const r of fullIslandResults) {
|
|
2416
|
-
if (r.ok) {
|
|
2417
|
-
outputs.push(r.result);
|
|
2418
|
-
} else {
|
|
2419
|
-
const errorStr = r.error;
|
|
2420
|
-
if (errorStr.includes("AggregateError") || errorStr.includes("Could not resolve")) {
|
|
2421
|
-
const clientModule = r.route.clientModule || "";
|
|
2422
|
-
errors.push(
|
|
2423
|
-
`[${r.route.id}] ${errorStr}\n` +
|
|
2424
|
-
` Hint: Check import paths and browser-compatible exports for this island. File: ${clientModule}`,
|
|
2425
|
-
);
|
|
2426
|
-
} else {
|
|
2427
|
-
errors.push(`[${r.route.id}] ${errorStr}`);
|
|
2428
|
-
}
|
|
2432
|
+
if (r.ok) {
|
|
2433
|
+
outputs.push(r.result);
|
|
2434
|
+
} else {
|
|
2435
|
+
const errorStr = r.error;
|
|
2436
|
+
if (errorStr.includes("AggregateError") || errorStr.includes("Could not resolve")) {
|
|
2437
|
+
const clientModule = r.route.clientModule || "";
|
|
2438
|
+
errors.push(
|
|
2439
|
+
`[${r.route.id}] ${errorStr}\n` +
|
|
2440
|
+
` Hint: Check import paths and browser-compatible exports for this island. File: ${clientModule}`,
|
|
2441
|
+
);
|
|
2442
|
+
} else {
|
|
2443
|
+
errors.push(`[${r.route.id}] ${errorStr}`);
|
|
2444
|
+
}
|
|
2429
2445
|
}
|
|
2430
2446
|
}
|
|
2431
2447
|
|
|
2432
2448
|
// 5.5. Per-island code splitting: scan and build individual island bundles
|
|
2433
2449
|
const islandFiles = await scanIslandFiles(hydratedRoutes, rootDir);
|
|
2434
|
-
const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
2450
|
+
const islandBundles: Array<{ name: string; js: string; route: string; priority: IslandFileEntry["priority"] }> = [];
|
|
2435
2451
|
|
|
2436
|
-
if (islandFiles.length > 0) {
|
|
2452
|
+
if (islandFiles.length > 0) {
|
|
2437
2453
|
const islandResults = await Promise.all(
|
|
2438
2454
|
islandFiles.map(async (entry) => {
|
|
2439
2455
|
try {
|
|
@@ -2447,27 +2463,27 @@ export async function buildClientBundles(
|
|
|
2447
2463
|
for (const result of islandResults) {
|
|
2448
2464
|
if (result) islandBundles.push(result);
|
|
2449
2465
|
}
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
const boundaryRecords = hydratedRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2453
|
-
const boundaryBundles: BoundaryBundleBuild[] = [];
|
|
2454
|
-
if (boundaryRecords.length > 0 && !pushDuplicateBoundaryIdErrors(boundaryRecords, errors)) {
|
|
2455
|
-
const boundaryResults = await Promise.all(
|
|
2456
|
-
boundaryRecords.map(async (boundary) => {
|
|
2457
|
-
try {
|
|
2458
|
-
return await buildBoundaryBundle(boundary, rootDir, outDir, options);
|
|
2459
|
-
} catch (error) {
|
|
2460
|
-
errors.push(`[boundary:${boundary.id}] ${String(error)}`);
|
|
2461
|
-
return null;
|
|
2462
|
-
}
|
|
2463
|
-
}),
|
|
2464
|
-
);
|
|
2465
|
-
for (const result of boundaryResults) {
|
|
2466
|
-
if (result) boundaryBundles.push(result);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
const partialBundles: PartialBundleBuild[] = [];
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
const boundaryRecords = hydratedRoutes.flatMap((route) => route.boundaries ?? []);
|
|
2469
|
+
const boundaryBundles: BoundaryBundleBuild[] = [];
|
|
2470
|
+
if (boundaryRecords.length > 0 && !pushDuplicateBoundaryIdErrors(boundaryRecords, errors)) {
|
|
2471
|
+
const boundaryResults = await Promise.all(
|
|
2472
|
+
boundaryRecords.map(async (boundary) => {
|
|
2473
|
+
try {
|
|
2474
|
+
return await buildBoundaryBundle(boundary, rootDir, outDir, options);
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
errors.push(`[boundary:${boundary.id}] ${String(error)}`);
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
}),
|
|
2480
|
+
);
|
|
2481
|
+
for (const result of boundaryResults) {
|
|
2482
|
+
if (result) boundaryBundles.push(result);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const partialBundles: PartialBundleBuild[] = [];
|
|
2471
2487
|
if (partialFiles.length > 0) {
|
|
2472
2488
|
const partialResults = await Promise.all(
|
|
2473
2489
|
partialFiles.map(async (entry) => {
|
|
@@ -2491,11 +2507,11 @@ export async function buildClientBundles(
|
|
|
2491
2507
|
runtimeResult.outputPath,
|
|
2492
2508
|
vendorResult,
|
|
2493
2509
|
routerResult.outputPath,
|
|
2494
|
-
env,
|
|
2495
|
-
islandBundles,
|
|
2496
|
-
partialBundles,
|
|
2497
|
-
boundaryBundles,
|
|
2498
|
-
);
|
|
2510
|
+
env,
|
|
2511
|
+
islandBundles,
|
|
2512
|
+
partialBundles,
|
|
2513
|
+
boundaryBundles,
|
|
2514
|
+
);
|
|
2499
2515
|
|
|
2500
2516
|
await fs.writeFile(
|
|
2501
2517
|
path.join(rootDir, ".mandu/manifest.json"),
|
|
@@ -2503,22 +2519,22 @@ export async function buildClientBundles(
|
|
|
2503
2519
|
);
|
|
2504
2520
|
|
|
2505
2521
|
// 7. 통계 계산
|
|
2506
|
-
const stats = calculateStats(
|
|
2507
|
-
outputs,
|
|
2508
|
-
startTime,
|
|
2509
|
-
[
|
|
2510
|
-
...partialBundles.map((partial) => ({
|
|
2511
|
-
routeId: `partial:${partial.name}`,
|
|
2512
|
-
size: partial.size,
|
|
2513
|
-
gzipSize: partial.gzipSize,
|
|
2514
|
-
})),
|
|
2515
|
-
...boundaryBundles.map((boundary) => ({
|
|
2516
|
-
routeId: `boundary:${boundary.id}`,
|
|
2517
|
-
size: boundary.size,
|
|
2518
|
-
gzipSize: boundary.gzipSize,
|
|
2519
|
-
})),
|
|
2520
|
-
],
|
|
2521
|
-
);
|
|
2522
|
+
const stats = calculateStats(
|
|
2523
|
+
outputs,
|
|
2524
|
+
startTime,
|
|
2525
|
+
[
|
|
2526
|
+
...partialBundles.map((partial) => ({
|
|
2527
|
+
routeId: `partial:${partial.name}`,
|
|
2528
|
+
size: partial.size,
|
|
2529
|
+
gzipSize: partial.gzipSize,
|
|
2530
|
+
})),
|
|
2531
|
+
...boundaryBundles.map((boundary) => ({
|
|
2532
|
+
routeId: `boundary:${boundary.id}`,
|
|
2533
|
+
size: boundary.size,
|
|
2534
|
+
gzipSize: boundary.gzipSize,
|
|
2535
|
+
})),
|
|
2536
|
+
],
|
|
2537
|
+
);
|
|
2522
2538
|
|
|
2523
2539
|
// Phase 18.τ — fire onBundleComplete(stats) before return.
|
|
2524
2540
|
await fireOnBundleComplete(stats);
|
|
@@ -2547,15 +2563,15 @@ export function formatSize(bytes: number): string {
|
|
|
2547
2563
|
*/
|
|
2548
2564
|
export function printBundleStats(result: BundleResult): void {
|
|
2549
2565
|
console.log("\n📦 Mandu Client Bundles");
|
|
2550
|
-
console.log("=".repeat(50));
|
|
2551
|
-
|
|
2552
|
-
const partialCount = Object.keys(result.manifest.partials ?? {}).length;
|
|
2553
|
-
const boundaryCount = Object.keys(result.manifest.boundaries ?? {}).length;
|
|
2554
|
-
if (result.outputs.length === 0 && partialCount === 0 && boundaryCount === 0) {
|
|
2555
|
-
console.log("No islands, partials, or boundaries to bundle (hydration: none or no client entry)");
|
|
2556
|
-
if (result.errors.length > 0) {
|
|
2557
|
-
console.log("\n⚠️ Errors:");
|
|
2558
|
-
for (const error of result.errors) {
|
|
2566
|
+
console.log("=".repeat(50));
|
|
2567
|
+
|
|
2568
|
+
const partialCount = Object.keys(result.manifest.partials ?? {}).length;
|
|
2569
|
+
const boundaryCount = Object.keys(result.manifest.boundaries ?? {}).length;
|
|
2570
|
+
if (result.outputs.length === 0 && partialCount === 0 && boundaryCount === 0) {
|
|
2571
|
+
console.log("No islands, partials, or boundaries to bundle (hydration: none or no client entry)");
|
|
2572
|
+
if (result.errors.length > 0) {
|
|
2573
|
+
console.log("\n⚠️ Errors:");
|
|
2574
|
+
for (const error of result.errors) {
|
|
2559
2575
|
console.log(` ${error}`);
|
|
2560
2576
|
}
|
|
2561
2577
|
}
|
|
@@ -2575,14 +2591,14 @@ export function printBundleStats(result: BundleResult): void {
|
|
|
2575
2591
|
` ${output.routeId}: ${formatSize(output.size)} (gzip: ${formatSize(output.gzipSize)})`
|
|
2576
2592
|
);
|
|
2577
2593
|
}
|
|
2578
|
-
if (partialCount > 0) {
|
|
2579
|
-
console.log(` Partials: ${partialCount}`);
|
|
2580
|
-
}
|
|
2581
|
-
if (boundaryCount > 0) {
|
|
2582
|
-
console.log(` Boundaries: ${boundaryCount}`);
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
if (result.errors.length > 0) {
|
|
2594
|
+
if (partialCount > 0) {
|
|
2595
|
+
console.log(` Partials: ${partialCount}`);
|
|
2596
|
+
}
|
|
2597
|
+
if (boundaryCount > 0) {
|
|
2598
|
+
console.log(` Boundaries: ${boundaryCount}`);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
if (result.errors.length > 0) {
|
|
2586
2602
|
console.log("\n⚠️ Errors:");
|
|
2587
2603
|
for (const error of result.errors) {
|
|
2588
2604
|
console.log(` ${error}`);
|