@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
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import type { RouteSpec } from "../spec/schema";
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { RouteSpec } from "../spec/schema";
|
|
4
|
+
import {
|
|
5
|
+
formatClientBoundaryDiagnostics,
|
|
6
|
+
validateClientBoundaryServerOnlyImports,
|
|
7
|
+
} from "../bundler/client-boundary-transform";
|
|
8
|
+
import { analyzeRouteSource, type RouteSourceImportRecord } from "./route-source-analyzer";
|
|
4
9
|
|
|
5
10
|
export interface ClientComponentImport {
|
|
6
11
|
module: string;
|
|
@@ -30,13 +35,13 @@ export function normalizeRouteModulePath(value: string | undefined): string {
|
|
|
30
35
|
return (value ?? "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export function hasUseClientDirective(source: string): boolean {
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function hasUseServerDirective(source: string): boolean {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
38
|
+
export function hasUseClientDirective(source: string): boolean {
|
|
39
|
+
return analyzeRouteSource(source).directives.useClient;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasUseServerDirective(source: string): boolean {
|
|
43
|
+
return analyzeRouteSource(source).directives.useServer;
|
|
44
|
+
}
|
|
40
45
|
|
|
41
46
|
export function clientModuleIsRouteComponent(route: RouteSpec, clientModule = route.clientModule): boolean {
|
|
42
47
|
if (route.kind !== "page" || !clientModule) return false;
|
|
@@ -71,77 +76,65 @@ function toPublicClientComponentImport(entry: ComponentImportRecord): ClientComp
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
function findComponentImportRecords(source: string): ComponentImportRecord[] {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const sideEffectPattern = /import\s+["']([^"']+)["']/g;
|
|
77
|
-
|
|
78
|
-
for (const match of source.matchAll(importFromPattern)) {
|
|
79
|
-
const clause = (match[1] ?? "").trim();
|
|
80
|
-
const module = match[2] ?? "";
|
|
81
|
-
const names: string[] = [];
|
|
82
|
-
const specifiers: ComponentImportSpecifier[] = [];
|
|
83
|
-
let hasDefault = false;
|
|
84
|
-
let hasNamed = false;
|
|
85
|
-
let hasNamespace = false;
|
|
86
|
-
|
|
87
|
-
const namedMatch = clause.match(/\{([^}]*)\}/);
|
|
88
|
-
if (namedMatch) {
|
|
89
|
-
hasNamed = true;
|
|
90
|
-
for (const rawName of namedMatch[1].split(",")) {
|
|
91
|
-
const name = rawName.trim();
|
|
92
|
-
if (!name) continue;
|
|
93
|
-
const parts = name.split(/\s+as\s+/i).map((part) => part.trim()).filter(Boolean);
|
|
94
|
-
const importedName = parts[0] ?? "";
|
|
95
|
-
const localName = parts[1] ?? importedName;
|
|
96
|
-
names.push(localName);
|
|
97
|
-
specifiers.push({ importedName, localName });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
79
|
+
return findComponentImportRecordsFromAnalysis(analyzeRouteSource(source).imports);
|
|
80
|
+
}
|
|
100
81
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (namespaceMatch?.[1]) {
|
|
105
|
-
names.push(namespaceMatch[1]);
|
|
106
|
-
specifiers.push({ importedName: "*", localName: namespaceMatch[1] });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
82
|
+
function findComponentImportRecordsFromAnalysis(imports: RouteSourceImportRecord[]): ComponentImportRecord[] {
|
|
83
|
+
return imports.flatMap(toComponentImportRecord);
|
|
84
|
+
}
|
|
109
85
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const localName = beforeNamed.split(",")[0].trim();
|
|
114
|
-
names.push(localName);
|
|
115
|
-
specifiers.push({ importedName: DEFAULT_EXPORT_NAME, localName });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const kind =
|
|
119
|
-
(hasDefault && (hasNamed || hasNamespace))
|
|
120
|
-
? "mixed"
|
|
121
|
-
: hasNamed
|
|
122
|
-
? "named"
|
|
123
|
-
: hasNamespace
|
|
124
|
-
? "namespace"
|
|
125
|
-
: "default";
|
|
126
|
-
|
|
127
|
-
imports.push({ module, kind, names, specifiers });
|
|
86
|
+
function toComponentImportRecord(entry: RouteSourceImportRecord): ComponentImportRecord[] {
|
|
87
|
+
if (entry.isSideEffectOnly) {
|
|
88
|
+
return [{ module: entry.source, kind: "side-effect", names: [], specifiers: [] }];
|
|
128
89
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
90
|
+
if (entry.isTypeOnly) return [];
|
|
91
|
+
|
|
92
|
+
const names: string[] = [];
|
|
93
|
+
const specifiers: ComponentImportSpecifier[] = [];
|
|
94
|
+
let hasDefault = false;
|
|
95
|
+
let hasNamed = false;
|
|
96
|
+
let hasNamespace = false;
|
|
97
|
+
|
|
98
|
+
if (entry.defaultName) {
|
|
99
|
+
hasDefault = true;
|
|
100
|
+
names.push(entry.defaultName);
|
|
101
|
+
specifiers.push({ importedName: DEFAULT_EXPORT_NAME, localName: entry.defaultName });
|
|
134
102
|
}
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
|
|
104
|
+
if (entry.namespaceName) {
|
|
105
|
+
hasNamespace = true;
|
|
106
|
+
names.push(entry.namespaceName);
|
|
107
|
+
specifiers.push({ importedName: "*", localName: entry.namespaceName });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const named of entry.named) {
|
|
111
|
+
if (named.isTypeOnly) continue;
|
|
112
|
+
hasNamed = true;
|
|
113
|
+
names.push(named.local);
|
|
114
|
+
specifiers.push({ importedName: named.imported, localName: named.local });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (names.length === 0) return [];
|
|
118
|
+
|
|
119
|
+
const kind =
|
|
120
|
+
(hasDefault && (hasNamed || hasNamespace))
|
|
121
|
+
? "mixed"
|
|
122
|
+
: hasNamed
|
|
123
|
+
? "named"
|
|
124
|
+
: hasNamespace
|
|
125
|
+
? "namespace"
|
|
126
|
+
: "default";
|
|
127
|
+
|
|
128
|
+
return [{ module: entry.source, kind, names, specifiers }];
|
|
129
|
+
}
|
|
138
130
|
|
|
139
131
|
export function findRouteLevelClientComponentImport(source: string): RouteLevelClientComponentImport | null {
|
|
140
132
|
return findRouteLevelClientComponentImports(source)[0] ?? null;
|
|
141
133
|
}
|
|
142
134
|
|
|
143
135
|
export function findRouteLevelClientComponentImports(source: string): RouteLevelClientComponentImport[] {
|
|
144
|
-
const
|
|
136
|
+
const analysis = analyzeRouteSource(source);
|
|
137
|
+
const candidates = findComponentImportRecordsFromAnalysis(analysis.imports).flatMap((entry) =>
|
|
145
138
|
isRouteLevelClientEntrySpecifier(entry.module)
|
|
146
139
|
? entry.specifiers
|
|
147
140
|
.filter((specifier) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(specifier.localName))
|
|
@@ -152,10 +145,10 @@ export function findRouteLevelClientComponentImports(source: string): RouteLevel
|
|
|
152
145
|
}))
|
|
153
146
|
: []
|
|
154
147
|
);
|
|
155
|
-
|
|
156
|
-
if (candidates.length === 0) return [];
|
|
157
|
-
return defaultExportRendersClientComponents(
|
|
158
|
-
}
|
|
148
|
+
|
|
149
|
+
if (candidates.length === 0) return [];
|
|
150
|
+
return defaultExportRendersClientComponents(analysis.defaultExport.renderedJsxNames, candidates);
|
|
151
|
+
}
|
|
159
152
|
|
|
160
153
|
function isRouteLevelClientEntrySpecifier(specifier: string): boolean {
|
|
161
154
|
const normalized = specifier.replace(/\\/g, "/");
|
|
@@ -268,241 +261,29 @@ function clientSpecifierLooksBrowserOnly(specifier: string): boolean {
|
|
|
268
261
|
return CLIENT_ENTRY_SPECIFIER_PATTERN.test(specifier.replace(/\\/g, "/"));
|
|
269
262
|
}
|
|
270
263
|
|
|
271
|
-
function defaultExportRendersClientComponents(
|
|
272
|
-
|
|
273
|
-
candidates: RouteLevelClientComponentImport[],
|
|
274
|
-
): RouteLevelClientComponentImport[] {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return returned !== null ? jsxExpressionRendersClientComponents(returned, candidates) : [];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function extractDefaultExportFunctionBody(source: string): string | null {
|
|
296
|
-
const match = /export\s+default\s+(?:async\s+)?function(?:\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*\([^)]*\)\s*(?::\s*[^{=]+)?\{/m.exec(source);
|
|
297
|
-
if (!match) return null;
|
|
298
|
-
|
|
299
|
-
const openBrace = match.index + match[0].lastIndexOf("{");
|
|
300
|
-
const closeBrace = findMatchingBrace(source, openBrace);
|
|
301
|
-
if (closeBrace === -1) return null;
|
|
302
|
-
return source.slice(openBrace + 1, closeBrace);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function extractDefaultExportArrowExpression(source: string): string | null {
|
|
306
|
-
const match = /export\s+default\s+(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*/m.exec(source);
|
|
307
|
-
if (!match) return null;
|
|
308
|
-
|
|
309
|
-
const start = match.index + match[0].length;
|
|
310
|
-
const rest = source.slice(start).trim();
|
|
311
|
-
if (rest.startsWith("{")) return null;
|
|
312
|
-
|
|
313
|
-
const semicolon = rest.indexOf(";");
|
|
314
|
-
return semicolon === -1 ? rest : rest.slice(0, semicolon);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function extractDefaultExportArrowFunctionBody(source: string): string | null {
|
|
318
|
-
const match = /export\s+default\s+(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/m.exec(source);
|
|
319
|
-
if (!match) return null;
|
|
320
|
-
|
|
321
|
-
const openBrace = match.index + match[0].lastIndexOf("{");
|
|
322
|
-
const closeBrace = findMatchingBrace(source, openBrace);
|
|
323
|
-
if (closeBrace === -1) return null;
|
|
324
|
-
return source.slice(openBrace + 1, closeBrace);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function extractTopLevelReturnExpression(body: string): string | null {
|
|
328
|
-
let quote: '"' | "'" | "`" | null = null;
|
|
329
|
-
let lineComment = false;
|
|
330
|
-
let blockComment = false;
|
|
331
|
-
let braceDepth = 0;
|
|
332
|
-
let parenDepth = 0;
|
|
333
|
-
let bracketDepth = 0;
|
|
334
|
-
|
|
335
|
-
for (let i = 0; i < body.length; i++) {
|
|
336
|
-
const char = body[i];
|
|
337
|
-
const next = body[i + 1];
|
|
338
|
-
const prev = body[i - 1];
|
|
339
|
-
|
|
340
|
-
if (lineComment) {
|
|
341
|
-
if (char === "\n" || char === "\r") lineComment = false;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (blockComment) {
|
|
346
|
-
if (char === "*" && next === "/") {
|
|
347
|
-
blockComment = false;
|
|
348
|
-
i++;
|
|
349
|
-
}
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (quote) {
|
|
354
|
-
if (char === quote && prev !== "\\") quote = null;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (char === "/" && next === "/") {
|
|
359
|
-
lineComment = true;
|
|
360
|
-
i++;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
if (char === "/" && next === "*") {
|
|
364
|
-
blockComment = true;
|
|
365
|
-
i++;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
369
|
-
quote = char;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (braceDepth === 0 && parenDepth === 0 && bracketDepth === 0 && body.startsWith("return", i)) {
|
|
374
|
-
const before = body[i - 1] ?? "";
|
|
375
|
-
const after = body[i + "return".length] ?? "";
|
|
376
|
-
if (!isIdentifierChar(before) && !isIdentifierChar(after)) {
|
|
377
|
-
const expr = body.slice(i + "return".length).trim();
|
|
378
|
-
return trimTrailingSemicolon(expr);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (char === "{") braceDepth++;
|
|
383
|
-
if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
384
|
-
if (char === "(") parenDepth++;
|
|
385
|
-
if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
386
|
-
if (char === "[") bracketDepth++;
|
|
387
|
-
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function trimTrailingSemicolon(value: string): string {
|
|
394
|
-
const trimmed = value.trim();
|
|
395
|
-
return trimmed.endsWith(";") ? trimmed.slice(0, -1).trim() : trimmed;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function jsxExpressionRendersClientComponents(
|
|
399
|
-
expression: string,
|
|
400
|
-
candidates: RouteLevelClientComponentImport[],
|
|
401
|
-
): RouteLevelClientComponentImport[] {
|
|
402
|
-
const expr = stripWrappingParentheses(expression.trim());
|
|
403
|
-
const seen = new Set<string>();
|
|
404
|
-
const rendered: RouteLevelClientComponentImport[] = [];
|
|
405
|
-
|
|
406
|
-
for (const candidate of candidates) {
|
|
407
|
-
const key = `${candidate.module}\0${candidate.localName}`;
|
|
408
|
-
if (seen.has(key)) continue;
|
|
409
|
-
if (!jsxExpressionContainsClientElement(expr, candidate.localName)) continue;
|
|
410
|
-
seen.add(key);
|
|
411
|
-
rendered.push(candidate);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return rendered;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function jsxExpressionContainsClientElement(expression: string, localName: string): boolean {
|
|
418
|
-
const escaped = escapeRegExp(localName);
|
|
419
|
-
return new RegExp(`<${escaped}(?:\\s|/|>)`).test(expression);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function stripWrappingParentheses(value: string): string {
|
|
423
|
-
let current = value.trim();
|
|
424
|
-
while (current.startsWith("(") && current.endsWith(")")) {
|
|
425
|
-
const close = findMatchingParen(current, 0);
|
|
426
|
-
if (close !== current.length - 1) break;
|
|
427
|
-
current = current.slice(1, -1).trim();
|
|
428
|
-
}
|
|
429
|
-
return current;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function findMatchingBrace(source: string, openIndex: number): number {
|
|
433
|
-
return findMatchingDelimiter(source, openIndex, "{", "}");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function findMatchingParen(source: string, openIndex: number): number {
|
|
437
|
-
return findMatchingDelimiter(source, openIndex, "(", ")");
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function findMatchingDelimiter(source: string, openIndex: number, open: string, close: string): number {
|
|
441
|
-
let depth = 0;
|
|
442
|
-
let quote: '"' | "'" | "`" | null = null;
|
|
443
|
-
let lineComment = false;
|
|
444
|
-
let blockComment = false;
|
|
445
|
-
|
|
446
|
-
for (let i = openIndex; i < source.length; i++) {
|
|
447
|
-
const char = source[i];
|
|
448
|
-
const next = source[i + 1];
|
|
449
|
-
const prev = source[i - 1];
|
|
450
|
-
|
|
451
|
-
if (lineComment) {
|
|
452
|
-
if (char === "\n" || char === "\r") lineComment = false;
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (blockComment) {
|
|
457
|
-
if (char === "*" && next === "/") {
|
|
458
|
-
blockComment = false;
|
|
459
|
-
i++;
|
|
460
|
-
}
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (quote) {
|
|
465
|
-
if (char === quote && prev !== "\\") quote = null;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (char === "/" && next === "/") {
|
|
470
|
-
lineComment = true;
|
|
471
|
-
i++;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (char === "/" && next === "*") {
|
|
475
|
-
blockComment = true;
|
|
476
|
-
i++;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
480
|
-
quote = char;
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (char === open) depth++;
|
|
485
|
-
if (char === close) {
|
|
486
|
-
depth--;
|
|
487
|
-
if (depth === 0) return i;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return -1;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function escapeRegExp(value: string): string {
|
|
495
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function isIdentifierChar(value: string): boolean {
|
|
499
|
-
return /[A-Za-z0-9_$]/.test(value);
|
|
500
|
-
}
|
|
264
|
+
function defaultExportRendersClientComponents(
|
|
265
|
+
renderedJsxNames: string[],
|
|
266
|
+
candidates: RouteLevelClientComponentImport[],
|
|
267
|
+
): RouteLevelClientComponentImport[] {
|
|
268
|
+
const renderedNames = new Set(renderedJsxNames);
|
|
269
|
+
const seen = new Set<string>();
|
|
270
|
+
const rendered: RouteLevelClientComponentImport[] = [];
|
|
271
|
+
|
|
272
|
+
for (const candidate of candidates) {
|
|
273
|
+
const key = `${candidate.module}\0${candidate.localName}`;
|
|
274
|
+
if (seen.has(key)) continue;
|
|
275
|
+
if (!renderedNames.has(candidate.localName)) continue;
|
|
276
|
+
seen.add(key);
|
|
277
|
+
rendered.push(candidate);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return rendered;
|
|
281
|
+
}
|
|
501
282
|
|
|
502
|
-
export async function validateClientModuleForBrowserBundle(
|
|
503
|
-
route: RouteSpec,
|
|
504
|
-
rootDir: string,
|
|
505
|
-
): Promise<string | null> {
|
|
283
|
+
export async function validateClientModuleForBrowserBundle(
|
|
284
|
+
route: RouteSpec,
|
|
285
|
+
rootDir: string,
|
|
286
|
+
): Promise<string | null> {
|
|
506
287
|
if (!route.clientModule) return null;
|
|
507
288
|
|
|
508
289
|
const source = await readRouteModule(rootDir, route.clientModule);
|
|
@@ -517,17 +298,34 @@ export async function validateClientModuleForBrowserBundle(
|
|
|
517
298
|
if (realClientEntry) {
|
|
518
299
|
route.clientModule = realClientEntry.modulePath;
|
|
519
300
|
route.clientExportName = realClientEntry.exportName;
|
|
520
|
-
|
|
301
|
+
const realSource = await readRouteModule(rootDir, route.clientModule);
|
|
302
|
+
return realSource === null ? null : formatClientModuleBrowserDiagnostics(route, realSource);
|
|
521
303
|
}
|
|
522
304
|
return (
|
|
523
305
|
`[${route.id}] Route component "${route.clientModule}" is configured as clientModule, ` +
|
|
524
306
|
`but it is a server page (missing "use client"). Mandu will not bundle server pages into client islands. ` +
|
|
525
307
|
`Remove the stale clientModule from .mandu/routes.manifest.json or use a *.partial.tsx / spec/slots/${route.id}.client.tsx client entry.`
|
|
526
308
|
);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return
|
|
530
|
-
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return formatClientModuleBrowserDiagnostics(route, source);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function formatClientModuleBrowserDiagnostics(route: RouteSpec, source: string): string | null {
|
|
315
|
+
if (!route.clientModule) return null;
|
|
316
|
+
const diagnostics = validateClientBoundaryServerOnlyImports(
|
|
317
|
+
source,
|
|
318
|
+
{
|
|
319
|
+
id: `${route.id}--client-module`,
|
|
320
|
+
routeId: route.id,
|
|
321
|
+
module: route.clientModule,
|
|
322
|
+
exportName: route.clientExportName ?? "default",
|
|
323
|
+
},
|
|
324
|
+
route.clientModule,
|
|
325
|
+
);
|
|
326
|
+
if (diagnostics.length === 0) return null;
|
|
327
|
+
return formatClientBoundaryDiagnostics(diagnostics);
|
|
328
|
+
}
|
|
531
329
|
|
|
532
330
|
async function routeComponentHasResolvableClientEntry(
|
|
533
331
|
rootDir: string,
|