@mandujs/core 0.54.18 → 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.
- package/package.json +1 -1
- package/src/agent/__tests__/context.test.ts +40 -9
- package/src/agent/verify.ts +55 -24
- package/src/bundler/build.test.ts +40 -3
- package/src/bundler/build.ts +35 -680
- package/src/client/__tests__/props-serialization.test.ts +37 -0
- package/src/client/hydrate.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/props-serialization.ts +233 -0
- package/src/client/runtime-entry.ts +567 -0
- package/src/client/runtime.ts +1 -1
- package/src/client/serialize.ts +50 -404
- package/src/diagnose/__tests__/checks.test.ts +15 -0
- package/src/diagnose/checks.ts +1 -1
- package/src/router/client-entry.test.ts +111 -23
- package/src/router/client-entry.ts +78 -301
- package/src/router/fs-routes.test.ts +55 -0
- package/src/router/fs-scanner.ts +11 -40
- package/src/router/fs-types.ts +7 -1
- package/src/router/route-source-analyzer.ts +521 -0
- package/src/runtime/__tests__/inline-client-hydration.test.ts +104 -1
- package/src/runtime/__tests__/page-render-response.test.ts +6 -0
- package/src/runtime/page-render-response.ts +23 -1
- package/src/runtime/server.ts +14 -0
|
@@ -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
|
+
});
|
package/src/client/hydrate.ts
CHANGED
|
@@ -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
|
|
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:
|
package/src/client/index.ts
CHANGED
|
@@ -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 (
|
|
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
|
+
}
|