@mandujs/core 0.12.1 → 0.13.0

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 (177) hide show
  1. package/README.ko.md +304 -304
  2. package/README.md +653 -653
  3. package/package.json +8 -8
  4. package/src/brain/architecture/analyzer.ts +28 -26
  5. package/src/brain/doctor/analyzer.ts +1 -1
  6. package/src/bundler/build.ts +91 -91
  7. package/src/bundler/css.ts +302 -302
  8. package/src/bundler/dev.ts +0 -1
  9. package/src/change/history.ts +3 -3
  10. package/src/change/snapshot.ts +10 -9
  11. package/src/change/transaction.ts +2 -2
  12. package/src/client/Link.tsx +227 -227
  13. package/src/client/globals.ts +44 -44
  14. package/src/client/hooks.ts +267 -267
  15. package/src/client/index.ts +5 -5
  16. package/src/client/island.ts +8 -8
  17. package/src/client/router.ts +435 -435
  18. package/src/client/runtime.ts +23 -23
  19. package/src/client/serialize.ts +404 -404
  20. package/src/client/window-state.ts +101 -101
  21. package/src/config/mandu.ts +94 -96
  22. package/src/config/validate.ts +213 -215
  23. package/src/config/watcher.ts +311 -311
  24. package/src/constants.ts +40 -40
  25. package/src/content/content-layer.ts +314 -314
  26. package/src/content/content.test.ts +433 -433
  27. package/src/content/data-store.ts +245 -245
  28. package/src/content/digest.ts +133 -133
  29. package/src/content/index.ts +164 -164
  30. package/src/content/loader-context.ts +172 -172
  31. package/src/content/loaders/api.ts +216 -216
  32. package/src/content/loaders/file.ts +169 -169
  33. package/src/content/loaders/glob.ts +252 -252
  34. package/src/content/loaders/index.ts +34 -34
  35. package/src/content/loaders/types.ts +137 -137
  36. package/src/content/meta-store.ts +209 -209
  37. package/src/content/types.ts +282 -282
  38. package/src/content/watcher.ts +135 -135
  39. package/src/contract/client-safe.test.ts +42 -42
  40. package/src/contract/client-safe.ts +114 -114
  41. package/src/contract/client.ts +16 -16
  42. package/src/contract/define.ts +459 -459
  43. package/src/contract/handler.ts +10 -10
  44. package/src/contract/normalize.test.ts +276 -276
  45. package/src/contract/normalize.ts +404 -404
  46. package/src/contract/registry.test.ts +206 -206
  47. package/src/contract/registry.ts +568 -568
  48. package/src/contract/schema.ts +48 -48
  49. package/src/contract/types.ts +58 -58
  50. package/src/contract/validator.ts +32 -32
  51. package/src/devtools/ai/context-builder.ts +375 -375
  52. package/src/devtools/ai/index.ts +25 -25
  53. package/src/devtools/ai/mcp-connector.ts +465 -465
  54. package/src/devtools/client/catchers/error-catcher.ts +327 -327
  55. package/src/devtools/client/catchers/index.ts +18 -18
  56. package/src/devtools/client/catchers/network-proxy.ts +363 -363
  57. package/src/devtools/client/components/index.ts +39 -39
  58. package/src/devtools/client/components/kitchen-root.tsx +362 -362
  59. package/src/devtools/client/components/mandu-character.tsx +241 -241
  60. package/src/devtools/client/components/overlay.tsx +368 -368
  61. package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
  62. package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
  63. package/src/devtools/client/components/panel/index.ts +32 -32
  64. package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
  65. package/src/devtools/client/components/panel/network-panel.tsx +292 -292
  66. package/src/devtools/client/components/panel/panel-container.tsx +259 -259
  67. package/src/devtools/client/filters/context-filters.ts +282 -282
  68. package/src/devtools/client/filters/index.ts +16 -16
  69. package/src/devtools/client/index.ts +63 -63
  70. package/src/devtools/client/persistence.ts +335 -335
  71. package/src/devtools/client/state-manager.ts +478 -478
  72. package/src/devtools/design-tokens.ts +263 -263
  73. package/src/devtools/hook/create-hook.ts +207 -207
  74. package/src/devtools/hook/index.ts +13 -13
  75. package/src/devtools/index.ts +439 -439
  76. package/src/devtools/init.ts +266 -266
  77. package/src/devtools/protocol.ts +237 -237
  78. package/src/devtools/server/index.ts +17 -17
  79. package/src/devtools/server/source-context.ts +444 -444
  80. package/src/devtools/types.ts +319 -319
  81. package/src/devtools/worker/index.ts +25 -25
  82. package/src/devtools/worker/redaction-worker.ts +222 -222
  83. package/src/devtools/worker/worker-manager.ts +409 -409
  84. package/src/error/classifier.ts +2 -2
  85. package/src/error/domains.ts +265 -265
  86. package/src/error/formatter.ts +32 -32
  87. package/src/error/result.ts +46 -46
  88. package/src/error/stack-analyzer.ts +5 -0
  89. package/src/error/types.ts +6 -6
  90. package/src/errors/extractor.ts +409 -409
  91. package/src/errors/index.ts +19 -19
  92. package/src/filling/auth.ts +308 -308
  93. package/src/filling/context.ts +569 -569
  94. package/src/filling/deps.ts +238 -238
  95. package/src/generator/contract-glue.ts +2 -1
  96. package/src/generator/generate.ts +12 -10
  97. package/src/generator/index.ts +3 -3
  98. package/src/generator/templates.ts +80 -79
  99. package/src/guard/analyzer.ts +360 -360
  100. package/src/guard/ast-analyzer.ts +806 -806
  101. package/src/guard/auto-correct.ts +1 -1
  102. package/src/guard/check.ts +128 -128
  103. package/src/guard/contract-guard.ts +9 -9
  104. package/src/guard/file-type.test.ts +24 -24
  105. package/src/guard/healing.ts +2 -0
  106. package/src/guard/index.ts +2 -0
  107. package/src/guard/negotiation.ts +430 -4
  108. package/src/guard/presets/atomic.ts +70 -70
  109. package/src/guard/presets/clean.ts +77 -77
  110. package/src/guard/presets/cqrs.test.ts +175 -0
  111. package/src/guard/presets/cqrs.ts +107 -0
  112. package/src/guard/presets/fsd.ts +79 -79
  113. package/src/guard/presets/hexagonal.ts +68 -68
  114. package/src/guard/presets/index.ts +291 -288
  115. package/src/guard/reporter.ts +445 -445
  116. package/src/guard/rules.ts +12 -12
  117. package/src/guard/statistics.ts +578 -578
  118. package/src/guard/suggestions.ts +358 -352
  119. package/src/guard/types.ts +348 -347
  120. package/src/guard/validator.ts +834 -834
  121. package/src/guard/watcher.ts +404 -404
  122. package/src/index.ts +1 -0
  123. package/src/intent/index.ts +310 -310
  124. package/src/island/index.ts +304 -304
  125. package/src/logging/index.ts +22 -22
  126. package/src/logging/transports.ts +365 -365
  127. package/src/paths.test.ts +47 -0
  128. package/src/paths.ts +47 -0
  129. package/src/plugins/index.ts +38 -38
  130. package/src/plugins/registry.ts +377 -377
  131. package/src/plugins/types.ts +363 -363
  132. package/src/report/build.ts +1 -1
  133. package/src/report/index.ts +1 -1
  134. package/src/router/fs-patterns.ts +387 -387
  135. package/src/router/fs-routes.ts +344 -401
  136. package/src/router/fs-scanner.ts +497 -497
  137. package/src/router/fs-types.ts +270 -278
  138. package/src/router/index.ts +81 -81
  139. package/src/runtime/boundary.tsx +232 -232
  140. package/src/runtime/compose.ts +222 -222
  141. package/src/runtime/lifecycle.ts +381 -381
  142. package/src/runtime/logger.test.ts +345 -345
  143. package/src/runtime/logger.ts +677 -677
  144. package/src/runtime/router.test.ts +476 -476
  145. package/src/runtime/router.ts +105 -105
  146. package/src/runtime/security.ts +155 -155
  147. package/src/runtime/server.ts +24 -24
  148. package/src/runtime/session-key.ts +328 -328
  149. package/src/runtime/ssr.ts +367 -367
  150. package/src/runtime/streaming-ssr.ts +1245 -1245
  151. package/src/runtime/trace.ts +144 -144
  152. package/src/seo/index.ts +214 -214
  153. package/src/seo/integration/ssr.ts +307 -307
  154. package/src/seo/render/basic.ts +427 -427
  155. package/src/seo/render/index.ts +143 -143
  156. package/src/seo/render/jsonld.ts +539 -539
  157. package/src/seo/render/opengraph.ts +191 -191
  158. package/src/seo/render/robots.ts +116 -116
  159. package/src/seo/render/sitemap.ts +137 -137
  160. package/src/seo/render/twitter.ts +126 -126
  161. package/src/seo/resolve/index.ts +353 -353
  162. package/src/seo/resolve/opengraph.ts +143 -143
  163. package/src/seo/resolve/robots.ts +73 -73
  164. package/src/seo/resolve/title.ts +94 -94
  165. package/src/seo/resolve/twitter.ts +73 -73
  166. package/src/seo/resolve/url.ts +97 -97
  167. package/src/seo/routes/index.ts +290 -290
  168. package/src/seo/types.ts +575 -575
  169. package/src/slot/validator.ts +39 -39
  170. package/src/spec/index.ts +3 -3
  171. package/src/spec/load.ts +76 -76
  172. package/src/spec/lock.ts +56 -56
  173. package/src/utils/bun.ts +8 -8
  174. package/src/utils/lru-cache.ts +75 -75
  175. package/src/utils/safe-io.ts +188 -188
  176. package/src/utils/string-safe.ts +298 -298
  177. package/src/watcher/rules.ts +5 -5
