@mandujs/core 0.54.17 → 0.54.18
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 +54 -16
- package/src/agent/context.ts +17 -0
- package/src/agent/types.ts +32 -12
- 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 +440 -8
- package/src/bundler/build.ts +455 -132
- 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/diagnose/__tests__/checks.test.ts +117 -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 +43 -6
- package/src/router/client-entry.ts +33 -12
- package/src/router/fs-routes.test.ts +388 -1
- package/src/router/fs-routes.ts +16 -3
- package/src/router/fs-scanner.ts +166 -18
- package/src/router/fs-types.ts +4 -1
- package/src/runtime/__tests__/page-render-response.test.ts +212 -0
- package/src/runtime/handlers.ts +50 -26
- package/src/runtime/page-render-response.ts +1 -0
- package/src/runtime/ssr.ts +16 -5
- package/src/runtime/streaming-ssr.ts +119 -76
- package/src/spec/schema.ts +31 -5
|
@@ -11,6 +11,391 @@ async function mkRepoTempDir(prefix: string): Promise<string> {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe("generateManifest hydration config", () => {
|
|
14
|
+
it("records compiler-discovered client boundaries on page routes", async () => {
|
|
15
|
+
const rootDir = await mkRepoTempDir("routes-boundaries-");
|
|
16
|
+
try {
|
|
17
|
+
await mkdir(path.join(rootDir, "app", "pledges", "[id]"), { recursive: true });
|
|
18
|
+
await mkdir(path.join(rootDir, "src", "client", "widgets", "comments-section"), { recursive: true });
|
|
19
|
+
await mkdir(path.join(rootDir, "src", "client", "widgets", "activity"), { recursive: true });
|
|
20
|
+
await writeFile(
|
|
21
|
+
path.join(rootDir, "app", "pledges", "[id]", "page.tsx"),
|
|
22
|
+
`
|
|
23
|
+
import { CommentsSection } from "@/client/widgets/comments-section/CommentsSection.client";
|
|
24
|
+
import ActivityFeed from "@/client/widgets/activity/ActivityFeed.client";
|
|
25
|
+
|
|
26
|
+
export default function Page({ params }) {
|
|
27
|
+
return (
|
|
28
|
+
<main>
|
|
29
|
+
<CommentsSection pledgeId={params.id} initialComments={[]} />
|
|
30
|
+
<ActivityFeed pledgeId={params.id} />
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
"utf-8",
|
|
36
|
+
);
|
|
37
|
+
await writeFile(
|
|
38
|
+
path.join(rootDir, "src", "client", "widgets", "comments-section", "CommentsSection.client.tsx"),
|
|
39
|
+
`"use client";
|
|
40
|
+
export function CommentsSection() {
|
|
41
|
+
return <section />;
|
|
42
|
+
}`,
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
await writeFile(
|
|
46
|
+
path.join(rootDir, "src", "client", "widgets", "activity", "ActivityFeed.client.tsx"),
|
|
47
|
+
`"use client";
|
|
48
|
+
export default function ActivityFeed() {
|
|
49
|
+
return <aside />;
|
|
50
|
+
}`,
|
|
51
|
+
"utf-8",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = await generateManifest(rootDir);
|
|
55
|
+
const route = result.manifest.routes.find((entry) => entry.id === "pledges-$id");
|
|
56
|
+
|
|
57
|
+
expect(route?.hydration?.strategy).toBe("island");
|
|
58
|
+
expect(route?.clientModule).toBeUndefined();
|
|
59
|
+
expect(route?.boundaries?.map(({ id, module, importSpecifier, exportName, localName, hydrate, propsSource, propsKeys, hasSpreadProps }) => ({
|
|
60
|
+
id,
|
|
61
|
+
module,
|
|
62
|
+
importSpecifier,
|
|
63
|
+
exportName,
|
|
64
|
+
localName,
|
|
65
|
+
hydrate,
|
|
66
|
+
propsSource,
|
|
67
|
+
propsKeys,
|
|
68
|
+
hasSpreadProps,
|
|
69
|
+
}))).toEqual([
|
|
70
|
+
{
|
|
71
|
+
id: "pledges-$id--0",
|
|
72
|
+
module: "src/client/widgets/comments-section/CommentsSection.client.tsx",
|
|
73
|
+
importSpecifier: "@/client/widgets/comments-section/CommentsSection.client",
|
|
74
|
+
exportName: "CommentsSection",
|
|
75
|
+
localName: "CommentsSection",
|
|
76
|
+
hydrate: "visible",
|
|
77
|
+
propsSource: "inline",
|
|
78
|
+
propsKeys: ["pledgeId", "initialComments"],
|
|
79
|
+
hasSpreadProps: false,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "pledges-$id--1",
|
|
83
|
+
module: "src/client/widgets/activity/ActivityFeed.client.tsx",
|
|
84
|
+
importSpecifier: "@/client/widgets/activity/ActivityFeed.client",
|
|
85
|
+
exportName: "default",
|
|
86
|
+
localName: "ActivityFeed",
|
|
87
|
+
hydrate: "visible",
|
|
88
|
+
propsSource: "inline",
|
|
89
|
+
propsKeys: ["pledgeId"],
|
|
90
|
+
hasSpreadProps: false,
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
expect(route?.boundaries?.[0]?.source).toEqual({
|
|
94
|
+
file: "app/pledges/[id]/page.tsx",
|
|
95
|
+
line: expect.any(Number),
|
|
96
|
+
column: expect.any(Number),
|
|
97
|
+
});
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not preserve stale route-level clientModule when compiler boundaries own the client imports", async () => {
|
|
104
|
+
const rootDir = await mkRepoTempDir("routes-boundaries-stale-client-");
|
|
105
|
+
try {
|
|
106
|
+
await mkdir(path.join(rootDir, ".mandu"), { recursive: true });
|
|
107
|
+
await mkdir(path.join(rootDir, "app", "pledges", "[id]"), { recursive: true });
|
|
108
|
+
await mkdir(path.join(rootDir, "src", "client", "widgets", "comments-section"), { recursive: true });
|
|
109
|
+
await writeFile(
|
|
110
|
+
path.join(rootDir, "app", "pledges", "[id]", "page.tsx"),
|
|
111
|
+
`
|
|
112
|
+
import { CommentsSection } from "@/client/widgets/comments-section/CommentsSection.client";
|
|
113
|
+
|
|
114
|
+
export default function Page({ params }) {
|
|
115
|
+
return <main><CommentsSection pledgeId={params.id} initialComments={[]} /></main>;
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
"utf-8",
|
|
119
|
+
);
|
|
120
|
+
await writeFile(
|
|
121
|
+
path.join(rootDir, "src", "client", "widgets", "comments-section", "CommentsSection.client.tsx"),
|
|
122
|
+
`"use client";
|
|
123
|
+
export function CommentsSection() {
|
|
124
|
+
return <section />;
|
|
125
|
+
}`,
|
|
126
|
+
"utf-8",
|
|
127
|
+
);
|
|
128
|
+
await writeFile(
|
|
129
|
+
path.join(rootDir, ".mandu", "routes.manifest.json"),
|
|
130
|
+
JSON.stringify({
|
|
131
|
+
version: 1,
|
|
132
|
+
routes: [
|
|
133
|
+
{
|
|
134
|
+
id: "pledges-$id",
|
|
135
|
+
pattern: "/pledges/:id",
|
|
136
|
+
module: "app/pledges/[id]/page.tsx",
|
|
137
|
+
kind: "page",
|
|
138
|
+
componentModule: "app/pledges/[id]/page.tsx",
|
|
139
|
+
clientModule: "src/client/widgets/comments-section/CommentsSection.client.tsx",
|
|
140
|
+
clientExportName: "CommentsSection",
|
|
141
|
+
hydration: { strategy: "island", priority: "visible", preload: false },
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
}, null, 2),
|
|
145
|
+
"utf-8",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = await generateManifest(rootDir);
|
|
149
|
+
const route = result.manifest.routes.find((entry) => entry.id === "pledges-$id");
|
|
150
|
+
|
|
151
|
+
expect(route?.clientModule).toBeUndefined();
|
|
152
|
+
expect(route?.clientExportName).toBeUndefined();
|
|
153
|
+
expect(route?.hydration?.strategy).toBe("island");
|
|
154
|
+
expect(route?.boundaries?.map(({ id, module, exportName }) => ({ id, module, exportName }))).toEqual([
|
|
155
|
+
{
|
|
156
|
+
id: "pledges-$id--0",
|
|
157
|
+
module: "src/client/widgets/comments-section/CommentsSection.client.tsx",
|
|
158
|
+
exportName: "CommentsSection",
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
} finally {
|
|
162
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("records client boundaries hidden behind route-owned server wrappers", async () => {
|
|
167
|
+
const rootDir = await mkRepoTempDir("routes-boundary-wrapper-");
|
|
168
|
+
try {
|
|
169
|
+
await mkdir(path.join(rootDir, "app", "pledges", "[id]"), { recursive: true });
|
|
170
|
+
await mkdir(path.join(rootDir, "src", "client", "widgets"), { recursive: true });
|
|
171
|
+
await writeFile(
|
|
172
|
+
path.join(rootDir, "app", "pledges", "[id]", "page.tsx"),
|
|
173
|
+
`
|
|
174
|
+
import { CommentsPanel } from "./CommentsPanel";
|
|
175
|
+
|
|
176
|
+
export default function Page({ params }) {
|
|
177
|
+
return <main><CommentsPanel pledgeId={params.id} /></main>;
|
|
178
|
+
}
|
|
179
|
+
`,
|
|
180
|
+
"utf-8",
|
|
181
|
+
);
|
|
182
|
+
await writeFile(
|
|
183
|
+
path.join(rootDir, "app", "pledges", "[id]", "CommentsPanel.tsx"),
|
|
184
|
+
`
|
|
185
|
+
import { CommentsSection } from "@/client/widgets/CommentsSection.client";
|
|
186
|
+
|
|
187
|
+
export function CommentsPanel({ pledgeId }) {
|
|
188
|
+
return <CommentsSection pledgeId={pledgeId} />;
|
|
189
|
+
}
|
|
190
|
+
`,
|
|
191
|
+
"utf-8",
|
|
192
|
+
);
|
|
193
|
+
await writeFile(
|
|
194
|
+
path.join(rootDir, "src", "client", "widgets", "CommentsSection.client.tsx"),
|
|
195
|
+
`"use client";
|
|
196
|
+
export function CommentsSection() {
|
|
197
|
+
return <section />;
|
|
198
|
+
}`,
|
|
199
|
+
"utf-8",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await generateManifest(rootDir);
|
|
203
|
+
const route = result.manifest.routes.find((entry) => entry.id === "pledges-$id");
|
|
204
|
+
|
|
205
|
+
expect(route?.clientModule).toBeUndefined();
|
|
206
|
+
expect(route?.hydration?.strategy).toBe("island");
|
|
207
|
+
expect(route?.boundaries?.map(({ id, module, importSpecifier, exportName, localName, source }) => ({
|
|
208
|
+
id,
|
|
209
|
+
module,
|
|
210
|
+
importSpecifier,
|
|
211
|
+
exportName,
|
|
212
|
+
localName,
|
|
213
|
+
sourceFile: source.file,
|
|
214
|
+
}))).toEqual([
|
|
215
|
+
{
|
|
216
|
+
id: "pledges-$id--0",
|
|
217
|
+
module: "src/client/widgets/CommentsSection.client.tsx",
|
|
218
|
+
importSpecifier: "@/client/widgets/CommentsSection.client",
|
|
219
|
+
exportName: "CommentsSection",
|
|
220
|
+
localName: "CommentsSection",
|
|
221
|
+
sourceFile: "app/pledges/[id]/CommentsPanel.tsx",
|
|
222
|
+
},
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
await writeFile(
|
|
226
|
+
path.join(rootDir, "app", "pledges", "[id]", "CommentsPanel.tsx"),
|
|
227
|
+
`
|
|
228
|
+
export function CommentsPanel({ pledgeId }) {
|
|
229
|
+
return <section>{pledgeId}</section>;
|
|
230
|
+
}
|
|
231
|
+
`,
|
|
232
|
+
"utf-8",
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const second = await generateManifest(rootDir);
|
|
236
|
+
const secondRoute = second.manifest.routes.find((entry) => entry.id === "pledges-$id");
|
|
237
|
+
expect(secondRoute?.clientModule).toBeUndefined();
|
|
238
|
+
expect(secondRoute?.boundaries).toBeUndefined();
|
|
239
|
+
expect(secondRoute?.hydration?.strategy).not.toBe("island");
|
|
240
|
+
} finally {
|
|
241
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("fails route manifest generation for unsupported client boundary children", async () => {
|
|
246
|
+
const rootDir = await mkRepoTempDir("routes-boundary-children-");
|
|
247
|
+
try {
|
|
248
|
+
await mkdir(path.join(rootDir, "app", "cards"), { recursive: true });
|
|
249
|
+
await mkdir(path.join(rootDir, "src", "client"), { recursive: true });
|
|
250
|
+
await writeFile(
|
|
251
|
+
path.join(rootDir, "app", "cards", "page.tsx"),
|
|
252
|
+
`
|
|
253
|
+
import Card from "@/client/Card.client";
|
|
254
|
+
|
|
255
|
+
export default function Page() {
|
|
256
|
+
return <main><Card><span>server child</span></Card></main>;
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
"utf-8",
|
|
260
|
+
);
|
|
261
|
+
await writeFile(
|
|
262
|
+
path.join(rootDir, "src", "client", "Card.client.tsx"),
|
|
263
|
+
`"use client";
|
|
264
|
+
export default function Card() {
|
|
265
|
+
return <section />;
|
|
266
|
+
}`,
|
|
267
|
+
"utf-8",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
await expect(generateManifest(rootDir)).rejects.toThrow(/MANDU_BOUNDARY_UNSUPPORTED_CHILDREN/);
|
|
271
|
+
await expect(generateManifest(rootDir)).rejects.toThrow(/app\/cards\/page\.tsx/);
|
|
272
|
+
} finally {
|
|
273
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("fails route manifest generation for statically visible non-serializable boundary props", async () => {
|
|
278
|
+
const rootDir = await mkRepoTempDir("routes-boundary-nonserializable-");
|
|
279
|
+
try {
|
|
280
|
+
await mkdir(path.join(rootDir, "app", "settings"), { recursive: true });
|
|
281
|
+
await mkdir(path.join(rootDir, "src", "client"), { recursive: true });
|
|
282
|
+
await writeFile(
|
|
283
|
+
path.join(rootDir, "app", "settings", "page.tsx"),
|
|
284
|
+
`
|
|
285
|
+
import SettingsPanel from "@/client/SettingsPanel.client";
|
|
286
|
+
|
|
287
|
+
export default function Page() {
|
|
288
|
+
return <main><SettingsPanel config={{ onSave: () => "server function" }} /></main>;
|
|
289
|
+
}
|
|
290
|
+
`,
|
|
291
|
+
"utf-8",
|
|
292
|
+
);
|
|
293
|
+
await writeFile(
|
|
294
|
+
path.join(rootDir, "src", "client", "SettingsPanel.client.tsx"),
|
|
295
|
+
`"use client";
|
|
296
|
+
export default function SettingsPanel() {
|
|
297
|
+
return <section />;
|
|
298
|
+
}`,
|
|
299
|
+
"utf-8",
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
let errorMessage = "";
|
|
303
|
+
try {
|
|
304
|
+
await generateManifest(rootDir);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
errorMessage = String(error);
|
|
307
|
+
}
|
|
308
|
+
expect(errorMessage).toContain("MANDU_BOUNDARY_UNSUPPORTED_PROP_VALUE");
|
|
309
|
+
expect(errorMessage).toContain("Suggestion:");
|
|
310
|
+
expect(errorMessage).toContain("route=settings");
|
|
311
|
+
} finally {
|
|
312
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("fails route manifest generation for unresolved client boundary named exports", async () => {
|
|
317
|
+
const rootDir = await mkRepoTempDir("routes-boundary-missing-export-");
|
|
318
|
+
try {
|
|
319
|
+
await mkdir(path.join(rootDir, "app", "profile"), { recursive: true });
|
|
320
|
+
await mkdir(path.join(rootDir, "src", "client"), { recursive: true });
|
|
321
|
+
await writeFile(
|
|
322
|
+
path.join(rootDir, "app", "profile", "page.tsx"),
|
|
323
|
+
`
|
|
324
|
+
import { MissingProfile } from "@/client/Profile.client";
|
|
325
|
+
|
|
326
|
+
export default function Page() {
|
|
327
|
+
return <main><MissingProfile label="profile" /></main>;
|
|
328
|
+
}
|
|
329
|
+
`,
|
|
330
|
+
"utf-8",
|
|
331
|
+
);
|
|
332
|
+
await writeFile(
|
|
333
|
+
path.join(rootDir, "src", "client", "Profile.client.tsx"),
|
|
334
|
+
`"use client";
|
|
335
|
+
export function ExistingProfile() {
|
|
336
|
+
return <section />;
|
|
337
|
+
}`,
|
|
338
|
+
"utf-8",
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
let errorMessage = "";
|
|
342
|
+
try {
|
|
343
|
+
await generateManifest(rootDir);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
errorMessage = String(error);
|
|
346
|
+
}
|
|
347
|
+
expect(errorMessage).toContain("MANDU_BOUNDARY_UNRESOLVED_EXPORT");
|
|
348
|
+
expect(errorMessage).toContain("MissingProfile");
|
|
349
|
+
expect(errorMessage).toContain("route=profile");
|
|
350
|
+
expect(errorMessage).toContain("Suggestion:");
|
|
351
|
+
} finally {
|
|
352
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("fails route manifest generation for server-only imports inside client boundaries", async () => {
|
|
357
|
+
const rootDir = await mkRepoTempDir("routes-boundary-server-only-");
|
|
358
|
+
try {
|
|
359
|
+
await mkdir(path.join(rootDir, "app", "files"), { recursive: true });
|
|
360
|
+
await mkdir(path.join(rootDir, "src", "client"), { recursive: true });
|
|
361
|
+
await writeFile(
|
|
362
|
+
path.join(rootDir, "app", "files", "page.tsx"),
|
|
363
|
+
`
|
|
364
|
+
import { FilePanel } from "@/client/FilePanel.client";
|
|
365
|
+
|
|
366
|
+
export default function Page() {
|
|
367
|
+
return <main><FilePanel label="files" /></main>;
|
|
368
|
+
}
|
|
369
|
+
`,
|
|
370
|
+
"utf-8",
|
|
371
|
+
);
|
|
372
|
+
await writeFile(
|
|
373
|
+
path.join(rootDir, "src", "client", "FilePanel.client.tsx"),
|
|
374
|
+
`"use client";
|
|
375
|
+
import { readFile } from "node:fs/promises";
|
|
376
|
+
import { Database } from "bun:sqlite";
|
|
377
|
+
export function FilePanel() {
|
|
378
|
+
return <section>{String(readFile)}{String(Database)}</section>;
|
|
379
|
+
}`,
|
|
380
|
+
"utf-8",
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
let errorMessage = "";
|
|
384
|
+
try {
|
|
385
|
+
await generateManifest(rootDir);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
errorMessage = String(error);
|
|
388
|
+
}
|
|
389
|
+
expect(errorMessage).toContain("MANDU_BOUNDARY_SERVER_ONLY_IMPORT");
|
|
390
|
+
expect(errorMessage).toContain("node:fs/promises");
|
|
391
|
+
expect(errorMessage).toContain("bun:sqlite");
|
|
392
|
+
expect(errorMessage).toContain("src/client/FilePanel.client.tsx");
|
|
393
|
+
expect(errorMessage).toContain("Suggestion:");
|
|
394
|
+
} finally {
|
|
395
|
+
await rm(rootDir, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
14
399
|
it("does not preserve stale island hydration after the client entry disappears", async () => {
|
|
15
400
|
const rootDir = await mkRepoTempDir("routes-hydration-stale-");
|
|
16
401
|
try {
|
|
@@ -39,7 +424,8 @@ describe("generateManifest hydration config", () => {
|
|
|
39
424
|
|
|
40
425
|
const first = await generateManifest(rootDir);
|
|
41
426
|
const firstRoute = first.manifest.routes.find((route) => route.id === "candidates-$id");
|
|
42
|
-
expect(firstRoute?.clientModule).
|
|
427
|
+
expect(firstRoute?.clientModule).toBeUndefined();
|
|
428
|
+
expect(firstRoute?.boundaries?.[0]?.module).toContain("PledgeAccordion.client.tsx");
|
|
43
429
|
expect(firstRoute?.hydration?.strategy).toBe("island");
|
|
44
430
|
|
|
45
431
|
await writeFile(
|
|
@@ -55,6 +441,7 @@ describe("generateManifest hydration config", () => {
|
|
|
55
441
|
const second = await generateManifest(rootDir);
|
|
56
442
|
const secondRoute = second.manifest.routes.find((route) => route.id === "candidates-$id");
|
|
57
443
|
expect(secondRoute?.clientModule).toBeUndefined();
|
|
444
|
+
expect(secondRoute?.boundaries).toBeUndefined();
|
|
58
445
|
expect(secondRoute?.hydration?.strategy).not.toBe("island");
|
|
59
446
|
} finally {
|
|
60
447
|
await rm(rootDir, { recursive: true, force: true });
|
package/src/router/fs-routes.ts
CHANGED
|
@@ -100,9 +100,21 @@ export function fsRouteToRouteSpec(fsRoute: FSRouteConfig): RouteSpec {
|
|
|
100
100
|
: fsRoute.hydration
|
|
101
101
|
? { hydration: fsRoute.hydration }
|
|
102
102
|
: {}),
|
|
103
|
-
...(fsRoute.
|
|
104
|
-
? {
|
|
105
|
-
|
|
103
|
+
...(fsRoute.boundaries && fsRoute.boundaries.length > 0
|
|
104
|
+
? {
|
|
105
|
+
boundaries: fsRoute.boundaries.map((boundary) => ({
|
|
106
|
+
...boundary,
|
|
107
|
+
module: normalizePath(boundary.module),
|
|
108
|
+
source: {
|
|
109
|
+
...boundary.source,
|
|
110
|
+
file: normalizePath(boundary.source.file),
|
|
111
|
+
},
|
|
112
|
+
})),
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
115
|
+
...(fsRoute.layoutChain && fsRoute.layoutChain.length > 0
|
|
116
|
+
? { layoutChain: fsRoute.layoutChain.map(normalizePath) }
|
|
117
|
+
: {}),
|
|
106
118
|
...(fsRoute.loadingModule ? { loadingModule: normalizePath(fsRoute.loadingModule) } : {}),
|
|
107
119
|
...(fsRoute.errorModule ? { errorModule: normalizePath(fsRoute.errorModule) } : {}),
|
|
108
120
|
...(fsRoute.notFoundModule ? { notFoundModule: normalizePath(fsRoute.notFoundModule) } : {}),
|
|
@@ -319,6 +331,7 @@ export async function generateManifest(
|
|
|
319
331
|
if (
|
|
320
332
|
prev.clientModule &&
|
|
321
333
|
!route.clientModule &&
|
|
334
|
+
!(route.kind === "page" && route.boundaries && route.boundaries.length > 0) &&
|
|
322
335
|
await shouldPreserveExistingClientModule(route, prev.clientModule, rootDir)
|
|
323
336
|
) {
|
|
324
337
|
route.clientModule = prev.clientModule;
|