@mandujs/core 0.5.6 โ†’ 0.6.0

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.
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Mandu Client-side Router ๐Ÿงญ
3
+ * SPA ์Šคํƒ€์ผ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์œ„ํ•œ ํด๋ผ์ด์–ธํŠธ ๋ผ์šฐํ„ฐ
4
+ */
5
+
6
+ import type { ReactNode } from "react";
7
+
8
+ // ========== Types ==========
9
+
10
+ export interface RouteInfo {
11
+ id: string;
12
+ pattern: string;
13
+ params: Record<string, string>;
14
+ }
15
+
16
+ export interface NavigationState {
17
+ state: "idle" | "loading";
18
+ location?: string;
19
+ }
20
+
21
+ export interface RouterState {
22
+ currentRoute: RouteInfo | null;
23
+ loaderData: unknown;
24
+ navigation: NavigationState;
25
+ }
26
+
27
+ export interface NavigateOptions {
28
+ /** history.replaceState ์‚ฌ์šฉ ์—ฌ๋ถ€ */
29
+ replace?: boolean;
30
+ /** ์Šคํฌ๋กค ์œ„์น˜ ๋ณต์› ์—ฌ๋ถ€ */
31
+ scroll?: boolean;
32
+ }
33
+
34
+ type RouterListener = (state: RouterState) => void;
35
+
36
+ // ========== Router State ==========
37
+
38
+ let routerState: RouterState = {
39
+ currentRoute: null,
40
+ loaderData: undefined,
41
+ navigation: { state: "idle" },
42
+ };
43
+
44
+ const listeners = new Set<RouterListener>();
45
+
46
+ /**
47
+ * ์ดˆ๊ธฐํ™”: ์„œ๋ฒ„์—์„œ ์ „๋‹ฌ๋œ ๋ผ์šฐํŠธ ์ •๋ณด๋กœ ์ƒํƒœ ์„ค์ •
48
+ */
49
+ function initializeFromServer(): void {
50
+ if (typeof window === "undefined") return;
51
+
52
+ const route = (window as any).__MANDU_ROUTE__;
53
+ const data = (window as any).__MANDU_DATA__;
54
+
55
+ if (route) {
56
+ // URL์—์„œ ์‹ค์ œ params ์ถ”์ถœ
57
+ const params = extractParamsFromPath(route.pattern, window.location.pathname);
58
+
59
+ routerState = {
60
+ currentRoute: {
61
+ id: route.id,
62
+ pattern: route.pattern,
63
+ params,
64
+ },
65
+ loaderData: data?.[route.id]?.serverData,
66
+ navigation: { state: "idle" },
67
+ };
68
+ }
69
+ }
70
+
71
+ // ========== Pattern Matching ==========
72
+
73
+ interface CompiledPattern {
74
+ regex: RegExp;
75
+ paramNames: string[];
76
+ }
77
+
78
+ const patternCache = new Map<string, CompiledPattern>();
79
+
80
+ /**
81
+ * ํŒจํ„ด์„ ์ •๊ทœ์‹์œผ๋กœ ์ปดํŒŒ์ผ
82
+ */
83
+ function compilePattern(pattern: string): CompiledPattern {
84
+ const cached = patternCache.get(pattern);
85
+ if (cached) return cached;
86
+
87
+ const paramNames: string[] = [];
88
+ const PARAM_PLACEHOLDER = "\x00PARAM\x00";
89
+ const paramMatches: string[] = [];
90
+
91
+ const withPlaceholders = pattern.replace(
92
+ /:([a-zA-Z_][a-zA-Z0-9_]*)/g,
93
+ (_, paramName) => {
94
+ paramMatches.push(paramName);
95
+ return PARAM_PLACEHOLDER;
96
+ }
97
+ );
98
+
99
+ const escaped = withPlaceholders.replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
100
+
101
+ let paramIndex = 0;
102
+ const regexStr = escaped.replace(
103
+ new RegExp(PARAM_PLACEHOLDER.replace(/\x00/g, "\\x00"), "g"),
104
+ () => {
105
+ paramNames.push(paramMatches[paramIndex++]);
106
+ return "([^/]+)";
107
+ }
108
+ );
109
+
110
+ const compiled = {
111
+ regex: new RegExp(`^${regexStr}$`),
112
+ paramNames,
113
+ };
114
+
115
+ patternCache.set(pattern, compiled);
116
+ return compiled;
117
+ }
118
+
119
+ /**
120
+ * ํŒจํ„ด์—์„œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”์ถœ
121
+ */
122
+ function extractParamsFromPath(
123
+ pattern: string,
124
+ pathname: string
125
+ ): Record<string, string> {
126
+ const compiled = compilePattern(pattern);
127
+ const match = pathname.match(compiled.regex);
128
+
129
+ if (!match) return {};
130
+
131
+ const params: Record<string, string> = {};
132
+ compiled.paramNames.forEach((name, index) => {
133
+ params[name] = match[index + 1];
134
+ });
135
+
136
+ return params;
137
+ }
138
+
139
+ // ========== Navigation ==========
140
+
141
+ /**
142
+ * ํŽ˜์ด์ง€ ๋„ค๋น„๊ฒŒ์ด์…˜
143
+ */
144
+ export async function navigate(
145
+ to: string,
146
+ options: NavigateOptions = {}
147
+ ): Promise<void> {
148
+ const { replace = false, scroll = true } = options;
149
+
150
+ try {
151
+ const url = new URL(to, window.location.origin);
152
+
153
+ // ์™ธ๋ถ€ URL์€ ์ผ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜
154
+ if (url.origin !== window.location.origin) {
155
+ window.location.href = to;
156
+ return;
157
+ }
158
+
159
+ // ๋กœ๋”ฉ ์ƒํƒœ ์‹œ์ž‘
160
+ routerState = {
161
+ ...routerState,
162
+ navigation: { state: "loading", location: to },
163
+ };
164
+ notifyListeners();
165
+
166
+ // ๋ฐ์ดํ„ฐ fetch
167
+ const dataUrl = `${url.pathname}${url.search ? url.search + "&" : "?"}_data=1`;
168
+ const response = await fetch(dataUrl);
169
+
170
+ if (!response.ok) {
171
+ // ์—๋Ÿฌ ์‹œ full navigation fallback
172
+ window.location.href = to;
173
+ return;
174
+ }
175
+
176
+ const data = await response.json();
177
+
178
+ // History ์—…๋ฐ์ดํŠธ
179
+ const historyState = { routeId: data.routeId, params: data.params };
180
+ if (replace) {
181
+ history.replaceState(historyState, "", to);
182
+ } else {
183
+ history.pushState(historyState, "", to);
184
+ }
185
+
186
+ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
187
+ routerState = {
188
+ currentRoute: {
189
+ id: data.routeId,
190
+ pattern: data.pattern,
191
+ params: data.params,
192
+ },
193
+ loaderData: data.loaderData,
194
+ navigation: { state: "idle" },
195
+ };
196
+
197
+ // __MANDU_DATA__ ์—…๋ฐ์ดํŠธ
198
+ if (typeof window !== "undefined") {
199
+ (window as any).__MANDU_DATA__ = {
200
+ ...(window as any).__MANDU_DATA__,
201
+ [data.routeId]: { serverData: data.loaderData },
202
+ };
203
+ }
204
+
205
+ notifyListeners();
206
+
207
+ // ์Šคํฌ๋กค ๋ณต์›
208
+ if (scroll) {
209
+ window.scrollTo(0, 0);
210
+ }
211
+ } catch (error) {
212
+ console.error("[Mandu Router] Navigation failed:", error);
213
+ // ์—๋Ÿฌ ์‹œ full navigation fallback
214
+ window.location.href = to;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * ๋’ค๋กœ๊ฐ€๊ธฐ/์•ž์œผ๋กœ๊ฐ€๊ธฐ ์ฒ˜๋ฆฌ
220
+ */
221
+ function handlePopState(event: PopStateEvent): void {
222
+ const state = event.state;
223
+
224
+ if (state?.routeId) {
225
+ // ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ํŽ˜์ด์ง€ - ๋ฐ์ดํ„ฐ ๋‹ค์‹œ fetch
226
+ navigate(window.location.pathname + window.location.search, {
227
+ replace: true,
228
+ scroll: false,
229
+ });
230
+ }
231
+ }
232
+
233
+ // ========== State Management ==========
234
+
235
+ /**
236
+ * ๋ฆฌ์Šค๋„ˆ์—๊ฒŒ ์ƒํƒœ ๋ณ€๊ฒฝ ์•Œ๋ฆผ
237
+ */
238
+ function notifyListeners(): void {
239
+ for (const listener of listeners) {
240
+ try {
241
+ listener(routerState);
242
+ } catch (error) {
243
+ console.error("[Mandu Router] Listener error:", error);
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * ์ƒํƒœ ๋ณ€๊ฒฝ ๊ตฌ๋…
250
+ */
251
+ export function subscribe(listener: RouterListener): () => void {
252
+ listeners.add(listener);
253
+ return () => listeners.delete(listener);
254
+ }
255
+
256
+ /**
257
+ * ํ˜„์žฌ ๋ผ์šฐํ„ฐ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
258
+ */
259
+ export function getRouterState(): RouterState {
260
+ return routerState;
261
+ }
262
+
263
+ /**
264
+ * ํ˜„์žฌ ๋ผ์šฐํŠธ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
265
+ */
266
+ export function getCurrentRoute(): RouteInfo | null {
267
+ return routerState.currentRoute;
268
+ }
269
+
270
+ /**
271
+ * ํ˜„์žฌ loader ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
272
+ */
273
+ export function getLoaderData<T = unknown>(): T | undefined {
274
+ return routerState.loaderData as T | undefined;
275
+ }
276
+
277
+ /**
278
+ * ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
279
+ */
280
+ export function getNavigationState(): NavigationState {
281
+ return routerState.navigation;
282
+ }
283
+
284
+ // ========== Link Click Handler ==========
285
+
286
+ /**
287
+ * ๋งํฌ ํด๋ฆญ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ (์ด๋ฒคํŠธ ์œ„์ž„์šฉ)
288
+ */
289
+ function handleLinkClick(event: MouseEvent): void {
290
+ // ๊ธฐ๋ณธ ๋™์ž‘ ์กฐ๊ฑด ์ฒดํฌ
291
+ if (
292
+ event.defaultPrevented ||
293
+ event.button !== 0 ||
294
+ event.metaKey ||
295
+ event.altKey ||
296
+ event.ctrlKey ||
297
+ event.shiftKey
298
+ ) {
299
+ return;
300
+ }
301
+
302
+ // ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ์•ต์ปค ํƒœ๊ทธ ์ฐพ๊ธฐ
303
+ const anchor = (event.target as HTMLElement).closest("a");
304
+ if (!anchor) return;
305
+
306
+ // data-mandu-link ์†์„ฑ์ด ์žˆ๋Š” ๋งํฌ๋งŒ ์ฒ˜๋ฆฌ
307
+ if (!anchor.hasAttribute("data-mandu-link")) return;
308
+
309
+ const href = anchor.getAttribute("href");
310
+ if (!href) return;
311
+
312
+ // ์™ธ๋ถ€ ๋งํฌ ์ฒดํฌ
313
+ try {
314
+ const url = new URL(href, window.location.origin);
315
+ if (url.origin !== window.location.origin) return;
316
+ } catch {
317
+ return;
318
+ }
319
+
320
+ // ๊ธฐ๋ณธ ๋™์ž‘ ๋ฐฉ์ง€ ๋ฐ Client-side ๋„ค๋น„๊ฒŒ์ด์…˜
321
+ event.preventDefault();
322
+ navigate(href);
323
+ }
324
+
325
+ // ========== Prefetch ==========
326
+
327
+ const prefetchedUrls = new Set<string>();
328
+
329
+ /**
330
+ * ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ ๋ฏธ๋ฆฌ ๋กœ๋“œ
331
+ */
332
+ export async function prefetch(url: string): Promise<void> {
333
+ if (prefetchedUrls.has(url)) return;
334
+
335
+ try {
336
+ const dataUrl = `${url}${url.includes("?") ? "&" : "?"}_data=1`;
337
+ await fetch(dataUrl, { priority: "low" } as RequestInit);
338
+ prefetchedUrls.add(url);
339
+ } catch {
340
+ // Prefetch ์‹คํŒจ๋Š” ๋ฌด์‹œ
341
+ }
342
+ }
343
+
344
+ // ========== Initialization ==========
345
+
346
+ let initialized = false;
347
+
348
+ /**
349
+ * ๋ผ์šฐํ„ฐ ์ดˆ๊ธฐํ™”
350
+ */
351
+ export function initializeRouter(): void {
352
+ if (typeof window === "undefined" || initialized) return;
353
+
354
+ initialized = true;
355
+
356
+ // ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™”
357
+ initializeFromServer();
358
+
359
+ // popstate ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
360
+ window.addEventListener("popstate", handlePopState);
361
+
362
+ // ๋งํฌ ํด๋ฆญ ์ด๋ฒคํŠธ ์œ„์ž„
363
+ document.addEventListener("click", handleLinkClick);
364
+
365
+ console.log("[Mandu Router] Initialized");
366
+ }
367
+
368
+ /**
369
+ * ๋ผ์šฐํ„ฐ ์ •๋ฆฌ
370
+ */
371
+ export function cleanupRouter(): void {
372
+ if (typeof window === "undefined" || !initialized) return;
373
+
374
+ window.removeEventListener("popstate", handlePopState);
375
+ document.removeEventListener("click", handleLinkClick);
376
+ listeners.clear();
377
+ initialized = false;
378
+ }
379
+
380
+ // ์ž๋™ ์ดˆ๊ธฐํ™” (DOM ์ค€๋น„ ์‹œ)
381
+ if (typeof window !== "undefined") {
382
+ if (document.readyState === "loading") {
383
+ document.addEventListener("DOMContentLoaded", initializeRouter);
384
+ } else {
385
+ initializeRouter();
386
+ }
387
+ }
@@ -190,6 +190,13 @@ function computeSlotImportPath(slotModule: string, fromDir: string): string {
190
190
 
191
191
  export function generatePageComponent(route: RouteSpec): string {
192
192
  const pageName = toPascalCase(route.id);
193
+
194
+ // slotModule์ด ์žˆ์œผ๋ฉด PageHandler ํ˜•์‹์œผ๋กœ ์ƒ์„ฑ (filling ํฌํ•จ)
195
+ if (route.slotModule) {
196
+ return generatePageHandlerWithSlot(route);
197
+ }
198
+
199
+ // slotModule์ด ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹
193
200
  return `// Generated by Mandu - DO NOT EDIT DIRECTLY
194
201
  // Route ID: ${route.id}
195
202
  // Pattern: ${route.pattern}
@@ -198,6 +205,7 @@ import React from "react";
198
205
 
199
206
  interface Props {
200
207
  params: Record<string, string>;
208
+ loaderData?: unknown;
201
209
  }
202
210
 
203
211
  export default function ${pageName}Page({ params }: Props): React.ReactElement {
@@ -210,6 +218,45 @@ export default function ${pageName}Page({ params }: Props): React.ReactElement {
210
218
  `;
211
219
  }
212
220
 
221
+ /**
222
+ * slotModule์ด ์žˆ๋Š” Page Route์šฉ Handler ์ƒ์„ฑ
223
+ * - component์™€ filling์„ ํ•จ๊ป˜ export
224
+ * - server.ts์—์„œ filling.executeLoader() ํ˜ธ์ถœ ๊ฐ€๋Šฅ
225
+ */
226
+ export function generatePageHandlerWithSlot(route: RouteSpec): string {
227
+ const pageName = toPascalCase(route.id);
228
+ const slotImportPath = computeSlotImportPath(route.slotModule!, "apps/server/generated/routes");
229
+
230
+ return `// Generated by Mandu - DO NOT EDIT DIRECTLY
231
+ // Route ID: ${route.id}
232
+ // Pattern: ${route.pattern}
233
+ // Slot Module: ${route.slotModule}
234
+
235
+ import React from "react";
236
+ import filling from "${slotImportPath}";
237
+
238
+ interface Props {
239
+ params: Record<string, string>;
240
+ loaderData?: unknown;
241
+ }
242
+
243
+ function ${pageName}Page({ params, loaderData }: Props): React.ReactElement {
244
+ return React.createElement("div", null,
245
+ React.createElement("h1", null, "${pageName} Page"),
246
+ React.createElement("p", null, "Route ID: ${route.id}"),
247
+ React.createElement("p", null, "Pattern: ${route.pattern}"),
248
+ loaderData ? React.createElement("pre", null, JSON.stringify(loaderData, null, 2)) : null
249
+ );
250
+ }
251
+
252
+ // PageRegistration ํ˜•์‹์œผ๋กœ export (server.ts์˜ registerPageHandler์šฉ)
253
+ export default {
254
+ component: ${pageName}Page,
255
+ filling: filling,
256
+ };
257
+ `;
258
+ }
259
+
213
260
  /**
214
261
  * Convert string to PascalCase (handles kebab-case, snake_case)
215
262
  * "todo-page" โ†’ "TodoPage"
@@ -1,6 +1,8 @@
1
1
  import type { Server } from "bun";
2
2
  import type { RoutesManifest } from "../spec/schema";
3
3
  import type { BundleManifest } from "../bundler/types";
4
+ import type { ManduFilling } from "../filling/filling";
5
+ import { ManduContext } from "../filling/context";
4
6
  import { Router } from "./router";
5
7
  import { renderSSR } from "./ssr";
6
8
  import React from "react";
@@ -103,18 +105,36 @@ export interface ManduServer {
103
105
  export type ApiHandler = (req: Request, params: Record<string, string>) => Response | Promise<Response>;
104
106
  export type PageLoader = () => Promise<{ default: React.ComponentType<{ params: Record<string, string> }> }>;
105
107
 
108
+ /**
109
+ * Page ๋“ฑ๋ก ์ •๋ณด
110
+ * - component: React ์ปดํฌ๋„ŒํŠธ
111
+ * - filling: Slot์˜ ManduFilling ์ธ์Šคํ„ด์Šค (loader ํฌํ•จ)
112
+ */
113
+ export interface PageRegistration {
114
+ component: React.ComponentType<{ params: Record<string, string>; loaderData?: unknown }>;
115
+ filling?: ManduFilling<unknown>;
116
+ }
117
+
118
+ /**
119
+ * Page Handler - ์ปดํฌ๋„ŒํŠธ์™€ filling์„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜
120
+ */
121
+ export type PageHandler = () => Promise<PageRegistration>;
122
+
106
123
  export interface AppContext {
107
124
  routeId: string;
108
125
  url: string;
109
126
  params: Record<string, string>;
127
+ /** SSR loader์—์„œ ๋กœ๋“œํ•œ ๋ฐ์ดํ„ฐ */
128
+ loaderData?: unknown;
110
129
  }
111
130
 
112
- type RouteComponent = (props: { params: Record<string, string> }) => React.ReactElement;
131
+ type RouteComponent = (props: { params: Record<string, string>; loaderData?: unknown }) => React.ReactElement;
113
132
  type CreateAppFn = (context: AppContext) => React.ReactElement;
114
133
 
115
134
  // Registry
116
135
  const apiHandlers: Map<string, ApiHandler> = new Map();
117
136
  const pageLoaders: Map<string, PageLoader> = new Map();
137
+ const pageHandlers: Map<string, PageHandler> = new Map();
118
138
  const routeComponents: Map<string, RouteComponent> = new Map();
119
139
  let createAppFn: CreateAppFn | null = null;
120
140
 
@@ -141,6 +161,14 @@ export function registerPageLoader(routeId: string, loader: PageLoader): void {
141
161
  pageLoaders.set(routeId, loader);
142
162
  }
143
163
 
164
+ /**
165
+ * Page Handler ๋“ฑ๋ก (์ปดํฌ๋„ŒํŠธ + filling)
166
+ * filling์ด ์žˆ์œผ๋ฉด loader๋ฅผ ์‹คํ–‰ํ•˜์—ฌ serverData ์ „๋‹ฌ
167
+ */
168
+ export function registerPageHandler(routeId: string, handler: PageHandler): void {
169
+ pageHandlers.set(routeId, handler);
170
+ }
171
+
144
172
  export function registerRouteComponent(routeId: string, component: RouteComponent): void {
145
173
  routeComponents.set(routeId, component);
146
174
  }
@@ -160,7 +188,10 @@ function defaultCreateApp(context: AppContext): React.ReactElement {
160
188
  );
161
189
  }
162
190
 
163
- return React.createElement(Component, { params: context.params });
191
+ return React.createElement(Component, {
192
+ params: context.params,
193
+ loaderData: context.loaderData,
194
+ });
164
195
  }
165
196
 
166
197
  // ========== Static File Serving ==========
@@ -281,11 +312,25 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
281
312
  }
282
313
 
283
314
  if (route.kind === "page") {
284
- const loader = pageLoaders.get(route.id);
285
- if (loader) {
315
+ let loaderData: unknown;
316
+ let component: RouteComponent | undefined;
317
+
318
+ // Client-side Routing: ๋ฐ์ดํ„ฐ ์š”์ฒญ ๊ฐ์ง€
319
+ const isDataRequest = url.searchParams.has("_data");
320
+
321
+ // 1. PageHandler ๋ฐฉ์‹ (์‹ ๊ทœ - filling ํฌํ•จ)
322
+ const pageHandler = pageHandlers.get(route.id);
323
+ if (pageHandler) {
286
324
  try {
287
- const module = await loader();
288
- registerRouteComponent(route.id, module.default);
325
+ const registration = await pageHandler();
326
+ component = registration.component as RouteComponent;
327
+ registerRouteComponent(route.id, component);
328
+
329
+ // Filling์˜ loader ์‹คํ–‰
330
+ if (registration.filling?.hasLoader()) {
331
+ const ctx = new ManduContext(req, params);
332
+ loaderData = await registration.filling.executeLoader(ctx);
333
+ }
289
334
  } catch (err) {
290
335
  const pageError = createPageLoadErrorResponse(
291
336
  route.id,
@@ -299,15 +344,54 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
299
344
  return Response.json(response, { status: 500 });
300
345
  }
301
346
  }
347
+ // 2. PageLoader ๋ฐฉ์‹ (๋ ˆ๊ฑฐ์‹œ ํ˜ธํ™˜)
348
+ else {
349
+ const loader = pageLoaders.get(route.id);
350
+ if (loader) {
351
+ try {
352
+ const module = await loader();
353
+ registerRouteComponent(route.id, module.default);
354
+ } catch (err) {
355
+ const pageError = createPageLoadErrorResponse(
356
+ route.id,
357
+ route.pattern,
358
+ err instanceof Error ? err : new Error(String(err))
359
+ );
360
+ console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
361
+ const response = formatErrorResponse(pageError, {
362
+ isDev: process.env.NODE_ENV !== "production",
363
+ });
364
+ return Response.json(response, { status: 500 });
365
+ }
366
+ }
367
+ }
302
368
 
369
+ // Client-side Routing: ๋ฐ์ดํ„ฐ๋งŒ ๋ฐ˜ํ™˜ (JSON)
370
+ if (isDataRequest) {
371
+ return Response.json({
372
+ routeId: route.id,
373
+ pattern: route.pattern,
374
+ params,
375
+ loaderData: loaderData ?? null,
376
+ timestamp: Date.now(),
377
+ });
378
+ }
379
+
380
+ // SSR ๋ Œ๋”๋ง (๊ธฐ์กด ๋กœ์ง)
303
381
  const appCreator = createAppFn || defaultCreateApp;
304
382
  try {
305
383
  const app = appCreator({
306
384
  routeId: route.id,
307
385
  url: req.url,
308
386
  params,
387
+ loaderData,
309
388
  });
310
389
 
390
+ // serverData ๊ตฌ์กฐ: { [routeId]: { serverData: loaderData } }
391
+ const serverData = loaderData
392
+ ? { [route.id]: { serverData: loaderData } }
393
+ : undefined;
394
+
311
395
  return renderSSR(app, {
312
396
  title: `${route.id} - Mandu`,
313
397
  isDev: serverSettings.isDev,
@@ -315,6 +399,10 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
315
399
  routeId: route.id,
316
400
  hydration: route.hydration,
317
401
  bundleManifest: serverSettings.bundleManifest,
402
+ serverData,
403
+ // Client-side Routing ํ™œ์„ฑํ™” ์ •๋ณด ์ „๋‹ฌ
404
+ enableClientRouter: true,
405
+ routePattern: route.pattern,
318
406
  });
319
407
  } catch (err) {
320
408
  const ssrError = createSSRErrorResponse(
@@ -418,8 +506,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
418
506
  export function clearRegistry(): void {
419
507
  apiHandlers.clear();
420
508
  pageLoaders.clear();
509
+ pageHandlers.clear();
421
510
  routeComponents.clear();
422
511
  createAppFn = null;
423
512
  }
424
513
 
425
- export { apiHandlers, pageLoaders, routeComponents };
514
+ export { apiHandlers, pageLoaders, pageHandlers, routeComponents };