@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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. 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.1",
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
  }
@@ -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 (!island || !island.__mandu_island) {
211
- throw new Error('[Mandu] Invalid island module: ' + id);
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
- const { definition } = island;
215
- const data = getServerData(id);
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
- // Hydrate (SSR DOM 재사용 + 이벤트 연결)
245
- const root = hydrateRoot(element, React.createElement(IslandComponent));
246
- hydratedRoots.set(id, root);
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
- element.setAttribute('data-mandu-hydrated', 'true');
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
- element.dispatchEvent(new CustomEvent('mandu:hydrated', {
258
- bubbles: true,
259
- detail: { id, data }
260
- }));
276
+ // 이벤트 발송
277
+ element.dispatchEvent(new CustomEvent('mandu:hydrated', {
278
+ bubbles: true,
279
+ detail: { id, data }
280
+ }));
261
281
 
262
- console.log('[Mandu] Hydrated:', id);
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