rsbuild-plugin-react-router 0.5.0 → 0.6.0

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 (110) hide show
  1. package/README.md +58 -208
  2. package/dist/511.js +370 -331
  3. package/dist/build-output-transforms.d.ts +30 -6
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -1
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/index.cjs +3957 -2208
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.js +3470 -1793
  19. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  20. package/dist/manifest.d.ts +16 -4
  21. package/dist/mode-plan.d.ts +82 -0
  22. package/dist/modify-browser-manifest.d.ts +8 -4
  23. package/dist/plugin-utils.d.ts +27 -1
  24. package/dist/prerender-build.d.ts +5 -5
  25. package/dist/prerender.d.ts +1 -5
  26. package/dist/react-router-config.d.ts +11 -3
  27. package/dist/route-artifacts.d.ts +4 -2
  28. package/dist/route-chunks.d.ts +2 -1
  29. package/dist/route-component-transform.d.ts +0 -2
  30. package/dist/route-export-pruning.d.ts +4 -1
  31. package/dist/route-imports.d.ts +18 -0
  32. package/dist/route-transform-tasks.d.ts +5 -0
  33. package/dist/route-watch.d.ts +11 -6
  34. package/dist/rsc-dev-server.d.ts +26 -0
  35. package/dist/rsc-prerender.d.ts +54 -0
  36. package/dist/rsc-route-config.d.ts +6 -0
  37. package/dist/rsc-route-exports.d.ts +15 -0
  38. package/dist/rsc-route-transform-loader.cjs +43 -0
  39. package/dist/rsc-route-transform-loader.d.ts +30 -0
  40. package/dist/rsc-route-transform-loader.js +16 -0
  41. package/dist/rsc-route-transform-registration.d.ts +12 -0
  42. package/dist/rsc-route-transforms.d.ts +21 -0
  43. package/dist/rsc-support.d.ts +23 -0
  44. package/dist/rsc-virtual-modules.d.ts +19 -0
  45. package/dist/server-build-plan.d.ts +2 -1
  46. package/dist/server-build-resolution.d.ts +1 -2
  47. package/dist/server-utils.d.ts +4 -5
  48. package/dist/ssr-asset-relocation.d.ts +98 -0
  49. package/dist/templates/entry.rsc.client.d.ts +1 -0
  50. package/dist/templates/entry.rsc.client.js +61 -0
  51. package/dist/templates/entry.rsc.d.ts +9 -0
  52. package/dist/templates/entry.rsc.js +38 -0
  53. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  54. package/dist/templates/entry.rsc.ssr.js +24 -0
  55. package/dist/typegen.d.ts +4 -2
  56. package/dist/types.d.ts +30 -1
  57. package/package.json +69 -14
  58. package/src/build-output-transforms.ts +155 -21
  59. package/src/classic-mode.ts +253 -0
  60. package/src/config-imports.ts +153 -6
  61. package/src/constants.ts +6 -2
  62. package/src/dev-background-resources.ts +46 -85
  63. package/src/dev-generation.ts +52 -33
  64. package/src/dev-hmr.ts +112 -80
  65. package/src/dev-runtime-artifacts.ts +11 -12
  66. package/src/dev-runtime-controller.ts +75 -85
  67. package/src/dev-runtime-session.ts +14 -18
  68. package/src/dev-server.ts +2 -0
  69. package/src/dev-source-maps.ts +257 -0
  70. package/src/effect-runtime.ts +105 -57
  71. package/src/entry-paths.ts +80 -0
  72. package/src/environment-output.ts +55 -0
  73. package/src/export-utils.ts +15 -11
  74. package/src/index.ts +661 -496
  75. package/src/lazy-compilation-prewarm.ts +20 -4
  76. package/src/manifest.ts +157 -82
  77. package/src/mode-plan.ts +367 -0
  78. package/src/modify-browser-manifest.ts +130 -124
  79. package/src/plugin-utils.ts +103 -39
  80. package/src/prerender-build.ts +82 -104
  81. package/src/prerender.ts +23 -24
  82. package/src/react-router-config.ts +72 -26
  83. package/src/route-artifacts.ts +96 -66
  84. package/src/route-chunks.ts +167 -51
  85. package/src/route-component-transform.ts +14 -21
  86. package/src/route-export-pruning.ts +4 -3
  87. package/src/route-imports.ts +100 -0
  88. package/src/route-transform-tasks.ts +39 -25
  89. package/src/route-watch.ts +166 -188
  90. package/src/rsc-dev-server.ts +112 -0
  91. package/src/rsc-prerender.ts +362 -0
  92. package/src/rsc-route-config.ts +175 -0
  93. package/src/rsc-route-exports.ts +67 -0
  94. package/src/rsc-route-transform-loader.ts +65 -0
  95. package/src/rsc-route-transform-registration.ts +145 -0
  96. package/src/rsc-route-transforms.ts +995 -0
  97. package/src/rsc-runtime.d.ts +143 -0
  98. package/src/rsc-support.ts +116 -0
  99. package/src/rsc-virtual-modules.ts +113 -0
  100. package/src/server-build-plan.ts +14 -3
  101. package/src/server-build-resolution.ts +37 -47
  102. package/src/server-utils.ts +21 -35
  103. package/src/ssr-asset-relocation.ts +183 -0
  104. package/src/ssr-externals.ts +8 -26
  105. package/src/templates/entry.rsc.client.tsx +168 -0
  106. package/src/templates/entry.rsc.ssr.tsx +45 -0
  107. package/src/templates/entry.rsc.tsx +80 -0
  108. package/src/typegen.ts +40 -23
  109. package/src/types.ts +39 -1
  110. package/src/warnings/warn-on-client-source-maps.ts +6 -10
