@mandujs/core 0.54.18 → 0.54.20

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.
@@ -514,651 +514,15 @@ function formatShimBindings(names: readonly string[], indent = " "): string {
514
514
  return names.map((name) => `${indent}${name},`).join("\n");
515
515
  }
516
516
 
517
- /**
518
- * Runtime 번들 소스 생성 (v0.8.0 재설계)
519
- *
520
- * 설계 원칙:
521
- * - 글로벌 레지스트리 없음 (Island가 스스로 등록 안함)
522
- * - Runtime이 Island를 dynamic import()로 로드
523
- * - HTML의 data-mandu-src 속성에서 번들 URL 읽기
524
- * - 실행 순서 문제 완전 해결
525
- */
526
- function generateRuntimeSource(): string {
527
- return `
528
- /**
529
- * Mandu Hydration Runtime v0.9.0 (Generated)
530
- * Fresh-style dynamic import architecture
531
- * + Error Boundary & Loading fallback support
532
- */
533
-
534
- // React 정적 import (Island와 같은 인스턴스 공유)
535
- import React, { useState, useEffect, Component } from 'react';
536
- import { hydrateRoot, createRoot } from 'react-dom/client';
537
-
538
- // Hydrated roots 추적 (unmount용) - 전역 초기화
539
- window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
540
- const hydratedRoots = window.__MANDU_ROOTS__;
541
- const warnedBoundaryPropFallbacks = new Set();
542
-
543
- const TYPE_MARKERS = {
544
- UNDEFINED: "\\u0000_",
545
- DATE: "\\u0000D",
546
- URL: "\\u0000U",
547
- REGEXP: "\\u0000R",
548
- MAP: "\\u0000M",
549
- SET: "\\u0000S",
550
- REF: "\\u0000$",
551
- BIGINT: "\\u0000B",
552
- SYMBOL: "\\u0000Y",
553
- ERROR: "\\u0000E",
554
- };
555
-
556
- function deserializeManduProps(json) {
557
- const ctx = { refs: [] };
558
- return deserializeManduValue(JSON.parse(json), ctx);
559
- }
560
-
561
- function deserializeManduValue(value, ctx) {
562
- if (value === null) return null;
563
- if (typeof value === 'string') {
564
- if (value === TYPE_MARKERS.UNDEFINED) return undefined;
565
- if (value.startsWith("\\u0000\\u0000")) return value.slice(2);
566
- if (value.startsWith(TYPE_MARKERS.DATE)) return new Date(value.slice(2));
567
- if (value.startsWith(TYPE_MARKERS.URL)) return new URL(value.slice(2));
568
- if (value.startsWith(TYPE_MARKERS.REGEXP)) {
569
- const str = value.slice(2);
570
- const match = str.match(/^\\/(.*)\\/([gimsuy]*)$/);
571
- return match ? new RegExp(match[1], match[2]) : str;
572
- }
573
- if (value.startsWith(TYPE_MARKERS.BIGINT)) return BigInt(value.slice(2));
574
- if (value.startsWith(TYPE_MARKERS.SYMBOL)) return Symbol(value.slice(2));
575
- if (value.startsWith(TYPE_MARKERS.REF)) return ctx.refs[parseInt(value.slice(2), 10)];
576
- return value;
577
- }
578
- if (typeof value === 'boolean' || typeof value === 'number') return value;
579
- if (Array.isArray(value)) {
580
- const marker = value[0];
581
- if (marker === TYPE_MARKERS.ERROR) {
582
- const error = new Error(value[2]);
583
- error.name = value[1];
584
- if (value[3]) error.stack = value[3];
585
- ctx.refs.push(error);
586
- return error;
587
- }
588
- if (marker === TYPE_MARKERS.MAP) {
589
- const map = new Map();
590
- ctx.refs.push(map);
591
- for (let i = 1; i < value.length; i++) {
592
- const entry = value[i];
593
- map.set(deserializeManduValue(entry[0], ctx), deserializeManduValue(entry[1], ctx));
594
- }
595
- return map;
596
- }
597
- if (marker === TYPE_MARKERS.SET) {
598
- const set = new Set();
599
- ctx.refs.push(set);
600
- for (let i = 1; i < value.length; i++) {
601
- set.add(deserializeManduValue(value[i], ctx));
602
- }
603
- return set;
604
- }
605
- const arr = [];
606
- ctx.refs.push(arr);
607
- for (const item of value) arr.push(deserializeManduValue(item, ctx));
608
- return arr;
609
- }
610
- if (typeof value === 'object') {
611
- const obj = {};
612
- ctx.refs.push(obj);
613
- for (const [key, nested] of Object.entries(value)) {
614
- obj[key] = deserializeManduValue(nested, ctx);
615
- }
616
- return obj;
617
- }
618
- return value;
619
- }
620
-
621
- // 서버 데이터
622
- function readManduData() {
623
- if (window.__MANDU_DATA__) return window.__MANDU_DATA__;
624
-
625
- const raw = window.__MANDU_DATA_RAW__ || document.getElementById('__MANDU_DATA__')?.textContent;
626
- if (!raw) {
627
- window.__MANDU_DATA__ = {};
628
- return window.__MANDU_DATA__;
629
- }
630
-
631
- try {
632
- window.__MANDU_DATA__ = deserializeManduProps(raw);
633
- } catch (error) {
634
- console.warn('[Mandu] Failed to parse server data:', error);
635
- window.__MANDU_DATA__ = {};
636
- }
637
-
638
- return window.__MANDU_DATA__;
639
- }
640
-
641
- const getServerData = (id, element) => {
642
- const data = readManduData();
643
- if (data[id] && Object.prototype.hasOwnProperty.call(data[id], 'serverData')) {
644
- return data[id].serverData;
645
- }
646
- const routeId = element?.getAttribute?.('data-mandu-route-id');
647
- if (
648
- routeId &&
649
- data[routeId] &&
650
- Object.prototype.hasOwnProperty.call(data[routeId], 'serverData')
651
- ) {
652
- return data[routeId].serverData;
653
- }
654
- return {};
655
- };
656
-
657
- function findPropsScript(id) {
658
- const scripts = document.querySelectorAll('script[data-mandu-props]');
659
- for (const script of scripts) {
660
- if (script.getAttribute('data-mandu-props') === id) {
661
- return script;
662
- }
663
- }
664
- return null;
665
- }
666
-
667
- function parsePropsScript(id) {
668
- const script = findPropsScript(id);
669
- if (!script || !script.textContent) return null;
670
- try {
671
- return deserializeManduProps(script.textContent);
672
- } catch (error) {
673
- console.warn('[Mandu] Failed to parse data-mandu-props for island ' + id + ':', error);
674
- return null;
675
- }
676
- }
677
-
678
- function readDataProps(element) {
679
- const propsEl = element.hasAttribute('data-props')
680
- ? element
681
- : element.querySelector('[data-props]');
682
- if (!propsEl) return null;
683
- try {
684
- return deserializeManduProps(propsEl.getAttribute('data-props') || '{}');
685
- } catch (error) {
686
- console.warn('[Mandu] Failed to parse data-props fallback:', error);
687
- return null;
688
- }
689
- }
690
-
691
- function getIslandProps(id, element) {
692
- const inlineProps = parsePropsScript(id);
693
- if (inlineProps) return inlineProps;
694
-
695
- const dataProps = readDataProps(element);
696
- if (dataProps) return dataProps;
697
-
698
- const boundaryId = element?.getAttribute?.('data-mandu-boundary-id');
699
- if (boundaryId && !warnedBoundaryPropFallbacks.has(id)) {
700
- warnedBoundaryPropFallbacks.add(id);
701
- console.warn(
702
- '[Mandu] Missing boundary-local props for transformed client boundary ' +
703
- boundaryId +
704
- '; falling back to route server data.'
705
- );
706
- }
707
-
708
- return getServerData(id, element);
709
- }
710
-
711
- function resolveIslandExport(module, element) {
712
- const exportName = element.getAttribute('data-mandu-client-export');
713
- if (exportName) {
714
- if (exportName === 'default' && module.default) return module.default;
715
- if (exportName !== 'default' && module[exportName]) return module[exportName];
716
- console.warn('[Mandu] Client boundary export "' + exportName + '" was not found; falling back to default export.');
717
- }
718
- return module.default;
719
- }
720
-
721
517
  /**
722
- * Error Boundary 컴포넌트 (Class Component)
723
- * Island의 errorBoundary 옵션을 지원
724
- */
725
- class IslandErrorBoundary extends Component {
726
- constructor(props) {
727
- super(props);
728
- this.state = { hasError: false, error: null };
729
- }
730
-
731
- static getDerivedStateFromError(error) {
732
- return { hasError: true, error };
733
- }
734
-
735
- componentDidCatch(error, errorInfo) {
736
- console.error('[Mandu] Island error:', this.props.islandId, error, errorInfo);
737
- }
738
-
739
- reset = () => {
740
- this.setState({ hasError: false, error: null });
741
- };
742
-
743
- render() {
744
- if (this.state.hasError) {
745
- // 커스텀 errorBoundary가 있으면 사용
746
- if (this.props.errorBoundary) {
747
- return this.props.errorBoundary(this.state.error, this.reset);
748
- }
749
- // 기본 에러 UI
750
- return React.createElement('div', {
751
- className: 'mandu-island-error',
752
- style: {
753
- padding: '16px',
754
- background: '#fef2f2',
755
- border: '1px solid #fecaca',
756
- borderRadius: '8px',
757
- color: '#dc2626',
758
- }
759
- }, [
760
- React.createElement('strong', { key: 'title' }, '⚠️ 오류 발생'),
761
- React.createElement('p', { key: 'msg', style: { margin: '8px 0', fontSize: '14px' } },
762
- this.state.error?.message || '알 수 없는 오류'
763
- ),
764
- React.createElement('button', {
765
- key: 'btn',
766
- onClick: this.reset,
767
- style: {
768
- padding: '6px 12px',
769
- background: '#dc2626',
770
- color: 'white',
771
- border: 'none',
772
- borderRadius: '4px',
773
- cursor: 'pointer',
774
- }
775
- }, '다시 시도')
776
- ]);
777
- }
778
- return this.props.children;
779
- }
780
- }
781
-
782
- /**
783
- * Loading Wrapper 컴포넌트
784
- * Island의 loading 옵션을 지원
785
- */
786
- function IslandLoadingWrapper({ children, loading, isReady }) {
787
- if (!isReady && loading) {
788
- return loading();
789
- }
790
- return children;
791
- }
792
-
793
- function resolveHydrationTarget(element) {
794
- if (!(element instanceof HTMLElement)) {
795
- return element;
796
- }
797
-
798
- if (getComputedStyle(element).display !== 'contents') {
799
- return element;
800
- }
801
-
802
- const queue = Array.from(element.children);
803
- while (queue.length > 0) {
804
- const candidate = queue.shift();
805
- if (candidate instanceof HTMLElement) {
806
- return candidate;
807
- }
808
- if (candidate) {
809
- queue.push(...candidate.children);
810
- }
811
- }
812
-
813
- return element.parentElement || element;
814
- }
815
-
816
- function hasHydratableMarkup(element) {
817
- for (const node of element.childNodes) {
818
- if (node.nodeType === Node.ELEMENT_NODE) {
819
- return true;
820
- }
821
-
822
- if (node.nodeType === Node.TEXT_NODE && node.textContent && node.textContent.trim() !== '') {
823
- return true;
824
- }
825
- }
826
-
827
- return false;
828
- }
829
-
830
- function shouldHydrateCompiledIsland(element) {
831
- return (
832
- element.getAttribute('data-mandu-loading') !== 'true' &&
833
- hasHydratableMarkup(element)
834
- );
835
- }
836
-
837
- function createHydrationOptions(element, id, mode) {
838
- return {
839
- onRecoverableError(error) {
840
- element.setAttribute('data-mandu-recoverable-error', 'true');
841
- console.warn('[Mandu] Recoverable hydration error:', id, mode, error);
842
- element.dispatchEvent(new CustomEvent('mandu:recoverable-hydration-error', {
843
- bubbles: true,
844
- detail: {
845
- id,
846
- mode,
847
- error: error instanceof Error ? error.message : String(error),
848
- },
849
- }));
850
- },
851
- };
852
- }
853
-
854
- /**
855
- * Hydration 스케줄러
856
- */
857
- function priorityToHydrateStrategy(priority) {
858
- return priority === 'immediate' ? 'load' : priority;
859
- }
860
-
861
- function scheduleHydration(element, src, strategy) {
862
- if (!strategy) strategy = 'load';
863
- if (strategy === 'immediate') strategy = 'load';
864
-
865
- if (strategy.startsWith('media(') && strategy.endsWith(')')) {
866
- const query = strategy.slice('media('.length, -1).trim();
867
- if (!query || !window.matchMedia) {
868
- loadAndHydrate(element, src);
869
- return;
870
- }
871
- const mql = window.matchMedia(query);
872
- if (mql.matches) {
873
- loadAndHydrate(element, src);
874
- return;
875
- }
876
- const onChange = (event) => {
877
- if (!event.matches) return;
878
- if (mql.removeEventListener) {
879
- mql.removeEventListener('change', onChange);
880
- } else if (mql.removeListener) {
881
- mql.removeListener(onChange);
882
- }
883
- loadAndHydrate(element, src);
884
- };
885
- if (mql.addEventListener) {
886
- mql.addEventListener('change', onChange);
887
- } else if (mql.addListener) {
888
- mql.addListener(onChange);
889
- }
890
- return;
891
- }
892
-
893
- switch (strategy) {
894
- case 'load':
895
- case 'immediate':
896
- loadAndHydrate(element, src);
897
- break;
898
-
899
- case 'visible':
900
- if ('IntersectionObserver' in window) {
901
- const observer = new IntersectionObserver((entries) => {
902
- if (entries[0].isIntersecting) {
903
- observer.disconnect();
904
- loadAndHydrate(element, src);
905
- }
906
- }, { rootMargin: '200px' });
907
- const target = resolveHydrationTarget(element);
908
- observer.observe(target);
909
- } else {
910
- loadAndHydrate(element, src);
911
- }
912
- break;
913
-
914
- case 'idle':
915
- if ('requestIdleCallback' in window) {
916
- requestIdleCallback(() => loadAndHydrate(element, src));
917
- } else {
918
- setTimeout(() => loadAndHydrate(element, src), 200);
919
- }
920
- break;
921
-
922
- case 'interaction': {
923
- const target = resolveHydrationTarget(element);
924
- const hydrate = () => {
925
- target.removeEventListener('touchstart', hydrate);
926
- target.removeEventListener('click', hydrate);
927
- target.removeEventListener('keydown', hydrate);
928
- loadAndHydrate(element, src);
929
- };
930
- target.addEventListener('touchstart', hydrate, { once: true, passive: true });
931
- target.addEventListener('click', hydrate, { once: true });
932
- target.addEventListener('keydown', hydrate, { once: true });
933
- break;
934
- }
935
-
936
- default:
937
- console.warn('[Mandu] Unknown hydrate strategy "' + strategy + '", falling back to load.');
938
- loadAndHydrate(element, src);
939
- }
940
- }
941
-
942
- /**
943
- * Island 로드 및 hydrate (핵심 함수)
944
- * Dynamic import로 Island 모듈 로드 후 렌더링
945
- * Error Boundary 및 Loading fallback 지원
946
- */
947
- async function loadAndHydrate(element, src) {
948
- const id = element.getAttribute('data-mandu-island');
949
- if (!id) {
950
- return;
951
- }
952
-
953
- if (
954
- hydratedRoots.has(id) ||
955
- element.hasAttribute('data-mandu-hydrated') ||
956
- element.getAttribute('data-mandu-hydrating') === 'true'
957
- ) {
958
- return;
959
- }
960
-
961
- element.setAttribute('data-mandu-hydrating', 'true');
962
-
963
- try {
964
- // Dynamic import - 이 시점에 Island 모듈 로드
965
- const module = await import(src);
966
- const island = resolveIslandExport(module, element);
967
- const data = getIslandProps(id, element);
968
-
969
- // Mandu Island (preferred)
970
- if (island && island.__mandu_island === true) {
971
- const { definition } = island;
972
- const shouldHydrate = shouldHydrateCompiledIsland(element);
973
- const renderMode = shouldHydrate ? 'hydrate' : 'mount';
974
-
975
- // Island 컴포넌트 (Error Boundary + Loading 지원)
976
- function IslandComponent({ initialReady }) {
977
- const [isReady, setIsReady] = useState(initialReady);
978
-
979
- useEffect(() => {
980
- setIsReady(true);
981
- }, []);
982
-
983
- // setup 호출 및 render
984
- const setupResult = definition.setup(data);
985
- const content = definition.render(setupResult);
986
-
987
- // Loading wrapper 적용
988
- const wrappedContent = definition.loading
989
- ? React.createElement(IslandLoadingWrapper, {
990
- loading: definition.loading,
991
- isReady,
992
- }, content)
993
- : content;
994
-
995
- // Error Boundary 적용
996
- return React.createElement(IslandErrorBoundary, {
997
- islandId: id,
998
- errorBoundary: definition.errorBoundary,
999
- }, wrappedContent);
1000
- }
1001
-
1002
- const root = shouldHydrate
1003
- ? hydrateRoot(
1004
- element,
1005
- React.createElement(IslandComponent, { initialReady: true }),
1006
- createHydrationOptions(element, id, renderMode)
1007
- )
1008
- : createRoot(element);
1009
-
1010
- if (!shouldHydrate) {
1011
- root.render(React.createElement(IslandComponent, { initialReady: false }));
1012
- }
1013
-
1014
- hydratedRoots.set(id, root);
1015
-
1016
- // 완료 표시
1017
- element.setAttribute('data-mandu-render-mode', renderMode);
1018
- element.setAttribute('data-mandu-hydrated', 'true');
1019
-
1020
- // 성능 마커
1021
- if (performance.mark) {
1022
- performance.mark('mandu-hydrated-' + id);
1023
- }
1024
-
1025
- // 이벤트 발송
1026
- element.dispatchEvent(new CustomEvent('mandu:hydrated', {
1027
- bubbles: true,
1028
- detail: { id, data, mode: renderMode }
1029
- }));
1030
-
1031
- // Kitchen DevTools에 island 등록
1032
- if (window.__MANDU_DEVTOOLS_HOOK__) {
1033
- const hydrateTime = performance.now ? performance.now() : Date.now();
1034
- window.__MANDU_DEVTOOLS_HOOK__.emit({
1035
- type: 'island:register',
1036
- timestamp: Date.now(),
1037
- data: {
1038
- id,
1039
- name: id,
1040
- strategy: element.getAttribute('data-mandu-priority') || 'visible',
1041
- status: 'hydrated',
1042
- renderMode,
1043
- hydrateStartTime: hydrateTime - 10,
1044
- hydrateEndTime: hydrateTime,
1045
- propsSize: JSON.stringify(data).length,
1046
- },
1047
- });
1048
- }
1049
-
1050
- console.log('[Mandu] Hydrated:', id, '(' + renderMode + ')');
1051
- }
1052
- // Plain React component fallback (e.g. "use client" pages)
1053
- else if (typeof island === 'function' || React.isValidElement(island)) {
1054
- console.warn('[Mandu] Plain component hydration:', id);
1055
- const shouldHydrate = hasHydratableMarkup(element);
1056
- const renderMode = shouldHydrate ? 'hydrate' : 'mount';
1057
-
1058
- const root = shouldHydrate
1059
- ? (typeof island === 'function'
1060
- ? hydrateRoot(
1061
- element,
1062
- React.createElement(island, data),
1063
- createHydrationOptions(element, id, renderMode)
1064
- )
1065
- : hydrateRoot(element, island, createHydrationOptions(element, id, renderMode)))
1066
- : createRoot(element);
1067
-
1068
- if (!shouldHydrate) {
1069
- root.render(typeof island === 'function' ? React.createElement(island, data) : island);
1070
- }
1071
-
1072
- hydratedRoots.set(id, root);
1073
-
1074
- // 완료 표시
1075
- element.setAttribute('data-mandu-render-mode', renderMode);
1076
- element.setAttribute('data-mandu-hydrated', 'true');
1077
-
1078
- // 성능 마커
1079
- if (performance.mark) {
1080
- performance.mark('mandu-hydrated-' + id);
1081
- }
1082
-
1083
- // 이벤트 발송
1084
- element.dispatchEvent(new CustomEvent('mandu:hydrated', {
1085
- bubbles: true,
1086
- detail: { id, data, mode: renderMode }
1087
- }));
1088
-
1089
- console.log('[Mandu] Plain component hydrated:', id, '(' + renderMode + ')');
1090
- }
1091
- else {
1092
- throw new Error('[Mandu] Invalid module: expected Mandu island or React component: ' + id);
1093
- }
1094
- } catch (error) {
1095
- console.error('[Mandu] Hydration failed for', id, error);
1096
- element.setAttribute('data-mandu-error', 'true');
1097
-
1098
- // 에러 이벤트 발송
1099
- element.dispatchEvent(new CustomEvent('mandu:hydration-error', {
1100
- bubbles: true,
1101
- detail: { id, error: error.message }
1102
- }));
1103
- } finally {
1104
- element.removeAttribute('data-mandu-hydrating');
1105
- }
1106
- }
1107
-
1108
- /**
1109
- * 모든 Island hydrate 시작
1110
- */
1111
- function hydrateIslands() {
1112
- const islands = document.querySelectorAll('[data-mandu-island]');
1113
- const seenIds = new Set();
1114
-
1115
- for (const el of islands) {
1116
- const id = el.getAttribute('data-mandu-island');
1117
- const src = el.getAttribute('data-mandu-src');
1118
- const priority = el.getAttribute('data-mandu-priority') || '${HYDRATION.DEFAULT_PRIORITY}';
1119
- const hydrateStrategy = el.getAttribute('data-hydrate') || priorityToHydrateStrategy(priority);
1120
-
1121
- if (!id || !src) {
1122
- console.warn('[Mandu] Island missing id or src:', el);
1123
- continue;
1124
- }
1125
-
1126
- // 중복 ID 경고
1127
- if (seenIds.has(id)) {
1128
- console.warn('[Mandu] Duplicate island id detected:', id, '- skipping');
1129
- continue;
1130
- }
1131
- seenIds.add(id);
1132
-
1133
- scheduleHydration(el, src, hydrateStrategy);
1134
- }
1135
- }
1136
-
1137
- /**
1138
- * Island unmount
518
+ * Runtime bundle entry point.
519
+ *
520
+ * The browser runtime lives in client/runtime-entry.ts so it is typechecked
521
+ * with the rest of core and imports the shared props deserializer directly.
1139
522
  */
1140
- function unmountIsland(id) {
1141
- const root = hydratedRoots.get(id);
1142
- if (root) {
1143
- root.unmount();
1144
- hydratedRoots.delete(id);
1145
- return true;
1146
- }
1147
- return false;
523
+ function getRuntimeEntryPath(): string {
524
+ return path.resolve(import.meta.dir, "..", "client", "runtime-entry.ts");
1148
525
  }
1149
-
1150
- // 자동 초기화
1151
- if (document.readyState === 'loading') {
1152
- document.addEventListener('DOMContentLoaded', hydrateIslands);
1153
- } else {
1154
- hydrateIslands();
1155
- }
1156
-
1157
- // Export for external use
1158
- export { hydrateIslands, unmountIsland, hydratedRoots };
1159
- `;
1160
- }
1161
-
1162
526
  /**
1163
527
  * React shim 소스 생성 (import map용)
1164
528
  * 주의: export *는 Bun bundler에서 제대로 작동하지 않으므로 명시적 export 필요
@@ -1759,21 +1123,17 @@ export default {
1759
1123
  /**
1760
1124
  * Runtime 번들 빌드
1761
1125
  */
