@mandujs/core 0.19.2 → 0.20.1
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 +94 -2
- package/src/bundler/css.ts +323 -353
- package/src/bundler/types.ts +20 -0
- package/src/devtools/client/components/mandu-character.tsx +77 -53
- package/src/devtools/client/components/panel/errors-panel.tsx +2 -2
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -31
- package/src/devtools/client/components/panel/islands-panel.tsx +30 -14
- package/src/devtools/client/components/panel/network-panel.tsx +2 -3
- package/src/devtools/client/components/panel/panel-container.tsx +485 -332
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -22
- package/src/devtools/init.ts +1 -1
- package/src/devtools/types.ts +35 -35
- package/src/filling/filling.ts +66 -66
- package/src/index.ts +1 -0
- package/src/kitchen/kitchen-handler.ts +80 -1
- package/src/observability/event-bus.ts +79 -0
- package/src/observability/index.ts +8 -0
- package/src/observability/logger-adapter.ts +36 -0
- package/src/runtime/index.ts +9 -8
- package/src/runtime/ppr.ts +74 -0
- package/src/runtime/server.ts +203 -147
- package/src/runtime/ssr.ts +17 -4
- package/src/runtime/streaming-ssr.ts +55 -36
- package/src/testing/index.ts +45 -0
|
@@ -15,12 +15,12 @@ import React, { Suspense } from "react";
|
|
|
15
15
|
import type { BundleManifest } from "../bundler/types";
|
|
16
16
|
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
17
17
|
import { serializeProps } from "../client/serialize";
|
|
18
|
-
import type { Metadata, MetadataItem } from "../seo/types";
|
|
19
|
-
import { injectSEOIntoOptions, resolveSEO, type SEOOptions } from "../seo/integration/ssr";
|
|
20
|
-
import { PORTS, TIMEOUTS } from "../constants";
|
|
21
|
-
import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript, escapeJsString } from "./escape";
|
|
22
|
-
import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
|
|
23
|
-
import { getRenderToString } from "./react-renderer";
|
|
18
|
+
import type { Metadata, MetadataItem } from "../seo/types";
|
|
19
|
+
import { injectSEOIntoOptions, resolveSEO, type SEOOptions } from "../seo/integration/ssr";
|
|
20
|
+
import { PORTS, TIMEOUTS } from "../constants";
|
|
21
|
+
import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript, escapeJsString } from "./escape";
|
|
22
|
+
import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
|
|
23
|
+
import { getRenderToString } from "./react-renderer";
|
|
24
24
|
|
|
25
25
|
// ========== Types ==========
|
|
26
26
|
|
|
@@ -704,16 +704,35 @@ export async function renderToStream(
|
|
|
704
704
|
streamingWarnings.markWarned();
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
-
const encoder = new TextEncoder();
|
|
708
|
-
const collectedHeadTags = collectStreamingHeadTags(element);
|
|
709
|
-
const resolvedOptions = collectedHeadTags
|
|
710
|
-
? { ...options, headTags: [options.headTags, collectedHeadTags].filter(Boolean).join("\n") }
|
|
711
|
-
: options;
|
|
712
|
-
const htmlShell = generateHTMLShell(resolvedOptions);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
707
|
+
const encoder = new TextEncoder();
|
|
708
|
+
const collectedHeadTags = collectStreamingHeadTags(element);
|
|
709
|
+
const resolvedOptions = collectedHeadTags
|
|
710
|
+
? { ...options, headTags: [options.headTags, collectedHeadTags].filter(Boolean).join("\n") }
|
|
711
|
+
: options;
|
|
712
|
+
const htmlShell = generateHTMLShell(resolvedOptions);
|
|
713
|
+
|
|
714
|
+
// Reset SSR head before real render so streaming components push fresh tags
|
|
715
|
+
try {
|
|
716
|
+
const headMod = require("../client/use-head") as { resetSSRHead?: () => void; getSSRHeadTags?: () => string };
|
|
717
|
+
headMod.resetSSRHead?.();
|
|
718
|
+
} catch {}
|
|
719
|
+
|
|
720
|
+
// Lazy tail: collect late head tags injected during streaming render
|
|
721
|
+
function buildHtmlTail(): string {
|
|
722
|
+
const baseTail = resolvedOptions._skipHtmlClose
|
|
723
|
+
? generateHTMLTailContent(resolvedOptions)
|
|
724
|
+
: generateHTMLTail(resolvedOptions);
|
|
725
|
+
try {
|
|
726
|
+
const headMod = require("../client/use-head") as { getSSRHeadTags?: () => string };
|
|
727
|
+
const lateHeadTags = headMod.getSSRHeadTags?.() ?? "";
|
|
728
|
+
if (lateHeadTags) {
|
|
729
|
+
const escaped = escapeJsonForInlineScript(JSON.stringify(lateHeadTags));
|
|
730
|
+
const script = `<script>(function(){var t=${escaped},d=document.createElement('div');d.innerHTML=t;var h=document.head;while(d.firstChild)h.appendChild(d.firstChild);})()</script>`;
|
|
731
|
+
return script + baseTail;
|
|
732
|
+
}
|
|
733
|
+
} catch {}
|
|
734
|
+
return baseTail;
|
|
735
|
+
}
|
|
717
736
|
|
|
718
737
|
let shellSent = false;
|
|
719
738
|
let timedOut = false;
|
|
@@ -832,7 +851,7 @@ export async function renderToStream(
|
|
|
832
851
|
controller.enqueue(encoder.encode(generateErrorScript(timeoutError, routeId)));
|
|
833
852
|
|
|
834
853
|
if (!tailSent) {
|
|
835
|
-
controller.enqueue(encoder.encode(
|
|
854
|
+
controller.enqueue(encoder.encode(buildHtmlTail()));
|
|
836
855
|
tailSent = true;
|
|
837
856
|
metrics.allReadyTime = Date.now() - metrics.startTime;
|
|
838
857
|
onMetrics?.(metrics);
|
|
@@ -851,7 +870,7 @@ export async function renderToStream(
|
|
|
851
870
|
|
|
852
871
|
if (done) {
|
|
853
872
|
if (!tailSent) {
|
|
854
|
-
controller.enqueue(encoder.encode(
|
|
873
|
+
controller.enqueue(encoder.encode(buildHtmlTail()));
|
|
855
874
|
tailSent = true;
|
|
856
875
|
// allReady가 아직 안 끝났을 수 있으므로 현재 시점으로 기록
|
|
857
876
|
if (metrics.allReadyTime === 0) {
|
|
@@ -884,7 +903,7 @@ export async function renderToStream(
|
|
|
884
903
|
controller.enqueue(encoder.encode(generateErrorScript(err, routeId)));
|
|
885
904
|
|
|
886
905
|
if (!tailSent) {
|
|
887
|
-
controller.enqueue(encoder.encode(
|
|
906
|
+
controller.enqueue(encoder.encode(buildHtmlTail()));
|
|
888
907
|
tailSent = true;
|
|
889
908
|
metrics.allReadyTime = Date.now() - metrics.startTime;
|
|
890
909
|
onMetrics?.(metrics);
|
|
@@ -901,23 +920,23 @@ export async function renderToStream(
|
|
|
901
920
|
}
|
|
902
921
|
} catch {}
|
|
903
922
|
},
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function collectStreamingHeadTags(element: ReactElement): string {
|
|
908
|
-
try {
|
|
909
|
-
const mod = require("../client/use-head") as {
|
|
910
|
-
resetSSRHead?: () => void;
|
|
911
|
-
getSSRHeadTags?: () => string;
|
|
912
|
-
};
|
|
913
|
-
mod.resetSSRHead?.();
|
|
914
|
-
const renderToString = getRenderToString();
|
|
915
|
-
renderToString(element);
|
|
916
|
-
return mod.getSSRHeadTags?.() ?? "";
|
|
917
|
-
} catch {
|
|
918
|
-
return "";
|
|
919
|
-
}
|
|
920
|
-
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function collectStreamingHeadTags(element: ReactElement): string {
|
|
927
|
+
try {
|
|
928
|
+
const mod = require("../client/use-head") as {
|
|
929
|
+
resetSSRHead?: () => void;
|
|
930
|
+
getSSRHeadTags?: () => string;
|
|
931
|
+
};
|
|
932
|
+
mod.resetSSRHead?.();
|
|
933
|
+
const renderToString = getRenderToString();
|
|
934
|
+
renderToString(element);
|
|
935
|
+
return mod.getSSRHeadTags?.() ?? "";
|
|
936
|
+
} catch {
|
|
937
|
+
return "";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
921
940
|
|
|
922
941
|
/**
|
|
923
942
|
* Streaming SSR Response 생성
|
package/src/testing/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ManduContext } from "../filling/context";
|
|
7
7
|
import type { ManduFilling } from "../filling/filling";
|
|
8
|
+
import type { RouteSpec, RoutesManifest } from "../spec/schema";
|
|
8
9
|
|
|
9
10
|
// ========== Types ==========
|
|
10
11
|
|
|
@@ -142,3 +143,47 @@ export function createTestContext(
|
|
|
142
143
|
const request = createTestRequest(path, options);
|
|
143
144
|
return new ManduContext(request, options.params);
|
|
144
145
|
}
|
|
146
|
+
|
|
147
|
+
// ========== Test Factories ==========
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a RoutesManifest from partial route definitions.
|
|
151
|
+
* Fills in sensible defaults so tests only specify the fields they care about.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const manifest = createTestManifest([
|
|
156
|
+
* { id: "home", kind: "page", pattern: "/" },
|
|
157
|
+
* { id: "api-users", kind: "api", pattern: "/api/users" },
|
|
158
|
+
* ]);
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function createTestManifest(routes: Partial<RouteSpec>[]): RoutesManifest {
|
|
162
|
+
return {
|
|
163
|
+
version: 1,
|
|
164
|
+
routes: routes.map((r, i) => ({
|
|
165
|
+
id: r.id ?? `test-route-${i}`,
|
|
166
|
+
kind: r.kind ?? "page",
|
|
167
|
+
pattern: r.pattern ?? `/test-${i}`,
|
|
168
|
+
module: r.module ?? `app/test-${i}/page.tsx`,
|
|
169
|
+
componentModule:
|
|
170
|
+
(r.kind ?? "page") === "page"
|
|
171
|
+
? (r.componentModule ?? r.module ?? `app/test-${i}/page.tsx`)
|
|
172
|
+
: undefined,
|
|
173
|
+
...r,
|
|
174
|
+
})) as RouteSpec[],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a minimal island descriptor for testing hydration logic.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* const island = createTestIsland("counter", "interaction");
|
|
184
|
+
* expect(island.__hydrate).toBe("interaction");
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function createTestIsland(name: string, strategy: string = "visible") {
|
|
188
|
+
return { __island: true, __hydrate: strategy, __name: name };
|
|
189
|
+
}
|