@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.
Files changed (67) hide show
  1. package/package.json +1 -1
  2. package/src/bundler/build.ts +91 -73
  3. package/src/bundler/dev.ts +21 -14
  4. package/src/client/globals.ts +44 -0
  5. package/src/client/index.ts +5 -4
  6. package/src/client/island.ts +8 -13
  7. package/src/client/router.ts +33 -41
  8. package/src/client/runtime.ts +23 -51
  9. package/src/client/window-state.ts +101 -0
  10. package/src/config/index.ts +1 -0
  11. package/src/config/mandu.ts +45 -9
  12. package/src/config/validate.ts +158 -0
  13. package/src/constants.ts +25 -0
  14. package/src/contract/client.ts +4 -3
  15. package/src/contract/define.ts +459 -0
  16. package/src/devtools/ai/context-builder.ts +375 -0
  17. package/src/devtools/ai/index.ts +25 -0
  18. package/src/devtools/ai/mcp-connector.ts +465 -0
  19. package/src/devtools/client/catchers/error-catcher.ts +327 -0
  20. package/src/devtools/client/catchers/index.ts +18 -0
  21. package/src/devtools/client/catchers/network-proxy.ts +363 -0
  22. package/src/devtools/client/components/index.ts +39 -0
  23. package/src/devtools/client/components/kitchen-root.tsx +362 -0
  24. package/src/devtools/client/components/mandu-character.tsx +241 -0
  25. package/src/devtools/client/components/overlay.tsx +368 -0
  26. package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
  27. package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
  28. package/src/devtools/client/components/panel/index.ts +32 -0
  29. package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
  30. package/src/devtools/client/components/panel/network-panel.tsx +292 -0
  31. package/src/devtools/client/components/panel/panel-container.tsx +259 -0
  32. package/src/devtools/client/filters/context-filters.ts +282 -0
  33. package/src/devtools/client/filters/index.ts +16 -0
  34. package/src/devtools/client/index.ts +63 -0
  35. package/src/devtools/client/persistence.ts +335 -0
  36. package/src/devtools/client/state-manager.ts +478 -0
  37. package/src/devtools/design-tokens.ts +263 -0
  38. package/src/devtools/hook/create-hook.ts +207 -0
  39. package/src/devtools/hook/index.ts +13 -0
  40. package/src/devtools/index.ts +439 -0
  41. package/src/devtools/init.ts +266 -0
  42. package/src/devtools/protocol.ts +237 -0
  43. package/src/devtools/server/index.ts +17 -0
  44. package/src/devtools/server/source-context.ts +444 -0
  45. package/src/devtools/types.ts +319 -0
  46. package/src/devtools/worker/index.ts +25 -0
  47. package/src/devtools/worker/redaction-worker.ts +222 -0
  48. package/src/devtools/worker/worker-manager.ts +409 -0
  49. package/src/error/formatter.ts +28 -24
  50. package/src/error/index.ts +13 -9
  51. package/src/error/result.ts +46 -0
  52. package/src/error/types.ts +6 -4
  53. package/src/filling/filling.ts +6 -5
  54. package/src/guard/check.ts +60 -56
  55. package/src/guard/types.ts +3 -1
  56. package/src/guard/watcher.ts +10 -1
  57. package/src/index.ts +81 -0
  58. package/src/intent/index.ts +310 -0
  59. package/src/island/index.ts +304 -0
  60. package/src/router/fs-patterns.ts +7 -0
  61. package/src/router/fs-routes.ts +20 -8
  62. package/src/router/fs-scanner.ts +117 -133
  63. package/src/runtime/server.ts +261 -201
  64. package/src/runtime/ssr.ts +5 -4
  65. package/src/runtime/streaming-ssr.ts +5 -4
  66. package/src/utils/bun.ts +8 -0
  67. package/src/utils/lru-cache.ts +75 -0
