@mandujs/core 0.54.18 → 0.54.20
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/agent/__tests__/context.test.ts +40 -9
- package/src/agent/verify.ts +55 -24
- package/src/bundler/build.test.ts +287 -246
- package/src/bundler/build.ts +35 -680
- package/src/client/__tests__/props-serialization.test.ts +37 -0
- package/src/client/hydrate.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/props-serialization.ts +233 -0
- package/src/client/runtime-entry.ts +567 -0
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -404
- package/src/diagnose/__tests__/checks.test.ts +15 -0
- package/src/diagnose/checks.ts +1 -1
- package/src/router/client-entry.test.ts +111 -23
- package/src/router/client-entry.ts +78 -301
- package/src/router/fs-routes.test.ts +55 -0
- package/src/router/fs-scanner.ts +11 -40
- package/src/router/fs-types.ts +7 -1
- package/src/router/route-source-analyzer.ts +521 -0
- package/src/runtime/__tests__/inline-client-hydration.test.ts +104 -1
- package/src/runtime/__tests__/page-render-response.test.ts +6 -0
- package/src/runtime/__tests__/searchparams-page-props.test.ts +81 -0
- package/src/runtime/page-render-response.ts +23 -1
- package/src/runtime/server.ts +179 -157
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import type { RoutesManifest } from "../../spec/schema";
|
|
4
|
+
import {
|
|
5
|
+
createServerRegistry,
|
|
6
|
+
startServer,
|
|
7
|
+
type ManduServer,
|
|
8
|
+
} from "../server";
|
|
9
|
+
|
|
10
|
+
// Issue #314 — server page components must receive `searchParams` (query
|
|
11
|
+
// string) as a prop, matching the value `generateMetadata` already gets.
|
|
12
|
+
// SearchPage renders the query so we can assert it survives SSR.
|
|
13
|
+
function SearchPage({
|
|
14
|
+
params,
|
|
15
|
+
searchParams,
|
|
16
|
+
}: {
|
|
17
|
+
params: Record<string, string>;
|
|
18
|
+
searchParams: Record<string, string>;
|
|
19
|
+
}): React.ReactElement {
|
|
20
|
+
return React.createElement(
|
|
21
|
+
"main",
|
|
22
|
+
null,
|
|
23
|
+
React.createElement("p", { "data-testid": "q" }, `q = ${searchParams.q ?? "(undefined)"}`),
|
|
24
|
+
React.createElement("p", { "data-testid": "id" }, `id = ${params.id ?? "(none)"}`),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function searchManifest(): RoutesManifest {
|
|
29
|
+
return {
|
|
30
|
+
version: 1,
|
|
31
|
+
routes: [
|
|
32
|
+
{
|
|
33
|
+
id: "search",
|
|
34
|
+
kind: "page",
|
|
35
|
+
pattern: "/search",
|
|
36
|
+
module: "app/search/page.tsx",
|
|
37
|
+
componentModule: "app/search/page.tsx",
|
|
38
|
+
hydration: { strategy: "none", priority: "visible", preload: false },
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchSearch(query: string): Promise<{ status: number; html: string }> {
|
|
45
|
+
const registry = createServerRegistry();
|
|
46
|
+
registry.registerRouteComponent("search", SearchPage);
|
|
47
|
+
|
|
48
|
+
let server: ManduServer | undefined;
|
|
49
|
+
try {
|
|
50
|
+
server = startServer(searchManifest(), {
|
|
51
|
+
port: 0,
|
|
52
|
+
registry,
|
|
53
|
+
transitions: false,
|
|
54
|
+
prefetch: false,
|
|
55
|
+
spa: false,
|
|
56
|
+
devtools: false,
|
|
57
|
+
silent: true,
|
|
58
|
+
});
|
|
59
|
+
const response = await fetch(`http://127.0.0.1:${server.server.port}/search${query}`);
|
|
60
|
+
return { status: response.status, html: await response.text() };
|
|
61
|
+
} finally {
|
|
62
|
+
server?.stop();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("server page searchParams prop (#314)", () => {
|
|
67
|
+
it("passes the query string to the page component", async () => {
|
|
68
|
+
const { status, html } = await fetchSearch("?q=foo");
|
|
69
|
+
|
|
70
|
+
expect(status).toBe(200);
|
|
71
|
+
expect(html).toContain("q = foo");
|
|
72
|
+
expect(html).not.toContain("q = (undefined)");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders gracefully when the query string is absent", async () => {
|
|
76
|
+
const { status, html } = await fetchSearch("");
|
|
77
|
+
|
|
78
|
+
expect(status).toBe(200);
|
|
79
|
+
expect(html).toContain("q = (undefined)");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -11,6 +11,13 @@ export interface InlineClientHydrationTarget {
|
|
|
11
11
|
src: string;
|
|
12
12
|
priority: NonNullable<HydrationConfig["priority"]>;
|
|
13
13
|
component: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Compatibility path for pre-F42 route-level client modules where the
|
|
16
|
+
* client component is hidden behind a server wrapper. Disabled by default
|
|
17
|
+
* because it invokes user function components outside React's renderer.
|
|
18
|
+
*/
|
|
19
|
+
legacyRuntimeScan?: boolean;
|
|
20
|
+
sourceFile?: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export interface PageRenderResponseOptions {
|
|
@@ -125,7 +132,8 @@ async function resolveAndWrapInlineClientHydration(
|
|
|
125
132
|
};
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
if (typeof type === "function" && !isClassComponent(type)) {
|
|
135
|
+
if (target.legacyRuntimeScan && typeof type === "function" && !isClassComponent(type)) {
|
|
136
|
+
warnLegacyRuntimePartialScan(target);
|
|
129
137
|
const rendered = await renderFunctionComponentForInlineHydration(
|
|
130
138
|
type,
|
|
131
139
|
element.props ?? {},
|
|
@@ -152,6 +160,20 @@ async function resolveAndWrapInlineClientHydration(
|
|
|
152
160
|
return { node: cloned, didWrap: resolvedChildren.didWrap };
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
const warnedLegacyRuntimePartialScanRoutes = new Set<string>();
|
|
164
|
+
|
|
165
|
+
function warnLegacyRuntimePartialScan(target: InlineClientHydrationTarget): void {
|
|
166
|
+
if (warnedLegacyRuntimePartialScanRoutes.has(target.routeId)) return;
|
|
167
|
+
warnedLegacyRuntimePartialScanRoutes.add(target.routeId);
|
|
168
|
+
|
|
169
|
+
const file = target.sourceFile ? ` file="${target.sourceFile}"` : "";
|
|
170
|
+
console.warn(
|
|
171
|
+
`[MANDU_LEGACY_RUNTIME_PARTIAL_SCAN] route="${target.routeId}"${file}: ` +
|
|
172
|
+
"runtime client component discovery is running under the compatibility flag. " +
|
|
173
|
+
"Migrate this route to compiler-owned client boundaries so SSR does not invoke user components during discovery.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
function isAsyncFunctionComponent(type: Function): boolean {
|
|
156
178
|
return !type.prototype?.isReactComponent &&
|
|
157
179
|
(type as { constructor?: { name?: string } }).constructor?.name === "AsyncFunction";
|
package/src/runtime/server.ts
CHANGED
|
@@ -93,7 +93,7 @@ import {
|
|
|
93
93
|
} from "./static-files";
|
|
94
94
|
export { __clearStaticEtagCacheForTests } from "./static-files";
|
|
95
95
|
import { extractShellHtml, createPPRResponse } from "./ppr";
|
|
96
|
-
import { renderPageResponse, type InlineClientHydrationTarget } from "./page-render-response";
|
|
96
|
+
import { renderPageResponse, type InlineClientHydrationTarget } from "./page-render-response";
|
|
97
97
|
import { isRedirectResponse } from "./redirect";
|
|
98
98
|
import { isNotFoundResponse } from "./not-found";
|
|
99
99
|
import { newId } from "../id";
|
|
@@ -479,12 +479,13 @@ export type ErrorLoader = () => Promise<{ default: ErrorComponent }>;
|
|
|
479
479
|
* - component: React 컴포넌트
|
|
480
480
|
* - filling: Slot의 ManduFilling 인스턴스 (loader 포함)
|
|
481
481
|
*/
|
|
482
|
-
export interface PageRegistration {
|
|
483
|
-
component: React.ComponentType<{
|
|
484
|
-
params: Record<string, string>;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
482
|
+
export interface PageRegistration {
|
|
483
|
+
component: React.ComponentType<{
|
|
484
|
+
params: Record<string, string>;
|
|
485
|
+
searchParams: Record<string, string>;
|
|
486
|
+
loaderData?: unknown;
|
|
487
|
+
__manduHydration?: InlineClientHydrationTarget;
|
|
488
|
+
}>;
|
|
488
489
|
filling?: ManduFilling<unknown>;
|
|
489
490
|
/** #186: page 모듈의 static `metadata` export (선택) */
|
|
490
491
|
metadata?: Metadata;
|
|
@@ -506,20 +507,23 @@ export type PageHandler = () => Promise<PageRegistration>;
|
|
|
506
507
|
*/
|
|
507
508
|
export type MetadataHandler = () => Promise<unknown>;
|
|
508
509
|
|
|
509
|
-
export interface AppContext {
|
|
510
|
-
routeId: string;
|
|
511
|
-
url: string;
|
|
512
|
-
params: Record<string, string>;
|
|
513
|
-
/**
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
510
|
+
export interface AppContext {
|
|
511
|
+
routeId: string;
|
|
512
|
+
url: string;
|
|
513
|
+
params: Record<string, string>;
|
|
514
|
+
/** 요청 URL의 쿼리스트링 (generateMetadata와 동일한 값) */
|
|
515
|
+
searchParams: Record<string, string>;
|
|
516
|
+
/** SSR loader에서 로드한 데이터 */
|
|
517
|
+
loaderData?: unknown;
|
|
518
|
+
__manduHydration?: InlineClientHydrationTarget;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
type RouteComponent = (props: {
|
|
522
|
+
params: Record<string, string>;
|
|
523
|
+
searchParams: Record<string, string>;
|
|
524
|
+
loaderData?: unknown;
|
|
525
|
+
__manduHydration?: InlineClientHydrationTarget;
|
|
526
|
+
}) => React.ReactElement;
|
|
523
527
|
type CreateAppFn = (context: AppContext) => React.ReactElement;
|
|
524
528
|
|
|
525
529
|
// ========== Server Registry (인스턴스별 분리) ==========
|
|
@@ -1095,8 +1099,8 @@ async function wrapWithLayouts(
|
|
|
1095
1099
|
}
|
|
1096
1100
|
|
|
1097
1101
|
// Default createApp implementation (registry 기반)
|
|
1098
|
-
function createDefaultAppFactory(registry: ServerRegistry) {
|
|
1099
|
-
return function defaultCreateApp(context: AppContext): React.ReactElement {
|
|
1102
|
+
function createDefaultAppFactory(registry: ServerRegistry) {
|
|
1103
|
+
return function defaultCreateApp(context: AppContext): React.ReactElement {
|
|
1100
1104
|
const Component = registry.routeComponents.get(context.routeId);
|
|
1101
1105
|
|
|
1102
1106
|
if (!Component) {
|
|
@@ -1106,90 +1110,103 @@ function createDefaultAppFactory(registry: ServerRegistry) {
|
|
|
1106
1110
|
);
|
|
1107
1111
|
}
|
|
1108
1112
|
|
|
1109
|
-
return React.createElement(Component, {
|
|
1110
|
-
params: context.params,
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
return
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1113
|
+
return React.createElement(Component, {
|
|
1114
|
+
params: context.params,
|
|
1115
|
+
searchParams: context.searchParams,
|
|
1116
|
+
loaderData: context.loaderData,
|
|
1117
|
+
__manduHydration: context.__manduHydration,
|
|
1118
|
+
});
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function resolveInlineClientHydrationTarget(
|
|
1123
|
+
route: {
|
|
1124
|
+
id: string;
|
|
1125
|
+
componentModule?: string;
|
|
1126
|
+
clientModule?: string;
|
|
1127
|
+
clientExportName?: string;
|
|
1128
|
+
hydration?: HydrationConfig;
|
|
1129
|
+
boundaries?: unknown[];
|
|
1130
|
+
},
|
|
1131
|
+
rootDir: string,
|
|
1132
|
+
src: string,
|
|
1133
|
+
): Promise<InlineClientHydrationTarget | undefined> {
|
|
1134
|
+
if (!route.clientModule || !src) {
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
if (route.boundaries && route.boundaries.length > 0) {
|
|
1138
|
+
return undefined;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const legacyRuntimeScan = process.env.MANDU_LEGACY_RUNTIME_PARTIAL_SCAN === "1";
|
|
1142
|
+
if (!legacyRuntimeScan) {
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
try {
|
|
1147
|
+
const module = await import(path.join(rootDir, route.clientModule));
|
|
1148
|
+
const component = resolveInlineClientHydrationComponent(module, route);
|
|
1149
|
+
|
|
1150
|
+
if (!component) return undefined;
|
|
1151
|
+
|
|
1152
|
+
return {
|
|
1153
|
+
routeId: route.id,
|
|
1154
|
+
src,
|
|
1155
|
+
priority: route.hydration?.priority ?? "visible",
|
|
1156
|
+
component,
|
|
1157
|
+
legacyRuntimeScan,
|
|
1158
|
+
sourceFile: route.componentModule ?? route.clientModule,
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.warn(
|
|
1162
|
+
`[Mandu] Failed to resolve inline client hydration target for "${route.id}":`,
|
|
1163
|
+
error,
|
|
1164
|
+
);
|
|
1165
|
+
return undefined;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function resolveInlineClientHydrationComponent(
|
|
1170
|
+
module: Record<string, unknown>,
|
|
1171
|
+
route: { id: string; clientModule?: string; clientExportName?: string },
|
|
1172
|
+
): unknown {
|
|
1173
|
+
if (module.default) return module.default;
|
|
1174
|
+
|
|
1175
|
+
const candidates = [
|
|
1176
|
+
route.clientExportName && route.clientExportName !== "default" ? route.clientExportName : undefined,
|
|
1177
|
+
inferInlineClientExportNameFromPath(route.clientModule),
|
|
1178
|
+
inferInlineClientExportNameFromRouteId(route.id),
|
|
1179
|
+
].filter((candidate, index, values): candidate is string =>
|
|
1180
|
+
!!candidate && values.indexOf(candidate) === index
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
for (const candidate of candidates) {
|
|
1184
|
+
if (module[candidate]) return module[candidate];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const runtimeExports = Object.keys(module).filter((name) => name !== "__esModule");
|
|
1188
|
+
return runtimeExports.length === 1 ? module[runtimeExports[0]] : undefined;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function inferInlineClientExportNameFromPath(clientModulePath: string | undefined): string | undefined {
|
|
1192
|
+
if (!clientModulePath) return undefined;
|
|
1193
|
+
const basename = path.basename(clientModulePath).replace(/\.[cm]?[jt]sx?$/, "");
|
|
1194
|
+
const withoutClientSuffix = basename.replace(/\.(client|island)$/, "");
|
|
1195
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(withoutClientSuffix)
|
|
1196
|
+
? withoutClientSuffix
|
|
1197
|
+
: undefined;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function inferInlineClientExportNameFromRouteId(routeId: string): string | undefined {
|
|
1201
|
+
const pascal = routeId
|
|
1202
|
+
.split(/[^A-Za-z0-9]+/)
|
|
1203
|
+
.filter(Boolean)
|
|
1204
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
1205
|
+
.join("");
|
|
1206
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(pascal) ? pascal : undefined;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const INTERNAL_CACHE_ENDPOINT = "/_mandu/cache";
|
|
1193
1210
|
|
|
1194
1211
|
// ========== Request Handler ==========
|
|
1195
1212
|
|
|
@@ -2102,19 +2119,21 @@ function extractTitleText(titleHtml: string): string | null {
|
|
|
2102
2119
|
/**
|
|
2103
2120
|
* SSR 렌더링 (Streaming/Non-streaming)
|
|
2104
2121
|
*/
|
|
2105
|
-
async function renderPageSSR(
|
|
2106
|
-
route: {
|
|
2107
|
-
id: string;
|
|
2108
|
-
pattern: string;
|
|
2109
|
-
layoutChain?: string[];
|
|
2110
|
-
streaming?: boolean;
|
|
2111
|
-
hydration?: HydrationConfig;
|
|
2112
|
-
errorModule?: string;
|
|
2113
|
-
loadingModule?: string;
|
|
2114
|
-
notFoundModule?: string;
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2122
|
+
async function renderPageSSR(
|
|
2123
|
+
route: {
|
|
2124
|
+
id: string;
|
|
2125
|
+
pattern: string;
|
|
2126
|
+
layoutChain?: string[];
|
|
2127
|
+
streaming?: boolean;
|
|
2128
|
+
hydration?: HydrationConfig;
|
|
2129
|
+
errorModule?: string;
|
|
2130
|
+
loadingModule?: string;
|
|
2131
|
+
notFoundModule?: string;
|
|
2132
|
+
componentModule?: string;
|
|
2133
|
+
clientModule?: string;
|
|
2134
|
+
clientExportName?: string;
|
|
2135
|
+
boundaries?: unknown[];
|
|
2136
|
+
},
|
|
2118
2137
|
params: Record<string, string>,
|
|
2119
2138
|
loaderData: unknown,
|
|
2120
2139
|
url: string,
|
|
@@ -2124,30 +2143,31 @@ async function renderPageSSR(
|
|
|
2124
2143
|
): Promise<Result<Response>> {
|
|
2125
2144
|
const settings = registry.settings;
|
|
2126
2145
|
const defaultAppCreator = createDefaultAppFactory(registry);
|
|
2127
|
-
const appCreator = registry.createAppFn || defaultAppCreator;
|
|
2128
|
-
|
|
2129
|
-
try {
|
|
2130
|
-
const useStreaming = route.streaming !== undefined
|
|
2131
|
-
? route.streaming
|
|
2132
|
-
: settings.streaming;
|
|
2133
|
-
const needsIslandHydration = !!(
|
|
2134
|
-
route.hydration &&
|
|
2135
|
-
route.hydration.strategy !== "none" &&
|
|
2136
|
-
settings.bundleManifest
|
|
2137
|
-
);
|
|
2138
|
-
const routeBundle = settings.bundleManifest?.bundles[route.id];
|
|
2139
|
-
const bundleSrc = routeBundle?.js ? `${routeBundle.js}?t=${Date.now()}` : "";
|
|
2140
|
-
const inlineClientHydration = needsIslandHydration && !useStreaming
|
|
2141
|
-
? await resolveInlineClientHydrationTarget(route, settings.rootDir, bundleSrc)
|
|
2142
|
-
: undefined;
|
|
2143
|
-
|
|
2144
|
-
let app = appCreator({
|
|
2145
|
-
routeId: route.id,
|
|
2146
|
-
url,
|
|
2147
|
-
params,
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2146
|
+
const appCreator = registry.createAppFn || defaultAppCreator;
|
|
2147
|
+
|
|
2148
|
+
try {
|
|
2149
|
+
const useStreaming = route.streaming !== undefined
|
|
2150
|
+
? route.streaming
|
|
2151
|
+
: settings.streaming;
|
|
2152
|
+
const needsIslandHydration = !!(
|
|
2153
|
+
route.hydration &&
|
|
2154
|
+
route.hydration.strategy !== "none" &&
|
|
2155
|
+
settings.bundleManifest
|
|
2156
|
+
);
|
|
2157
|
+
const routeBundle = settings.bundleManifest?.bundles[route.id];
|
|
2158
|
+
const bundleSrc = routeBundle?.js ? `${routeBundle.js}?t=${Date.now()}` : "";
|
|
2159
|
+
const inlineClientHydration = needsIslandHydration && !useStreaming
|
|
2160
|
+
? await resolveInlineClientHydrationTarget(route, settings.rootDir, bundleSrc)
|
|
2161
|
+
: undefined;
|
|
2162
|
+
|
|
2163
|
+
let app = appCreator({
|
|
2164
|
+
routeId: route.id,
|
|
2165
|
+
url,
|
|
2166
|
+
params,
|
|
2167
|
+
searchParams: extractSearchParams(url),
|
|
2168
|
+
loaderData,
|
|
2169
|
+
__manduHydration: inlineClientHydration,
|
|
2170
|
+
});
|
|
2151
2171
|
|
|
2152
2172
|
// Phase 18.β — per-route Suspense wrapper (Next.js `loading.tsx` parity).
|
|
2153
2173
|
// If the route declared a `loading.tsx`, wrap the page element in a
|
|
@@ -2175,13 +2195,13 @@ async function renderPageSSR(
|
|
|
2175
2195
|
|
|
2176
2196
|
// Island 래핑: 레이아웃 적용 전에 페이지 콘텐츠만 island div로 감쌈
|
|
2177
2197
|
// 이렇게 하면 레이아웃은 island 바깥에 위치하여 하이드레이션 시 레이아웃이 유지됨
|
|
2178
|
-
const needsIslandWrap = needsIslandHydration && bundleSrc.length > 0 && !inlineClientHydration;
|
|
2198
|
+
const needsIslandWrap = needsIslandHydration && bundleSrc.length > 0 && !inlineClientHydration;
|
|
2179
2199
|
|
|
2180
|
-
if (needsIslandHydration && bundleSrc.length === 0 && settings.isDev) {
|
|
2181
|
-
console.warn(
|
|
2182
|
-
`[Mandu] Hydration requested for route "${route.id}" but no client bundle was found. ` +
|
|
2183
|
-
`Run mandu build/generate and ensure the route has a clientModule.`,
|
|
2184
|
-
);
|
|
2200
|
+
if (needsIslandHydration && bundleSrc.length === 0 && settings.isDev) {
|
|
2201
|
+
console.warn(
|
|
2202
|
+
`[Mandu] Hydration requested for route "${route.id}" but no client bundle was found. ` +
|
|
2203
|
+
`Run mandu build/generate and ensure the route has a clientModule.`,
|
|
2204
|
+
);
|
|
2185
2205
|
}
|
|
2186
2206
|
|
|
2187
2207
|
if (needsIslandWrap) {
|
|
@@ -2202,7 +2222,7 @@ async function renderPageSSR(
|
|
|
2202
2222
|
// #186: layout chain + page metadata 병합
|
|
2203
2223
|
const builtMeta = await buildSSRMetadata(route, params, url, registry);
|
|
2204
2224
|
|
|
2205
|
-
const pageResponse = await renderPageResponse({
|
|
2225
|
+
const pageResponse = await renderPageResponse({
|
|
2206
2226
|
app,
|
|
2207
2227
|
useStreaming,
|
|
2208
2228
|
title: builtMeta.title,
|
|
@@ -2213,12 +2233,12 @@ async function renderPageSSR(
|
|
|
2213
2233
|
routePattern: route.pattern,
|
|
2214
2234
|
layoutChain: route.layoutChain,
|
|
2215
2235
|
hydration: route.hydration,
|
|
2216
|
-
bundleManifest: settings.bundleManifest,
|
|
2217
|
-
loaderData,
|
|
2218
|
-
cssPath: settings.cssPath,
|
|
2219
|
-
islandPreWrapped: needsIslandWrap,
|
|
2220
|
-
inlineClientHydration,
|
|
2221
|
-
transitions: settings.transitions,
|
|
2236
|
+
bundleManifest: settings.bundleManifest,
|
|
2237
|
+
loaderData,
|
|
2238
|
+
cssPath: settings.cssPath,
|
|
2239
|
+
islandPreWrapped: needsIslandWrap,
|
|
2240
|
+
inlineClientHydration,
|
|
2241
|
+
transitions: settings.transitions,
|
|
2222
2242
|
prefetch: settings.prefetch,
|
|
2223
2243
|
spa: settings.spa,
|
|
2224
2244
|
devtools: settings.devtools,
|
|
@@ -2408,6 +2428,7 @@ async function renderNotFoundPage(
|
|
|
2408
2428
|
// streaming, no island bundling — a 404 page is plain).
|
|
2409
2429
|
let app: React.ReactElement = React.createElement(NotFoundComponent, {
|
|
2410
2430
|
params,
|
|
2431
|
+
searchParams: extractSearchParams(req.url),
|
|
2411
2432
|
loaderData,
|
|
2412
2433
|
});
|
|
2413
2434
|
if (route.layoutChain && route.layoutChain.length > 0) {
|
|
@@ -3436,6 +3457,7 @@ async function handleRequestInternal(
|
|
|
3436
3457
|
}
|
|
3437
3458
|
const rawApp = React.createElement(registration.component, {
|
|
3438
3459
|
params: {},
|
|
3460
|
+
searchParams: extractSearchParams(req.url),
|
|
3439
3461
|
loaderData,
|
|
3440
3462
|
});
|
|
3441
3463
|
// Issue #198 — pre-resolve in case the not-found component is async.
|