@@ -0,0 +1,995 @@
1
+ import { dirname } from 'pathe';
2
+
3
+ import { generate, parse } from './yuku.js';
4
+ import {
5
+ CLIENT_NON_COMPONENT_EXPORTS,
6
+ SERVER_ONLY_ROUTE_EXPORTS,
7
+ } from './constants.js';
8
+ import { getExportNames } from './export-utils.js';
9
+ import {
10
+ getExportedName,
11
+ getPatternIdentifierNames,
12
+ getProgram,
13
+ type AnyNode,
14
+ type ProgramNode,
15
+ } from './route-ast.js';
16
+ import {
17
+ collectReferencedNames,
18
+ removeExports,
19
+ removeUnusedImports,
20
+ } from './route-export-pruning.js';
21
+ import { resolveQuerylessRouteImportRequest } from './route-imports.js';
22
+ import {
23
+ createEmptyRouteChunkByExportName,
24
+ detectRouteChunks,
25
+ routeChunkExportNames,
26
+ validateRouteChunks,
27
+ type RouteChunkCache,
28
+ type RouteChunkConfig,
29
+ type RouteChunkExportName,
30
+ } from './route-chunks.js';
31
+ import {
32
+ RSC_CLIENT_COMPONENT_EXPORTS,
33
+ RSC_MUTUALLY_EXCLUSIVE_ROUTE_EXPORTS,
34
+ RSC_SERVER_COMPONENT_EXPORTS,
35
+ } from './rsc-route-exports.js';
36
+ import type { Route } from './types.js';
37
+
38
+ // Canonical client HMR navigate snippet for generated route chunks: read the
39
+ // live data router, strip the configured basename from the current URL, and
40
+ // perform a scroll-preserving `replace` navigation so a hot update re-renders
41
+ // the route tree in place.
42
+ const RSC_HMR_NAVIGATE_SNIPPET = `const router = globalThis.__reactRouterDataRouter;
43
+ if (router?.navigate) {
44
+ const basename = router.basename || "/";
45
+ let pathname = location.pathname;
46
+ if (basename !== "/" && pathname.startsWith(basename)) {
47
+ pathname = pathname.slice(basename.length) || "/";
48
+ if (pathname[0] !== "/") pathname = "/" + pathname;
49
+ }
50
+ router.navigate(pathname + location.search + location.hash, { replace: true, preventScrollReset: true });
51
+ }`;
52
+
53
+ const ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR = `
54
+ import * as ___EnsureClientRouteModuleForHMR_REACT___ from "react";
55
+ export function EnsureClientRouteModuleForHMR___() { return ___EnsureClientRouteModuleForHMR_REACT___.createElement(___EnsureClientRouteModuleForHMR_REACT___.Fragment, null) }
56
+ `;
57
+
58
+ const CLIENT_ROUTE_EXPORTS = [
59
+ ...CLIENT_NON_COMPONENT_EXPORTS,
60
+ ...RSC_CLIENT_COMPONENT_EXPORTS,
61
+ ] as const;
62
+
63
+ const SERVER_ROUTE_EXPORTS = [
64
+ ...RSC_SERVER_COMPONENT_EXPORTS,
65
+ ...SERVER_ONLY_ROUTE_EXPORTS,
66
+ ] as const;
67
+
68
+ const CLIENT_ROUTE_EXPORTS_SET = new Set<string>(CLIENT_ROUTE_EXPORTS);
69
+ const SERVER_COMPONENT_EXPORTS_SET = new Set<string>(
70
+ RSC_SERVER_COMPONENT_EXPORTS
71
+ );
72
+ const SERVER_ROUTE_EXPORTS_SET = new Set<string>(SERVER_ROUTE_EXPORTS);
73
+
74
+ const CLIENT_CHUNK_QUERY = 'client-route-module';
75
+ const SERVER_MODULE_QUERY = 'server-route-module';
76
+ const ROUTE_CLIENT_MODULE_CHUNK = 'route';
77
+
78
+ // Bare, specifier-less side-effect style imports (`import "./x.css"`). Matches
79
+ // plain CSS, vanilla-extract `.css.ts`/`.css.js`, and preprocessor extensions,
80
+ // with or without a trailing query.
81
+ const STYLE_SIDE_EFFECT_IMPORT_SOURCE =
82
+ /\.(css|scss|sass|less|styl|stylus)(\.(ts|js))?(\?|$)/;
83
+
84
+ const isSideEffectStyleImport = (statement: AnyNode): boolean => {
85
+ if (
86
+ statement.type !== 'ImportDeclaration' ||
87
+ (statement.specifiers ?? []).length > 0
88
+ ) {
89
+ return false;
90
+ }
91
+ const source = statement.source;
92
+ return (
93
+ typeof source?.value === 'string' &&
94
+ STYLE_SIDE_EFFECT_IMPORT_SOURCE.test(source.value)
95
+ );
96
+ };
97
+
98
+ const programHasSideEffectStyleImports = (program: ProgramNode): boolean =>
99
+ (program.body ?? []).some(isSideEffectStyleImport);
100
+
101
+ // A vanilla-extract style imported for its VALUE exports — the generated scoped
102
+ // class names, e.g. `import * as s from "./x.css"` consumed as `s.index`, or
103
+ // `import { index } from "./x.css"`. Vanilla-extract files live on disk as
104
+ // `x.css.ts`/`x.css.js` but are imported through the `.css` specifier, so match
105
+ // the specifier's `.css` extension. Plain side-effect `.css` imports have no
106
+ // value specifiers and never reach this predicate; CSS Modules (`.module.css`,
107
+ // handled by rspack's native CSS pipeline and retained) and `?url` asset imports
108
+ // are excluded so only vanilla's value imports qualify.
109
+ const isVanillaValueImportSource = (source: string): boolean => {
110
+ const queryIndex = source.indexOf('?');
111
+ const pathPart = queryIndex === -1 ? source : source.slice(0, queryIndex);
112
+ const query = queryIndex === -1 ? '' : source.slice(queryIndex + 1);
113
+ if (!pathPart.endsWith('.css') || pathPart.endsWith('.module.css')) {
114
+ return false;
115
+ }
116
+ // `?url` (and any query flagging an asset-URL import) never carries value
117
+ // exports; keep those on the existing `?url` relocation path.
118
+ return !/(^|&)url(&|=|$)/.test(query);
119
+ };
120
+
121
+ // Collect the sources of vanilla-extract value imports that lack a co-located
122
+ // bare side-effect import of the same file.
123
+ //
124
+ // On a server-component route, the component consuming those class names renders
125
+ // only in the RSC (node) compilation. Because the module is imported for its JS
126
+ // value (not as a `import "./x.css"` side effect), rspack tree-shakes the style
127
+ // module's CSS-emitting side effect out of the graph the client build and the
128
+ // RSC runtime's `entryCssFiles` are derived from — the class name is applied but
129
+ // no stylesheet defines it (padding stays `0px`). A plain `globalStyle`
130
+ // side-effect import (`import "./x.css"`) is retained and extracts fine; adding
131
+ // a matching bare side-effect import for value imports forces the scoped CSS
132
+ // back into the module graph so it extracts to a client asset and streams as a
133
+ // `<link>` at first paint, mirroring the global-style path.
134
+ const collectVanillaValueImportSources = (program: ProgramNode): string[] => {
135
+ const valueSources = new Set<string>();
136
+ const bareSources = new Set<string>();
137
+ for (const statement of program.body ?? []) {
138
+ if (statement.type !== 'ImportDeclaration') {
139
+ continue;
140
+ }
141
+ const source = statement.source;
142
+ if (typeof source?.value !== 'string') {
143
+ continue;
144
+ }
145
+ if (!isVanillaValueImportSource(source.value)) {
146
+ continue;
147
+ }
148
+ if ((statement.specifiers ?? []).length > 0) {
149
+ valueSources.add(source.value);
150
+ } else {
151
+ bareSources.add(source.value);
152
+ }
153
+ }
154
+ return [...valueSources].filter(source => !bareSources.has(source));
155
+ };
156
+
157
+ // Synthetic export appended to a *client* route's server module so the native
158
+ // rspack `RscServerPlugin` records the route's bundled CSS in `entryCssFiles`.
159
+ // A client route's default component is a client reference whose bundled
160
+ // (non-vanilla) side-effect CSS is hoisted into the initial browser entry chunk
161
+ // rather than the reference's own async chunk, so the reference's `cssFiles`
162
+ // never captures it and no `<link>` is streamed. Marking the server module
163
+ // `'use server-entry'` and giving it a component export makes the RSC runtime
164
+ // attribute that module graph's CSS to this carrier, which
165
+ // `createServerRouteEntry` then streams as stylesheet links (mirrors the
166
+ // server-component path).
167
+ const RSC_ROUTE_STYLE_ENTRY_EXPORT = 'RscRouteStyleEntry___';
168
+
169
+ type RscRouteTransformOptions = {
170
+ code: string;
171
+ resourcePath: string;
172
+ resourceQuery?: string;
173
+ isRootRoute: boolean;
174
+ routeId: string;
175
+ routeByFilePath?: ReadonlyMap<string, Route>;
176
+ routeChunkCache: RouteChunkCache;
177
+ routeChunkConfig: RouteChunkConfig;
178
+ isServerEnvironment: boolean;
179
+ isDev: boolean;
180
+ };
181
+
182
+ type RscRouteTransformResult = {
183
+ code: string;
184
+ map: null | ReturnType<typeof generate>['map'];
185
+ };
186
+
187
+ type RscRouteTransformTarget =
188
+ | { kind: 'client-route-module'; chunk: string }
189
+ | { kind: 'server-route-module' }
190
+ | { kind: 'server-route-entry' }
191
+ | { kind: 'client-route-entry' };
192
+
193
+ type RscRouteExportPlan = {
194
+ exportNames: readonly string[];
195
+ exportNameSet: ReadonlySet<string>;
196
+ routeChunks: ReturnType<typeof getRouteChunks>;
197
+ sharedClientTarget: string;
198
+ serverTarget: string;
199
+ needsDefaultRootErrorBoundary: boolean;
200
+ clientTargetFor(exportName: string): string;
201
+ };
202
+
203
+ const createResourceQueryParams = (
204
+ resourceQuery: string | undefined
205
+ ): URLSearchParams =>
206
+ new URLSearchParams((resourceQuery ?? '').replace(/^\?/, ''));
207
+
208
+ const createRouteModuleQueryParams = (
209
+ resourceQuery: string | undefined
210
+ ): URLSearchParams => {
211
+ const params = createResourceQueryParams(resourceQuery);
212
+ params.delete(CLIENT_CHUNK_QUERY);
213
+ params.delete(SERVER_MODULE_QUERY);
214
+ return params;
215
+ };
216
+
217
+ const createClientRouteModuleId = (
218
+ resourcePath: string,
219
+ resourceQuery: string | undefined,
220
+ value: string
221
+ ): string => {
222
+ const params = createRouteModuleQueryParams(resourceQuery);
223
+ params.set(CLIENT_CHUNK_QUERY, value);
224
+ return `${resourcePath}?${params.toString()}`;
225
+ };
226
+
227
+ const createServerRouteModuleId = (
228
+ resourcePath: string,
229
+ resourceQuery: string | undefined
230
+ ): string => {
231
+ const params = createRouteModuleQueryParams(resourceQuery);
232
+ params.set(SERVER_MODULE_QUERY, '');
233
+ return `${resourcePath}?${params.toString()}`;
234
+ };
235
+
236
+ const isClientRouteExport = (name: string): boolean =>
237
+ CLIENT_ROUTE_EXPORTS_SET.has(name);
238
+
239
+ const isServerRouteExport = (name: string): boolean =>
240
+ SERVER_ROUTE_EXPORTS_SET.has(name);
241
+
242
+ const isServerComponentExport = (name: string): boolean =>
243
+ SERVER_COMPONENT_EXPORTS_SET.has(name);
244
+
245
+ const isSplittableRouteChunkExport = (
246
+ exportName: string
247
+ ): exportName is RouteChunkExportName =>
248
+ routeChunkExportNames.includes(exportName as RouteChunkExportName);
249
+
250
+ const createReexport = (exportName: string, target: string): string =>
251
+ exportName === 'default'
252
+ ? `export { default } from ${JSON.stringify(target)};`
253
+ : `export { ${exportName} } from ${JSON.stringify(target)};`;
254
+
255
+ const shouldSplitRouteModules = ({
256
+ isRootRoute,
257
+ routeChunkConfig,
258
+ }: Pick<
259
+ RscRouteTransformOptions,
260
+ 'isRootRoute' | 'routeChunkConfig'
261
+ >): boolean => routeChunkConfig.splitRouteModules === 'enforce' && !isRootRoute;
262
+
263
+ const resolveRscRouteTransformTarget = ({
264
+ resourceQuery,
265
+ isServerEnvironment,
266
+ }: Pick<
267
+ RscRouteTransformOptions,
268
+ 'resourceQuery' | 'isServerEnvironment'
269
+ >): RscRouteTransformTarget => {
270
+ const params = createResourceQueryParams(resourceQuery);
271
+ const clientRouteChunk = params.get(CLIENT_CHUNK_QUERY);
272
+ if (clientRouteChunk) {
273
+ return {
274
+ kind: 'client-route-module',
275
+ chunk: clientRouteChunk,
276
+ };
277
+ }
278
+
279
+ if (params.has(SERVER_MODULE_QUERY)) {
280
+ return { kind: 'server-route-module' };
281
+ }
282
+
283
+ return {
284
+ kind: isServerEnvironment ? 'server-route-entry' : 'client-route-entry',
285
+ };
286
+ };
287
+
288
+ const getRouteChunks = ({
289
+ code,
290
+ resourcePath,
291
+ isRootRoute,
292
+ routeChunkCache,
293
+ }: Pick<
294
+ RscRouteTransformOptions,
295
+ 'code' | 'resourcePath' | 'isRootRoute' | 'routeChunkCache'
296
+ >) => {
297
+ const emptyRouteChunks = () => ({
298
+ chunkedExports: [] as RouteChunkExportName[],
299
+ hasRouteChunks: false,
300
+ hasRouteChunkByExportName: createEmptyRouteChunkByExportName(),
301
+ });
302
+
303
+ if (isRootRoute) {
304
+ return emptyRouteChunks();
305
+ }
306
+
307
+ if (!routeChunkExportNames.some(exportName => code.includes(exportName))) {
308
+ return emptyRouteChunks();
309
+ }
310
+
311
+ return detectRouteChunks(code, routeChunkCache, resourcePath);
312
+ };
313
+
314
+ const getRouteChunkValidation = (
315
+ exportNames: readonly string[],
316
+ routeChunks: ReturnType<typeof getRouteChunks>
317
+ ): Record<RouteChunkExportName, boolean> => {
318
+ const exports = new Set(exportNames);
319
+ return Object.fromEntries(
320
+ routeChunkExportNames.map(exportName => [
321
+ exportName,
322
+ !exports.has(exportName) ||
323
+ routeChunks.hasRouteChunkByExportName[exportName],
324
+ ])
325
+ ) as Record<RouteChunkExportName, boolean>;
326
+ };
327
+
328
+ const createExportReferenceMap = (program: ReturnType<typeof getProgram>) => {
329
+ const referencesByExportName = new Map<string, Set<string>>();
330
+ const addReferences = (exportName: string, node: AnyNode) => {
331
+ referencesByExportName.set(exportName, collectReferencedNames(node));
332
+ };
333
+
334
+ for (const statement of program.body ?? []) {
335
+ if (statement.type === 'ExportDefaultDeclaration') {
336
+ addReferences('default', statement);
337
+ continue;
338
+ }
339
+
340
+ if (statement.type !== 'ExportNamedDeclaration') {
341
+ continue;
342
+ }
343
+
344
+ for (const specifier of statement.specifiers ?? []) {
345
+ if (specifier.type !== 'ExportSpecifier') {
346
+ continue;
347
+ }
348
+ const exportedName = getExportedName(specifier);
349
+ if (exportedName) {
350
+ addReferences(exportedName, specifier);
351
+ }
352
+ }
353
+
354
+ const declaration = statement.declaration;
355
+ if (declaration?.type === 'VariableDeclaration') {
356
+ for (const declarator of declaration.declarations ?? []) {
357
+ for (const name of getPatternIdentifierNames(declarator.id)) {
358
+ addReferences(name, declarator);
359
+ }
360
+ }
361
+ continue;
362
+ }
363
+
364
+ if (
365
+ (declaration?.type === 'FunctionDeclaration' ||
366
+ declaration?.type === 'ClassDeclaration') &&
367
+ declaration.id?.name
368
+ ) {
369
+ addReferences(declaration.id.name, declaration);
370
+ }
371
+ }
372
+
373
+ return referencesByExportName;
374
+ };
375
+
376
+ const collectRouteClientDependencyExports = (
377
+ program: ReturnType<typeof getProgram>,
378
+ plan: RscRouteExportPlan
379
+ ): Set<string> => {
380
+ const referencesByExportName = createExportReferenceMap(program);
381
+ const dependencyExports = new Set<string>();
382
+ const pending: string[] = [];
383
+
384
+ for (const exportName of plan.exportNames) {
385
+ if (isClientRouteExport(exportName)) {
386
+ pending.push(...(referencesByExportName.get(exportName) ?? []));
387
+ }
388
+ }
389
+
390
+ while (pending.length > 0) {
391
+ const exportName = pending.pop();
392
+ if (
393
+ !exportName ||
394
+ dependencyExports.has(exportName) ||
395
+ isClientRouteExport(exportName) ||
396
+ isServerRouteExport(exportName)
397
+ ) {
398
+ continue;
399
+ }
400
+
401
+ const references = referencesByExportName.get(exportName);
402
+ if (!references) {
403
+ continue;
404
+ }
405
+
406
+ dependencyExports.add(exportName);
407
+ pending.push(...references);
408
+ }
409
+
410
+ return dependencyExports;
411
+ };
412
+
413
+ const stripPreservedDependencyExports = (
414
+ program: ReturnType<typeof getProgram>,
415
+ dependencyExports: ReadonlySet<string>
416
+ ): void => {
417
+ if (dependencyExports.size === 0) {
418
+ return;
419
+ }
420
+
421
+ for (let index = 0; index < program.body.length; index += 1) {
422
+ const statement = program.body[index];
423
+ if (statement.type !== 'ExportNamedDeclaration') {
424
+ continue;
425
+ }
426
+
427
+ if (statement.specifiers?.length) {
428
+ statement.specifiers = statement.specifiers.filter(
429
+ (specifier: AnyNode) => {
430
+ const exportedName =
431
+ specifier.type === 'ExportSpecifier'
432
+ ? getExportedName(specifier)
433
+ : null;
434
+ return !exportedName || !dependencyExports.has(exportedName);
435
+ }
436
+ );
437
+ if (statement.specifiers.length === 0 && !statement.declaration) {
438
+ program.body.splice(index, 1);
439
+ index -= 1;
440
+ continue;
441
+ }
442
+ }
443
+
444
+ const declaration = statement.declaration;
445
+ if (declaration?.type === 'VariableDeclaration') {
446
+ const preservedDeclarations: AnyNode[] = [];
447
+ const exportedDeclarations: AnyNode[] = [];
448
+ for (const declarator of declaration.declarations ?? []) {
449
+ const names = getPatternIdentifierNames(declarator.id);
450
+ if ([...names].some(name => dependencyExports.has(name))) {
451
+ preservedDeclarations.push(declarator);
452
+ } else {
453
+ exportedDeclarations.push(declarator);
454
+ }
455
+ }
456
+
457
+ if (preservedDeclarations.length === 0) {
458
+ continue;
459
+ }
460
+
461
+ const localDeclaration = {
462
+ ...declaration,
463
+ declarations: preservedDeclarations,
464
+ };
465
+ if (exportedDeclarations.length === 0) {
466
+ program.body[index] = localDeclaration;
467
+ continue;
468
+ }
469
+
470
+ program.body.splice(index, 1, localDeclaration, {
471
+ ...statement,
472
+ declaration: {
473
+ ...declaration,
474
+ declarations: exportedDeclarations,
475
+ },
476
+ });
477
+ index += 1;
478
+ continue;
479
+ }
480
+
481
+ if (
482
+ (declaration?.type === 'FunctionDeclaration' ||
483
+ declaration?.type === 'ClassDeclaration') &&
484
+ declaration.id?.name &&
485
+ dependencyExports.has(declaration.id.name)
486
+ ) {
487
+ program.body[index] = declaration;
488
+ }
489
+ }
490
+ };
491
+
492
+ const validateRouteModuleExports = (exportNames: readonly string[]): void => {
493
+ const exported = new Set(exportNames);
494
+ const errors: string[] = [];
495
+ for (const [
496
+ clientExport,
497
+ serverExport,
498
+ ] of RSC_MUTUALLY_EXCLUSIVE_ROUTE_EXPORTS) {
499
+ if (exported.has(clientExport) && exported.has(serverExport)) {
500
+ errors.push(`- ${clientExport} and ${serverExport}`);
501
+ }
502
+ }
503
+ if (errors.length > 0) {
504
+ throw new Error(
505
+ 'Invalid route module exports. The following pairs of exports are ' +
506
+ `mutually exclusive and cannot be exported from the same module:\n${errors.join(
507
+ '\n'
508
+ )}`
509
+ );
510
+ }
511
+ };
512
+
513
+ const createRscRouteExportPlan = async (
514
+ options: RscRouteTransformOptions
515
+ ): Promise<RscRouteExportPlan> => {
516
+ const exportNames = await getExportNames(options.code);
517
+ const exportNameSet = new Set(exportNames);
518
+ const routeChunks = getRouteChunks(options);
519
+ const sharedClientTarget = createClientRouteModuleId(
520
+ options.resourcePath,
521
+ options.resourceQuery,
522
+ 'shared'
523
+ );
524
+ const serverTarget = createServerRouteModuleId(
525
+ options.resourcePath,
526
+ options.resourceQuery
527
+ );
528
+
529
+ return {
530
+ exportNames,
531
+ exportNameSet,
532
+ routeChunks,
533
+ sharedClientTarget,
534
+ serverTarget,
535
+ needsDefaultRootErrorBoundary:
536
+ options.isRootRoute &&
537
+ !exportNameSet.has('ErrorBoundary') &&
538
+ !exportNameSet.has('ServerErrorBoundary'),
539
+ clientTargetFor(exportName) {
540
+ if (isSplittableRouteChunkExport(exportName)) {
541
+ const chunkName = routeChunks.hasRouteChunkByExportName[exportName]
542
+ ? exportName
543
+ : ROUTE_CLIENT_MODULE_CHUNK;
544
+ return createClientRouteModuleId(
545
+ options.resourcePath,
546
+ options.resourceQuery,
547
+ chunkName
548
+ );
549
+ }
550
+ if (isClientRouteExport(exportName)) {
551
+ return createClientRouteModuleId(
552
+ options.resourcePath,
553
+ options.resourceQuery,
554
+ ROUTE_CLIENT_MODULE_CHUNK
555
+ );
556
+ }
557
+ return createClientRouteModuleId(
558
+ options.resourcePath,
559
+ options.resourceQuery,
560
+ 'shared'
561
+ );
562
+ },
563
+ };
564
+ };
565
+
566
+ const validateRscRouteExportPlan = (
567
+ plan: RscRouteExportPlan,
568
+ options: RscRouteTransformOptions
569
+ ): void => {
570
+ validateRouteModuleExports(plan.exportNames);
571
+ if (shouldSplitRouteModules(options)) {
572
+ validateRouteChunks({
573
+ config: options.routeChunkConfig,
574
+ id: options.routeId,
575
+ valid: getRouteChunkValidation(plan.exportNames, plan.routeChunks),
576
+ });
577
+ }
578
+ };
579
+
580
+ const createClientRouteEntry = async (
581
+ options: RscRouteTransformOptions
582
+ ): Promise<RscRouteTransformResult> => {
583
+ const plan = await createRscRouteExportPlan(options);
584
+ validateRscRouteExportPlan(plan, options);
585
+ let needsReactImport = false;
586
+
587
+ const reexports = plan.exportNames
588
+ .filter(exportName => !isServerRouteExport(exportName))
589
+ .map(exportName => {
590
+ const target = plan.clientTargetFor(exportName);
591
+ if (exportName === 'HydrateFallback') {
592
+ needsReactImport = true;
593
+ return `export const HydrateFallback = React.lazy(() => import(${JSON.stringify(
594
+ target
595
+ )}).then(mod => ({ default: mod.HydrateFallback })));`;
596
+ }
597
+ if (
598
+ exportName === 'clientAction' ||
599
+ exportName === 'clientLoader' ||
600
+ exportName === 'clientMiddleware'
601
+ ) {
602
+ return `export const ${exportName} = async (...args) => import(${JSON.stringify(
603
+ target
604
+ )}).then(mod => mod.${exportName}(...args));`;
605
+ }
606
+ return createReexport(exportName, target);
607
+ });
608
+
609
+ return {
610
+ code: `"use client";\n${
611
+ needsReactImport ? 'import * as React from "react";\n' : ''
612
+ }${reexports.join('\n')}\n`,
613
+ map: null,
614
+ };
615
+ };
616
+
617
+ const createServerRouteEntry = async (
618
+ options: RscRouteTransformOptions
619
+ ): Promise<RscRouteTransformResult> => {
620
+ const plan = await createRscRouteExportPlan(options);
621
+ validateRscRouteExportPlan(plan, options);
622
+ const lines: string[] = [];
623
+ let needsReactImport = false;
624
+ let needsEnsureHmrImport = false;
625
+ let needsStyleEntryImport = false;
626
+
627
+ // A client route whose default component is a client reference: its bundled
628
+ // (non-vanilla) side-effect CSS is orphaned in the initial browser entry
629
+ // chunk, so nothing streams a `<link>` for it (see `createServerRouteModule`,
630
+ // which promotes the server module to a `'use server-entry'` CSS carrier).
631
+ // When that carrier exists, wrap the default component so its
632
+ // `entryCssFiles` are streamed as stylesheet links at first paint.
633
+ const hasServerComponent = plan.exportNames.some(isServerComponentExport);
634
+ const streamsClientRouteCss =
635
+ !hasServerComponent &&
636
+ plan.exportNames.includes('default') &&
637
+ programHasSideEffectStyleImports(
638
+ getProgram(parse(options.code, { sourceType: 'module' }))
639
+ );
640
+
641
+ for (const exportName of plan.exportNames) {
642
+ if (streamsClientRouteCss && exportName === 'default') {
643
+ needsReactImport = true;
644
+ needsStyleEntryImport = true;
645
+ lines.push(
646
+ `import RscClientRouteDefault___ from ${JSON.stringify(
647
+ plan.clientTargetFor('default')
648
+ )};`
649
+ );
650
+ lines.push(
651
+ 'export default function RscClientRouteWithStyles___(props) {'
652
+ );
653
+ lines.push(' return React.createElement(React.Fragment, null,');
654
+ lines.push(
655
+ ` ...(${RSC_ROUTE_STYLE_ENTRY_EXPORT}.entryCssFiles ?? []).map(href =>`
656
+ );
657
+ lines.push(
658
+ ' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
659
+ );
660
+ lines.push(' React.createElement(RscClientRouteDefault___, props),');
661
+ lines.push(' );');
662
+ lines.push('}');
663
+ continue;
664
+ }
665
+ if (isClientRouteExport(exportName)) {
666
+ lines.push(createReexport(exportName, plan.clientTargetFor(exportName)));
667
+ continue;
668
+ }
669
+ if (isServerComponentExport(exportName)) {
670
+ needsReactImport = true;
671
+ needsEnsureHmrImport = true;
672
+ lines.push(
673
+ `import { ${exportName} as ${exportName}WithoutClientChunk } from ${JSON.stringify(
674
+ plan.serverTarget
675
+ )};`
676
+ );
677
+ lines.push(`export function ${exportName}(props) {`);
678
+ lines.push(' return React.createElement(React.Fragment, null,');
679
+ // The server route module carries the `'use server-entry'` directive, so
680
+ // the rspack RSC runtime wraps its component export with
681
+ // `createServerEntry`, attaching `entryCssFiles` for the CSS the module
682
+ // graph contributes. Render those as stylesheet links at the top of the
683
+ // stream (mirrors upstream's `import.meta.viteRsc.loadCss()`); React's
684
+ // float support hoists them into `<head>`.
685
+ lines.push(
686
+ ` ...(${exportName}WithoutClientChunk.entryCssFiles ?? []).map(href =>`
687
+ );
688
+ lines.push(
689
+ ' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
690
+ );
691
+ lines.push(
692
+ ' React.createElement(EnsureClientRouteModuleForHMR___, null),'
693
+ );
694
+ lines.push(
695
+ ` React.createElement(${exportName}WithoutClientChunk, props),`
696
+ );
697
+ lines.push(' );');
698
+ lines.push('}');
699
+ continue;
700
+ }
701
+ lines.push(createReexport(exportName, plan.serverTarget));
702
+ }
703
+
704
+ if (plan.needsDefaultRootErrorBoundary) {
705
+ lines.push(
706
+ `export { ErrorBoundary } from ${JSON.stringify(plan.sharedClientTarget)};`
707
+ );
708
+ }
709
+
710
+ const prefixParts: string[] = [];
711
+ if (needsReactImport) {
712
+ prefixParts.push('import * as React from "react";');
713
+ }
714
+ if (needsEnsureHmrImport) {
715
+ prefixParts.push(
716
+ `import { EnsureClientRouteModuleForHMR___ } from ${JSON.stringify(
717
+ plan.sharedClientTarget
718
+ )};`
719
+ );
720
+ }
721
+ if (needsStyleEntryImport) {
722
+ prefixParts.push(
723
+ `import { ${RSC_ROUTE_STYLE_ENTRY_EXPORT} } from ${JSON.stringify(
724
+ plan.serverTarget
725
+ )};`
726
+ );
727
+ }
728
+ const prefix = prefixParts.length > 0 ? `${prefixParts.join('\n')}\n` : '';
729
+ return {
730
+ code: `${prefix}${lines.join('\n')}\n`,
731
+ map: null,
732
+ };
733
+ };
734
+
735
+ const createClientRouteModule = async (
736
+ code: string,
737
+ sourceFileName: string,
738
+ clientRouteChunk: string,
739
+ options: RscRouteTransformOptions
740
+ ): Promise<RscRouteTransformResult> => {
741
+ const ast = parse(code, { sourceType: 'module' });
742
+ const program = getProgram(ast);
743
+ const plan = await createRscRouteExportPlan(options);
744
+ // Captured before export/import pruning: on a server-component route the
745
+ // component consuming a vanilla-extract style's class names is a server export
746
+ // stripped from the client module below, so its value import is pruned and the
747
+ // scoped CSS never enters the client compilation. Re-adding it as a bare
748
+ // side-effect import (see `collectVanillaValueImportSources`) makes the client
749
+ // build extract the stylesheet — matching a plain `import "./x.css"` global
750
+ // style — so the server route entry's `entryCssFiles` link resolves.
751
+ const vanillaValueImportSources = plan.exportNames.some(
752
+ isServerComponentExport
753
+ )
754
+ ? collectVanillaValueImportSources(program)
755
+ : [];
756
+ if (clientRouteChunk === ROUTE_CLIENT_MODULE_CHUNK) {
757
+ stripPreservedDependencyExports(
758
+ program,
759
+ collectRouteClientDependencyExports(program, plan)
760
+ );
761
+ }
762
+ // Which of the source module's exports this chunk keeps. Every non-`shared`
763
+ // chunk strips all server exports plus every client export the predicate
764
+ // rejects; `shared` is the one exception that removes a fixed export list.
765
+ const keepsClientExport =
766
+ clientRouteChunk === ROUTE_CLIENT_MODULE_CHUNK
767
+ ? isClientRouteExport
768
+ : (exportName: string) => exportName === clientRouteChunk;
769
+ const exportsToRemove =
770
+ clientRouteChunk === 'shared'
771
+ ? [...SERVER_ROUTE_EXPORTS, ...CLIENT_ROUTE_EXPORTS]
772
+ : [
773
+ ...SERVER_ROUTE_EXPORTS,
774
+ ...plan.exportNames.filter(
775
+ exportName => !keepsClientExport(exportName)
776
+ ),
777
+ ];
778
+ const removed = removeExports(ast, exportsToRemove);
779
+ if (removed) {
780
+ removeUnusedImports(ast);
781
+ }
782
+ rewriteRscClientRouteImports(ast, sourceFileName, clientRouteChunk, options);
783
+ const generated = generate(ast, {
784
+ sourceMaps: false,
785
+ filename: sourceFileName,
786
+ sourceFileName,
787
+ });
788
+ const vanillaSideEffectImports =
789
+ vanillaValueImportSources.length > 0
790
+ ? `${vanillaValueImportSources
791
+ .map(source => `import ${JSON.stringify(source)};`)
792
+ .join('\n')}\n`
793
+ : '';
794
+ let clientModuleCode = `"use client";\n${vanillaSideEffectImports}${generated.code}`;
795
+
796
+ if (clientRouteChunk === 'shared' && plan.needsDefaultRootErrorBoundary) {
797
+ const hasRootLayout =
798
+ plan.exportNameSet.has('Layout') ||
799
+ plan.exportNameSet.has('ServerLayout');
800
+ clientModuleCode += `\nimport { createElement as __rr_createElement } from "react";\n`;
801
+ if (options.isDev) {
802
+ // Mirror React Router's own dev default root boundary: surface the real
803
+ // error with its stack (already source-mapped by the dev server's
804
+ // prepareStackTrace hook) instead of swallowing it behind a static
805
+ // message. The <main><pre> shape matches RemixRootDefaultErrorBoundary,
806
+ // which the dev error-stacktrace integration test asserts on.
807
+ clientModuleCode += `import { useRouteError as __rr_useRouteError } from "react-router";\n`;
808
+ // Errors carry a (source-mapped) stack; non-Error rejections (e.g. a
809
+ // react-router ErrorResponse) do not, and `String(plainObject)` collapses
810
+ // to "[object Object]". Fall back to a pretty-printed JSON dump so the
811
+ // boundary surfaces the actual payload instead of an opaque marker.
812
+ clientModuleCode += `function __rr_formatRouteError(error) {\n`;
813
+ clientModuleCode += ` if (error && error.stack) return String(error.stack);\n`;
814
+ clientModuleCode += ` if (error && typeof error === "object") {\n`;
815
+ clientModuleCode += ` try { return JSON.stringify(error, null, 2); } catch (e) { return String(error); }\n`;
816
+ clientModuleCode += ` }\n`;
817
+ clientModuleCode += ` return String(error);\n`;
818
+ clientModuleCode += `}\n`;
819
+ clientModuleCode += `export function ErrorBoundary() {\n`;
820
+ clientModuleCode += ` const error = __rr_useRouteError();\n`;
821
+ clientModuleCode += ` return __rr_createElement("main", null, __rr_createElement("pre", null, __rr_formatRouteError(error)));\n`;
822
+ clientModuleCode += `}\n`;
823
+ } else {
824
+ clientModuleCode += `export function ErrorBoundary() {\n`;
825
+ clientModuleCode += ` return __rr_createElement(${JSON.stringify(
826
+ hasRootLayout ? 'main' : 'div'
827
+ )}, null, "Unexpected Server Error");\n`;
828
+ clientModuleCode += `}\n`;
829
+ }
830
+ }
831
+
832
+ // A single route edit emits several client chunks (shared + route + one per
833
+ // split export), and each chunk's `accept` callback fires independently.
834
+ // Letting every callback navigate meant one route change
835
+ // triggered several redundant navigations. Coalesce them through a
836
+ // globalThis-guarded requestAnimationFrame: the first accept in a frame
837
+ // schedules the navigate and the rest short-circuit.
838
+ const rscHmrAcceptCallback = `() => {
839
+ const __rrHmr = globalThis;
840
+ if (__rrHmr.__rrRscHmrNavigateScheduled) { return; }
841
+ __rrHmr.__rrRscHmrNavigateScheduled = true;
842
+ requestAnimationFrame(() => {
843
+ __rrHmr.__rrRscHmrNavigateScheduled = false;
844
+ ${RSC_HMR_NAVIGATE_SNIPPET}
845
+ });
846
+ }`;
847
+
848
+ const hmrFooter = !options.isDev
849
+ ? ''
850
+ : `\nif (import.meta.webpackHot) { import.meta.webpackHot.accept(${rscHmrAcceptCallback}); }\n`;
851
+
852
+ return {
853
+ code:
854
+ clientModuleCode +
855
+ (clientRouteChunk === 'shared'
856
+ ? `\n${ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR}`
857
+ : '') +
858
+ hmrFooter,
859
+ map: null,
860
+ };
861
+ };
862
+
863
+ const rewriteRouteImportSource = (
864
+ statement: AnyNode,
865
+ sourceFileName: string,
866
+ clientRouteChunk: string,
867
+ options: RscRouteTransformOptions
868
+ ): void => {
869
+ const source = statement.source;
870
+ if (!source || typeof source.value !== 'string' || !options.routeByFilePath) {
871
+ return;
872
+ }
873
+ const resolved = resolveQuerylessRouteImportRequest({
874
+ compilerName: options.isServerEnvironment ? 'node' : 'web',
875
+ context: dirname(sourceFileName),
876
+ issuer: `${sourceFileName}?client-route-module=${clientRouteChunk}`,
877
+ request: source.value,
878
+ routeByFilePath: options.routeByFilePath,
879
+ });
880
+ if (resolved) {
881
+ source.value = resolved;
882
+ source.raw = JSON.stringify(resolved);
883
+ }
884
+ };
885
+
886
+ const rewriteRscClientRouteImports = (
887
+ ast: ReturnType<typeof parse>,
888
+ sourceFileName: string,
889
+ clientRouteChunk: string,
890
+ options: RscRouteTransformOptions
891
+ ): void => {
892
+ for (const statement of getProgram(ast).body ?? []) {
893
+ if (
894
+ statement.type === 'ImportDeclaration' ||
895
+ statement.type === 'ExportAllDeclaration' ||
896
+ statement.type === 'ExportNamedDeclaration'
897
+ ) {
898
+ rewriteRouteImportSource(
899
+ statement,
900
+ sourceFileName,
901
+ clientRouteChunk,
902
+ options
903
+ );
904
+ }
905
+ }
906
+ };
907
+
908
+ const createServerRouteModule = async (
909
+ code: string,
910
+ sourceFileName: string
911
+ ): Promise<RscRouteTransformResult> => {
912
+ const ast = parse(code, { sourceType: 'module' });
913
+ const removedClientLogicExports = removeExports(
914
+ ast,
915
+ CLIENT_NON_COMPONENT_EXPORTS
916
+ );
917
+ const removedClientComponentExports = removeExports(
918
+ ast,
919
+ RSC_CLIENT_COMPONENT_EXPORTS,
920
+ new Set(RSC_CLIENT_COMPONENT_EXPORTS),
921
+ { pruneDeadDeclarations: false }
922
+ );
923
+ const removed = removedClientLogicExports || removedClientComponentExports;
924
+ if (removed) {
925
+ removeUnusedImports(ast);
926
+ }
927
+ const generated = generate(ast, {
928
+ sourceMaps: false,
929
+ filename: sourceFileName,
930
+ sourceFileName,
931
+ });
932
+ // Server-first routes import their CSS from the server (RSC) layer, so it
933
+ // never reaches the client compilation's `<Links>`. Mark modules that export
934
+ // a server component with the `'use server-entry'` directive so the rspack
935
+ // RSC runtime records the module graph's CSS in `entryCssFiles`; the server
936
+ // route entry then streams those links (see `createServerRouteEntry`).
937
+ const exportNames = new Set(await getExportNames(code));
938
+ const hasServerComponentExport = RSC_SERVER_COMPONENT_EXPORTS.some(name =>
939
+ exportNames.has(name)
940
+ );
941
+ if (hasServerComponentExport) {
942
+ // Force-retain the CSS side effect of any vanilla-extract style imported for
943
+ // its class-name values, so the server component's scoped styles reach the
944
+ // client build and stream as `<link>`s (see
945
+ // `collectVanillaValueImportSources`). Placed after the `'use server-entry'`
946
+ // directive prologue and before the pruned body.
947
+ const vanillaSideEffectImports = collectVanillaValueImportSources(
948
+ getProgram(ast)
949
+ )
950
+ .map(source => `import ${JSON.stringify(source)};`)
951
+ .join('\n');
952
+ const prefix = vanillaSideEffectImports
953
+ ? `${vanillaSideEffectImports}\n`
954
+ : '';
955
+ return {
956
+ code: `'use server-entry';\n${prefix}${generated.code}`,
957
+ map: null,
958
+ };
959
+ }
960
+ // Client route with bundled side-effect CSS: the pruned server module still
961
+ // carries those `import "./x.css"` side effects (specifier-less style imports
962
+ // survive `removeUnusedImports`). Promote it to a `'use server-entry'` module
963
+ // with a null-rendering carrier component so the RSC runtime attaches this
964
+ // graph's CSS as the carrier's `entryCssFiles`. `createServerRouteEntry`
965
+ // streams those links so the client route's bundled CSS renders at first
966
+ // paint without JavaScript.
967
+ if (programHasSideEffectStyleImports(getProgram(ast))) {
968
+ return {
969
+ code: `'use server-entry';\n${generated.code}\nexport function ${RSC_ROUTE_STYLE_ENTRY_EXPORT}() { return null; }\n`,
970
+ map: null,
971
+ };
972
+ }
973
+ return { code: generated.code, map: null };
974
+ };
975
+
976
+ export const transformRscRouteModule = async (
977
+ options: RscRouteTransformOptions
978
+ ): Promise<RscRouteTransformResult> => {
979
+ const target = resolveRscRouteTransformTarget(options);
980
+ switch (target.kind) {
981
+ case 'client-route-module':
982
+ return createClientRouteModule(
983
+ options.code,
984
+ options.resourcePath,
985
+ target.chunk,
986
+ options
987
+ );
988
+ case 'server-route-module':
989
+ return createServerRouteModule(options.code, options.resourcePath);
990
+ case 'server-route-entry':
991
+ return createServerRouteEntry(options);
992
+ case 'client-route-entry':
993
+ return createClientRouteEntry(options);
994
+ }
995
+ };