@mandujs/core 0.54.17 → 0.54.19

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 (46) hide show
  1. package/package.json +3 -1
  2. package/src/agent/__tests__/context.test.ts +94 -25
  3. package/src/agent/context.ts +17 -0
  4. package/src/agent/types.ts +32 -12
  5. package/src/agent/verify.ts +55 -24
  6. package/src/bundler/__snapshots__/build.test.ts.snap +5 -0
  7. package/src/bundler/__tests__/build-runner.ts +130 -17
  8. package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -0
  9. package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -33
  10. package/src/bundler/build.test.ts +478 -9
  11. package/src/bundler/build.ts +424 -746
  12. package/src/bundler/client-boundary-transform.ts +977 -0
  13. package/src/bundler/dev.ts +39 -112
  14. package/src/bundler/fast-refresh-preamble.ts +47 -0
  15. package/src/bundler/index.ts +3 -2
  16. package/src/bundler/manifest-schema.ts +10 -0
  17. package/src/bundler/types.ts +20 -2
  18. package/src/client/__tests__/props-serialization.test.ts +37 -0
  19. package/src/client/hydrate.ts +2 -2
  20. package/src/client/index.ts +1 -1
  21. package/src/client/props-serialization.ts +233 -0
  22. package/src/client/runtime-entry.ts +567 -0
  23. package/src/client/runtime.ts +1 -1
  24. package/src/client/serialize.ts +50 -404
  25. package/src/diagnose/__tests__/checks.test.ts +132 -17
  26. package/src/diagnose/checks.ts +184 -3
  27. package/src/diagnose/run.ts +10 -8
  28. package/src/generator/templates.test.ts +48 -5
  29. package/src/generator/templates.ts +10 -1
  30. package/src/internal/client-boundary.ts +266 -0
  31. package/src/internal/index.ts +2 -1
  32. package/src/router/client-entry.test.ts +154 -29
  33. package/src/router/client-entry.ts +111 -313
  34. package/src/router/fs-routes.test.ts +443 -1
  35. package/src/router/fs-routes.ts +16 -3
  36. package/src/router/fs-scanner.ts +176 -57
  37. package/src/router/fs-types.ts +11 -2
  38. package/src/router/route-source-analyzer.ts +521 -0
  39. package/src/runtime/__tests__/inline-client-hydration.test.ts +104 -1
  40. package/src/runtime/__tests__/page-render-response.test.ts +218 -0
  41. package/src/runtime/handlers.ts +50 -26
  42. package/src/runtime/page-render-response.ts +24 -1
  43. package/src/runtime/server.ts +14 -0
  44. package/src/runtime/ssr.ts +16 -5
  45. package/src/runtime/streaming-ssr.ts +119 -76
  46. package/src/spec/schema.ts +31 -5
@@ -21,12 +21,13 @@ import {
21
21
  scanFileImports,
22
22
  DEFAULT_MAX_CLOSURE_DEPTH,
23
23
  } from "./reverse-import-graph";
24
- import path from "path";
25
- import fs from "fs";
26
- import { LRUCache } from "../utils/lru-cache";
27
- import { registerCacheSize, unregisterCacheSize } from "../observability/metrics";
28
-
29
- /**
24
+ import path from "path";
25
+ import fs from "fs";
26
+ import { LRUCache } from "../utils/lru-cache";
27
+ import { registerCacheSize, unregisterCacheSize } from "../observability/metrics";
28
+ export { generateFastRefreshPreamble } from "./fast-refresh-preamble";
29
+
30
+ /**
30
31
  * #184: 공통 디렉토리 변경 시 사용하는 sentinel.
31
32
  * `onSSRChange`에 특정 파일 경로 대신 이 상수를 전달하면 "전체 SSR 레지스트리 invalidate" 의미.
32
33
  */
@@ -135,12 +136,14 @@ export interface RebuildResult {
135
136
  error?: string;
136
137
  }
137
138
 