@@ -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
- formatErrorResponse,
13
- createNotFoundResponse,
14
- createHandlerNotFoundResponse,
15
- createPageLoadErrorResponse,
16
- createSSRErrorResponse,
17
- } from "../error";
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
- const resolvedPath = path.resolve(filePath);
509
- const resolvedAllowedDir = path.resolve(allowedDir);
510
- // 경로가 허용된 디렉토리로 시작하는지 확인 (디렉토리 구분자 포함)
511
- return resolvedPath.startsWith(resolvedAllowedDir + path.sep) ||
512
- resolvedPath === resolvedAllowedDir;
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
- // Path traversal 시도 조기 차단 (정규화 전 raw 체크)
529
- if (pathname.includes("..")) {
530
- return null;
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
- const relativePath = pathname.slice("/.mandu/client/".length);
537
- allowedBaseDir = path.join(settings.rootDir, ".mandu", "client");
538
- filePath = path.join(allowedBaseDir, relativePath);
539
- isBundleFile = true;
540
- }
541
- // 2. Public 폴더 파일 (/public/*)
542
- else if (pathname.startsWith("/public/")) {
543
- const relativePath = pathname.slice("/public/".length);
544
- allowedBaseDir = path.join(settings.rootDir, "public");
545
- filePath = path.join(allowedBaseDir, relativePath);
546
- }
547
- // 3. Public 폴더의 루트 파일 (favicon.ico, robots.txt 등)
548
- else if (
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
- const filename = path.basename(pathname);
556
- allowedBaseDir = path.join(settings.rootDir, settings.publicDir);
557
- filePath = path.join(allowedBaseDir, filename);
558
- } else {
559
- return null; // 정적 파일이 아님
560
- }
561
-
562
- // 최종 경로 검증: 허용된 디렉토리 내에 있는지 확인
563
- if (!isPathSafe(filePath, allowedBaseDir!)) {
564
- console.warn(`[Mandu Security] Path traversal attempt blocked: ${pathname}`);
565
- return null;
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 url = new URL(req.url);
606
- const pathname = url.pathname;
607
- const settings = registry.settings;
608
-
609
- // 0. CORS Preflight 요청 처리
610
- if (settings.cors && isPreflightRequest(req)) {
611
- const corsOptions = settings.cors === true ? {} : settings.cors;
612
- return handlePreflightRequest(req, corsOptions);
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
- const error = createNotFoundResponse(pathname);
631
- const response = formatErrorResponse(error, {
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
- const error = createHandlerNotFoundResponse(route.id, route.pattern);
643
- const response = formatErrorResponse(error, {
644
- isDev: process.env.NODE_ENV !== "production",
645
- });
646
- return Response.json(response, { status: 500 });
647
- }
648
- return handler(req, params);
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 (err) {
672
- const pageError = createPageLoadErrorResponse(
673
- route.id,
674
- route.pattern,
675
- err instanceof Error ? err : new Error(String(err))
676
- );
677
- console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
678
- const response = formatErrorResponse(pageError, {
679
- isDev: process.env.NODE_ENV !== "production",
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 (err) {
704
- const pageError = createPageLoadErrorResponse(
705
- route.id,
706
- route.pattern,
707
- err instanceof Error ? err : new Error(String(err))
708
- );
709
- console.error(`[Mandu] ${pageError.errorType}:`, pageError.message);
710
- const response = formatErrorResponse(pageError, {
711
- isDev: process.env.NODE_ENV !== "production",
712
- });
713
- return Response.json(response, { status: 500 });
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 (err) {
798
- const ssrError = createSSRErrorResponse(
799
- route.id,
800
- route.pattern,
801
- err instanceof Error ? err : new Error(String(err))
802
- );
803
- console.error(`[Mandu] ${ssrError.errorType}:`, ssrError.message);
804
- const response = formatErrorResponse(ssrError, {
805
- isDev: process.env.NODE_ENV !== "production",
806
- });
807
- return Response.json(response, { status: 500 });
808
- }
809
- }
810
-
811
- return Response.json({
812
- errorType: "FRAMEWORK_BUG",
813
- code: "MANDU_F003",
814
- message: `Unknown route kind: ${route.kind}`,
815
- summary: "알 수 없는 라우트 종류 - 프레임워크 버그",
816
- fix: {
817
- file: "spec/routes.manifest.json",
818
- suggestion: "라우트의 kind는 'api' 또는 'page'여야 합니다",
819
- },
820
- route: {
821
- id: route.id,
822
- pattern: route.pattern,
823
- },
824
- timestamp: new Date().toISOString(),
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 + 1}`);
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`);
@@ -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 + 1;
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 = 10;
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, 1000 * reconnectAttempts);
302
+ setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
302
303
  }
303
304
  };
304
305
  } catch(err) {
305
- setTimeout(connect, 1000);
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 + 1;
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 = 10;
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, 1000 * reconnectAttempts);
594
+ setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
594
595
  }
595
596
  };
596
597
  } catch(err) {
597
- setTimeout(connect, 1000);
598
+ setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
598
599
  }
599
600
  }
600
601
  connect();
@@ -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
+ }