@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.
- package/package.json +3 -1
- package/src/agent/__tests__/context.test.ts +94 -25
- package/src/agent/context.ts +17 -0
- package/src/agent/types.ts +32 -12
- package/src/agent/verify.ts +55 -24
- package/src/bundler/__snapshots__/build.test.ts.snap +5 -0
- package/src/bundler/__tests__/build-runner.ts +130 -17
- package/src/bundler/__tests__/client-boundary-transform.test.ts +524 -0
- package/src/bundler/__tests__/reverse-import-graph.test.ts +42 -33
- package/src/bundler/build.test.ts +478 -9
- package/src/bundler/build.ts +424 -746
- package/src/bundler/client-boundary-transform.ts +977 -0
- package/src/bundler/dev.ts +39 -112
- package/src/bundler/fast-refresh-preamble.ts +47 -0
- package/src/bundler/index.ts +3 -2
- package/src/bundler/manifest-schema.ts +10 -0
- package/src/bundler/types.ts +20 -2
- 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 +132 -17
- package/src/diagnose/checks.ts +184 -3
- package/src/diagnose/run.ts +10 -8
- package/src/generator/templates.test.ts +48 -5
- package/src/generator/templates.ts +10 -1
- package/src/internal/client-boundary.ts +266 -0
- package/src/internal/index.ts +2 -1
- package/src/router/client-entry.test.ts +154 -29
- package/src/router/client-entry.ts +111 -313
- package/src/router/fs-routes.test.ts +443 -1
- package/src/router/fs-routes.ts +16 -3
- package/src/router/fs-scanner.ts +176 -57
- package/src/router/fs-types.ts +11 -2
- 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 +218 -0
- package/src/runtime/handlers.ts +50 -26
- package/src/runtime/page-render-response.ts +24 -1
- package/src/runtime/server.ts +14 -0
- package/src/runtime/ssr.ts +16 -5
- package/src/runtime/streaming-ssr.ts +119 -76
- package/src/spec/schema.ts +31 -5
package/src/client/serialize.ts
CHANGED
|
@@ -1,404 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 public API.
|
|
3
|
+
*
|
|
4
|
+
* Core serialization/deserialization lives in `props-serialization.ts` so the
|
|
5
|
+
* browser runtime and server render path share exactly one wire format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { deserializeProps, serializeProps } from "./props-serialization";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
deserializeProps,
|
|
12
|
+
isSerializable,
|
|
13
|
+
serializeProps,
|
|
14
|
+
} from "./props-serialization";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* SSRμμ ν΄λΌμ΄μΈνΈλ‘ props μ λ¬μ© μ€ν¬λ¦½νΈ μμ±
|
|
18
|
+
*/
|
|
19
|
+
export function generatePropsScript(
|
|
20
|
+
islandId: string,
|
|
21
|
+
props: Record<string, unknown>
|
|
22
|
+
): string {
|
|
23
|
+
const json = serializeProps(props);
|
|
24
|
+
const escaped = json
|
|
25
|
+
.replace(/</g, "\\u003c")
|
|
26
|
+
.replace(/>/g, "\\u003e")
|
|
27
|
+
.replace(/&/g, "\\u0026");
|
|
28
|
+
|
|
29
|
+
return `<script type="application/json" data-mandu-props="${islandId}">${escaped}</script>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ν΄λΌμ΄μΈνΈμμ props μ€ν¬λ¦½νΈ νμ±
|
|
34
|
+
*/
|
|
35
|
+
export function parsePropsScript(islandId: string): Record<string, unknown> | null {
|
|
36
|
+
if (typeof document === "undefined") return null;
|
|
37
|
+
|
|
38
|
+
const script = document.querySelector(
|
|
39
|
+
`script[data-mandu-props="${islandId}"]`
|
|
40
|
+
) as HTMLScriptElement | null;
|
|
41
|
+
|
|
42
|
+
if (!script?.textContent) return null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return deserializeProps(script.textContent);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(`[Mandu] Failed to parse props for island ${islandId}:`, err);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
checkPrerenderPollution,
|
|
17
17
|
checkCloneElementWarnings,
|
|
18
18
|
checkDevArtifactsInProd,
|
|
19
|
-
checkPackageExportGaps,
|
|
20
|
-
checkNestedInternalCore,
|
|
21
|
-
|
|
19
|
+
checkPackageExportGaps,
|
|
20
|
+
checkNestedInternalCore,
|
|
21
|
+
checkClientBoundaryManifests,
|
|
22
|
+
} from "../checks";
|
|
22
23
|
import { runExtendedDiagnose, buildReport } from "../run";
|
|
23
24
|
|
|
24
25
|
async function mkTmpRoot(): Promise<string> {
|
|
@@ -324,7 +325,7 @@ describe("checkPackageExportGaps", () => {
|
|
|
324
325
|
// nested_internal_core (#261)
|
|
325
326
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
326
327
|
|
|
327
|
-
describe("checkNestedInternalCore", () => {
|
|
328
|
+
describe("checkNestedInternalCore", () => {
|
|
328
329
|
let rootDir: string;
|
|
329
330
|
beforeEach(async () => { rootDir = await mkTmpRoot(); });
|
|
330
331
|
afterEach(async () => { await fs.rm(rootDir, { recursive: true, force: true }); });
|
|
@@ -387,21 +388,134 @@ describe("checkNestedInternalCore", () => {
|
|
|
387
388
|
expect(result.ok).toBe(false);
|
|
388
389
|
expect(result.details?.mismatchCount).toBe(2);
|
|
389
390
|
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
393
|
-
//
|
|
394
|
-
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
394
|
+
// client_boundary_manifests
|
|
395
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
396
|
+
|
|
397
|
+
describe("checkClientBoundaryManifests", () => {
|
|
398
|
+
let rootDir: string;
|
|
399
|
+
beforeEach(async () => { rootDir = await mkTmpRoot(); });
|
|
400
|
+
afterEach(async () => { await fs.rm(rootDir, { recursive: true, force: true }); });
|
|
401
|
+
|
|
402
|
+
it("skips gracefully when routes manifest is missing", async () => {
|
|
403
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
404
|
+
expect(result.ok).toBe(true);
|
|
405
|
+
expect(result.details?.skipped).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("passes when the routes manifest has no compiler-owned boundaries", async () => {
|
|
409
|
+
await writeFile(rootDir, ".mandu/routes.manifest.json", JSON.stringify({
|
|
410
|
+
routes: [{ id: "home", kind: "page" }],
|
|
411
|
+
}));
|
|
412
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
413
|
+
expect(result.ok).toBe(true);
|
|
414
|
+
expect(result.details?.boundaryCount).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("flags duplicate client boundary ids in the routes manifest", async () => {
|
|
418
|
+
await writeFile(rootDir, ".mandu/routes.manifest.json", JSON.stringify({
|
|
419
|
+
routes: [
|
|
420
|
+
{
|
|
421
|
+
id: "a",
|
|
422
|
+
boundaries: [{ id: "dup--0", routeId: "a", module: "src/client/A.client.tsx", exportName: "A" }],
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: "b",
|
|
426
|
+
boundaries: [{ id: "dup--0", routeId: "b", module: "src/client/B.client.tsx", exportName: "B" }],
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
}));
|
|
430
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
431
|
+
expect(result.ok).toBe(false);
|
|
432
|
+
expect(result.severity).toBe("error");
|
|
433
|
+
expect(result.message).toMatch(/duplicate/);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("flags missing boundary bundle manifest entries", async () => {
|
|
437
|
+
await writeFile(rootDir, ".mandu/routes.manifest.json", JSON.stringify({
|
|
438
|
+
routes: [
|
|
439
|
+
{
|
|
440
|
+
id: "home",
|
|
441
|
+
boundaries: [{ id: "home--0", routeId: "home", module: "src/client/Home.client.tsx", exportName: "Home" }],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
}));
|
|
445
|
+
await writeFile(rootDir, ".mandu/manifest.json", JSON.stringify({
|
|
446
|
+
version: 1,
|
|
447
|
+
buildTime: "x",
|
|
448
|
+
env: "production",
|
|
449
|
+
bundles: {},
|
|
450
|
+
boundaries: {},
|
|
451
|
+
shared: { runtime: "/.mandu/client/_runtime.js", vendor: "/.mandu/client/_react.js" },
|
|
452
|
+
}));
|
|
453
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
454
|
+
expect(result.ok).toBe(false);
|
|
455
|
+
expect(result.severity).toBe("error");
|
|
456
|
+
expect(result.message).toMatch(/incomplete/);
|
|
457
|
+
expect(result.details?.missingEntries).toEqual(["home--0"]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("treats a missing bundle manifest as an error when routes declare compiler-owned boundaries", async () => {
|
|
461
|
+
await writeFile(rootDir, ".mandu/routes.manifest.json", JSON.stringify({
|
|
462
|
+
routes: [
|
|
463
|
+
{
|
|
464
|
+
id: "home",
|
|
465
|
+
boundaries: [{ id: "home--0", routeId: "home", module: "src/client/Home.client.tsx", exportName: "Home" }],
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
}));
|
|
469
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
470
|
+
expect(result.ok).toBe(false);
|
|
471
|
+
expect(result.severity).toBe("error");
|
|
472
|
+
expect(result.message).toMatch(/bundle manifest is missing/);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("passes when route and bundle boundary manifests line up", async () => {
|
|
476
|
+
await writeFile(rootDir, ".mandu/routes.manifest.json", JSON.stringify({
|
|
477
|
+
routes: [
|
|
478
|
+
{
|
|
479
|
+
id: "home",
|
|
480
|
+
boundaries: [{ id: "home--0", routeId: "home", module: "src/client/Home.client.tsx", exportName: "Home" }],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
}));
|
|
484
|
+
await writeFile(rootDir, ".mandu/client/home--0.boundary.js", "export default function Home() {}\n");
|
|
485
|
+
await writeFile(rootDir, ".mandu/manifest.json", JSON.stringify({
|
|
486
|
+
version: 1,
|
|
487
|
+
buildTime: "x",
|
|
488
|
+
env: "production",
|
|
489
|
+
bundles: {},
|
|
490
|
+
boundaries: {
|
|
491
|
+
"home--0": {
|
|
492
|
+
route: "home",
|
|
493
|
+
js: "/.mandu/client/home--0.boundary.js",
|
|
494
|
+
module: "src/client/Home.client.tsx",
|
|
495
|
+
exportName: "Home",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
shared: { runtime: "/.mandu/client/_runtime.js", vendor: "/.mandu/client/_react.js" },
|
|
499
|
+
}));
|
|
500
|
+
const result = await checkClientBoundaryManifests(rootDir);
|
|
501
|
+
expect(result.ok).toBe(true);
|
|
502
|
+
expect(result.details?.boundaryCount).toBe(1);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
507
|
+
// aggregator
|
|
508
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
395
509
|
|
|
396
510
|
describe("runExtendedDiagnose", () => {
|
|
397
511
|
let rootDir: string;
|
|
398
512
|
beforeEach(async () => { rootDir = await mkTmpRoot(); });
|
|
399
513
|
afterEach(async () => { await fs.rm(rootDir, { recursive: true, force: true }); });
|
|
400
514
|
|
|
401
|
-
it("runs all
|
|
402
|
-
const report = await runExtendedDiagnose(rootDir);
|
|
403
|
-
//
|
|
404
|
-
expect(report.summary.total).toBe(
|
|
515
|
+
it("runs all 8 extended checks and returns a structured report", async () => {
|
|
516
|
+
const report = await runExtendedDiagnose(rootDir);
|
|
517
|
+
// F42/F45 added `client_boundary_manifests` β total is now 8.
|
|
518
|
+
expect(report.summary.total).toBe(8);
|
|
405
519
|
// manifest is missing β at least one error
|
|
406
520
|
expect(report.healthy).toBe(false);
|
|
407
521
|
expect(report.errorCount).toBeGreaterThanOrEqual(1);
|
|
@@ -410,10 +524,11 @@ describe("runExtendedDiagnose", () => {
|
|
|
410
524
|
expect(rules).toContain("prerender_pollution");
|
|
411
525
|
expect(rules).toContain("cloneelement_warnings");
|
|
412
526
|
expect(rules).toContain("dev_artifacts_in_prod");
|
|
413
|
-
expect(rules).toContain("package_export_gaps");
|
|
414
|
-
expect(rules).toContain("nested_internal_core");
|
|
415
|
-
expect(rules).toContain("
|
|
416
|
-
|
|
527
|
+
expect(rules).toContain("package_export_gaps");
|
|
528
|
+
expect(rules).toContain("nested_internal_core");
|
|
529
|
+
expect(rules).toContain("client_boundary_manifests");
|
|
530
|
+
expect(rules).toContain("a11y_hints");
|
|
531
|
+
});
|
|
417
532
|
|
|
418
533
|
it("returns healthy=true when all checks pass (production manifest, no gaps)", async () => {
|
|
419
534
|
await writeFile(rootDir, ".mandu/manifest.json", JSON.stringify({
|