@mandujs/core 0.18.2 → 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 +1 -1
- package/src/bundler/build.ts +32 -19
- 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
package/src/bundler/build.ts
CHANGED
|
@@ -205,11 +205,11 @@ 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
210
|
// Mandu Island (preferred)
|
|
210
211
|
if (island && island.__mandu_island === true) {
|
|
211
212
|
const { definition } = island;
|
|
212
|
-
const data = getServerData(id);
|
|
213
213
|
|
|
214
214
|
// Island 컴포넌트 (Error Boundary + Loading 지원)
|
|
215
215
|
function IslandComponent() {
|
|
@@ -258,11 +258,14 @@ async function loadAndHydrate(element, src) {
|
|
|
258
258
|
|
|
259
259
|
console.log('[Mandu] Hydrated:', id);
|
|
260
260
|
}
|
|
261
|
-
// Plain React component fallback
|
|
261
|
+
// Plain React component fallback (e.g. "use client" pages)
|
|
262
262
|
else if (typeof island === 'function' || React.isValidElement(island)) {
|
|
263
263
|
console.warn('[Mandu] Plain component hydration:', id);
|
|
264
|
-
|
|
265
|
-
const root =
|
|
264
|
+
|
|
265
|
+
const root = typeof island === 'function'
|
|
266
|
+
? hydrateRoot(element, React.createElement(island, data))
|
|
267
|
+
: hydrateRoot(element, island);
|
|
268
|
+
|
|
266
269
|
hydratedRoots.set(id, root);
|
|
267
270
|
|
|
268
271
|
// 완료 표시
|
|
@@ -390,6 +393,8 @@ import React, {
|
|
|
390
393
|
Suspense,
|
|
391
394
|
StrictMode,
|
|
392
395
|
Profiler,
|
|
396
|
+
// Misc
|
|
397
|
+
version,
|
|
393
398
|
// Types
|
|
394
399
|
Component,
|
|
395
400
|
PureComponent,
|
|
@@ -401,15 +406,16 @@ import { jsx, jsxs } from 'react/jsx-runtime';
|
|
|
401
406
|
import { jsxDEV } from 'react/jsx-dev-runtime';
|
|
402
407
|
|
|
403
408
|
// React internals (ReactDOM이 내부적으로 접근 필요)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// React 19 client internals (Playwright headless 환경 호환성)
|
|
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
|
|
407
411
|
const __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE =
|
|
408
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 || {};
|
|
409
415
|
|
|
410
|
-
// Null safety for Playwright headless browsers
|
|
416
|
+
// Null safety for Playwright headless browsers (React 19)
|
|
411
417
|
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() {};
|
|
418
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.S = function () {};
|
|
413
419
|
}
|
|
414
420
|
|
|
415
421
|
// 전역 React 설정 (모든 모듈에서 동일 인스턴스 공유)
|
|
@@ -447,20 +453,18 @@ export {
|
|
|
447
453
|
Suspense,
|
|
448
454
|
StrictMode,
|
|
449
455
|
Profiler,
|
|
456
|
+
version,
|
|
450
457
|
Component,
|
|
451
458
|
PureComponent,
|
|
452
459
|
Children,
|
|
453
|
-
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
454
460
|
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
461
|
+
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
455
462
|
// JSX Runtime exports
|
|
456
463
|
jsx,
|
|
457
464
|
jsxs,
|
|
458
465
|
jsxDEV,
|
|
459
466
|
};
|
|
460
467
|
|
|
461
|
-
// Version export (React 19 compatibility)
|
|
462
|
-
export const version = React.version;
|
|
463
|
-
|
|
464
468
|
// Default export
|
|
465
469
|
export default React;
|
|
466
470
|
`;
|
|
@@ -849,10 +853,8 @@ async function buildRuntime(
|
|
|
849
853
|
},
|
|
850
854
|
});
|
|
851
855
|
|
|
852
|
-
// 소스 파일 정리
|
|
853
|
-
await fs.unlink(runtimePath).catch(() => {});
|
|
854
|
-
|
|
855
856
|
if (!result.success) {
|
|
857
|
+
// 실패 시 디버깅을 위해 소스 파일을 남겨둠 (_runtime.src.js)
|
|
856
858
|
return {
|
|
857
859
|
success: false,
|
|
858
860
|
outputPath: "",
|
|
@@ -860,17 +862,28 @@ async function buildRuntime(
|
|
|
860
862
|
};
|
|
861
863
|
}
|
|
862
864
|
|
|
865
|
+
// 성공 시에만 소스 파일 정리
|
|
866
|
+
await fs.unlink(runtimePath).catch(() => {});
|
|
867
|
+
|
|
863
868
|
return {
|
|
864
869
|
success: true,
|
|
865
870
|
outputPath: `/.mandu/client/${outputName}`,
|
|
866
871
|
errors: [],
|
|
867
872
|
};
|
|
868
|
-
} catch (error) {
|
|
869
|
-
|
|
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
|
+
|
|
870
883
|
return {
|
|
871
884
|
success: false,
|
|
872
885
|
outputPath: "",
|
|
873
|
-
errors: [String(error)],
|
|
886
|
+
errors: [String(error), ...extra].filter(Boolean),
|
|
874
887
|
};
|
|
875
888
|
}
|
|
876
889
|
}
|
|
@@ -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>`);
|