@mandujs/core 0.9.27 → 0.9.29
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 +12 -5
- package/src/runtime/server.ts +145 -71
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -1118,12 +1118,23 @@ export async function buildClientBundles(
|
|
|
1118
1118
|
// 1. Hydration이 필요한 라우트 필터링
|
|
1119
1119
|
const hydratedRoutes = getHydratedRoutes(manifest);
|
|
1120
1120
|
|
|
1121
|
+
// 2. 출력 디렉토리 생성 (항상 필요 - 매니페스트 저장용)
|
|
1122
|
+
const outDir = options.outDir || path.join(rootDir, ".mandu/client");
|
|
1123
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
1124
|
+
|
|
1125
|
+
// Hydration 라우트가 없어도 빈 매니페스트를 저장해야 함
|
|
1126
|
+
// (이전 빌드의 stale 매니페스트 참조 방지)
|
|
1121
1127
|
if (hydratedRoutes.length === 0) {
|
|
1128
|
+
const emptyManifest = createEmptyManifest(env);
|
|
1129
|
+
await fs.writeFile(
|
|
1130
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
1131
|
+
JSON.stringify(emptyManifest, null, 2)
|
|
1132
|
+
);
|
|
1122
1133
|
return {
|
|
1123
1134
|
success: true,
|
|
1124
1135
|
outputs: [],
|
|
1125
1136
|
errors: [],
|
|
1126
|
-
manifest:
|
|
1137
|
+
manifest: emptyManifest,
|
|
1127
1138
|
stats: {
|
|
1128
1139
|
totalSize: 0,
|
|
1129
1140
|
totalGzipSize: 0,
|
|
@@ -1134,10 +1145,6 @@ export async function buildClientBundles(
|
|
|
1134
1145
|
};
|
|
1135
1146
|
}
|
|
1136
1147
|
|
|
1137
|
-
// 2. 출력 디렉토리 생성
|
|
1138
|
-
const outDir = options.outDir || path.join(rootDir, ".mandu/client");
|
|
1139
|
-
await fs.mkdir(outDir, { recursive: true });
|
|
1140
|
-
|
|
1141
1148
|
// 3. Runtime 번들 빌드
|
|
1142
1149
|
const runtimeResult = await buildRuntime(outDir, options);
|
|
1143
1150
|
if (!runtimeResult.success) {
|
package/src/runtime/server.ts
CHANGED
|
@@ -100,11 +100,19 @@ export interface ServerOptions {
|
|
|
100
100
|
* - false: 기존 renderToString 사용 (기본값)
|
|
101
101
|
*/
|
|
102
102
|
streaming?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* 커스텀 레지스트리 (핸들러/설정 분리)
|
|
105
|
+
* - 제공하지 않으면 기본 전역 레지스트리 사용
|
|
106
|
+
* - 테스트나 멀티앱 시나리오에서 createServerRegistry()로 생성한 인스턴스 전달
|
|
107
|
+
*/
|
|
108
|
+
registry?: ServerRegistry;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
export interface ManduServer {
|
|
106
112
|
server: Server;
|
|
107
113
|
router: Router;
|
|
114
|
+
/** 이 서버 인스턴스의 레지스트리 */
|
|
115
|
+
registry: ServerRegistry;
|
|
108
116
|
stop: () => void;
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -137,15 +145,13 @@ export interface AppContext {
|
|
|
137
145
|
type RouteComponent = (props: { params: Record<string, string>; loaderData?: unknown }) => React.ReactElement;
|
|
138
146
|
type CreateAppFn = (context: AppContext) => React.ReactElement;
|
|
139
147
|
|
|
140
|
-
// Registry
|
|
141
|
-
const apiHandlers: Map<string, ApiHandler> = new Map();
|
|
142
|
-
const pageLoaders: Map<string, PageLoader> = new Map();
|
|
143
|
-
const pageHandlers: Map<string, PageHandler> = new Map();
|
|
144
|
-
const routeComponents: Map<string, RouteComponent> = new Map();
|
|
145
|
-
let createAppFn: CreateAppFn | null = null;
|
|
148
|
+
// ========== Server Registry (인스턴스별 분리) ==========
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
/**
|
|
151
|
+
* 서버 인스턴스별 핸들러/설정 레지스트리
|
|
152
|
+
* 같은 프로세스에서 여러 서버를 띄울 때 핸들러가 섞이는 문제 방지
|
|
153
|
+
*/
|
|
154
|
+
export interface ServerRegistrySettings {
|
|
149
155
|
isDev: boolean;
|
|
150
156
|
hmrPort?: number;
|
|
151
157
|
bundleManifest?: BundleManifest;
|
|
@@ -153,20 +159,82 @@ let serverSettings: {
|
|
|
153
159
|
publicDir: string;
|
|
154
160
|
cors?: CorsOptions | false;
|
|
155
161
|
streaming: boolean;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class ServerRegistry {
|
|
165
|
+
readonly apiHandlers: Map<string, ApiHandler> = new Map();
|
|
166
|
+
readonly pageLoaders: Map<string, PageLoader> = new Map();
|
|
167
|
+
readonly pageHandlers: Map<string, PageHandler> = new Map();
|
|
168
|
+
readonly routeComponents: Map<string, RouteComponent> = new Map();
|
|
169
|
+
createAppFn: CreateAppFn | null = null;
|
|
170
|
+
settings: ServerRegistrySettings = {
|
|
171
|
+
isDev: false,
|
|
172
|
+
rootDir: process.cwd(),
|
|
173
|
+
publicDir: "public",
|
|
174
|
+
cors: false,
|
|
175
|
+
streaming: false,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
registerApiHandler(routeId: string, handler: ApiHandler): void {
|
|
179
|
+
this.apiHandlers.set(routeId, handler);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
registerPageLoader(routeId: string, loader: PageLoader): void {
|
|
183
|
+
this.pageLoaders.set(routeId, loader);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
registerPageHandler(routeId: string, handler: PageHandler): void {
|
|
187
|
+
this.pageHandlers.set(routeId, handler);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
registerRouteComponent(routeId: string, component: RouteComponent): void {
|
|
191
|
+
this.routeComponents.set(routeId, component);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setCreateApp(fn: CreateAppFn): void {
|
|
195
|
+
this.createAppFn = fn;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 모든 핸들러/컴포넌트 초기화 (테스트용)
|
|
200
|
+
*/
|
|
201
|
+
clear(): void {
|
|
202
|
+
this.apiHandlers.clear();
|
|
203
|
+
this.pageLoaders.clear();
|
|
204
|
+
this.pageHandlers.clear();
|
|
205
|
+
this.routeComponents.clear();
|
|
206
|
+
this.createAppFn = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 기본 전역 레지스트리 (하위 호환성)
|
|
212
|
+
*/
|
|
213
|
+
const defaultRegistry = new ServerRegistry();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 새 레지스트리 인스턴스 생성
|
|
217
|
+
* 테스트나 멀티앱 시나리오에서 사용
|
|
218
|
+
*/
|
|
219
|
+
export function createServerRegistry(): ServerRegistry {
|
|
220
|
+
return new ServerRegistry();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 기본 레지스트리 초기화 (테스트용)
|
|
225
|
+
*/
|
|
226
|
+
export function clearDefaultRegistry(): void {
|
|
227
|
+
defaultRegistry.clear();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ========== 하위 호환성을 위한 전역 함수들 (defaultRegistry 사용) ==========
|
|
163
231
|
|
|
164
232
|
export function registerApiHandler(routeId: string, handler: ApiHandler): void {
|
|
165
|
-
|
|
233
|
+
defaultRegistry.registerApiHandler(routeId, handler);
|
|
166
234
|
}
|
|
167
235
|
|
|
168
236
|
export function registerPageLoader(routeId: string, loader: PageLoader): void {
|
|
169
|
-
|
|
237
|
+
defaultRegistry.registerPageLoader(routeId, loader);
|
|
170
238
|
}
|
|
171
239
|
|
|
172
240
|
/**
|
|
@@ -174,32 +242,34 @@ export function registerPageLoader(routeId: string, loader: PageLoader): void {
|
|
|
174
242
|
* filling이 있으면 loader를 실행하여 serverData 전달
|
|
175
243
|
*/
|
|
176
244
|
export function registerPageHandler(routeId: string, handler: PageHandler): void {
|
|
177
|
-
|
|
245
|
+
defaultRegistry.registerPageHandler(routeId, handler);
|
|
178
246
|
}
|
|
179
247
|
|
|
180
248
|
export function registerRouteComponent(routeId: string, component: RouteComponent): void {
|
|
181
|
-
|
|
249
|
+
defaultRegistry.registerRouteComponent(routeId, component);
|
|
182
250
|
}
|
|
183
251
|
|
|
184
252
|
export function setCreateApp(fn: CreateAppFn): void {
|
|
185
|
-
|
|
253
|
+
defaultRegistry.setCreateApp(fn);
|
|
186
254
|
}
|
|
187
255
|
|
|
188
|
-
// Default createApp implementation
|
|
189
|
-
function
|
|
190
|
-
|
|
256
|
+
// Default createApp implementation (registry 기반)
|
|
257
|
+
function createDefaultAppFactory(registry: ServerRegistry) {
|
|
258
|
+
return function defaultCreateApp(context: AppContext): React.ReactElement {
|
|
259
|
+
const Component = registry.routeComponents.get(context.routeId);
|
|
191
260
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
261
|
+
if (!Component) {
|
|
262
|
+
return React.createElement("div", null,
|
|
263
|
+
React.createElement("h1", null, "404 - Route Not Found"),
|
|
264
|
+
React.createElement("p", null, `Route ID: ${context.routeId}`)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
198
267
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
268
|
+
return React.createElement(Component, {
|
|
269
|
+
params: context.params,
|
|
270
|
+
loaderData: context.loaderData,
|
|
271
|
+
});
|
|
272
|
+
};
|
|
203
273
|
}
|
|
204
274
|
|
|
205
275
|
// ========== Static File Serving ==========
|
|
@@ -224,7 +294,7 @@ function isPathSafe(filePath: string, allowedDir: string): boolean {
|
|
|
224
294
|
*
|
|
225
295
|
* 보안: Path traversal 공격 방지를 위해 모든 경로를 검증합니다.
|
|
226
296
|
*/
|
|
227
|
-
async function serveStaticFile(pathname: string): Promise<Response | null> {
|
|
297
|
+
async function serveStaticFile(pathname: string, settings: ServerRegistrySettings): Promise<Response | null> {
|
|
228
298
|
let filePath: string | null = null;
|
|
229
299
|
let isBundleFile = false;
|
|
230
300
|
let allowedBaseDir: string;
|
|
@@ -238,14 +308,14 @@ async function serveStaticFile(pathname: string): Promise<Response | null> {
|
|
|
238
308
|
if (pathname.startsWith("/.mandu/client/")) {
|
|
239
309
|
// pathname에서 prefix 제거 후 안전하게 조합
|
|
240
310
|
const relativePath = pathname.slice("/.mandu/client/".length);
|
|
241
|
-
allowedBaseDir = path.join(
|
|
311
|
+
allowedBaseDir = path.join(settings.rootDir, ".mandu", "client");
|
|
242
312
|
filePath = path.join(allowedBaseDir, relativePath);
|
|
243
313
|
isBundleFile = true;
|
|
244
314
|
}
|
|
245
315
|
// 2. Public 폴더 파일 (/public/*)
|
|
246
316
|
else if (pathname.startsWith("/public/")) {
|
|
247
317
|
const relativePath = pathname.slice("/public/".length);
|
|
248
|
-
allowedBaseDir = path.join(
|
|
318
|
+
allowedBaseDir = path.join(settings.rootDir, "public");
|
|
249
319
|
filePath = path.join(allowedBaseDir, relativePath);
|
|
250
320
|
}
|
|
251
321
|
// 3. Public 폴더의 루트 파일 (favicon.ico, robots.txt 등)
|
|
@@ -257,7 +327,7 @@ async function serveStaticFile(pathname: string): Promise<Response | null> {
|
|
|
257
327
|
) {
|
|
258
328
|
// 고정된 파일명만 허용 (이미 위에서 정확히 매칭됨)
|
|
259
329
|
const filename = path.basename(pathname);
|
|
260
|
-
allowedBaseDir = path.join(
|
|
330
|
+
allowedBaseDir = path.join(settings.rootDir, settings.publicDir);
|
|
261
331
|
filePath = path.join(allowedBaseDir, filename);
|
|
262
332
|
} else {
|
|
263
333
|
return null; // 정적 파일이 아님
|
|
@@ -281,7 +351,7 @@ async function serveStaticFile(pathname: string): Promise<Response | null> {
|
|
|
281
351
|
|
|
282
352
|
// Cache-Control 헤더 설정
|
|
283
353
|
let cacheControl: string;
|
|
284
|
-
if (
|
|
354
|
+
if (settings.isDev) {
|
|
285
355
|
// 개발 모드: 캐시 없음
|
|
286
356
|
cacheControl = "no-cache, no-store, must-revalidate";
|
|
287
357
|
} else if (isBundleFile) {
|
|
@@ -305,22 +375,23 @@ async function serveStaticFile(pathname: string): Promise<Response | null> {
|
|
|
305
375
|
|
|
306
376
|
// ========== Request Handler ==========
|
|
307
377
|
|
|
308
|
-
async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
378
|
+
async function handleRequest(req: Request, router: Router, registry: ServerRegistry): Promise<Response> {
|
|
309
379
|
const url = new URL(req.url);
|
|
310
380
|
const pathname = url.pathname;
|
|
381
|
+
const settings = registry.settings;
|
|
311
382
|
|
|
312
383
|
// 0. CORS Preflight 요청 처리
|
|
313
|
-
if (
|
|
314
|
-
const corsOptions =
|
|
384
|
+
if (settings.cors && isPreflightRequest(req)) {
|
|
385
|
+
const corsOptions = settings.cors === true ? {} : settings.cors;
|
|
315
386
|
return handlePreflightRequest(req, corsOptions);
|
|
316
387
|
}
|
|
317
388
|
|
|
318
389
|
// 1. 정적 파일 서빙 시도 (최우선)
|
|
319
|
-
const staticResponse = await serveStaticFile(pathname);
|
|
390
|
+
const staticResponse = await serveStaticFile(pathname, settings);
|
|
320
391
|
if (staticResponse) {
|
|
321
392
|
// 정적 파일에도 CORS 헤더 적용
|
|
322
|
-
if (
|
|
323
|
-
const corsOptions =
|
|
393
|
+
if (settings.cors && isCorsRequest(req)) {
|
|
394
|
+
const corsOptions = settings.cors === true ? {} : settings.cors;
|
|
324
395
|
return applyCorsToResponse(staticResponse, req, corsOptions);
|
|
325
396
|
}
|
|
326
397
|
return staticResponse;
|
|
@@ -340,7 +411,7 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
340
411
|
const { route, params } = match;
|
|
341
412
|
|
|
342
413
|
if (route.kind === "api") {
|
|
343
|
-
const handler = apiHandlers.get(route.id);
|
|
414
|
+
const handler = registry.apiHandlers.get(route.id);
|
|
344
415
|
if (!handler) {
|
|
345
416
|
const error = createHandlerNotFoundResponse(route.id, route.pattern);
|
|
346
417
|
const response = formatErrorResponse(error, {
|
|
@@ -359,12 +430,12 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
359
430
|
const isDataRequest = url.searchParams.has("_data");
|
|
360
431
|
|
|
361
432
|
// 1. PageHandler 방식 (신규 - filling 포함)
|
|
362
|
-
const pageHandler = pageHandlers.get(route.id);
|
|
433
|
+
const pageHandler = registry.pageHandlers.get(route.id);
|
|
363
434
|
if (pageHandler) {
|
|
364
435
|
try {
|
|
365
436
|
const registration = await pageHandler();
|
|
366
437
|
component = registration.component as RouteComponent;
|
|
367
|
-
registerRouteComponent(route.id, component);
|
|
438
|
+
registry.registerRouteComponent(route.id, component);
|
|
368
439
|
|
|
369
440
|
// Filling의 loader 실행
|
|
370
441
|
if (registration.filling?.hasLoader()) {
|
|
@@ -386,7 +457,7 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
386
457
|
}
|
|
387
458
|
// 2. PageLoader 방식 (레거시 호환)
|
|
388
459
|
else {
|
|
389
|
-
const loader = pageLoaders.get(route.id);
|
|
460
|
+
const loader = registry.pageLoaders.get(route.id);
|
|
390
461
|
if (loader) {
|
|
391
462
|
try {
|
|
392
463
|
const module = await loader();
|
|
@@ -395,7 +466,7 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
395
466
|
const component = typeof exported === 'function'
|
|
396
467
|
? exported
|
|
397
468
|
: exported?.component ?? exported;
|
|
398
|
-
registerRouteComponent(route.id, component);
|
|
469
|
+
registry.registerRouteComponent(route.id, component);
|
|
399
470
|
|
|
400
471
|
// filling이 있으면 loader 실행
|
|
401
472
|
const filling = typeof exported === 'object' ? exported?.filling : null;
|
|
@@ -430,7 +501,8 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
430
501
|
}
|
|
431
502
|
|
|
432
503
|
// SSR 렌더링
|
|
433
|
-
const
|
|
504
|
+
const defaultAppCreator = createDefaultAppFactory(registry);
|
|
505
|
+
const appCreator = registry.createAppFn || defaultAppCreator;
|
|
434
506
|
try {
|
|
435
507
|
const app = appCreator({
|
|
436
508
|
routeId: route.id,
|
|
@@ -445,29 +517,29 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
445
517
|
: undefined;
|
|
446
518
|
|
|
447
519
|
// Streaming SSR 모드 결정
|
|
448
|
-
// 우선순위: route.streaming >
|
|
520
|
+
// 우선순위: route.streaming > settings.streaming
|
|
449
521
|
const useStreaming = route.streaming !== undefined
|
|
450
522
|
? route.streaming
|
|
451
|
-
:
|
|
523
|
+
: settings.streaming;
|
|
452
524
|
|
|
453
525
|
if (useStreaming) {
|
|
454
526
|
return await renderStreamingResponse(app, {
|
|
455
527
|
title: `${route.id} - Mandu`,
|
|
456
|
-
isDev:
|
|
457
|
-
hmrPort:
|
|
528
|
+
isDev: settings.isDev,
|
|
529
|
+
hmrPort: settings.hmrPort,
|
|
458
530
|
routeId: route.id,
|
|
459
531
|
routePattern: route.pattern,
|
|
460
532
|
hydration: route.hydration,
|
|
461
|
-
bundleManifest:
|
|
533
|
+
bundleManifest: settings.bundleManifest,
|
|
462
534
|
criticalData: loaderData as Record<string, unknown> | undefined,
|
|
463
535
|
enableClientRouter: true,
|
|
464
536
|
onShellReady: () => {
|
|
465
|
-
if (
|
|
537
|
+
if (settings.isDev) {
|
|
466
538
|
console.log(`[Mandu Streaming] Shell ready: ${route.id}`);
|
|
467
539
|
}
|
|
468
540
|
},
|
|
469
541
|
onMetrics: (metrics) => {
|
|
470
|
-
if (
|
|
542
|
+
if (settings.isDev) {
|
|
471
543
|
console.log(`[Mandu Streaming] Metrics for ${route.id}:`, {
|
|
472
544
|
shellReadyTime: `${metrics.shellReadyTime}ms`,
|
|
473
545
|
allReadyTime: `${metrics.allReadyTime}ms`,
|
|
@@ -481,11 +553,11 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
481
553
|
// 기존 renderToString 방식
|
|
482
554
|
return renderSSR(app, {
|
|
483
555
|
title: `${route.id} - Mandu`,
|
|
484
|
-
isDev:
|
|
485
|
-
hmrPort:
|
|
556
|
+
isDev: settings.isDev,
|
|
557
|
+
hmrPort: settings.hmrPort,
|
|
486
558
|
routeId: route.id,
|
|
487
559
|
hydration: route.hydration,
|
|
488
|
-
bundleManifest:
|
|
560
|
+
bundleManifest: settings.bundleManifest,
|
|
489
561
|
serverData,
|
|
490
562
|
// Client-side Routing 활성화 정보 전달
|
|
491
563
|
enableClientRouter: true,
|
|
@@ -535,13 +607,14 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
535
607
|
publicDir = "public",
|
|
536
608
|
cors = false,
|
|
537
609
|
streaming = false,
|
|
610
|
+
registry = defaultRegistry,
|
|
538
611
|
} = options;
|
|
539
612
|
|
|
540
613
|
// CORS 옵션 파싱
|
|
541
614
|
const corsOptions: CorsOptions | false = cors === true ? {} : cors;
|
|
542
615
|
|
|
543
|
-
//
|
|
544
|
-
|
|
616
|
+
// Registry settings 저장
|
|
617
|
+
registry.settings = {
|
|
545
618
|
isDev,
|
|
546
619
|
hmrPort,
|
|
547
620
|
bundleManifest,
|
|
@@ -553,9 +626,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
553
626
|
|
|
554
627
|
const router = new Router(manifest.routes);
|
|
555
628
|
|
|
556
|
-
// Fetch handler with CORS support
|
|
629
|
+
// Fetch handler with CORS support (registry를 클로저로 캡처)
|
|
557
630
|
const fetchHandler = async (req: Request): Promise<Response> => {
|
|
558
|
-
const response = await handleRequest(req, router);
|
|
631
|
+
const response = await handleRequest(req, router, registry);
|
|
559
632
|
|
|
560
633
|
// API 라우트 응답에 CORS 헤더 적용
|
|
561
634
|
if (corsOptions && isCorsRequest(req)) {
|
|
@@ -593,17 +666,18 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
593
666
|
return {
|
|
594
667
|
server,
|
|
595
668
|
router,
|
|
669
|
+
registry,
|
|
596
670
|
stop: () => server.stop(),
|
|
597
671
|
};
|
|
598
672
|
}
|
|
599
673
|
|
|
600
|
-
// Clear registries (useful for testing)
|
|
674
|
+
// Clear registries (useful for testing) - deprecated, use clearDefaultRegistry()
|
|
601
675
|
export function clearRegistry(): void {
|
|
602
|
-
|
|
603
|
-
pageLoaders.clear();
|
|
604
|
-
pageHandlers.clear();
|
|
605
|
-
routeComponents.clear();
|
|
606
|
-
createAppFn = null;
|
|
676
|
+
clearDefaultRegistry();
|
|
607
677
|
}
|
|
608
678
|
|
|
609
|
-
|
|
679
|
+
// Export registry maps for backward compatibility (defaultRegistry 사용)
|
|
680
|
+
export const apiHandlers = defaultRegistry.apiHandlers;
|
|
681
|
+
export const pageLoaders = defaultRegistry.pageLoaders;
|
|
682
|
+
export const pageHandlers = defaultRegistry.pageHandlers;
|
|
683
|
+
export const routeComponents = defaultRegistry.routeComponents;
|