@mandujs/core 0.9.40 → 0.9.42
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 +91 -73
- package/src/bundler/dev.ts +21 -14
- package/src/client/globals.ts +44 -0
- package/src/client/index.ts +5 -4
- package/src/client/island.ts +8 -13
- package/src/client/router.ts +33 -41
- package/src/client/runtime.ts +23 -51
- package/src/client/window-state.ts +101 -0
- package/src/config/index.ts +1 -0
- package/src/config/mandu.ts +45 -9
- package/src/config/validate.ts +158 -0
- package/src/constants.ts +25 -0
- package/src/contract/client.ts +4 -3
- package/src/contract/define.ts +459 -0
- package/src/devtools/ai/context-builder.ts +375 -0
- package/src/devtools/ai/index.ts +25 -0
- package/src/devtools/ai/mcp-connector.ts +465 -0
- package/src/devtools/client/catchers/error-catcher.ts +327 -0
- package/src/devtools/client/catchers/index.ts +18 -0
- package/src/devtools/client/catchers/network-proxy.ts +363 -0
- package/src/devtools/client/components/index.ts +39 -0
- package/src/devtools/client/components/kitchen-root.tsx +362 -0
- package/src/devtools/client/components/mandu-character.tsx +241 -0
- package/src/devtools/client/components/overlay.tsx +368 -0
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
- package/src/devtools/client/components/panel/index.ts +32 -0
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
- package/src/devtools/client/components/panel/network-panel.tsx +292 -0
- package/src/devtools/client/components/panel/panel-container.tsx +259 -0
- package/src/devtools/client/filters/context-filters.ts +282 -0
- package/src/devtools/client/filters/index.ts +16 -0
- package/src/devtools/client/index.ts +63 -0
- package/src/devtools/client/persistence.ts +335 -0
- package/src/devtools/client/state-manager.ts +478 -0
- package/src/devtools/design-tokens.ts +263 -0
- package/src/devtools/hook/create-hook.ts +207 -0
- package/src/devtools/hook/index.ts +13 -0
- package/src/devtools/index.ts +439 -0
- package/src/devtools/init.ts +266 -0
- package/src/devtools/protocol.ts +237 -0
- package/src/devtools/server/index.ts +17 -0
- package/src/devtools/server/source-context.ts +444 -0
- package/src/devtools/types.ts +319 -0
- package/src/devtools/worker/index.ts +25 -0
- package/src/devtools/worker/redaction-worker.ts +222 -0
- package/src/devtools/worker/worker-manager.ts +409 -0
- package/src/error/formatter.ts +28 -24
- package/src/error/index.ts +13 -9
- package/src/error/result.ts +46 -0
- package/src/error/types.ts +6 -4
- package/src/filling/filling.ts +6 -5
- package/src/guard/check.ts +60 -56
- package/src/guard/types.ts +3 -1
- package/src/guard/watcher.ts +10 -1
- package/src/index.ts +81 -0
- package/src/intent/index.ts +310 -0
- package/src/island/index.ts +304 -0
- package/src/router/fs-patterns.ts +7 -0
- package/src/router/fs-routes.ts +20 -8
- package/src/router/fs-scanner.ts +117 -133
- package/src/runtime/server.ts +261 -201
- package/src/runtime/ssr.ts +5 -4
- package/src/runtime/streaming-ssr.ts +5 -4
- package/src/utils/bun.ts +8 -0
- package/src/utils/lru-cache.ts +75 -0
package/src/runtime/server.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
-
import type { RoutesManifest } from "../spec/schema";
|
|
3
|
-
import type { BundleManifest } from "../bundler/types";
|
|
4
|
-
import type { ManduFilling } from "../filling/filling";
|
|
5
|
-
import { ManduContext } from "../filling/context";
|
|
6
|
-
import { Router } from "./router";
|
|
7
|
-
import { renderSSR, renderStreamingResponse } from "./ssr";
|
|
8
|
-
import { PageBoundary, DefaultLoading, DefaultError, type ErrorFallbackProps } from "./boundary";
|
|
9
|
-
import React, { type ReactNode } from "react";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
3
|
+
import type { BundleManifest } from "../bundler/types";
|
|
4
|
+
import type { ManduFilling } from "../filling/filling";
|
|
5
|
+
import { ManduContext } from "../filling/context";
|
|
6
|
+
import { Router } from "./router";
|
|
7
|
+
import { renderSSR, renderStreamingResponse } from "./ssr";
|
|
8
|
+
import { PageBoundary, DefaultLoading, DefaultError, type ErrorFallbackProps } from "./boundary";
|
|
9
|
+
import React, { type ReactNode } from "react";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import { PORTS } from "../constants";
|
|
13
|
+
import {
|
|
14
|
+
createNotFoundResponse,
|
|
15
|
+
createHandlerNotFoundResponse,
|
|
16
|
+
createPageLoadErrorResponse,
|
|
17
|
+
createSSRErrorResponse,
|
|
18
|
+
errorToResponse,
|
|
19
|
+
err,
|
|
20
|
+
ok,
|
|
21
|
+
type Result,
|
|
22
|
+
} from "../error";
|
|
18
23
|
import {
|
|
19
24
|
type CorsOptions,
|
|
20
25
|
isPreflightRequest,
|
|
@@ -504,13 +509,34 @@ function createDefaultAppFactory(registry: ServerRegistry) {
|
|
|
504
509
|
* 경로가 허용된 디렉토리 내에 있는지 검증
|
|
505
510
|
* Path traversal 공격 방지
|
|
506
511
|
*/
|
|
507
|
-
function isPathSafe(filePath: string, allowedDir: string): boolean {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
async function isPathSafe(filePath: string, allowedDir: string): Promise<boolean> {
|
|
513
|
+
try {
|
|
514
|
+
const resolvedPath = path.resolve(filePath);
|
|
515
|
+
const resolvedAllowedDir = path.resolve(allowedDir);
|
|
516
|
+
|
|
517
|
+
if (!resolvedPath.startsWith(resolvedAllowedDir + path.sep) &&
|
|
518
|
+
resolvedPath !== resolvedAllowedDir) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 파일이 없으면 안전 (존재하지 않는 경로)
|
|
523
|
+
try {
|
|
524
|
+
await fs.access(resolvedPath);
|
|
525
|
+
} catch {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Symlink 해결 후 재검증
|
|
530
|
+
const realPath = await fs.realpath(resolvedPath);
|
|
531
|
+
const realAllowedDir = await fs.realpath(resolvedAllowedDir);
|
|
532
|
+
|
|
533
|
+
return realPath.startsWith(realAllowedDir + path.sep) ||
|
|
534
|
+
realPath === realAllowedDir;
|
|
535
|
+
} catch (error) {
|
|
536
|
+
console.warn(`[Mandu Security] Path validation failed: ${filePath}`, error);
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
514
540
|
|
|
515
541
|
/**
|
|
516
542
|
* 정적 파일 서빙
|
|
@@ -520,50 +546,73 @@ function isPathSafe(filePath: string, allowedDir: string): boolean {
|
|
|
520
546
|
*
|
|
521
547
|
* 보안: Path traversal 공격 방지를 위해 모든 경로를 검증합니다.
|
|
522
548
|
*/
|
|
523
|
-
async function serveStaticFile(pathname: string, settings: ServerRegistrySettings): Promise<Response | null> {
|
|
524
|
-
let filePath: string | null = null;
|
|
525
|
-
let isBundleFile = false;
|
|
526
|
-
let allowedBaseDir: string;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
549
|
+
async function serveStaticFile(pathname: string, settings: ServerRegistrySettings): Promise<Response | null> {
|
|
550
|
+
let filePath: string | null = null;
|
|
551
|
+
let isBundleFile = false;
|
|
552
|
+
let allowedBaseDir: string;
|
|
553
|
+
let relativePath: string;
|
|
554
|
+
|
|
555
|
+
// Path traversal 시도 조기 차단 (정규화 전 raw 체크)
|
|
556
|
+
if (pathname.includes("..")) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
532
559
|
|
|
533
560
|
// 1. 클라이언트 번들 파일 (/.mandu/client/*)
|
|
534
561
|
if (pathname.startsWith("/.mandu/client/")) {
|
|
535
562
|
// pathname에서 prefix 제거 후 안전하게 조합
|
|
536
|
-
|
|
537
|
-
allowedBaseDir = path.join(settings.rootDir, ".mandu", "client");
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
pathname === "/favicon.ico" ||
|
|
550
|
-
pathname === "/robots.txt" ||
|
|
563
|
+
relativePath = pathname.slice("/.mandu/client/".length);
|
|
564
|
+
allowedBaseDir = path.join(settings.rootDir, ".mandu", "client");
|
|
565
|
+
isBundleFile = true;
|
|
566
|
+
}
|
|
567
|
+
// 2. Public 폴더 파일 (/public/*)
|
|
568
|
+
else if (pathname.startsWith("/public/")) {
|
|
569
|
+
relativePath = pathname.slice("/public/".length);
|
|
570
|
+
allowedBaseDir = path.join(settings.rootDir, settings.publicDir);
|
|
571
|
+
}
|
|
572
|
+
// 3. Public 폴더의 루트 파일 (favicon.ico, robots.txt 등)
|
|
573
|
+
else if (
|
|
574
|
+
pathname === "/favicon.ico" ||
|
|
575
|
+
pathname === "/robots.txt" ||
|
|
551
576
|
pathname === "/sitemap.xml" ||
|
|
552
577
|
pathname === "/manifest.json"
|
|
553
|
-
) {
|
|
554
|
-
// 고정된 파일명만 허용 (이미 위에서 정확히 매칭됨)
|
|
555
|
-
|
|
556
|
-
allowedBaseDir = path.join(settings.rootDir, settings.publicDir);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
578
|
+
) {
|
|
579
|
+
// 고정된 파일명만 허용 (이미 위에서 정확히 매칭됨)
|
|
580
|
+
relativePath = path.basename(pathname);
|
|
581
|
+
allowedBaseDir = path.join(settings.rootDir, settings.publicDir);
|
|
582
|
+
} else {
|
|
583
|
+
return null; // 정적 파일이 아님
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// URL 디코딩 (실패 시 차단)
|
|
587
|
+
let decodedPath: string;
|
|
588
|
+
try {
|
|
589
|
+
decodedPath = decodeURIComponent(relativePath);
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 정규화 + Null byte 방지
|
|
595
|
+
const normalizedPath = path.posix.normalize(decodedPath);
|
|
596
|
+
if (normalizedPath.includes("\0")) {
|
|
597
|
+
console.warn(`[Mandu Security] Null byte attack detected: ${pathname}`);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 선행 슬래시 제거 → path.join이 base를 무시하지 않도록 보장
|
|
602
|
+
const safeRelativePath = normalizedPath.replace(/^\/+/, "");
|
|
603
|
+
|
|
604
|
+
// 상대 경로 탈출 차단
|
|
605
|
+
if (safeRelativePath.startsWith("..")) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
filePath = path.join(allowedBaseDir, safeRelativePath);
|
|
610
|
+
|
|
611
|
+
// 최종 경로 검증: 허용된 디렉토리 내에 있는지 확인
|
|
612
|
+
if (!(await isPathSafe(filePath, allowedBaseDir!))) {
|
|
613
|
+
console.warn(`[Mandu Security] Path traversal attempt blocked: ${pathname}`);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
567
616
|
|
|
568
617
|
try {
|
|
569
618
|
const file = Bun.file(filePath);
|
|
@@ -601,52 +650,64 @@ async function serveStaticFile(pathname: string, settings: ServerRegistrySetting
|
|
|
601
650
|
|
|
602
651
|
// ========== Request Handler ==========
|
|
603
652
|
|
|
604
|
-
async function handleRequest(req: Request, router: Router, registry: ServerRegistry): Promise<Response> {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
653
|
+
async function handleRequest(req: Request, router: Router, registry: ServerRegistry): Promise<Response> {
|
|
654
|
+
const result = await handleRequestInternal(req, router, registry);
|
|
655
|
+
|
|
656
|
+
if (!result.ok) {
|
|
657
|
+
return errorToResponse(result.error, registry.settings.isDev);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return result.value;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function handleRequestInternal(
|
|
664
|
+
req: Request,
|
|
665
|
+
router: Router,
|
|
666
|
+
registry: ServerRegistry
|
|
667
|
+
): Promise<Result<Response>> {
|
|
668
|
+
const url = new URL(req.url);
|
|
669
|
+
const pathname = url.pathname;
|
|
670
|
+
const settings = registry.settings;
|
|
671
|
+
|
|
672
|
+
// 0. CORS Preflight 요청 처리
|
|
673
|
+
if (settings.cors && isPreflightRequest(req)) {
|
|
674
|
+
const corsOptions = settings.cors === true ? {} : settings.cors;
|
|
675
|
+
return ok(handlePreflightRequest(req, corsOptions));
|
|
676
|
+
}
|
|
614
677
|
|
|
615
678
|
// 1. 정적 파일 서빙 시도 (최우선)
|
|
616
|
-
const staticResponse = await serveStaticFile(pathname, settings);
|
|
617
|
-
if (staticResponse) {
|
|
618
|
-
// 정적 파일에도 CORS 헤더 적용
|
|
619
|
-
if (settings.cors && isCorsRequest(req)) {
|
|
620
|
-
const corsOptions = settings.cors === true ? {} : settings.cors;
|
|
621
|
-
return applyCorsToResponse(staticResponse, req, corsOptions);
|
|
622
|
-
}
|
|
623
|
-
return staticResponse;
|
|
624
|
-
}
|
|
679
|
+
const staticResponse = await serveStaticFile(pathname, settings);
|
|
680
|
+
if (staticResponse) {
|
|
681
|
+
// 정적 파일에도 CORS 헤더 적용
|
|
682
|
+
if (settings.cors && isCorsRequest(req)) {
|
|
683
|
+
const corsOptions = settings.cors === true ? {} : settings.cors;
|
|
684
|
+
return ok(applyCorsToResponse(staticResponse, req, corsOptions));
|
|
685
|
+
}
|
|
686
|
+
return ok(staticResponse);
|
|
687
|
+
}
|
|
625
688
|
|
|
626
689
|
// 2. 라우트 매칭
|
|
627
690
|
const match = router.match(pathname);
|
|
628
691
|
|
|
629
|
-
if (!match) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
isDev: process.env.NODE_ENV !== "production",
|
|
633
|
-
});
|
|
634
|
-
return Response.json(response, { status: 404 });
|
|
635
|
-
}
|
|
692
|
+
if (!match) {
|
|
693
|
+
return err(createNotFoundResponse(pathname));
|
|
694
|
+
}
|
|
636
695
|
|
|
637
696
|
const { route, params } = match;
|
|
638
697
|
|
|
639
|
-
if (route.kind === "api") {
|
|
640
|
-
const handler = registry.apiHandlers.get(route.id);
|
|
641
|
-
if (!handler) {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
return
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
698
|
+
if (route.kind === "api") {
|
|
699
|
+
const handler = registry.apiHandlers.get(route.id);
|
|
700
|
+
if (!handler) {
|
|
701
|
+
return err(createHandlerNotFoundResponse(route.id, route.pattern));
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const response = await handler(req, params);
|
|
705
|
+
return ok(response);
|
|
706
|
+
} catch (errValue) {
|
|
707
|
+
const error = errValue instanceof Error ? errValue : new Error(String(errValue));
|
|
708
|
+
return err(createSSRErrorResponse(route.id, route.pattern, error));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
650
711
|
|
|
651
712
|
if (route.kind === "page") {
|
|
652
713
|
let loaderData: unknown;
|
|
@@ -657,30 +718,27 @@ async function handleRequest(req: Request, router: Router, registry: ServerRegis
|
|
|
657
718
|
|
|
658
719
|
// 1. PageHandler 방식 (신규 - filling 포함)
|
|
659
720
|
const pageHandler = registry.pageHandlers.get(route.id);
|
|
660
|
-
if (pageHandler) {
|
|
661
|
-
try {
|
|
662
|
-
const registration = await pageHandler();
|
|
663
|
-
component = registration.component as RouteComponent;
|
|
664
|
-
registry.registerRouteComponent(route.id, component);
|
|
721
|
+
if (pageHandler) {
|
|
722
|
+
try {
|
|
723
|
+
const registration = await pageHandler();
|
|
724
|
+
component = registration.component as RouteComponent;
|
|
725
|
+
registry.registerRouteComponent(route.id, component);
|
|
665
726
|
|
|
666
727
|
// Filling의 loader 실행
|
|
667
728
|
if (registration.filling?.hasLoader()) {
|
|
668
729
|
const ctx = new ManduContext(req, params);
|
|
669
730
|
loaderData = await registration.filling.executeLoader(ctx);
|
|
670
731
|
}
|
|
671
|
-
} catch (
|
|
672
|
-
const pageError = createPageLoadErrorResponse(
|
|
673
|
-
route.id,
|
|
674
|
-
route.pattern,
|
|
675
|
-
|
|
676
|
-
);
|
|
677
|
-
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return Response.json(response, { status: 500 });
|
|
682
|
-
}
|
|
683
|
-
}
|
|
732
|
+
} catch (error) {
|
|
733
|
+
const pageError = createPageLoadErrorResponse(
|
|
734
|
+
route.id,
|
|
735
|
+
route.pattern,
|
|
736
|
+
error instanceof Error ? error : new Error(String(error))
|
|
737
|
+
);
|
|
738
|
+
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
739
|
+
return err(pageError);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
684
742
|
// 2. PageLoader 방식 (레거시 호환)
|
|
685
743
|
else {
|
|
686
744
|
const loader = registry.pageLoaders.get(route.id);
|
|
@@ -700,31 +758,28 @@ async function handleRequest(req: Request, router: Router, registry: ServerRegis
|
|
|
700
758
|
const ctx = new ManduContext(req, params);
|
|
701
759
|
loaderData = await filling.executeLoader(ctx);
|
|
702
760
|
}
|
|
703
|
-
} catch (
|
|
704
|
-
const pageError = createPageLoadErrorResponse(
|
|
705
|
-
route.id,
|
|
706
|
-
route.pattern,
|
|
707
|
-
|
|
708
|
-
);
|
|
709
|
-
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
const pageError = createPageLoadErrorResponse(
|
|
763
|
+
route.id,
|
|
764
|
+
route.pattern,
|
|
765
|
+
error instanceof Error ? error : new Error(String(error))
|
|
766
|
+
);
|
|
767
|
+
console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
|
|
768
|
+
return err(pageError);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
717
772
|
|
|
718
773
|
// Client-side Routing: 데이터만 반환 (JSON)
|
|
719
|
-
if (isDataRequest) {
|
|
720
|
-
return Response.json({
|
|
721
|
-
routeId: route.id,
|
|
722
|
-
pattern: route.pattern,
|
|
723
|
-
params,
|
|
724
|
-
loaderData: loaderData ?? null,
|
|
725
|
-
timestamp: Date.now(),
|
|
726
|
-
});
|
|
727
|
-
}
|
|
774
|
+
if (isDataRequest) {
|
|
775
|
+
return ok(Response.json({
|
|
776
|
+
routeId: route.id,
|
|
777
|
+
pattern: route.pattern,
|
|
778
|
+
params,
|
|
779
|
+
loaderData: loaderData ?? null,
|
|
780
|
+
timestamp: Date.now(),
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
728
783
|
|
|
729
784
|
// SSR 렌더링
|
|
730
785
|
const defaultAppCreator = createDefaultAppFactory(registry);
|
|
@@ -753,12 +808,12 @@ async function handleRequest(req: Request, router: Router, registry: ServerRegis
|
|
|
753
808
|
? route.streaming
|
|
754
809
|
: settings.streaming;
|
|
755
810
|
|
|
756
|
-
if (useStreaming) {
|
|
757
|
-
return await renderStreamingResponse(app, {
|
|
758
|
-
title: `${route.id} - Mandu`,
|
|
759
|
-
isDev: settings.isDev,
|
|
760
|
-
hmrPort: settings.hmrPort,
|
|
761
|
-
routeId: route.id,
|
|
811
|
+
if (useStreaming) {
|
|
812
|
+
return ok(await renderStreamingResponse(app, {
|
|
813
|
+
title: `${route.id} - Mandu`,
|
|
814
|
+
isDev: settings.isDev,
|
|
815
|
+
hmrPort: settings.hmrPort,
|
|
816
|
+
routeId: route.id,
|
|
762
817
|
routePattern: route.pattern,
|
|
763
818
|
hydration: route.hydration,
|
|
764
819
|
bundleManifest: settings.bundleManifest,
|
|
@@ -776,63 +831,61 @@ async function handleRequest(req: Request, router: Router, registry: ServerRegis
|
|
|
776
831
|
allReadyTime: `${metrics.allReadyTime}ms`,
|
|
777
832
|
hasError: metrics.hasError,
|
|
778
833
|
});
|
|
779
|
-
}
|
|
780
|
-
},
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// 기존 renderToString 방식
|
|
785
|
-
return renderSSR(app, {
|
|
786
|
-
title: `${route.id} - Mandu`,
|
|
787
|
-
isDev: settings.isDev,
|
|
788
|
-
hmrPort: settings.hmrPort,
|
|
789
|
-
routeId: route.id,
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
}));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 기존 renderToString 방식
|
|
840
|
+
return ok(renderSSR(app, {
|
|
841
|
+
title: `${route.id} - Mandu`,
|
|
842
|
+
isDev: settings.isDev,
|
|
843
|
+
hmrPort: settings.hmrPort,
|
|
844
|
+
routeId: route.id,
|
|
790
845
|
hydration: route.hydration,
|
|
791
846
|
bundleManifest: settings.bundleManifest,
|
|
792
847
|
serverData,
|
|
793
|
-
// Client-side Routing 활성화 정보 전달
|
|
794
|
-
enableClientRouter: true,
|
|
795
|
-
routePattern: route.pattern,
|
|
796
|
-
});
|
|
797
|
-
} catch (
|
|
798
|
-
const ssrError = createSSRErrorResponse(
|
|
799
|
-
route.id,
|
|
800
|
-
route.pattern,
|
|
801
|
-
|
|
802
|
-
);
|
|
803
|
-
console.error(`[Mandu] ${ssrError.errorType}:`, ssrError.message);
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
}, { status: 500 });
|
|
826
|
-
}
|
|
848
|
+
// Client-side Routing 활성화 정보 전달
|
|
849
|
+
enableClientRouter: true,
|
|
850
|
+
routePattern: route.pattern,
|
|
851
|
+
}));
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const ssrError = createSSRErrorResponse(
|
|
854
|
+
route.id,
|
|
855
|
+
route.pattern,
|
|
856
|
+
error instanceof Error ? error : new Error(String(error))
|
|
857
|
+
);
|
|
858
|
+
console.error(`[Mandu] ${ssrError.errorType}:`, ssrError.message);
|
|
859
|
+
return err(ssrError);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return err({
|
|
864
|
+
errorType: "FRAMEWORK_BUG",
|
|
865
|
+
code: "MANDU_F003",
|
|
866
|
+
httpStatus: 500,
|
|
867
|
+
message: `Unknown route kind: ${route.kind}`,
|
|
868
|
+
summary: "알 수 없는 라우트 종류 - 프레임워크 버그",
|
|
869
|
+
fix: {
|
|
870
|
+
file: "spec/routes.manifest.json",
|
|
871
|
+
suggestion: "라우트의 kind는 'api' 또는 'page'여야 합니다",
|
|
872
|
+
},
|
|
873
|
+
route: {
|
|
874
|
+
id: route.id,
|
|
875
|
+
pattern: route.pattern,
|
|
876
|
+
},
|
|
877
|
+
timestamp: new Date().toISOString(),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
827
880
|
|
|
828
881
|
// ========== Server Startup ==========
|
|
829
882
|
|
|
830
883
|
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
831
|
-
const {
|
|
832
|
-
port = 3000,
|
|
833
|
-
hostname = "localhost",
|
|
834
|
-
rootDir = process.cwd(),
|
|
835
|
-
isDev = false,
|
|
884
|
+
const {
|
|
885
|
+
port = 3000,
|
|
886
|
+
hostname = "localhost",
|
|
887
|
+
rootDir = process.cwd(),
|
|
888
|
+
isDev = false,
|
|
836
889
|
hmrPort,
|
|
837
890
|
bundleManifest,
|
|
838
891
|
publicDir = "public",
|
|
@@ -841,8 +894,15 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
841
894
|
registry = defaultRegistry,
|
|
842
895
|
} = options;
|
|
843
896
|
|
|
844
|
-
// CORS 옵션 파싱
|
|
845
|
-
const corsOptions: CorsOptions | false = cors === true ? {} : cors;
|
|
897
|
+
// CORS 옵션 파싱
|
|
898
|
+
const corsOptions: CorsOptions | false = cors === true ? {} : cors;
|
|
899
|
+
|
|
900
|
+
if (!isDev && cors === true) {
|
|
901
|
+
console.warn("⚠️ [Security Warning] CORS is set to allow all origins.");
|
|
902
|
+
console.warn(" This is not recommended for production environments.");
|
|
903
|
+
console.warn(" Consider specifying allowed origins explicitly:");
|
|
904
|
+
console.warn(" cors: { origin: ['https://yourdomain.com'] }");
|
|
905
|
+
}
|
|
846
906
|
|
|
847
907
|
// Registry settings 저장
|
|
848
908
|
registry.settings = {
|
|
@@ -876,10 +936,10 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
876
936
|
});
|
|
877
937
|
|
|
878
938
|
if (isDev) {
|
|
879
|
-
console.log(`🥟 Mandu Dev Server running at http://${hostname}:${port}`);
|
|
880
|
-
if (hmrPort) {
|
|
881
|
-
console.log(`🔥 HMR enabled on port ${hmrPort +
|
|
882
|
-
}
|
|
939
|
+
console.log(`🥟 Mandu Dev Server running at http://${hostname}:${port}`);
|
|
940
|
+
if (hmrPort) {
|
|
941
|
+
console.log(`🔥 HMR enabled on port ${hmrPort + PORTS.HMR_OFFSET}`);
|
|
942
|
+
}
|
|
883
943
|
console.log(`📂 Static files: /${publicDir}/, /.mandu/client/`);
|
|
884
944
|
if (corsOptions) {
|
|
885
945
|
console.log(`🌐 CORS enabled`);
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { serializeProps } from "../client/serialize";
|
|
|
3
3
|
import type { ReactElement } from "react";
|
|
4
4
|
import type { BundleManifest } from "../bundler/types";
|
|
5
5
|
import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
6
|
+
import { PORTS, TIMEOUTS } from "../constants";
|
|
6
7
|
|
|
7
8
|
// Re-export streaming SSR utilities
|
|
8
9
|
export {
|
|
@@ -270,12 +271,12 @@ function generateClientRouterScript(manifest: BundleManifest): string {
|
|
|
270
271
|
* HMR 스크립트 생성
|
|
271
272
|
*/
|
|
272
273
|
function generateHMRScript(port: number): string {
|
|
273
|
-
const hmrPort = port +
|
|
274
|
+
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
274
275
|
return `<script>
|
|
275
276
|
(function() {
|
|
276
277
|
var ws = null;
|
|
277
278
|
var reconnectAttempts = 0;
|
|
278
|
-
var maxReconnectAttempts =
|
|
279
|
+
var maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
|
|
279
280
|
|
|
280
281
|
function connect() {
|
|
281
282
|
try {
|
|
@@ -298,11 +299,11 @@ function generateHMRScript(port: number): string {
|
|
|
298
299
|
ws.onclose = function() {
|
|
299
300
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
300
301
|
reconnectAttempts++;
|
|
301
|
-
setTimeout(connect,
|
|
302
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
|
|
302
303
|
}
|
|
303
304
|
};
|
|
304
305
|
} catch(err) {
|
|
305
|
-
setTimeout(connect,
|
|
306
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
309
|
connect();
|
|
@@ -19,6 +19,7 @@ import type { HydrationConfig, HydrationPriority } from "../spec/schema";
|
|
|
19
19
|
import { serializeProps } from "../client/serialize";
|
|
20
20
|
import type { Metadata, MetadataItem } from "../seo/types";
|
|
21
21
|
import { injectSEOIntoOptions, resolveSEO, type SEOOptions } from "../seo/integration/ssr";
|
|
22
|
+
import { PORTS, TIMEOUTS } from "../constants";
|
|
22
23
|
|
|
23
24
|
// ========== Types ==========
|
|
24
25
|
|
|
@@ -564,12 +565,12 @@ function generateDeferredDataScript(routeId: string, key: string, data: unknown)
|
|
|
564
565
|
* HMR 스크립트 생성
|
|
565
566
|
*/
|
|
566
567
|
function generateHMRScript(port: number): string {
|
|
567
|
-
const hmrPort = port +
|
|
568
|
+
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
568
569
|
return `<script>
|
|
569
570
|
(function() {
|
|
570
571
|
var ws = null;
|
|
571
572
|
var reconnectAttempts = 0;
|
|
572
|
-
var maxReconnectAttempts =
|
|
573
|
+
var maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
|
|
573
574
|
|
|
574
575
|
function connect() {
|
|
575
576
|
try {
|
|
@@ -590,11 +591,11 @@ function generateHMRScript(port: number): string {
|
|
|
590
591
|
ws.onclose = function() {
|
|
591
592
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
592
593
|
reconnectAttempts++;
|
|
593
|
-
setTimeout(connect,
|
|
594
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
|
|
594
595
|
}
|
|
595
596
|
};
|
|
596
597
|
} catch(err) {
|
|
597
|
-
setTimeout(connect,
|
|
598
|
+
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
|
|
598
599
|
}
|
|
599
600
|
}
|
|
600
601
|
connect();
|
package/src/utils/bun.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function readJsonFile(filePath: string): Promise<unknown> {
|
|
2
|
+
try {
|
|
3
|
+
return await Bun.file(filePath).json();
|
|
4
|
+
} catch (error) {
|
|
5
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
+
throw new Error(`Failed to parse JSON in ${filePath}: ${message}`);
|
|
7
|
+
}
|
|
8
|
+
}
|