@mandujs/core 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/bundler/build.ts +175 -138
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": "^19.0.0",
|
|
47
|
-
"react-dom": "^19.0.0"
|
|
48
|
-
"zod": ">=3.0.0"
|
|
47
|
+
"react-dom": "^19.0.0"
|
|
49
48
|
},
|
|
50
49
|
"dependencies": {
|
|
51
50
|
"chokidar": "^5.0.0",
|
|
52
51
|
"fast-glob": "^3.3.2",
|
|
53
52
|
"glob": "^13.0.0",
|
|
54
53
|
"minimatch": "^10.1.1",
|
|
55
|
-
"ollama": "^0.6.3"
|
|
54
|
+
"ollama": "^0.6.3",
|
|
55
|
+
"zod": "^3.23.8"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/bundler/build.ts
CHANGED
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
* Bun.build 기반 클라이언트 번들 빌드
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { RoutesManifest, RouteSpec } from "../spec/schema";
|
|
7
|
-
import { needsHydration, getRouteHydration } from "../spec/schema";
|
|
8
|
-
import type {
|
|
9
|
-
BundleResult,
|
|
10
|
-
BundleOutput,
|
|
11
|
-
BundleManifest,
|
|
12
|
-
BundleStats,
|
|
13
|
-
BundlerOptions,
|
|
14
|
-
} from "./types";
|
|
15
|
-
import { HYDRATION } from "../constants";
|
|
16
|
-
import path from "path";
|
|
17
|
-
import fs from "fs/promises";
|
|
6
|
+
import type { RoutesManifest, RouteSpec } from "../spec/schema";
|
|
7
|
+
import { needsHydration, getRouteHydration } from "../spec/schema";
|
|
8
|
+
import type {
|
|
9
|
+
BundleResult,
|
|
10
|
+
BundleOutput,
|
|
11
|
+
BundleManifest,
|
|
12
|
+
BundleStats,
|
|
13
|
+
BundlerOptions,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import { HYDRATION } from "../constants";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs/promises";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 빈 매니페스트 생성
|
|
@@ -206,60 +206,84 @@ async function loadAndHydrate(element, src) {
|
|
|
206
206
|
const module = await import(src);
|
|
207
207
|
const island = module.default;
|
|
208
208
|
|
|
209
|
-
// Island
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
// Mandu Island (preferred)
|
|
210
|
+
if (island && island.__mandu_island === true) {
|
|
211
|
+
const { definition } = island;
|
|
212
|
+
const data = getServerData(id);
|
|
213
|
+
|
|
214
|
+
// Island 컴포넌트 (Error Boundary + Loading 지원)
|
|
215
|
+
function IslandComponent() {
|
|
216
|
+
const [isReady, setIsReady] = useState(false);
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
setIsReady(true);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
// setup 호출 및 render
|
|
223
|
+
const setupResult = definition.setup(data);
|
|
224
|
+
const content = definition.render(setupResult);
|
|
225
|
+
|
|
226
|
+
// Loading wrapper 적용
|
|
227
|
+
const wrappedContent = definition.loading
|
|
228
|
+
? React.createElement(IslandLoadingWrapper, {
|
|
229
|
+
loading: definition.loading,
|
|
230
|
+
isReady,
|
|
231
|
+
}, content)
|
|
232
|
+
: content;
|
|
233
|
+
|
|
234
|
+
// Error Boundary 적용
|
|
235
|
+
return React.createElement(IslandErrorBoundary, {
|
|
236
|
+
islandId: id,
|
|
237
|
+
errorBoundary: definition.errorBoundary,
|
|
238
|
+
}, wrappedContent);
|
|
239
|
+
}
|
|
213
240
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Island 컴포넌트 (Error Boundary + Loading 지원)
|
|
218
|
-
function IslandComponent() {
|
|
219
|
-
const [isReady, setIsReady] = useState(false);
|
|
220
|
-
|
|
221
|
-
useEffect(() => {
|
|
222
|
-
setIsReady(true);
|
|
223
|
-
}, []);
|
|
224
|
-
|
|
225
|
-
// setup 호출 및 render
|
|
226
|
-
const setupResult = definition.setup(data);
|
|
227
|
-
const content = definition.render(setupResult);
|
|
228
|
-
|
|
229
|
-
// Loading wrapper 적용
|
|
230
|
-
const wrappedContent = definition.loading
|
|
231
|
-
? React.createElement(IslandLoadingWrapper, {
|
|
232
|
-
loading: definition.loading,
|
|
233
|
-
isReady,
|
|
234
|
-
}, content)
|
|
235
|
-
: content;
|
|
236
|
-
|
|
237
|
-
// Error Boundary 적용
|
|
238
|
-
return React.createElement(IslandErrorBoundary, {
|
|
239
|
-
islandId: id,
|
|
240
|
-
errorBoundary: definition.errorBoundary,
|
|
241
|
-
}, wrappedContent);
|
|
242
|
-
}
|
|
241
|
+
// Hydrate (SSR DOM 재사용 + 이벤트 연결)
|
|
242
|
+
const root = hydrateRoot(element, React.createElement(IslandComponent));
|
|
243
|
+
hydratedRoots.set(id, root);
|
|
243
244
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
// 완료 표시
|
|
246
|
+
element.setAttribute('data-mandu-hydrated', 'true');
|
|
247
|
+
|
|
248
|
+
// 성능 마커
|
|
249
|
+
if (performance.mark) {
|
|
250
|
+
performance.mark('mandu-hydrated-' + id);
|
|
251
|
+
}
|
|
247
252
|
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
// 이벤트 발송
|
|
254
|
+
element.dispatchEvent(new CustomEvent('mandu:hydrated', {
|
|
255
|
+
bubbles: true,
|
|
256
|
+
detail: { id, data }
|
|
257
|
+
}));
|
|
250
258
|
|
|
251
|
-
|
|
252
|
-
if (performance.mark) {
|
|
253
|
-
performance.mark('mandu-hydrated-' + id);
|
|
259
|
+
console.log('[Mandu] Hydrated:', id);
|
|
254
260
|
}
|
|
261
|
+
// Plain React component fallback
|
|
262
|
+
else if (typeof island === 'function' || React.isValidElement(island)) {
|
|
263
|
+
console.warn('[Mandu] Plain component hydration:', id);
|
|
264
|
+
const data = getServerData(id);
|
|
265
|
+
const root = hydrateRoot(element, React.createElement(island, data));
|
|
266
|
+
hydratedRoots.set(id, root);
|
|
267
|
+
|
|
268
|
+
// 완료 표시
|
|
269
|
+
element.setAttribute('data-mandu-hydrated', 'true');
|
|
270
|
+
|
|
271
|
+
// 성능 마커
|
|
272
|
+
if (performance.mark) {
|
|
273
|
+
performance.mark('mandu-hydrated-' + id);
|
|
274
|
+
}
|
|
255
275
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
276
|
+
// 이벤트 발송
|
|
277
|
+
element.dispatchEvent(new CustomEvent('mandu:hydrated', {
|
|
278
|
+
bubbles: true,
|
|
279
|
+
detail: { id, data }
|
|
280
|
+
}));
|
|
261
281
|
|
|
262
|
-
|
|
282
|
+
console.log('[Mandu] Plain component hydrated:', id);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
throw new Error('[Mandu] Invalid module: expected Mandu island or React component: ' + id);
|
|
286
|
+
}
|
|
263
287
|
} catch (error) {
|
|
264
288
|
console.error('[Mandu] Hydration failed for', id, error);
|
|
265
289
|
element.setAttribute('data-mandu-error', 'true');
|
|
@@ -282,7 +306,7 @@ function hydrateIslands() {
|
|
|
282
306
|
for (const el of islands) {
|
|
283
307
|
const id = el.getAttribute('data-mandu-island');
|
|
284
308
|
const src = el.getAttribute('data-mandu-src');
|
|
285
|
-
const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
|
|
309
|
+
const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
|
|
286
310
|
|
|
287
311
|
if (!id || !src) {
|
|
288
312
|
console.warn('[Mandu] Island missing id or src:', el);
|
|
@@ -379,6 +403,15 @@ import { jsxDEV } from 'react/jsx-dev-runtime';
|
|
|
379
403
|
// React internals (ReactDOM이 내부적으로 접근 필요)
|
|
380
404
|
const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
|
381
405
|
|
|
406
|
+
// React 19 client internals (Playwright headless 환경 호환성)
|
|
407
|
+
const __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE =
|
|
408
|
+
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE || {};
|
|
409
|
+
|
|
410
|
+
// Null safety for Playwright headless browsers
|
|
411
|
+
if (__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.S == null) {
|
|
412
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.S = function() {};
|
|
413
|
+
}
|
|
414
|
+
|
|
382
415
|
// 전역 React 설정 (모든 모듈에서 동일 인스턴스 공유)
|
|
383
416
|
if (typeof window !== 'undefined') {
|
|
384
417
|
window.React = React;
|
|
@@ -418,12 +451,16 @@ export {
|
|
|
418
451
|
PureComponent,
|
|
419
452
|
Children,
|
|
420
453
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
454
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
421
455
|
// JSX Runtime exports
|
|
422
456
|
jsx,
|
|
423
457
|
jsxs,
|
|
424
458
|
jsxDEV,
|
|
425
459
|
};
|
|
426
460
|
|
|
461
|
+
// Version export (React 19 compatibility)
|
|
462
|
+
export const version = React.version;
|
|
463
|
+
|
|
427
464
|
// Default export
|
|
428
465
|
export default React;
|
|
429
466
|
`;
|
|
@@ -855,21 +892,21 @@ interface VendorBuildResult {
|
|
|
855
892
|
* Vendor shim 번들 빌드
|
|
856
893
|
* React, ReactDOM, ReactDOMClient를 각각의 shim으로 빌드
|
|
857
894
|
*/
|
|
858
|
-
async function buildVendorShims(
|
|
859
|
-
outDir: string,
|
|
860
|
-
options: BundlerOptions
|
|
861
|
-
): Promise<VendorBuildResult> {
|
|
862
|
-
const errors: string[] = [];
|
|
863
|
-
type VendorShimKey = "react" | "reactDom" | "reactDomClient" | "jsxRuntime" | "jsxDevRuntime";
|
|
864
|
-
const results: Record<VendorShimKey, string> = {
|
|
865
|
-
react: "",
|
|
866
|
-
reactDom: "",
|
|
867
|
-
reactDomClient: "",
|
|
868
|
-
jsxRuntime: "",
|
|
869
|
-
jsxDevRuntime: "",
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
const shims: Array<{ name: string; source: string; key: VendorShimKey }> = [
|
|
895
|
+
async function buildVendorShims(
|
|
896
|
+
outDir: string,
|
|
897
|
+
options: BundlerOptions
|
|
898
|
+
): Promise<VendorBuildResult> {
|
|
899
|
+
const errors: string[] = [];
|
|
900
|
+
type VendorShimKey = "react" | "reactDom" | "reactDomClient" | "jsxRuntime" | "jsxDevRuntime";
|
|
901
|
+
const results: Record<VendorShimKey, string> = {
|
|
902
|
+
react: "",
|
|
903
|
+
reactDom: "",
|
|
904
|
+
reactDomClient: "",
|
|
905
|
+
jsxRuntime: "",
|
|
906
|
+
jsxDevRuntime: "",
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const shims: Array<{ name: string; source: string; key: VendorShimKey }> = [
|
|
873
910
|
{ name: "_react", source: generateReactShimSource(), key: "react" },
|
|
874
911
|
{ name: "_react-dom", source: generateReactDOMShimSource(), key: "reactDom" },
|
|
875
912
|
{ name: "_react-dom-client", source: generateReactDOMClientShimSource(), key: "reactDomClient" },
|
|
@@ -877,68 +914,68 @@ async function buildVendorShims(
|
|
|
877
914
|
{ name: "_jsx-dev-runtime", source: generateJsxDevRuntimeShimSource(), key: "jsxDevRuntime" },
|
|
878
915
|
];
|
|
879
916
|
|
|
880
|
-
const buildShim = async (
|
|
881
|
-
shim: { name: string; source: string; key: VendorShimKey }
|
|
882
|
-
): Promise<{ key: VendorShimKey; outputPath?: string; error?: string }> => {
|
|
883
|
-
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
884
|
-
const outputName = `${shim.name}.js`;
|
|
885
|
-
|
|
886
|
-
try {
|
|
887
|
-
await Bun.write(srcPath, shim.source);
|
|
888
|
-
|
|
889
|
-
// _react.js는 external 없이 React 전체를 번들링
|
|
890
|
-
// _react-dom*, jsx-runtime은 react를 external로 처리하여 동일한 React 인스턴스 공유
|
|
891
|
-
let shimExternal: string[] = [];
|
|
892
|
-
if (shim.name === "_react-dom" || shim.name === "_react-dom-client") {
|
|
893
|
-
shimExternal = ["react"];
|
|
894
|
-
} else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
|
|
895
|
-
shimExternal = ["react"];
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const result = await Bun.build({
|
|
899
|
-
entrypoints: [srcPath],
|
|
900
|
-
outdir: outDir,
|
|
901
|
-
naming: outputName,
|
|
902
|
-
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
903
|
-
sourcemap: options.sourcemap ? "external" : "none",
|
|
904
|
-
target: "browser",
|
|
905
|
-
external: shimExternal,
|
|
906
|
-
define: {
|
|
907
|
-
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
908
|
-
...options.define,
|
|
909
|
-
},
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
await fs.unlink(srcPath).catch(() => {});
|
|
913
|
-
|
|
914
|
-
if (!result.success) {
|
|
915
|
-
return {
|
|
916
|
-
key: shim.key,
|
|
917
|
-
error: `[${shim.name}] ${result.logs.map((l) => l.message).join(", ")}`,
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
return {
|
|
922
|
-
key: shim.key,
|
|
923
|
-
outputPath: `/.mandu/client/${outputName}`,
|
|
924
|
-
};
|
|
925
|
-
} catch (error) {
|
|
926
|
-
await fs.unlink(srcPath).catch(() => {});
|
|
927
|
-
return {
|
|
928
|
-
key: shim.key,
|
|
929
|
-
error: `[${shim.name}] ${String(error)}`,
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
935
|
-
for (const result of buildResults) {
|
|
936
|
-
if (result.error) {
|
|
937
|
-
errors.push(result.error);
|
|
938
|
-
} else if (result.outputPath) {
|
|
939
|
-
results[result.key] = result.outputPath;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
917
|
+
const buildShim = async (
|
|
918
|
+
shim: { name: string; source: string; key: VendorShimKey }
|
|
919
|
+
): Promise<{ key: VendorShimKey; outputPath?: string; error?: string }> => {
|
|
920
|
+
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
921
|
+
const outputName = `${shim.name}.js`;
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
await Bun.write(srcPath, shim.source);
|
|
925
|
+
|
|
926
|
+
// _react.js는 external 없이 React 전체를 번들링
|
|
927
|
+
// _react-dom*, jsx-runtime은 react를 external로 처리하여 동일한 React 인스턴스 공유
|
|
928
|
+
let shimExternal: string[] = [];
|
|
929
|
+
if (shim.name === "_react-dom" || shim.name === "_react-dom-client") {
|
|
930
|
+
shimExternal = ["react"];
|
|
931
|
+
} else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
|
|
932
|
+
shimExternal = ["react"];
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const result = await Bun.build({
|
|
936
|
+
entrypoints: [srcPath],
|
|
937
|
+
outdir: outDir,
|
|
938
|
+
naming: outputName,
|
|
939
|
+
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
940
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
941
|
+
target: "browser",
|
|
942
|
+
external: shimExternal,
|
|
943
|
+
define: {
|
|
944
|
+
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
945
|
+
...options.define,
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
await fs.unlink(srcPath).catch(() => {});
|
|
950
|
+
|
|
951
|
+
if (!result.success) {
|
|
952
|
+
return {
|
|
953
|
+
key: shim.key,
|
|
954
|
+
error: `[${shim.name}] ${result.logs.map((l) => l.message).join(", ")}`,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
key: shim.key,
|
|
960
|
+
outputPath: `/.mandu/client/${outputName}`,
|
|
961
|
+
};
|
|
962
|
+
} catch (error) {
|
|
963
|
+
await fs.unlink(srcPath).catch(() => {});
|
|
964
|
+
return {
|
|
965
|
+
key: shim.key,
|
|
966
|
+
error: `[${shim.name}] ${String(error)}`,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
972
|
+
for (const result of buildResults) {
|
|
973
|
+
if (result.error) {
|
|
974
|
+
errors.push(result.error);
|
|
975
|
+
} else if (result.outputPath) {
|
|
976
|
+
results[result.key] = result.outputPath;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
942
979
|
|
|
943
980
|
return {
|
|
944
981
|
success: errors.length === 0,
|
|
@@ -1052,7 +1089,7 @@ function createBundleManifest(
|
|
|
1052
1089
|
bundles[output.routeId] = {
|
|
1053
1090
|
js: output.outputPath,
|
|
1054
1091
|
dependencies: ["_runtime", "_react"],
|
|
1055
|
-
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1092
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1056
1093
|
};
|
|
1057
1094
|
}
|
|
1058
1095
|
|