1762
- async function buildRuntime(
1763
- outDir: string,
1764
- options: BundlerOptions
1765
- ): Promise<{ success: boolean; outputPath: string; errors: string[] }> {
1766
- const runtimePath = path.join(outDir, "_runtime.src.js");
1767
- const outputName = "_runtime.js";
1768
-
1769
- try {
1770
- // 런타임 소스 작성
1771
- await Bun.write(runtimePath, generateRuntimeSource());
1772
-
1773
- // 빌드
1774
- const result = await safeBuild({
1775
- entrypoints: [runtimePath],
1776
- outdir: outDir,
1126
+ async function buildRuntime(
1127
+ outDir: string,
1128
+ options: BundlerOptions
1129
+ ): Promise<{ success: boolean; outputPath: string; errors: string[] }> {
1130
+ const runtimePath = getRuntimeEntryPath();
1131
+ const outputName = "_runtime.js";
1132
+
1133
+ try {
1134
+ const result = await safeBuild({
1135
+ entrypoints: [runtimePath],
1136
+ outdir: outDir,
1777
1137
  naming: outputName,
1778
1138
  minify: shouldMinify(options),
1779
1139
  sourcemap: options.sourcemap ? "external" : "none",
@@ -1784,29 +1144,24 @@ async function buildRuntime(
1784
1144
  "process.env.NODE_ENV": nodeEnvDefine(options),
1785
1145
  ...options.define,
1786
1146
  },
1787
- });
1788
-
1789
- if (!result.success) {
1790
- // 실패 디버깅을 위해 소스 파일을 남겨둠 (_runtime.src.js)
1791
- const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
1792
- return {
1793
- success: false,
1794
- outputPath: "",
1795
- errors: [`Runtime bundle build failed (source: ${runtimePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types. The source file has been kept for debugging.`],
1796
- };
1797
- }
1798
-
1799
- // 성공 시에만 소스 파일 정리
1800
- await fs.unlink(runtimePath).catch(() => {});
1801
-
1802
- return {
1803
- success: true,
1804
- outputPath: `/.mandu/client/${outputName}`,
1805
- errors: [],
1806
- };
1807
- } catch (error: unknown) {
1808
- // 예외 발생 시에도 디버깅을 위해 소스 파일을 남겨둠
1809
- const extra: string[] = [];
1147
+ });
1148
+
1149
+ if (!result.success) {
1150
+ const grouped = result.logs.map((l) => ` - ${l.message}`).join("\n");
1151
+ return {
1152
+ success: false,
1153
+ outputPath: "",
1154
+ errors: [`Runtime bundle build failed (source: ${runtimePath}):\n${grouped}\n Hint: Check the import paths and TypeScript types.`],
1155
+ };
1156
+ }
1157
+
1158
+ return {
1159
+ success: true,
1160
+ outputPath: `/.mandu/client/${outputName}`,
1161
+ errors: [],
1162
+ };
1163
+ } catch (error: unknown) {
1164
+ const extra: string[] = [];
1810
1165
  const errObj = error as Record<string, unknown> | null;
1811
1166
  if (errObj && Array.isArray(errObj.errors)) {
1812
1167
  extra.push(...errObj.errors.map((e: unknown) => String((e as Record<string, unknown>)?.message || e)));