@@ -1,404 +1,404 @@
1
- /**
2
- * Mandu Props Serialization 📦
3
- * Fresh 스타일 고급 직렬화/역직렬화
4
- *
5
- * @see https://fresh.deno.dev/docs/concepts/islands
6
- *
7
- * 지원 타입:
8
- * - 원시형: null, boolean, number, string, bigint, undefined
9
- * - 특수 객체: Date, URL, RegExp, Map, Set
10
- * - 순환 참조
11
- * - 중첩 객체/배열
12
- */
13
-
14
- // ============================================
15
- // 타입 마커
16
- // ============================================
17
-
18
- const TYPE_MARKERS = {
19
- /** undefined */
20
- UNDEFINED: "\x00_",
21
- /** Date */
22
- DATE: "\x00D",
23
- /** URL */
24
- URL: "\x00U",
25
- /** RegExp */
26
- REGEXP: "\x00R",
27
- /** Map */
28
- MAP: "\x00M",
29
- /** Set */
30
- SET: "\x00S",
31
- /** 순환 참조 */
32
- REF: "\x00$",
33
- /** BigInt */
34
- BIGINT: "\x00B",
35
- /** Symbol (제한적 지원) */
36
- SYMBOL: "\x00Y",
37
- /** Error */
38
- ERROR: "\x00E",
39
- } as const;
40
-
41
- // ============================================
42
- // 직렬화
43
- // ============================================
44
-
45
- /**
46
- * 직렬화 컨텍스트 (순환 참조 추적)
47
- */
48
- interface SerializeContext {
49
- /** 이미 본 객체 → 인덱스 */
50
- seen: Map<object, number>;
51
- /** 참조 테이블 */
52
- refs: object[];
53
- }
54
-
55
- /**
56
- * Props 직렬화
57
- *
58
- * @example
59
- * ```typescript
60
- * const props = {
61
- * date: new Date(),
62
- * url: new URL('https://example.com'),
63
- * items: new Set([1, 2, 3]),
64
- * cache: new Map([['key', 'value']]),
65
- * };
66
- *
67
- * const json = serializeProps(props);
68
- * // 클라이언트로 전송
69
- * ```
70
- */
71
- export function serializeProps(props: Record<string, unknown>): string {
72
- const ctx: SerializeContext = { seen: new Map(), refs: [] };
73
- return JSON.stringify(serialize(props, ctx));
74
- }
75
-
76
- /**
77
- * 값 직렬화 (재귀)
78
- */
79
- function serialize(value: unknown, ctx: SerializeContext): unknown {
80
- // null
81
- if (value === null) return null;
82
-
83
- // undefined
84
- if (value === undefined) return TYPE_MARKERS.UNDEFINED;
85
-
86
- // 원시형
87
- if (typeof value === "boolean" || typeof value === "number") {
88
- return value;
89
- }
90
-
91
- if (typeof value === "string") {
92
- // 타입 마커와 충돌 방지 (첫 문자가 \x00인 경우)
93
- if (value.startsWith("\x00")) {
94
- return "\x00\x00" + value;
95
- }
96
- return value;
97
- }
98
-
99
- if (typeof value === "bigint") {
100
- return TYPE_MARKERS.BIGINT + value.toString();
101
- }
102
-
103
- if (typeof value === "symbol") {
104
- // Symbol은 description만 보존
105
- return TYPE_MARKERS.SYMBOL + (value.description ?? "");
106
- }
107
-
108
- // 함수는 직렬화 불가
109
- if (typeof value === "function") {
110
- console.warn("[Mandu Serialize] Functions cannot be serialized, skipping");
111
- return undefined;
112
- }
113
-
114
- // 객체 순환 참조 체크
115
- if (typeof value === "object") {
116
- const existing = ctx.seen.get(value);
117
- if (existing !== undefined) {
118
- return TYPE_MARKERS.REF + existing;
119
- }
120
-
121
- const idx = ctx.refs.length;
122
- ctx.seen.set(value, idx);
123
- ctx.refs.push(value);
124
- }
125
-
126
- // Date
127
- if (value instanceof Date) {
128
- return TYPE_MARKERS.DATE + value.toISOString();
129
- }
130
-
131
- // URL
132
- if (value instanceof URL) {
133
- return TYPE_MARKERS.URL + value.href;
134
- }
135
-
136
- // RegExp
137
- if (value instanceof RegExp) {
138
- return TYPE_MARKERS.REGEXP + value.toString();
139
- }
140
-
141
- // Error
142
- if (value instanceof Error) {
143
- return [
144
- TYPE_MARKERS.ERROR,
145
- value.name,
146
- value.message,
147
- value.stack ?? "",
148
- ];
149
- }
150
-
151
- // Map
152
- if (value instanceof Map) {
153
- const entries: [unknown, unknown][] = [];
154
- for (const [k, v] of value.entries()) {
155
- entries.push([serialize(k, ctx), serialize(v, ctx)]);
156
- }
157
- return [TYPE_MARKERS.MAP, ...entries];
158
- }
159
-
160
- // Set
161
- if (value instanceof Set) {
162
- const items: unknown[] = [];
163
- for (const item of value) {
164
- items.push(serialize(item, ctx));
165
- }
166
- return [TYPE_MARKERS.SET, ...items];
167
- }
168
-
169
- // 배열
170
- if (Array.isArray(value)) {
171
- return value.map((item) => serialize(item, ctx));
172
- }
173
-
174
- // 일반 객체
175
- const result: Record<string, unknown> = {};
176
- for (const [k, v] of Object.entries(value as object)) {
177
- const serialized = serialize(v, ctx);
178
- if (serialized !== undefined) {
179
- result[k] = serialized;
180
- }
181
- }
182
- return result;
183
- }
184
-
185
- // ============================================
186
- // 역직렬화
187
- // ============================================
188
-
189
- /**
190
- * 역직렬화 컨텍스트 (순환 참조 복원)
191
- */
192
- interface DeserializeContext {
193
- refs: unknown[];
194
- }
195
-
196
- /**
197
- * Props 역직렬화
198
- *
199
- * @example
200
- * ```typescript
201
- * // 서버에서 받은 JSON
202
- * const json = '{"date":"\x00D2025-01-28T00:00:00.000Z"}';
203
- *
204
- * const props = deserializeProps(json);
205
- * console.log(props.date instanceof Date); // true
206
- * ```
207
- */
208
- export function deserializeProps(json: string): Record<string, unknown> {
209
- const ctx: DeserializeContext = { refs: [] };
210
- const parsed = JSON.parse(json);
211
- return deserialize(parsed, ctx) as Record<string, unknown>;
212
- }
213
-
214
- /**
215
- * 값 역직렬화 (재귀)
216
- */
217
- function deserialize(value: unknown, ctx: DeserializeContext): unknown {
218
- // null
219
- if (value === null) return null;
220
-
221
- // 문자열 → 타입 마커 체크
222
- if (typeof value === "string") {
223
- // undefined
224
- if (value === TYPE_MARKERS.UNDEFINED) return undefined;
225
-
226
- // 이스케이프된 문자열 (\x00\x00 → \x00)
227
- if (value.startsWith("\x00\x00")) {
228
- return value.slice(2);
229
- }
230
-
231
- // Date
232
- if (value.startsWith(TYPE_MARKERS.DATE)) {
233
- return new Date(value.slice(2));
234
- }
235
-
236
- // URL
237
- if (value.startsWith(TYPE_MARKERS.URL)) {
238
- return new URL(value.slice(2));
239
- }
240
-
241
- // RegExp
242
- if (value.startsWith(TYPE_MARKERS.REGEXP)) {
243
- const str = value.slice(2);
244
- const match = str.match(/^\/(.*)\/([gimsuy]*)$/);
245
- if (match) {
246
- return new RegExp(match[1], match[2]);
247
- }
248
- return str; // 파싱 실패 시 문자열 반환
249
- }
250
-
251
- // BigInt
252
- if (value.startsWith(TYPE_MARKERS.BIGINT)) {
253
- return BigInt(value.slice(2));
254
- }
255
-
256
- // Symbol
257
- if (value.startsWith(TYPE_MARKERS.SYMBOL)) {
258
- return Symbol(value.slice(2));
259
- }
260
-
261
- // 순환 참조
262
- if (value.startsWith(TYPE_MARKERS.REF)) {
263
- const idx = parseInt(value.slice(2), 10);
264
- return ctx.refs[idx];
265
- }
266
-
267
- return value;
268
- }
269
-
270
- // 원시형
271
- if (typeof value === "boolean" || typeof value === "number") {
272
- return value;
273
- }
274
-
275
- // 배열 → 특수 타입 체크
276
- if (Array.isArray(value)) {
277
- const marker = value[0];
278
-
279
- // Error
280
- if (marker === TYPE_MARKERS.ERROR) {
281
- const [, name, message, stack] = value as [string, string, string, string];
282
- const error = new Error(message);
283
- error.name = name;
284
- if (stack) error.stack = stack;
285
- ctx.refs.push(error);
286
- return error;
287
- }
288
-
289
- // Map
290
- if (marker === TYPE_MARKERS.MAP) {
291
- const map = new Map();
292
- ctx.refs.push(map);
293
- for (let i = 1; i < value.length; i++) {
294
- const [k, v] = value[i] as [unknown, unknown];
295
- map.set(deserialize(k, ctx), deserialize(v, ctx));
296
- }
297
- return map;
298
- }
299
-
300
- // Set
301
- if (marker === TYPE_MARKERS.SET) {
302
- const set = new Set();
303
- ctx.refs.push(set);
304
- for (let i = 1; i < value.length; i++) {
305
- set.add(deserialize(value[i], ctx));
306
- }
307
- return set;
308
- }
309
-
310
- // 일반 배열
311
- const arr: unknown[] = [];
312
- ctx.refs.push(arr);
313
- for (const item of value) {
314
- arr.push(deserialize(item, ctx));
315
- }
316
- return arr;
317
- }
318
-
319
- // 일반 객체
320
- if (typeof value === "object") {
321
- const obj: Record<string, unknown> = {};
322
- ctx.refs.push(obj);
323
- for (const [k, v] of Object.entries(value)) {
324
- obj[k] = deserialize(v, ctx);
325
- }
326
- return obj;
327
- }
328
-
329
- return value;
330
- }
331
-
332
- // ============================================
333
- // 유틸리티
334
- // ============================================
335
-
336
- /**
337
- * 직렬화 가능 여부 체크
338
- */
339
- export function isSerializable(value: unknown): boolean {
340
- if (value === null || value === undefined) return true;
341
-
342
- const type = typeof value;
343
- if (type === "boolean" || type === "number" || type === "string" || type === "bigint") {
344
- return true;
345
- }
346
-
347
- if (type === "function" || type === "symbol") {
348
- return false;
349
- }
350
-
351
- if (value instanceof Date || value instanceof URL || value instanceof RegExp) {
352
- return true;
353
- }
354
-
355
- if (value instanceof Map || value instanceof Set) {
356
- return true;
357
- }
358
-
359
- if (Array.isArray(value)) {
360
- return value.every(isSerializable);
361
- }
362
-
363
- if (type === "object") {
364
- return Object.values(value as object).every(isSerializable);
365
- }
366
-
367
- return false;
368
- }
369
-
370
- /**
371
- * SSR에서 클라이언트로 props 전달용 스크립트 생성
372
- */
373
- export function generatePropsScript(
374
- islandId: string,
375
- props: Record<string, unknown>
376
- ): string {
377
- const json = serializeProps(props);
378
- const escaped = json
379
- .replace(/</g, "\\u003c")
380
- .replace(/>/g, "\\u003e")
381
- .replace(/&/g, "\\u0026");
382
-
383
- return `<script type="application/json" data-mandu-props="${islandId}">${escaped}</script>`;
384
- }
385
-
386
- /**
387
- * 클라이언트에서 props 스크립트 파싱
388
- */
389
- export function parsePropsScript(islandId: string): Record<string, unknown> | null {
390
- if (typeof document === "undefined") return null;
391
-
392
- const script = document.querySelector(
393
- `script[data-mandu-props="${islandId}"]`
394
- ) as HTMLScriptElement | null;
395
-
396
- if (!script?.textContent) return null;
397
-
398
- try {
399
- return deserializeProps(script.textContent);
400
- } catch (err) {
401
- console.error(`[Mandu] Failed to parse props for island ${islandId}:`, err);
402
- return null;
403
- }
404
- }
1
+ /**
2
+ * Mandu Props Serialization 📦
3
+ * Fresh 스타일 고급 직렬화/역직렬화
4
+ *
5
+ * @see https://fresh.deno.dev/docs/concepts/islands
6
+ *
7
+ * 지원 타입:
8
+ * - 원시형: null, boolean, number, string, bigint, undefined
9
+ * - 특수 객체: Date, URL, RegExp, Map, Set
10
+ * - 순환 참조
11
+ * - 중첩 객체/배열
12
+ */
13
+
14
+ // ============================================
15
+ // 타입 마커
16
+ // ============================================
17
+
18
+ const TYPE_MARKERS = {
19
+ /** undefined */
20
+ UNDEFINED: "\x00_",
21
+ /** Date */
22
+ DATE: "\x00D",
23
+ /** URL */
24
+ URL: "\x00U",
25
+ /** RegExp */
26
+ REGEXP: "\x00R",
27
+ /** Map */
28
+ MAP: "\x00M",
29
+ /** Set */
30
+ SET: "\x00S",
31
+ /** 순환 참조 */
32
+ REF: "\x00$",
33
+ /** BigInt */
34
+ BIGINT: "\x00B",
35
+ /** Symbol (제한적 지원) */
36
+ SYMBOL: "\x00Y",
37
+ /** Error */
38
+ ERROR: "\x00E",
39
+ } as const;
40
+
41
+ // ============================================
42
+ // 직렬화
43
+ // ============================================
44
+
45
+ /**
46
+ * 직렬화 컨텍스트 (순환 참조 추적)
47
+ */
48
+ interface SerializeContext {
49
+ /** 이미 본 객체 → 인덱스 */
50
+ seen: Map<object, number>;
51
+ /** 참조 테이블 */
52
+ refs: object[];
53
+ }
54
+
55
+ /**
56
+ * Props 직렬화
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const props = {
61
+ * date: new Date(),
62
+ * url: new URL('https://example.com'),
63
+ * items: new Set([1, 2, 3]),
64
+ * cache: new Map([['key', 'value']]),
65
+ * };
66
+ *
67
+ * const json = serializeProps(props);
68
+ * // 클라이언트로 전송
69
+ * ```
70
+ */
71
+ export function serializeProps(props: Record<string, unknown>): string {
72
+ const ctx: SerializeContext = { seen: new Map(), refs: [] };
73
+ return JSON.stringify(serialize(props, ctx));
74
+ }
75
+
76
+ /**
77
+ * 값 직렬화 (재귀)
78
+ */
79
+ function serialize(value: unknown, ctx: SerializeContext): unknown {
80
+ // null
81
+ if (value === null) return null;
82
+
83
+ // undefined
84
+ if (value === undefined) return TYPE_MARKERS.UNDEFINED;
85
+
86
+ // 원시형
87
+ if (typeof value === "boolean" || typeof value === "number") {
88
+ return value;
89
+ }
90
+
91
+ if (typeof value === "string") {
92
+ // 타입 마커와 충돌 방지 (첫 문자가 \x00인 경우)
93
+ if (value.startsWith("\x00")) {
94
+ return "\x00\x00" + value;
95
+ }
96
+ return value;
97
+ }
98
+
99
+ if (typeof value === "bigint") {
100
+ return TYPE_MARKERS.BIGINT + value.toString();
101
+ }
102
+
103
+ if (typeof value === "symbol") {
104
+ // Symbol은 description만 보존
105
+ return TYPE_MARKERS.SYMBOL + (value.description ?? "");
106
+ }
107
+
108
+ // 함수는 직렬화 불가
109
+ if (typeof value === "function") {
110
+ console.warn("[Mandu Serialize] Functions cannot be serialized, skipping");
111
+ return undefined;
112
+ }
113
+
114
+ // 객체 순환 참조 체크
115
+ if (typeof value === "object") {
116
+ const existing = ctx.seen.get(value);
117
+ if (existing !== undefined) {
118
+ return TYPE_MARKERS.REF + existing;
119
+ }
120
+
121
+ const idx = ctx.refs.length;
122
+ ctx.seen.set(value, idx);
123
+ ctx.refs.push(value);
124
+ }
125
+
126
+ // Date
127
+ if (value instanceof Date) {
128
+ return TYPE_MARKERS.DATE + value.toISOString();
129
+ }
130
+
131
+ // URL
132
+ if (value instanceof URL) {
133
+ return TYPE_MARKERS.URL + value.href;
134
+ }
135
+
136
+ // RegExp
137
+ if (value instanceof RegExp) {
138
+ return TYPE_MARKERS.REGEXP + value.toString();
139
+ }
140
+
141
+ // Error
142
+ if (value instanceof Error) {
143
+ return [
144
+ TYPE_MARKERS.ERROR,
145
+ value.name,
146
+ value.message,
147
+ value.stack ?? "",
148
+ ];
149
+ }
150
+
151
+ // Map
152
+ if (value instanceof Map) {
153
+ const entries: [unknown, unknown][] = [];
154
+ for (const [k, v] of value.entries()) {
155
+ entries.push([serialize(k, ctx), serialize(v, ctx)]);
156
+ }
157
+ return [TYPE_MARKERS.MAP, ...entries];
158
+ }
159
+
160
+ // Set
161
+ if (value instanceof Set) {
162
+ const items: unknown[] = [];
163
+ for (const item of value) {
164
+ items.push(serialize(item, ctx));
165
+ }
166
+ return [TYPE_MARKERS.SET, ...items];
167
+ }
168
+
169
+ // 배열
170
+ if (Array.isArray(value)) {
171
+ return value.map((item) => serialize(item, ctx));
172
+ }
173
+
174
+ // 일반 객체
175
+ const result: Record<string, unknown> = {};
176
+ for (const [k, v] of Object.entries(value as object)) {
177
+ const serialized = serialize(v, ctx);
178
+ if (serialized !== undefined) {
179
+ result[k] = serialized;
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+
185
+ // ============================================
186
+ // 역직렬화
187
+ // ============================================
188
+
189
+ /**
190
+ * 역직렬화 컨텍스트 (순환 참조 복원)
191
+ */
192
+ interface DeserializeContext {
193
+ refs: unknown[];
194
+ }
195
+
196
+ /**
197
+ * Props 역직렬화
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // 서버에서 받은 JSON
202
+ * const json = '{"date":"\x00D2025-01-28T00:00:00.000Z"}';
203
+ *
204
+ * const props = deserializeProps(json);
205
+ * console.log(props.date instanceof Date); // true
206
+ * ```
207
+ */
208
+ export function deserializeProps(json: string): Record<string, unknown> {
209
+ const ctx: DeserializeContext = { refs: [] };
210
+ const parsed = JSON.parse(json);
211
+ return deserialize(parsed, ctx) as Record<string, unknown>;
212
+ }
213
+
214
+ /**
215
+ * 값 역직렬화 (재귀)
216
+ */
217
+ function deserialize(value: unknown, ctx: DeserializeContext): unknown {
218
+ // null
219
+ if (value === null) return null;
220
+
221
+ // 문자열 → 타입 마커 체크
222
+ if (typeof value === "string") {
223
+ // undefined
224
+ if (value === TYPE_MARKERS.UNDEFINED) return undefined;
225
+
226
+ // 이스케이프된 문자열 (\x00\x00 → \x00)
227
+ if (value.startsWith("\x00\x00")) {
228
+ return value.slice(2);
229
+ }
230
+
231
+ // Date
232
+ if (value.startsWith(TYPE_MARKERS.DATE)) {
233
+ return new Date(value.slice(2));
234
+ }
235
+
236
+ // URL
237
+ if (value.startsWith(TYPE_MARKERS.URL)) {
238
+ return new URL(value.slice(2));
239
+ }
240
+
241
+ // RegExp
242
+ if (value.startsWith(TYPE_MARKERS.REGEXP)) {
243
+ const str = value.slice(2);
244
+ const match = str.match(/^\/(.*)\/([gimsuy]*)$/);
245
+ if (match) {
246
+ return new RegExp(match[1], match[2]);
247
+ }
248
+ return str; // 파싱 실패 시 문자열 반환
249
+ }
250
+
251
+ // BigInt
252
+ if (value.startsWith(TYPE_MARKERS.BIGINT)) {
253
+ return BigInt(value.slice(2));
254
+ }
255
+
256
+ // Symbol
257
+ if (value.startsWith(TYPE_MARKERS.SYMBOL)) {
258
+ return Symbol(value.slice(2));
259
+ }
260
+
261
+ // 순환 참조
262
+ if (value.startsWith(TYPE_MARKERS.REF)) {
263
+ const idx = parseInt(value.slice(2), 10);
264
+ return ctx.refs[idx];
265
+ }
266
+
267
+ return value;
268
+ }
269
+
270
+ // 원시형
271
+ if (typeof value === "boolean" || typeof value === "number") {
272
+ return value;
273
+ }
274
+
275
+ // 배열 → 특수 타입 체크
276
+ if (Array.isArray(value)) {
277
+ const marker = value[0];
278
+
279
+ // Error
280
+ if (marker === TYPE_MARKERS.ERROR) {
281
+ const [, name, message, stack] = value as [string, string, string, string];
282
+ const error = new Error(message);
283
+ error.name = name;
284
+ if (stack) error.stack = stack;
285
+ ctx.refs.push(error);
286
+ return error;
287
+ }
288
+
289
+ // Map
290
+ if (marker === TYPE_MARKERS.MAP) {
291
+ const map = new Map();
292
+ ctx.refs.push(map);
293
+ for (let i = 1; i < value.length; i++) {
294
+ const [k, v] = value[i] as [unknown, unknown];
295
+ map.set(deserialize(k, ctx), deserialize(v, ctx));
296
+ }
297
+ return map;
298
+ }
299
+
300
+ // Set
301
+ if (marker === TYPE_MARKERS.SET) {
302
+ const set = new Set();
303
+ ctx.refs.push(set);
304
+ for (let i = 1; i < value.length; i++) {
305
+ set.add(deserialize(value[i], ctx));
306
+ }
307
+ return set;
308
+ }
309
+
310
+ // 일반 배열
311
+ const arr: unknown[] = [];
312
+ ctx.refs.push(arr);
313
+ for (const item of value) {
314
+ arr.push(deserialize(item, ctx));
315
+ }
316
+ return arr;
317
+ }
318
+
319
+ // 일반 객체
320
+ if (typeof value === "object") {
321
+ const obj: Record<string, unknown> = {};
322
+ ctx.refs.push(obj);
323
+ for (const [k, v] of Object.entries(value)) {
324
+ obj[k] = deserialize(v, ctx);
325
+ }
326
+ return obj;
327
+ }
328
+
329
+ return value;
330
+ }
331
+
332
+ // ============================================
333
+ // 유틸리티
334
+ // ============================================
335
+
336
+ /**
337
+ * 직렬화 가능 여부 체크
338
+ */
339
+ export function isSerializable(value: unknown): boolean {
340
+ if (value === null || value === undefined) return true;
341
+
342
+ const type = typeof value;
343
+ if (type === "boolean" || type === "number" || type === "string" || type === "bigint") {
344
+ return true;
345
+ }
346
+
347
+ if (type === "function" || type === "symbol") {
348
+ return false;
349
+ }
350
+
351
+ if (value instanceof Date || value instanceof URL || value instanceof RegExp) {
352
+ return true;
353
+ }
354
+
355
+ if (value instanceof Map || value instanceof Set) {
356
+ return true;
357
+ }
358
+
359
+ if (Array.isArray(value)) {
360
+ return value.every(isSerializable);
361
+ }
362
+
363
+ if (type === "object") {
364
+ return Object.values(value as object).every(isSerializable);
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ /**
371
+ * SSR에서 클라이언트로 props 전달용 스크립트 생성
372
+ */
373
+ export function generatePropsScript(
374
+ islandId: string,
375
+ props: Record<string, unknown>
376
+ ): string {
377
+ const json = serializeProps(props);
378
+ const escaped = json
379
+ .replace(/</g, "\\u003c")
380
+ .replace(/>/g, "\\u003e")
381
+ .replace(/&/g, "\\u0026");
382
+
383
+ return `<script type="application/json" data-mandu-props="${islandId}">${escaped}</script>`;
384
+ }
385
+
386
+ /**
387
+ * 클라이언트에서 props 스크립트 파싱
388
+ */
389
+ export function parsePropsScript(islandId: string): Record<string, unknown> | null {
390
+ if (typeof document === "undefined") return null;
391
+
392
+ const script = document.querySelector(
393
+ `script[data-mandu-props="${islandId}"]`
394
+ ) as HTMLScriptElement | null;
395
+
396
+ if (!script?.textContent) return null;
397
+
398
+ try {
399
+ return deserializeProps(script.textContent);
400
+ } catch (err) {
401
+ console.error(`[Mandu] Failed to parse props for island ${islandId}:`, err);
402
+ return null;
403
+ }
404
+ }