138
- export interface DevBundler {
139
- /** 초기 빌드 결과 */
140
- initialBuild: BundleResult;
141
- /** 파일 감시 중지 */
142
- close: () => void;
143
- }
139
+ export interface DevBundler {
140
+ /** 초기 빌드 결과 */
141
+ initialBuild: BundleResult;
142
+ /** Reverse import graph initial seed completion for deterministic tests. */
143
+ reverseGraphReady: Promise<void>;
144
+ /** 파일 감시 중지 */
145
+ close: () => void;
146
+ }
144
147
 
145
148
  /**
146
149
  * #180: 파일 경로 비교를 위한 정규화.
@@ -647,7 +650,8 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
647
650
  // surface typed-only stubs that have no source yet); misses simply
648
651
  // keep the legacy "silent drop" behavior so no project is made
649
652
  // worse by turning this on.
650
- const reverseGraph = new ReverseImportGraph();
653
+ const reverseGraph = new ReverseImportGraph();
654
+ let reverseGraphSeedPromise: Promise<void> | null = null;
651
655
 
652
656
  /**
653
657
  * Re-scan a single file's imports and update its row in the
@@ -1239,11 +1243,15 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1239
1243
  * stay silent. Dispatch is idempotent — each root is touched at
1240
1244
  * most once per change via the `dispatched` set.
1241
1245
  */
