@mandujs/core 0.18.1 → 0.18.3
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 +194 -144
- package/src/runtime/shims.ts +48 -0
- package/src/runtime/ssr.ts +2 -0
- package/src/runtime/streaming-ssr.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
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
|
* 빈 매니페스트 생성
|
|
@@ -205,61 +205,88 @@ async function loadAndHydrate(element, src) {
|
|
|
205
205
|
// Dynamic import - 이 시점에 Island 모듈 로드
|
|
206
206
|
const module = await import(src);
|
|
207
207
|
const island = module.default;
|
|
208
|
+
const data = getServerData(id);
|
|
208
209
|
|
|
209
|
-
// Island
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
// Mandu Island (preferred)
|
|
211
|
+
if (island && island.__mandu_island === true) {
|
|
212
|
+
const { definition } = island;
|
|
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
|
-
|
|
241
|
+
// Hydrate (SSR DOM 재사용 + 이벤트 연결)
|
|
242
|
+
const root = hydrateRoot(element, React.createElement(IslandComponent));
|
|
243
|
+
hydratedRoots.set(id, root);
|
|
216
244
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
}
|
|
245
|
+
// 완료 표시
|
|
246
|
+
element.setAttribute('data-mandu-hydrated', 'true');
|
|
243
247
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 (e.g. "use client" pages)
|
|
262
|
+
else if (typeof island === 'function' || React.isValidElement(island)) {
|
|
263
|
+
console.warn('[Mandu] Plain component hydration:', id);
|
|
255
264
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
const root = typeof island === 'function'
|
|
266
|
+
? hydrateRoot(element, React.createElement(island, data))
|
|
267
|
+
: hydrateRoot(element, island);
|
|
268
|
+
|
|
269
|
+
hydratedRoots.set(id, root);
|
|
270
|
+
|
|
271
|
+
// 완료 표시
|
|
272
|
+
element.setAttribute('data-mandu-hydrated', 'true');
|
|
273
|
+
|
|
274
|
+
// 성능 마커
|
|
275
|
+
if (performance.mark) {
|
|
276
|
+
performance.mark('mandu-hydrated-' + id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 이벤트 발송
|
|
280
|
+
element.dispatchEvent(new CustomEvent('mandu:hydrated', {
|
|
281
|
+
bubbles: true,
|
|
282
|
+
detail: { id, data }
|
|
283
|
+
}));
|
|
261
284
|
|
|
262
|
-
|
|
285
|
+
console.log('[Mandu] Plain component hydrated:', id);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
throw new Error('[Mandu] Invalid module: expected Mandu island or React component: ' + id);
|
|
289
|
+
}
|
|
263
290
|
} catch (error) {
|
|
264
291
|
console.error('[Mandu] Hydration failed for', id, error);
|
|
265
292
|
element.setAttribute('data-mandu-error', 'true');
|
|
@@ -282,7 +309,7 @@ function hydrateIslands() {
|
|
|
282
309
|
for (const el of islands) {
|
|
283
310
|
const id = el.getAttribute('data-mandu-island');
|
|
284
311
|
const src = el.getAttribute('data-mandu-src');
|
|
285
|
-
const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
|
|
312
|
+
const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
|
|
286
313
|
|
|
287
314
|
if (!id || !src) {
|
|
288
315
|
console.warn('[Mandu] Island missing id or src:', el);
|
|
@@ -366,6 +393,8 @@ import React, {
|
|
|
366
393
|
Suspense,
|
|
367
394
|
StrictMode,
|
|
368
395
|
Profiler,
|
|
396
|
+
// Misc
|
|
397
|
+
version,
|
|
369
398
|
// Types
|
|
370
399
|
Component,
|
|
371
400
|
PureComponent,
|
|
@@ -377,7 +406,17 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
377
406
|
import { jsxDEV } from 'react/jsx-dev-runtime';
|
|
378
407
|
|
|
379
408
|
// React internals (ReactDOM이 내부적으로 접근 필요)
|
|
380
|
-
|
|
409
|
+
// React 19+: __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
|
|
410
|
+
// React <=18: __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
|
411
|
+
const __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE =
|
|
412
|
+
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE || {};
|
|
413
|
+
const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED =
|
|
414
|
+
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED || {};
|
|
415
|
+
|
|
416
|
+
// Null safety for Playwright headless browsers (React 19)
|
|
417
|
+
if (__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.S == null) {
|
|
418
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.S = function () {};
|
|
419
|
+
}
|
|
381
420
|
|
|
382
421
|
// 전역 React 설정 (모든 모듈에서 동일 인스턴스 공유)
|
|
383
422
|
if (typeof window !== 'undefined') {
|
|
@@ -414,9 +453,11 @@ export {
|
|
|
414
453
|
Suspense,
|
|
415
454
|
StrictMode,
|
|
416
455
|
Profiler,
|
|
456
|
+
version,
|
|
417
457
|
Component,
|
|
418
458
|
PureComponent,
|
|
419
459
|
Children,
|
|
460
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
420
461
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
421
462
|
// JSX Runtime exports
|
|
422
463
|
jsx,
|
|
@@ -812,10 +853,8 @@ async function buildRuntime(
|
|
|
812
853
|
},
|
|
813
854
|
});
|
|
814
855
|
|
|
815
|
-
// 소스 파일 정리
|
|
816
|
-
await fs.unlink(runtimePath).catch(() => {});
|
|
817
|
-
|
|
818
856
|
if (!result.success) {
|
|
857
|
+
// 실패 시 디버깅을 위해 소스 파일을 남겨둠 (_runtime.src.js)
|
|
819
858
|
return {
|
|
820
859
|
success: false,
|
|
821
860
|
outputPath: "",
|
|
@@ -823,17 +862,28 @@ async function buildRuntime(
|
|
|
823
862
|
};
|
|
824
863
|
}
|
|
825
864
|
|
|
865
|
+
// 성공 시에만 소스 파일 정리
|
|
866
|
+
await fs.unlink(runtimePath).catch(() => {});
|
|
867
|
+
|
|
826
868
|
return {
|
|
827
869
|
success: true,
|
|
828
870
|
outputPath: `/.mandu/client/${outputName}`,
|
|
829
871
|
errors: [],
|
|
830
872
|
};
|
|
831
|
-
} catch (error) {
|
|
832
|
-
|
|
873
|
+
} catch (error: any) {
|
|
874
|
+
// 예외 발생 시에도 디버깅을 위해 소스 파일을 남겨둠
|
|
875
|
+
const extra: string[] = [];
|
|
876
|
+
if (error?.errors && Array.isArray(error.errors)) {
|
|
877
|
+
extra.push(...error.errors.map((e: any) => String(e?.message || e)));
|
|
878
|
+
}
|
|
879
|
+
if (error?.logs && Array.isArray(error.logs)) {
|
|
880
|
+
extra.push(...error.logs.map((l: any) => String(l?.message || l)));
|
|
881
|
+
}
|
|
882
|
+
|
|
833
883
|
return {
|
|
834
884
|
success: false,
|
|
835
885
|
outputPath: "",
|
|
836
|
-
errors: [String(error)],
|
|
886
|
+
errors: [String(error), ...extra].filter(Boolean),
|
|
837
887
|
};
|
|
838
888
|
}
|
|
839
889
|
}
|
|
@@ -855,21 +905,21 @@ interface VendorBuildResult {
|
|
|
855
905
|
* Vendor shim 번들 빌드
|
|
856
906
|
* React, ReactDOM, ReactDOMClient를 각각의 shim으로 빌드
|
|
857
907
|
*/
|
|
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 }> = [
|
|
908
|
+
async function buildVendorShims(
|
|
909
|
+
outDir: string,
|
|
910
|
+
options: BundlerOptions
|
|
911
|
+
): Promise<VendorBuildResult> {
|
|
912
|
+
const errors: string[] = [];
|
|
913
|
+
type VendorShimKey = "react" | "reactDom" | "reactDomClient" | "jsxRuntime" | "jsxDevRuntime";
|
|
914
|
+
const results: Record<VendorShimKey, string> = {
|
|
915
|
+
react: "",
|
|
916
|
+
reactDom: "",
|
|
917
|
+
reactDomClient: "",
|
|
918
|
+
jsxRuntime: "",
|
|
919
|
+
jsxDevRuntime: "",
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const shims: Array<{ name: string; source: string; key: VendorShimKey }> = [
|
|
873
923
|
{ name: "_react", source: generateReactShimSource(), key: "react" },
|
|
874
924
|
{ name: "_react-dom", source: generateReactDOMShimSource(), key: "reactDom" },
|
|
875
925
|
{ name: "_react-dom-client", source: generateReactDOMClientShimSource(), key: "reactDomClient" },
|
|
@@ -877,68 +927,68 @@ async function buildVendorShims(
|
|
|
877
927
|
{ name: "_jsx-dev-runtime", source: generateJsxDevRuntimeShimSource(), key: "jsxDevRuntime" },
|
|
878
928
|
];
|
|
879
929
|
|
|
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
|
-
}
|
|
930
|
+
const buildShim = async (
|
|
931
|
+
shim: { name: string; source: string; key: VendorShimKey }
|
|
932
|
+
): Promise<{ key: VendorShimKey; outputPath?: string; error?: string }> => {
|
|
933
|
+
const srcPath = path.join(outDir, `${shim.name}.src.js`);
|
|
934
|
+
const outputName = `${shim.name}.js`;
|
|
935
|
+
|
|
936
|
+
try {
|
|
937
|
+
await Bun.write(srcPath, shim.source);
|
|
938
|
+
|
|
939
|
+
// _react.js는 external 없이 React 전체를 번들링
|
|
940
|
+
// _react-dom*, jsx-runtime은 react를 external로 처리하여 동일한 React 인스턴스 공유
|
|
941
|
+
let shimExternal: string[] = [];
|
|
942
|
+
if (shim.name === "_react-dom" || shim.name === "_react-dom-client") {
|
|
943
|
+
shimExternal = ["react"];
|
|
944
|
+
} else if (shim.name === "_jsx-runtime" || shim.name === "_jsx-dev-runtime") {
|
|
945
|
+
shimExternal = ["react"];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const result = await Bun.build({
|
|
949
|
+
entrypoints: [srcPath],
|
|
950
|
+
outdir: outDir,
|
|
951
|
+
naming: outputName,
|
|
952
|
+
minify: options.minify ?? process.env.NODE_ENV === "production",
|
|
953
|
+
sourcemap: options.sourcemap ? "external" : "none",
|
|
954
|
+
target: "browser",
|
|
955
|
+
external: shimExternal,
|
|
956
|
+
define: {
|
|
957
|
+
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
|
|
958
|
+
...options.define,
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
await fs.unlink(srcPath).catch(() => {});
|
|
963
|
+
|
|
964
|
+
if (!result.success) {
|
|
965
|
+
return {
|
|
966
|
+
key: shim.key,
|
|
967
|
+
error: `[${shim.name}] ${result.logs.map((l) => l.message).join(", ")}`,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
key: shim.key,
|
|
973
|
+
outputPath: `/.mandu/client/${outputName}`,
|
|
974
|
+
};
|
|
975
|
+
} catch (error) {
|
|
976
|
+
await fs.unlink(srcPath).catch(() => {});
|
|
977
|
+
return {
|
|
978
|
+
key: shim.key,
|
|
979
|
+
error: `[${shim.name}] ${String(error)}`,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
const buildResults = await Promise.all(shims.map((shim) => buildShim(shim)));
|
|
985
|
+
for (const result of buildResults) {
|
|
986
|
+
if (result.error) {
|
|
987
|
+
errors.push(result.error);
|
|
988
|
+
} else if (result.outputPath) {
|
|
989
|
+
results[result.key] = result.outputPath;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
942
992
|
|
|
943
993
|
return {
|
|
944
994
|
success: errors.length === 0,
|
|
@@ -1052,7 +1102,7 @@ function createBundleManifest(
|
|
|
1052
1102
|
bundles[output.routeId] = {
|
|
1053
1103
|
js: output.outputPath,
|
|
1054
1104
|
dependencies: ["_runtime", "_react"],
|
|
1055
|
-
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1105
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1056
1106
|
};
|
|
1057
1107
|
}
|
|
1058
1108
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Shims for React Compatibility
|
|
3
|
+
*
|
|
4
|
+
* React 19+ 호환성을 위한 런타임 shim 스크립트들을 제공합니다.
|
|
5
|
+
*
|
|
6
|
+
* @module runtime/shims
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React 19 Client Internals Shim Script
|
|
11
|
+
*
|
|
12
|
+
* React 19에서 react-dom/client가 실행되기 전에 ReactSharedInternals.S가
|
|
13
|
+
* 존재하는지 확인하고 필요시 초기화하는 inline 스크립트입니다.
|
|
14
|
+
*
|
|
15
|
+
* **사용 목적**:
|
|
16
|
+
* - Playwright headless 환경에서 hydration 실패 방지
|
|
17
|
+
* - React 19의 __CLIENT_INTERNALS.S가 null일 수 있는 문제 해결
|
|
18
|
+
* - SSR HTML에 삽입하여 번들 로드 전에 실행
|
|
19
|
+
*
|
|
20
|
+
* **안전성**:
|
|
21
|
+
* - try-catch로 감싸져 있어 오류 발생 시에도 안전
|
|
22
|
+
* - 기존 값이 있으면 덮어쓰지 않음
|
|
23
|
+
* - React가 없거나 internals가 없어도 실패하지 않음
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // SSR HTML에 삽입
|
|
28
|
+
* const html = `
|
|
29
|
+
* ${hydrationScripts}
|
|
30
|
+
* ${REACT_INTERNALS_SHIM_SCRIPT}
|
|
31
|
+
* ${routerScript}
|
|
32
|
+
* `;
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const REACT_INTERNALS_SHIM_SCRIPT = `<script>
|
|
36
|
+
// React 19 internals shim: ensure ReactSharedInternals.S exists before react-dom/client runs.
|
|
37
|
+
// Some builds expect React.__CLIENT_INTERNALS... .S to be a function, but it may be null.
|
|
38
|
+
// This shim is safe: it only fills the slot if missing.
|
|
39
|
+
(function(){
|
|
40
|
+
try {
|
|
41
|
+
var React = window.React;
|
|
42
|
+
var i = React && React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
43
|
+
if (i && i.S == null) {
|
|
44
|
+
i.S = function(){};
|
|
45
|
+
}
|
|
46
|
+
} catch(e) {}
|
|
47
|
+
})();
|
|
48
|
+
</script>`;
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { BundleManifest } from "../bundler/types";
|
|
|
6
6
|
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
7
7
|
import { PORTS, TIMEOUTS } from "../constants";
|
|
8
8
|
import { escapeHtmlAttr, escapeJsonForInlineScript } from "./escape";
|
|
9
|
+
import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
|
|
9
10
|
|
|
10
11
|
// Re-export streaming SSR utilities
|
|
11
12
|
export {
|
|
@@ -251,6 +252,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
|
|
|
251
252
|
${dataScript}
|
|
252
253
|
${routeScript}
|
|
253
254
|
${hydrationScripts}
|
|
255
|
+
${needsHydration ? REACT_INTERNALS_SHIM_SCRIPT : ""}
|
|
254
256
|
${routerScript}
|
|
255
257
|
${hmrScript}
|
|
256
258
|
${bodyEndTags}
|
|
@@ -19,6 +19,7 @@ import type { Metadata, MetadataItem } from "../seo/types";
|
|
|
19
19
|
import { injectSEOIntoOptions, resolveSEO, type SEOOptions } from "../seo/integration/ssr";
|
|
20
20
|
import { PORTS, TIMEOUTS } from "../constants";
|
|
21
21
|
import { escapeHtmlAttr, escapeJsonForInlineScript, escapeJsString } from "./escape";
|
|
22
|
+
import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
|
|
22
23
|
|
|
23
24
|
// ========== Types ==========
|
|
24
25
|
|
|
@@ -507,6 +508,11 @@ function generateHTMLTailContent(options: StreamingSSROptions): string {
|
|
|
507
508
|
scripts.push(`<script type="module" src="${escapeHtmlAttr(bundleManifest.shared.runtime)}"></script>`);
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
// 7.5 React internals shim (must run before react-dom/client runs)
|
|
512
|
+
if (hydration && hydration.strategy !== "none") {
|
|
513
|
+
scripts.push(REACT_INTERNALS_SHIM_SCRIPT);
|
|
514
|
+
}
|
|
515
|
+
|
|
510
516
|
// 8. Router 스크립트
|
|
511
517
|
if (enableClientRouter && bundleManifest?.shared?.router) {
|
|
512
518
|
scripts.push(`<script type="module" src="${escapeHtmlAttr(bundleManifest.shared.router)}"></script>`);
|