@mandujs/core 0.5.6 → 0.5.7
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 +40 -0
- package/src/generator/templates.ts +47 -0
- package/src/runtime/server.ts +78 -7
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -249,6 +249,36 @@ export default ReactDOMClient;
|
|
|
249
249
|
`;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* JSX Runtime shim 소스 생성
|
|
254
|
+
*/
|
|
255
|
+
function generateJsxRuntimeShimSource(): string {
|
|
256
|
+
return `
|
|
257
|
+
/**
|
|
258
|
+
* Mandu JSX Runtime Shim (Generated)
|
|
259
|
+
* Production JSX 변환용
|
|
260
|
+
*/
|
|
261
|
+
import * as jsxRuntime from 'react/jsx-runtime';
|
|
262
|
+
export * from 'react/jsx-runtime';
|
|
263
|
+
export default jsxRuntime;
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* JSX Dev Runtime shim 소스 생성
|
|
269
|
+
*/
|
|
270
|
+
function generateJsxDevRuntimeShimSource(): string {
|
|
271
|
+
return `
|
|
272
|
+
/**
|
|
273
|
+
* Mandu JSX Dev Runtime Shim (Generated)
|
|
274
|
+
* Development JSX 변환용
|
|
275
|
+
*/
|
|
276
|
+
import * as jsxDevRuntime from 'react/jsx-dev-runtime';
|
|
277
|
+
export * from 'react/jsx-dev-runtime';
|
|
278
|
+
export default jsxDevRuntime;
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
252
282
|
/**
|
|
253
283
|
* Island 엔트리 래퍼 생성
|
|
254
284
|
*/
|
|
@@ -332,6 +362,8 @@ interface VendorBuildResult {
|
|
|
332
362
|
react: string;
|
|
333
363
|
reactDom: string;
|
|
334
364
|
reactDomClient: string;
|
|
365
|
+
jsxRuntime: string;
|
|
366
|
+
jsxDevRuntime: string;
|
|
335
367
|
errors: string[];
|
|
336
368
|
}
|
|
337
369
|
|
|
@@ -348,12 +380,16 @@ async function buildVendorShims(
|
|
|
348
380
|
react: "",
|
|
349
381
|
reactDom: "",
|
|
350
382
|
reactDomClient: "",
|
|
383
|
+
jsxRuntime: "",
|
|
384
|
+
jsxDevRuntime: "",
|
|
351
385
|
};
|
|
352
386
|
|
|
353
387
|
const shims = [
|
|
354
388
|
{ name: "_react", source: generateReactShimSource(), key: "react" },
|
|
355
389
|
{ name: "_react-dom", source: generateReactDOMShimSource(), key: "reactDom" },
|
|
356
390
|
{ name: "_react-dom-client", source: generateReactDOMClientShimSource(), key: "reactDomClient" },
|
|
391
|
+
{ name: "_jsx-runtime", source: generateJsxRuntimeShimSource(), key: "jsxRuntime" },
|
|
392
|
+
{ name: "_jsx-dev-runtime", source: generateJsxDevRuntimeShimSource(), key: "jsxDevRuntime" },
|
|
357
393
|
];
|
|
358
394
|
|
|
359
395
|
for (const shim of shims) {
|
|
@@ -394,6 +430,8 @@ async function buildVendorShims(
|
|
|
394
430
|
react: results.react,
|
|
395
431
|
reactDom: results.reactDom,
|
|
396
432
|
reactDomClient: results.reactDomClient,
|
|
433
|
+
jsxRuntime: results.jsxRuntime,
|
|
434
|
+
jsxDevRuntime: results.jsxDevRuntime,
|
|
397
435
|
errors,
|
|
398
436
|
};
|
|
399
437
|
}
|
|
@@ -494,6 +532,8 @@ function createBundleManifest(
|
|
|
494
532
|
"react": vendorResult.react,
|
|
495
533
|
"react-dom": vendorResult.reactDom,
|
|
496
534
|
"react-dom/client": vendorResult.reactDomClient,
|
|
535
|
+
"react/jsx-runtime": vendorResult.jsxRuntime,
|
|
536
|
+
"react/jsx-dev-runtime": vendorResult.jsxDevRuntime,
|
|
497
537
|
},
|
|
498
538
|
},
|
|
499
539
|
};
|
|
@@ -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"
|
package/src/runtime/server.ts
CHANGED
|
@@ -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
|
|
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, {
|
|
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,22 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
281
312
|
}
|
|
282
313
|
|
|
283
314
|
if (route.kind === "page") {
|
|
284
|
-
|
|
285
|
-
|
|
315
|
+
let loaderData: unknown;
|
|
316
|
+
let component: RouteComponent | undefined;
|
|
317
|
+
|
|
318
|
+
// 1. PageHandler 방식 (신규 - filling 포함)
|
|
319
|
+
const pageHandler = pageHandlers.get(route.id);
|
|
320
|
+
if (pageHandler) {
|
|
286
321
|
try {
|
|
287
|
-
const
|
|
288
|
-
|
|
322
|
+
const registration = await pageHandler();
|
|
323
|
+
component = registration.component as RouteComponent;
|
|
324
|
+
registerRouteComponent(route.id, component);
|
|
325
|
+
|
|
326
|
+
// Filling의 loader 실행
|
|
327
|
+
if (registration.filling?.hasLoader()) {
|
|
328
|
+
const ctx = new ManduContext(req, params);
|
|
329
|
+
loaderData = await registration.filling.executeLoader(ctx);
|
|
330
|
+
}
|
|
289
331
|
} catch (err) {
|
|
290
332
|
const pageError = createPageLoadErrorResponse(
|
|
291
333
|
route.id,
|
|
@@ -299,6 +341,27 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
299
341
|
return Response.json(response, { status: 500 });
|
|
300
342
|
}
|
|
301
343
|
}
|
|
344
|
+
// 2. PageLoader 방식 (레거시 호환)
|
|
345
|
+
else {
|
|
346
|
+
const loader = pageLoaders.get(route.id);
|
|
347
|
+
if (loader) {
|
|
348
|
+
try {
|
|
349
|
+
const module = await loader();
|
|
350
|
+
registerRouteComponent(route.id, module.default);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const pageError = createPageLoadErrorResponse(
|
|
353
|
+
route.id,
|
|
354
|
+
route.pattern,
|
|
355
|
+
err instanceof Error ? err : new Error(String(err))
|
|
356
|
+
);
|
|
357
|
+
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
358
|
+
const response = formatErrorResponse(pageError, {
|
|
359
|
+
isDev: process.env.NODE_ENV !== "production",
|
|
360
|
+
});
|
|
361
|
+
return Response.json(response, { status: 500 });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
302
365
|
|
|
303
366
|
const appCreator = createAppFn || defaultCreateApp;
|
|
304
367
|
try {
|
|
@@ -306,8 +369,14 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
306
369
|
routeId: route.id,
|
|
307
370
|
url: req.url,
|
|
308
371
|
params,
|
|
372
|
+
loaderData,
|
|
309
373
|
});
|
|
310
374
|
|
|
375
|
+
// serverData 구조: { [routeId]: { serverData: loaderData } }
|
|
376
|
+
const serverData = loaderData
|
|
377
|
+
? { [route.id]: { serverData: loaderData } }
|
|
378
|
+
: undefined;
|
|
379
|
+
|
|
311
380
|
return renderSSR(app, {
|
|
312
381
|
title: `${route.id} - Mandu`,
|
|
313
382
|
isDev: serverSettings.isDev,
|
|
@@ -315,6 +384,7 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
315
384
|
routeId: route.id,
|
|
316
385
|
hydration: route.hydration,
|
|
317
386
|
bundleManifest: serverSettings.bundleManifest,
|
|
387
|
+
serverData,
|
|
318
388
|
});
|
|
319
389
|
} catch (err) {
|
|
320
390
|
const ssrError = createSSRErrorResponse(
|
|
@@ -418,8 +488,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
418
488
|
export function clearRegistry(): void {
|
|
419
489
|
apiHandlers.clear();
|
|
420
490
|
pageLoaders.clear();
|
|
491
|
+
pageHandlers.clear();
|
|
421
492
|
routeComponents.clear();
|
|
422
493
|
createAppFn = null;
|
|
423
494
|
}
|
|
424
495
|
|
|
425
|
-
export { apiHandlers, pageLoaders, routeComponents };
|
|
496
|
+
export { apiHandlers, pageLoaders, pageHandlers, routeComponents };
|