1242
- const dispatchByTransitiveImporters = async (
1243
- changedFile: string,
1244
- ): Promise<number> => {
1245
- const absChanged = path.resolve(rootDir, changedFile);
1246
- // Refresh the changed file's OWN imports first. A leaf edit can
1246
+ const dispatchByTransitiveImporters = async (
1247
+ changedFile: string,
1248
+ ): Promise<number> => {
1249
+ const absChanged = path.resolve(rootDir, changedFile);
1250
+ if (reverseGraphSeedPromise) {
1251
+ await reverseGraphSeedPromise;
1252
+ }
1253
+
1254
+ // Refresh the changed file's OWN imports first. A leaf edit can
1247
1255
  // legitimately add or remove imports (e.g. switching a barrel
1248
1256
  // from `./en` to `./ko`); the reverse graph must track that so
1249
1257
  // the NEXT unknown-file change uses the correct set.
@@ -1655,16 +1663,17 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
1655
1663
  // and the first user edit can't race the seed (first event goes
1656
1664
  // through the 100 ms debounce, by which point the scan has finished
1657
1665
  // for any realistic project size).
1658
- seedReverseGraph().catch((err) => {
1659
- console.warn(
1660
- "[Mandu HMR] reverse import-graph seed skipped:",
1661
- err instanceof Error ? err.message : String(err),
1662
- );
1663
- });
1664
-
1665
- return {
1666
- initialBuild,
1667
- close: () => {
1666
+ reverseGraphSeedPromise = seedReverseGraph().catch((err) => {
1667
+ console.warn(
1668
+ "[Mandu HMR] reverse import-graph seed skipped:",
1669
+ err instanceof Error ? err.message : String(err),
1670
+ );
1671
+ });
1672
+
1673
+ return {
1674
+ initialBuild,
1675
+ reverseGraphReady: reverseGraphSeedPromise,
1676
+ close: () => {
1668
1677
  // B6: clear all per-file timers to release event-loop refs.
1669
1678
  // Phase 17 — `LRUCache.clear()` fires the registered `onEvict`
1670
1679
  // (`clearTimeout(timer)`) for every entry before dropping them,
@@ -2208,90 +2217,8 @@ export function createHMRServer(
2208
2217
  };
2209
2218
  }
2210
2219
 
2211
- /**
2212
- * Phase 7.1 B-3 — HTML preamble for React Fast Refresh.
2213
- *
2214
- * Emitted by the SSR renderer in dev mode, **before** any island JS
2215
- * evaluates. Two concerns, one <script>:
2216
- *
2217
- * 1. Install inert stubs for `$RefreshReg$` / `$RefreshSig$` on
2218
- * `window`. Bun's `reactFastRefresh: true` transform inserts calls
2219
- * to these at the top of every transformed module; if they are
2220
- * undefined when the module body runs, we get a runtime error and
2221
- * the island never hydrates. Vite's preamble does the same inline
2222
- * stub install for the same reason.
2223
- *
2224
- * 2. Fire a dynamic `import()` of the bundled glue (`_fast-refresh-
2225
- * runtime.js`), which in turn `await`s the real `react-refresh/
2226
- * runtime`, installs `window.__MANDU_HMR__`, and upgrades the
2227
- * stubs to live wrappers that forward to the refresh runtime. The
2228
- * race between "module evaluates and calls `$RefreshReg$`" and
2229
- * "glue has upgraded the stubs" is benign — registrations that
2230
- * land on the stub are simply no-ops, which at worst means the
2231
- * very first mount isn't tracked. Subsequent hot swaps land on
2232
- * the live wrappers and work normally.
2233
- *
2234
- * The emitted script is **inline** (no `type="module"`, no external
2235
- * src). This is deliberate: the stubs must exist before *any* module
2236
- * script runs, and inline execution blocks the parser. The dynamic
2237
- * import inside the inline script is non-blocking so we don't stall
2238
- * First Contentful Paint.
2239
- *
2240
- * CSP note: the inline <script> uses no `eval` or `new Function`; it
2241
- * only calls `Object.assign`, defines functions, and initiates an
2242
- * `import()`. All of these are permitted under `script-src 'self'
2243
- * 'unsafe-inline'` which is Mandu's default dev CSP (production CSP
2244
- * forbids `unsafe-inline`, but this preamble is dev-only).
2245
- *
2246
- * `glueUrl` and `runtimeUrl` come from the build manifest's
2247
- * `shared.fastRefresh` block (populated only in dev). Both must be
2248
- * absolute URLs served from the same origin as the HTML, which our
2249
- * bundler always guarantees (`/.mandu/client/...`).
2250
- */
2251
- export function generateFastRefreshPreamble(
2252
- glueUrl: string,
2253
- runtimeUrl: string,
2254
- ): string {
2255
- // Both URLs must be non-empty. If either is missing (e.g. vendor
2256
- // shim build failed), the caller (ssr.ts) should skip this preamble
2257
- // entirely — defensive guard here keeps the output valid regardless.
2258
- if (!glueUrl || !runtimeUrl) {
2259
- return `<script>/* Mandu Fast Refresh: missing runtime assets, preamble skipped */</script>`;
2260
- }
2261
- // JSON.stringify escapes the URLs safely for inline `<script>`:
2262
- // - quotes produce a valid JS string literal
2263
- // - forward-slashes / backslashes are handled
2264
- // We also `split('</')` to avoid a stray `</script>` sequence in the
2265
- // URL bytes breaking the enclosing tag. This is the same defense
2266
- // Vite uses in its own preamble emitter.
2267
- const glueLit = JSON.stringify(glueUrl).split("</").join('<"+"/');
2268
- const runtimeLit = JSON.stringify(runtimeUrl).split("</").join('<"+"/');
2269
- return `<script>
2270
- // Phase 7.1 B-3 React Fast Refresh preamble (Mandu dev-only)
2271
- (function () {
2272
- if (typeof window === "undefined") return;
2273
- // Install inert stubs so transformed modules that run BEFORE the
2274
- // async runtime upgrade don't hit ReferenceError on $RefreshReg$.
2275
- if (!window.$RefreshReg$) window.$RefreshReg$ = function () {};
2276
- if (!window.$RefreshSig$) window.$RefreshSig$ = function () { return function (t) { return t; }; };
2277
- // Async-load the glue; failures are reported but never throw out of
2278
- // the preamble — a missing runtime degrades to full-reload HMR.
2279
- import(${glueLit})
2280
- .then(function (mod) {
2281
- var runtimeImport = function () { return import(${runtimeLit}); };
2282
- if (mod && typeof mod.installGlobal === "function") {
2283
- return mod.installGlobal({ runtimeImport: runtimeImport });
2284
- }
2285
- })
2286
- .catch(function (err) {
2287
- console.error("[Mandu Fast Refresh] preamble failed:", err);
2288
- });
2289
- })();
2290
- </script>`;
2291
- }
2292
-
2293
- /**
2294
- * HMR 클라이언트 스크립트 생성
2220
+ /**
2221
+ * HMR 클라이언트 스크립트 생성
2295
2222
  * 브라우저에서 실행되어 HMR 서버와 연결.
2296
2223
  *
2297
2224
  * Phase 7.0 R1 Agent C additions:
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Small runtime-safe Fast Refresh preamble helper.
3
+ *
4
+ * Keep this separate from `bundler/dev.ts`: SSR runtimes import the helper
5
+ * while edge bundles must not pull the dev bundler, build graph, or TypeScript
6
+ * compiler into production worker output.
7
+ */
8
+ export function generateFastRefreshPreamble(
9
+ glueUrl: string,
10
+ runtimeUrl: string,
11
+ ): string {
12
+ // Both URLs must be non-empty. If either is missing (e.g. vendor
13
+ // shim build failed), the caller should skip this preamble entirely.
14
+ if (!glueUrl || !runtimeUrl) {
15
+ return `<script>/* Mandu Fast Refresh: missing runtime assets, preamble skipped */</script>`;
16
+ }
17
+ // JSON.stringify escapes the URLs safely for inline `<script>`:
18
+ // - quotes produce a valid JS string literal
19
+ // - forward-slashes / backslashes are handled
20
+ // We also `split('</')` to avoid a stray `</script>` sequence in the
21
+ // URL bytes breaking the enclosing tag. This is the same defense
22
+ // Vite uses in its own preamble emitter.
23
+ const glueLit = JSON.stringify(glueUrl).split("</").join('<"+"/');
24
+ const runtimeLit = JSON.stringify(runtimeUrl).split("</").join('<"+"/');
25
+ return `<script>
26
+ // Phase 7.1 B-3 React Fast Refresh preamble (Mandu dev-only)
27
+ (function () {
28
+ if (typeof window === "undefined") return;
29
+ // Install inert stubs so transformed modules that run BEFORE the
30
+ // async runtime upgrade don't hit ReferenceError on $RefreshReg$.
31
+ if (!window.$RefreshReg$) window.$RefreshReg$ = function () {};
32
+ if (!window.$RefreshSig$) window.$RefreshSig$ = function () { return function (t) { return t; }; };
33
+ // Async-load the glue; failures are reported but never throw out of
34
+ // the preamble — a missing runtime degrades to full-reload HMR.
35
+ import(${glueLit})
36
+ .then(function (mod) {
37
+ var runtimeImport = function () { return import(${runtimeLit}); };
38
+ if (mod && typeof mod.installGlobal === "function") {
39
+ return mod.installGlobal({ runtimeImport: runtimeImport });
40
+ }
41
+ })
42
+ .catch(function (err) {
43
+ console.error("[Mandu Fast Refresh] preamble failed:", err);
44
+ });
45
+ })();
46
+ </script>`;
47
+ }
@@ -4,7 +4,8 @@
4
4
  */
5
5
 
6
6
  export * from "./types";
7
- export * from "./build";
8
- export * from "./dev";
7
+ export * from "./build";
8
+ export * from "./client-boundary-transform";
9
+ export * from "./dev";
9
10
  export * from "./css";
10
11
  export * from "./plugins";
@@ -173,6 +173,15 @@ const PartialEntrySchema = z.object({
173
173
  priority: PrioritySchema,
174
174
  });
175
175
 
176
+ const BoundaryEntrySchema = z.object({
177
+ route: z.string().min(1),
178
+ js: safeManduUrl("boundaries[].js"),
179
+ module: z.string().min(1),
180
+ exportName: z.string().min(1),
181
+ priority: PrioritySchema,
182
+ hydrate: z.string().min(1),
183
+ });
184
+
176
185
  const FastRefreshSchema = z.object({
177
186
  runtime: safeManduUrl("shared.fastRefresh.runtime"),
178
187
  glue: safeManduUrl("shared.fastRefresh.glue"),
@@ -229,6 +238,7 @@ export const BundleManifestSchema = z
229
238
  bundles: z.record(z.string(), BundleEntrySchema),
230
239
  islands: z.record(z.string(), IslandEntrySchema).optional(),
231
240
  partials: z.record(z.string(), PartialEntrySchema).optional(),
241
+ boundaries: z.record(z.string(), BoundaryEntrySchema).optional(),
232
242
  shared: SharedSchema,
233
243
  importMap: ImportMapSchema.optional(),
234
244
  })
@@ -77,8 +77,26 @@ export interface BundleManifest {
77
77
  priority: "immediate" | "visible" | "idle" | "interaction";
78
78
  }
79
79
  >;
80
- /** 공유 청크 */
81
- shared: {
80
+ /** Compiler-owned client boundary bundles keyed by boundary id. */
81
+ boundaries?: Record<
82
+ string,
83
+ {
84
+ /** Owning route id */
85
+ route: string;
86
+ /** JavaScript bundle path */
87
+ js: string;
88
+ /** Source client module */
89
+ module: string;
90
+ /** Client export name */
91
+ exportName: string;
92
+ /** Hydration priority */
93
+ priority: "immediate" | "visible" | "idle" | "interaction";
94
+ /** Hydration mode emitted into data-hydrate */
95
+ hydrate: string;
96
+ }
97
+ >;
98
+ /** 공유 청크 */
99
+ shared: {
82
100
  /** Hydration 런타임 */
83
101
  runtime: string;
84
102
  /** React 번들 경로 */
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { deserializeProps, serializeProps } from "../props-serialization";
3
+
4
+ describe("props serialization", () => {
5
+ it("roundtrips complex browser hydration props through the shared deserializer", () => {
6
+ const input = {
7
+ date: new Date("2026-05-23T00:00:00.000Z"),
8
+ map: new Map<unknown, unknown>([
9
+ ["count", 3],
10
+ ["nested", { ok: true }],
11
+ ]),
12
+ set: new Set<unknown>(["a", "b"]),
13
+ url: new URL("https://mandu.dev/docs?phase=4"),
14
+ missing: undefined,
15
+ nested: {
16
+ list: [1, undefined, { value: "x" }],
17
+ },
18
+ };
19
+
20
+ const output = deserializeProps(serializeProps(input));
21
+
22
+ expect(output.date).toBeInstanceOf(Date);
23
+ expect((output.date as Date).toISOString()).toBe("2026-05-23T00:00:00.000Z");
24
+ expect(output.map).toBeInstanceOf(Map);
25
+ expect((output.map as Map<unknown, unknown>).get("count")).toBe(3);
26
+ expect((output.map as Map<unknown, unknown>).get("nested")).toEqual({ ok: true });
27
+ expect(output.set).toBeInstanceOf(Set);
28
+ expect(Array.from(output.set as Set<unknown>)).toEqual(["a", "b"]);
29
+ expect(output.url).toBeInstanceOf(URL);
30
+ expect((output.url as URL).href).toBe("https://mandu.dev/docs?phase=4");
31
+ expect(Object.prototype.hasOwnProperty.call(output, "missing")).toBe(true);
32
+ expect(output.missing).toBeUndefined();
33
+ expect(output.nested).toEqual({
34
+ list: [1, undefined, { value: "x" }],
35
+ });
36
+ });
37
+ });
@@ -16,8 +16,8 @@
16
16
  * 2. Unit tests can import it under happy-dom/JSDOM without triggering
17
17
  * global mutation (no `document.querySelectorAll` on module eval).
18
18
  *
19
- * The bundler-generated runtime (`bundler/build.ts::generateRuntimeSource`)
20
- * delegates strategy selection to `scheduleHydration()` here — SSR emits the
19
+ * The bundled runtime entry (`client/runtime-entry.ts`)
20
+ * delegates strategy selection to `scheduleHydration()` here — SSR emits the
21
21
  * `data-hydrate` attribute, the runtime reads it and dispatches.
22
22
  *
23
23
  * Design contract:
@@ -168,7 +168,7 @@ import { Link, NavLink } from "./Link";
168
168
 
169
169
  /**
170
170
  * Mandu Client namespace
171
- * v0.8.0: Hydration is handled automatically (generateRuntimeSource)
171
+ * v0.8.0: Hydration is handled automatically (runtime-entry)
172
172
  * Note: Use `ManduClient` to avoid conflict with other Mandu exports
173
173
  */
174
174
  export const ManduClient = {
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Browser-safe Mandu props serialization.
3
+ *
4
+ * This module intentionally has no DOM, Bun, or Node imports. Runtime entry
5
+ * code may read from the document, but serialization semantics live here.
6
+ */
7
+
8
+ const TYPE_MARKERS = {
9
+ UNDEFINED: "\x00_",
10
+ DATE: "\x00D",
11
+ URL: "\x00U",
12
+ REGEXP: "\x00R",
13
+ MAP: "\x00M",
14
+ SET: "\x00S",
15
+ REF: "\x00$",
16
+ BIGINT: "\x00B",
17
+ SYMBOL: "\x00Y",
18
+ ERROR: "\x00E",
19
+ } as const;
20
+
21
+ interface SerializeContext {
22
+ seen: Map<object, number>;
23
+ refs: object[];
24
+ }
25
+
26
+ interface DeserializeContext {
27
+ refs: unknown[];
28
+ }
29
+
30
+ export function serializeProps(props: Record<string, unknown>): string {
31
+ const ctx: SerializeContext = { seen: new Map(), refs: [] };
32
+ return JSON.stringify(serialize(props, ctx));
33
+ }
34
+
35
+ function serialize(value: unknown, ctx: SerializeContext): unknown {
36
+ if (value === null) return null;
37
+ if (value === undefined) return TYPE_MARKERS.UNDEFINED;
38
+
39
+ if (typeof value === "boolean" || typeof value === "number") {
40
+ return value;
41
+ }
42
+
43
+ if (typeof value === "string") {
44
+ return value.startsWith("\x00") ? "\x00\x00" + value : value;
45
+ }
46
+
47
+ if (typeof value === "bigint") {
48
+ return TYPE_MARKERS.BIGINT + value.toString();
49
+ }
50
+
51
+ if (typeof value === "symbol") {
52
+ return TYPE_MARKERS.SYMBOL + (value.description ?? "");
53
+ }
54
+
55
+ if (typeof value === "function") {
56
+ console.warn("[Mandu Serialize] Functions cannot be serialized, skipping");
57
+ return undefined;
58
+ }
59
+
60
+ if (typeof value === "object") {
61
+ const existing = ctx.seen.get(value);
62
+ if (existing !== undefined) {
63
+ return TYPE_MARKERS.REF + existing;
64
+ }
65
+
66
+ const idx = ctx.refs.length;
67
+ ctx.seen.set(value, idx);
68
+ ctx.refs.push(value);
69
+ }
70
+
71
+ if (value instanceof Date) {
72
+ return TYPE_MARKERS.DATE + value.toISOString();
73
+ }
74
+
75
+ if (value instanceof URL) {
76
+ return TYPE_MARKERS.URL + value.href;
77
+ }
78
+
79
+ if (value instanceof RegExp) {
80
+ return TYPE_MARKERS.REGEXP + value.toString();
81
+ }
82
+
83
+ if (value instanceof Error) {
84
+ return [
85
+ TYPE_MARKERS.ERROR,
86
+ value.name,
87
+ value.message,
88
+ value.stack ?? "",
89
+ ];
90
+ }
91
+
92
+ if (value instanceof Map) {
93
+ const entries: [unknown, unknown][] = [];
94
+ for (const [key, nested] of value.entries()) {
95
+ entries.push([serialize(key, ctx), serialize(nested, ctx)]);
96
+ }
97
+ return [TYPE_MARKERS.MAP, ...entries];
98
+ }
99
+
100
+ if (value instanceof Set) {
101
+ const items: unknown[] = [];
102
+ for (const item of value) {
103
+ items.push(serialize(item, ctx));
104
+ }
105
+ return [TYPE_MARKERS.SET, ...items];
106
+ }
107
+
108
+ if (Array.isArray(value)) {
109
+ return value.map((item) => serialize(item, ctx));
110
+ }
111
+
112
+ const result: Record<string, unknown> = {};
113
+ for (const [key, nested] of Object.entries(value as object)) {
114
+ const serialized = serialize(nested, ctx);
115
+ if (serialized !== undefined) {
116
+ result[key] = serialized;
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+
122
+ export function deserializeProps(json: string): Record<string, unknown> {
123
+ const ctx: DeserializeContext = { refs: [] };
124
+ const parsed = JSON.parse(json);
125
+ return deserialize(parsed, ctx) as Record<string, unknown>;
126
+ }
127
+
128
+ function deserialize(value: unknown, ctx: DeserializeContext): unknown {
129
+ if (value === null) return null;
130
+
131
+ if (typeof value === "string") {
132
+ if (value === TYPE_MARKERS.UNDEFINED) return undefined;
133
+ if (value.startsWith("\x00\x00")) return value.slice(2);
134
+ if (value.startsWith(TYPE_MARKERS.DATE)) return new Date(value.slice(2));
135
+ if (value.startsWith(TYPE_MARKERS.URL)) return new URL(value.slice(2));
136
+ if (value.startsWith(TYPE_MARKERS.REGEXP)) {
137
+ const str = value.slice(2);
138
+ const match = str.match(/^\/(.*)\/([gimsuy]*)$/);
139
+ return match ? new RegExp(match[1], match[2]) : str;
140
+ }
141
+ if (value.startsWith(TYPE_MARKERS.BIGINT)) return BigInt(value.slice(2));
142
+ if (value.startsWith(TYPE_MARKERS.SYMBOL)) return Symbol(value.slice(2));
143
+ if (value.startsWith(TYPE_MARKERS.REF)) {
144
+ return ctx.refs[parseInt(value.slice(2), 10)];
145
+ }
146
+ return value;
147
+ }
148
+
149
+ if (typeof value === "boolean" || typeof value === "number") {
150
+ return value;
151
+ }
152
+
153
+ if (Array.isArray(value)) {
154
+ const marker = value[0];
155
+
156
+ if (marker === TYPE_MARKERS.ERROR) {
157
+ const [, name, message, stack] = value as [string, string, string, string];
158
+ const error = new Error(message);
159
+ error.name = name;
160
+ if (stack) error.stack = stack;
161
+ ctx.refs.push(error);
162
+ return error;
163
+ }
164
+
165
+ if (marker === TYPE_MARKERS.MAP) {
166
+ const map = new Map();
167
+ ctx.refs.push(map);
168
+ for (let i = 1; i < value.length; i++) {
169
+ const [key, nested] = value[i] as [unknown, unknown];
170
+ map.set(deserialize(key, ctx), deserialize(nested, ctx));
171
+ }
172
+ return map;
173
+ }
174
+
175
+ if (marker === TYPE_MARKERS.SET) {
176
+ const set = new Set();
177
+ ctx.refs.push(set);
178
+ for (let i = 1; i < value.length; i++) {
179
+ set.add(deserialize(value[i], ctx));
180
+ }
181
+ return set;
182
+ }
183
+
184
+ const arr: unknown[] = [];
185
+ ctx.refs.push(arr);
186
+ for (const item of value) {
187
+ arr.push(deserialize(item, ctx));
188
+ }
189
+ return arr;
190
+ }
191
+
192
+ if (typeof value === "object") {
193
+ const obj: Record<string, unknown> = {};
194
+ ctx.refs.push(obj);
195
+ for (const [key, nested] of Object.entries(value)) {
196
+ obj[key] = deserialize(nested, ctx);
197
+ }
198
+ return obj;
199
+ }
200
+
201
+ return value;
202
+ }
203
+
204
+ export function isSerializable(value: unknown): boolean {
205
+ if (value === null || value === undefined) return true;
206
+
207
+ const type = typeof value;
208
+ if (type === "boolean" || type === "number" || type === "string" || type === "bigint") {
209
+ return true;
210
+ }
211
+
212
+ if (type === "function" || type === "symbol") {
213
+ return false;
214
+ }
215
+
216
+ if (value instanceof Date || value instanceof URL || value instanceof RegExp) {
217
+ return true;
218
+ }
219
+
220
+ if (value instanceof Map || value instanceof Set) {
221
+ return true;
222
+ }
223
+
224
+ if (Array.isArray(value)) {
225
+ return value.every(isSerializable);
226
+ }
227
+
228
+ if (type === "object") {
229
+ return Object.values(value as object).every(isSerializable);
230
+ }
231
+
232
+ return false;
233
+ }