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.
- package/README.md +58 -208
- package/dist/511.js +370 -331
- package/dist/build-output-transforms.d.ts +30 -6
- package/dist/classic-mode.d.ts +56 -0
- package/dist/config-imports.d.ts +8 -2
- package/dist/constants.d.ts +3 -0
- package/dist/dev-background-resources.d.ts +3 -1
- package/dist/dev-generation.d.ts +2 -3
- package/dist/dev-hmr.d.ts +8 -2
- package/dist/dev-runtime-controller.d.ts +6 -1
- package/dist/dev-source-maps.d.ts +4 -0
- package/dist/effect-runtime.d.ts +17 -5
- package/dist/entry-paths.d.ts +16 -0
- package/dist/environment-output.d.ts +6 -0
- package/dist/export-utils.d.ts +2 -1
- package/dist/index.cjs +3957 -2208
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3470 -1793
- package/dist/lazy-compilation-prewarm.d.ts +8 -5
- package/dist/manifest.d.ts +16 -4
- package/dist/mode-plan.d.ts +82 -0
- package/dist/modify-browser-manifest.d.ts +8 -4
- package/dist/plugin-utils.d.ts +27 -1
- package/dist/prerender-build.d.ts +5 -5
- package/dist/prerender.d.ts +1 -5
- package/dist/react-router-config.d.ts +11 -3
- package/dist/route-artifacts.d.ts +4 -2
- package/dist/route-chunks.d.ts +2 -1
- package/dist/route-component-transform.d.ts +0 -2
- package/dist/route-export-pruning.d.ts +4 -1
- package/dist/route-imports.d.ts +18 -0
- package/dist/route-transform-tasks.d.ts +5 -0
- package/dist/route-watch.d.ts +11 -6
- package/dist/rsc-dev-server.d.ts +26 -0
- package/dist/rsc-prerender.d.ts +54 -0
- package/dist/rsc-route-config.d.ts +6 -0
- package/dist/rsc-route-exports.d.ts +15 -0
- package/dist/rsc-route-transform-loader.cjs +43 -0
- package/dist/rsc-route-transform-loader.d.ts +30 -0
- package/dist/rsc-route-transform-loader.js +16 -0
- package/dist/rsc-route-transform-registration.d.ts +12 -0
- package/dist/rsc-route-transforms.d.ts +21 -0
- package/dist/rsc-support.d.ts +23 -0
- package/dist/rsc-virtual-modules.d.ts +19 -0
- package/dist/server-build-plan.d.ts +2 -1
- package/dist/server-build-resolution.d.ts +1 -2
- package/dist/server-utils.d.ts +4 -5
- package/dist/ssr-asset-relocation.d.ts +98 -0
- package/dist/templates/entry.rsc.client.d.ts +1 -0
- package/dist/templates/entry.rsc.client.js +61 -0
- package/dist/templates/entry.rsc.d.ts +9 -0
- package/dist/templates/entry.rsc.js +38 -0
- package/dist/templates/entry.rsc.ssr.d.ts +4 -0
- package/dist/templates/entry.rsc.ssr.js +24 -0
- package/dist/typegen.d.ts +4 -2
- package/dist/types.d.ts +30 -1
- package/package.json +69 -14
- package/src/build-output-transforms.ts +155 -21
- package/src/classic-mode.ts +253 -0
- package/src/config-imports.ts +153 -6
- package/src/constants.ts +6 -2
- package/src/dev-background-resources.ts +46 -85
- package/src/dev-generation.ts +52 -33
- package/src/dev-hmr.ts +112 -80
- package/src/dev-runtime-artifacts.ts +11 -12
- package/src/dev-runtime-controller.ts +75 -85
- package/src/dev-runtime-session.ts +14 -18
- package/src/dev-server.ts +2 -0
- package/src/dev-source-maps.ts +257 -0
- package/src/effect-runtime.ts +105 -57
- package/src/entry-paths.ts +80 -0
- package/src/environment-output.ts +55 -0
- package/src/export-utils.ts +15 -11
- package/src/index.ts +661 -496
- package/src/lazy-compilation-prewarm.ts +20 -4
- package/src/manifest.ts +157 -82
- package/src/mode-plan.ts +367 -0
- package/src/modify-browser-manifest.ts +130 -124
- package/src/plugin-utils.ts +103 -39
- package/src/prerender-build.ts +82 -104
- package/src/prerender.ts +23 -24
- package/src/react-router-config.ts +72 -26
- package/src/route-artifacts.ts +96 -66
- package/src/route-chunks.ts +167 -51
- package/src/route-component-transform.ts +14 -21
- package/src/route-export-pruning.ts +4 -3
- package/src/route-imports.ts +100 -0
- package/src/route-transform-tasks.ts +39 -25
- package/src/route-watch.ts +166 -188
- package/src/rsc-dev-server.ts +112 -0
- package/src/rsc-prerender.ts +362 -0
- package/src/rsc-route-config.ts +175 -0
- package/src/rsc-route-exports.ts +67 -0
- package/src/rsc-route-transform-loader.ts +65 -0
- package/src/rsc-route-transform-registration.ts +145 -0
- package/src/rsc-route-transforms.ts +995 -0
- package/src/rsc-runtime.d.ts +143 -0
- package/src/rsc-support.ts +116 -0
- package/src/rsc-virtual-modules.ts +113 -0
- package/src/server-build-plan.ts +14 -3
- package/src/server-build-resolution.ts +37 -47
- package/src/server-utils.ts +21 -35
- package/src/ssr-asset-relocation.ts +183 -0
- package/src/ssr-externals.ts +8 -26
- package/src/templates/entry.rsc.client.tsx +168 -0
- package/src/templates/entry.rsc.ssr.tsx +45 -0
- package/src/templates/entry.rsc.tsx +80 -0
- package/src/typegen.ts +40 -23
- package/src/types.ts +39 -1
- 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
|
+
};
|