rsbuild-plugin-react-router 0.6.5 → 0.7.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 +45 -0
- package/dist/511.js +10 -3
- package/dist/environment-output.d.ts +16 -2
- package/dist/index.cjs +110 -74
- package/dist/index.js +109 -81
- package/dist/manifest.d.ts +2 -0
- package/dist/mode-plan.d.ts +2 -1
- package/dist/plugin-utils.d.ts +19 -13
- package/dist/rsc-virtual-modules.d.ts +4 -4
- package/dist/templates/entry.rsc.js +2 -2
- package/package.json +1 -1
- package/src/environment-output.ts +50 -1
- package/src/index.ts +44 -61
- package/src/manifest.ts +26 -20
- package/src/mode-plan.ts +11 -13
- package/src/modify-browser-manifest.ts +4 -4
- package/src/plugin-utils.ts +37 -23
- package/src/rsc-route-transforms.ts +8 -12
- package/src/rsc-runtime.d.ts +11 -0
- package/src/rsc-virtual-modules.ts +62 -9
- package/src/templates/entry.rsc.tsx +1 -1
package/src/index.ts
CHANGED
|
@@ -6,11 +6,7 @@ import { rspack, type RsbuildPlugin, type Rspack } from '@rsbuild/core';
|
|
|
6
6
|
import { relative, resolve } from 'pathe';
|
|
7
7
|
|
|
8
8
|
import { getDefaultConcurrency } from './concurrency.js';
|
|
9
|
-
import {
|
|
10
|
-
DEFAULT_JS_DIST_PATH,
|
|
11
|
-
JS_EXTENSIONS,
|
|
12
|
-
PLUGIN_NAME,
|
|
13
|
-
} from './constants.js';
|
|
9
|
+
import { JS_EXTENSIONS, PLUGIN_NAME } from './constants.js';
|
|
14
10
|
import { guardReactRouterLazyCompilation } from './lazy-compilation.js';
|
|
15
11
|
import {
|
|
16
12
|
findEntryFile,
|
|
@@ -222,13 +218,25 @@ export const pluginReactRouter = (
|
|
|
222
218
|
warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
|
|
223
219
|
});
|
|
224
220
|
|
|
221
|
+
// The manifest / server `publicPath` follows the web environment's asset
|
|
222
|
+
// prefix because that is where the browser assets are served from. A web
|
|
223
|
+
// prefix the server cannot use (`'auto'`) falls back to the root prefix,
|
|
224
|
+
// so `output.assetPrefix: 'https://cdn/'` + web `'auto'` still emits CDN
|
|
225
|
+
// URLs from the server.
|
|
225
226
|
api.onBeforeCreateCompiler(() => {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
const root = api.getNormalizedConfig();
|
|
228
|
+
// `getNormalizedConfig({ environment: 'web' })` throws when the build was
|
|
229
|
+
// narrowed to other environments (`--environment node`), so look the web
|
|
230
|
+
// environment up on the root config instead.
|
|
231
|
+
const web = root.environments.web;
|
|
232
|
+
assetPrefix = resolveEffectiveAssetPrefix(
|
|
233
|
+
{
|
|
234
|
+
dev: web?.dev,
|
|
235
|
+
output: web?.output,
|
|
236
|
+
isBuild: api.context.action === 'build',
|
|
237
|
+
},
|
|
238
|
+
{ dev: root.dev, output: root.output }
|
|
239
|
+
);
|
|
232
240
|
});
|
|
233
241
|
|
|
234
242
|
const configPath = findEntryFile(resolve('react-router.config'));
|
|
@@ -585,6 +593,7 @@ export const pluginReactRouter = (
|
|
|
585
593
|
buildDirectory,
|
|
586
594
|
finalEntryRscClientPath,
|
|
587
595
|
finalEntryRscPath,
|
|
596
|
+
finalEntryRscSsrPath,
|
|
588
597
|
outputClientPath,
|
|
589
598
|
pluginName: PLUGIN_NAME,
|
|
590
599
|
serverBuildFile,
|
|
@@ -831,39 +840,35 @@ export const pluginReactRouter = (
|
|
|
831
840
|
}
|
|
832
841
|
|
|
833
842
|
// Public requests stay bare while Rspack resolves seeded virtual files.
|
|
834
|
-
const createVirtualModulePlugin = (
|
|
835
|
-
publicPath: string,
|
|
836
|
-
jsDistPath: string
|
|
837
|
-
) => {
|
|
843
|
+
const createVirtualModulePlugin = (publicPath: string) => {
|
|
838
844
|
return new rspack.experiments.VirtualModulesPlugin(
|
|
839
|
-
mapVirtualModules(modePlan.createVirtualModules(publicPath
|
|
845
|
+
mapVirtualModules(modePlan.createVirtualModules(publicPath))
|
|
840
846
|
);
|
|
841
847
|
};
|
|
842
848
|
|
|
843
849
|
api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
options.federation && resolvedServerOutput === 'commonjs';
|
|
861
|
-
let nodeChunkLoading: 'import' | 'async-node' | 'require' = 'require';
|
|
862
|
-
if (resolvedServerOutput === 'module') {
|
|
863
|
-
nodeChunkLoading = 'import';
|
|
864
|
-
} else if (useAsyncNodeChunkLoading) {
|
|
865
|
-
nodeChunkLoading = 'async-node';
|
|
850
|
+
const webConfig = config.environments?.web;
|
|
851
|
+
const webJsFilename =
|
|
852
|
+
webConfig?.output?.filename?.js ?? config.output?.filename?.js;
|
|
853
|
+
// Rspack's RSC manifest only records browser entry files named `*.js`
|
|
854
|
+
// (`entryJsFiles`), and the server renders its bootstrap scripts from
|
|
855
|
+
// that list. Reject filename schemes it would silently drop up front.
|
|
856
|
+
if (
|
|
857
|
+
isRscMode &&
|
|
858
|
+
typeof webJsFilename === 'string' &&
|
|
859
|
+
!/\.js$/.test(webJsFilename)
|
|
860
|
+
) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`[${PLUGIN_NAME}] RSC mode requires web \`output.filename.js\` to end in ".js" (got ${JSON.stringify(
|
|
863
|
+
webJsFilename
|
|
864
|
+
)}): rspack's RSC manifest omits entry files with a query or another extension, so the server could not render bootstrap scripts.`
|
|
865
|
+
);
|
|
866
866
|
}
|
|
867
|
+
const assetPrefix = resolveEffectiveAssetPrefix(
|
|
868
|
+
{ dev: webConfig?.dev, output: webConfig?.output, isBuild },
|
|
869
|
+
{ dev: config.dev, output: config.output }
|
|
870
|
+
);
|
|
871
|
+
const vmodPlugin = createVirtualModulePlugin(assetPrefix);
|
|
867
872
|
const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
|
|
868
873
|
options,
|
|
869
874
|
'lazyCompilation'
|
|
@@ -963,9 +968,6 @@ export const pluginReactRouter = (
|
|
|
963
968
|
}),
|
|
964
969
|
},
|
|
965
970
|
output: {
|
|
966
|
-
filename: {
|
|
967
|
-
js: '[name].js',
|
|
968
|
-
},
|
|
969
971
|
distPath: {
|
|
970
972
|
root: outputClientPath,
|
|
971
973
|
},
|
|
@@ -984,15 +986,6 @@ export const pluginReactRouter = (
|
|
|
984
986
|
],
|
|
985
987
|
},
|
|
986
988
|
externalsType: modePlan.webExternalsType,
|
|
987
|
-
output: {
|
|
988
|
-
...modePlan.webOutput,
|
|
989
|
-
publicPath: assetPrefix,
|
|
990
|
-
...(options.federation
|
|
991
|
-
? {
|
|
992
|
-
chunkLoading: 'import',
|
|
993
|
-
}
|
|
994
|
-
: {}),
|
|
995
|
-
},
|
|
996
989
|
optimization: modePlan.webOptimization,
|
|
997
990
|
},
|
|
998
991
|
},
|
|
@@ -1037,17 +1030,6 @@ export const pluginReactRouter = (
|
|
|
1037
1030
|
externals: modePlan.nodeExternals,
|
|
1038
1031
|
...modePlan.nodeDependencies,
|
|
1039
1032
|
externalsType: resolvedServerOutput,
|
|
1040
|
-
output: {
|
|
1041
|
-
chunkFormat: resolvedServerOutput,
|
|
1042
|
-
chunkLoading: nodeChunkLoading,
|
|
1043
|
-
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
|
1044
|
-
devtoolFallbackModuleFilenameTemplate:
|
|
1045
|
-
'[absolute-resource-path]?[hash]',
|
|
1046
|
-
workerChunkLoading: nodeChunkLoading,
|
|
1047
|
-
wasmLoading: 'fetch',
|
|
1048
|
-
module: resolvedServerOutput === 'module',
|
|
1049
|
-
chunkFilename: 'static/js/async/[name].js',
|
|
1050
|
-
},
|
|
1051
1033
|
},
|
|
1052
1034
|
},
|
|
1053
1035
|
},
|
|
@@ -1059,6 +1041,7 @@ export const pluginReactRouter = (
|
|
|
1059
1041
|
api,
|
|
1060
1042
|
federation: pluginOptions.federation,
|
|
1061
1043
|
resolvedServerOutput,
|
|
1044
|
+
webOutput: modePlan.webOutput,
|
|
1062
1045
|
});
|
|
1063
1046
|
|
|
1064
1047
|
if (modePlan.kind === 'classic' && useRouteModuleTransformLoader) {
|
package/src/manifest.ts
CHANGED
|
@@ -167,19 +167,17 @@ type ReactRouterManifestStatsCompilation = {
|
|
|
167
167
|
entrypoints?: ReactRouterManifestStatsLookup<ReactRouterManifestStatsEntrypoint>;
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
];
|
|
182
|
-
};
|
|
170
|
+
// Emitted asset names may carry a query (`output.filename.js:
|
|
171
|
+
// '[name].js?v=[contenthash:8]'`); classify on the pathname but keep the full
|
|
172
|
+
// reference, since the query is part of the URL the browser must request.
|
|
173
|
+
// A chunk's own script is whatever JavaScript the compilation rendered for it,
|
|
174
|
+
// regardless of `output.filename` scheme. In development, HMR update files are
|
|
175
|
+
// also recorded on `chunk.files`; they are never the module to load.
|
|
176
|
+
export const isManifestJsAsset = (asset: string): boolean =>
|
|
177
|
+
/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset);
|
|
178
|
+
|
|
179
|
+
export const isManifestCssAsset = (asset: string): boolean =>
|
|
180
|
+
/\.css(?:\?.*)?$/.test(asset);
|
|
183
181
|
|
|
184
182
|
const collectManifestFilesByName = <T>(
|
|
185
183
|
items: ReactRouterManifestStatsLookup<T>,
|
|
@@ -241,8 +239,7 @@ export const createReactRouterManifestStats = (
|
|
|
241
239
|
const assetsByChunkName = collectManifestFilesByName(
|
|
242
240
|
compilation.namedChunks,
|
|
243
241
|
chunkNames,
|
|
244
|
-
(
|
|
245
|
-
orderChunkFiles(chunkName, Array.from(chunk.files ?? []))
|
|
242
|
+
(_chunkName, chunk) => Array.from(chunk.files ?? [])
|
|
246
243
|
);
|
|
247
244
|
const entrypointFilesByName = compilation.entrypoints
|
|
248
245
|
? collectManifestFilesByName(
|
|
@@ -303,22 +300,31 @@ const createChunkAssetResolver = (
|
|
|
303
300
|
|
|
304
301
|
const cssAssets = new Set<string>();
|
|
305
302
|
const jsAssets = new Set<string>();
|
|
303
|
+
// The chunk's own files come first so `js[0]` is the route module itself;
|
|
304
|
+
// entrypoint files (runtime, shared vendor chunks) follow as `imports`.
|
|
306
305
|
for (const asset of assets) {
|
|
307
|
-
if (asset
|
|
306
|
+
if (isManifestCssAsset(asset)) {
|
|
308
307
|
cssAssets.add(asset);
|
|
309
|
-
} else if (asset
|
|
308
|
+
} else if (isManifestJsAsset(asset)) {
|
|
310
309
|
jsAssets.add(asset);
|
|
311
310
|
}
|
|
312
311
|
}
|
|
313
312
|
for (const asset of clientStats?.entrypointFilesByName?.[chunkName] ?? []) {
|
|
314
|
-
if (asset
|
|
313
|
+
if (isManifestCssAsset(asset)) {
|
|
315
314
|
cssAssets.add(asset);
|
|
316
|
-
} else if (includeEntrypointJs && asset
|
|
315
|
+
} else if (includeEntrypointJs && isManifestJsAsset(asset)) {
|
|
317
316
|
jsAssets.add(asset);
|
|
318
317
|
}
|
|
319
318
|
}
|
|
320
319
|
if (jsAssets.size === 0) {
|
|
321
|
-
|
|
320
|
+
// Compilation metadata exists for this chunk but names no module script.
|
|
321
|
+
// Guessing `<dir>/<chunk>.js` here would turn an identifiable build
|
|
322
|
+
// problem into a browser 404, so surface it at build time instead.
|
|
323
|
+
throw new Error(
|
|
324
|
+
`[react-router] Chunk "${chunkName}" emitted no JavaScript asset the browser manifest can reference (files: ${
|
|
325
|
+
assets.join(', ') || 'none'
|
|
326
|
+
}). Check the web \`output.filename.js\` scheme.`
|
|
327
|
+
);
|
|
322
328
|
}
|
|
323
329
|
|
|
324
330
|
const result = { js: [...jsAssets], css: [...cssAssets] };
|
package/src/mode-plan.ts
CHANGED
|
@@ -36,10 +36,7 @@ type CommonModePlan = {
|
|
|
36
36
|
manifestChunkNames: Set<string>;
|
|
37
37
|
webEntries: Record<string, string | RsbuildEntryDescription>;
|
|
38
38
|
nodeEntries: Record<string, string | RsbuildEntryDescription>;
|
|
39
|
-
createVirtualModules(
|
|
40
|
-
publicPath: string,
|
|
41
|
-
jsDistPath: string
|
|
42
|
-
): Record<string, string>;
|
|
39
|
+
createVirtualModules(publicPath: string): Record<string, string>;
|
|
43
40
|
createResolveConfig(rootPath: string): Rspack.Configuration['resolve'];
|
|
44
41
|
server: RsbuildConfig['server'] | undefined;
|
|
45
42
|
webExternalsType: 'module' | undefined;
|
|
@@ -104,6 +101,7 @@ type CreateRscModePlanOptions = ModePlanContext & {
|
|
|
104
101
|
buildDirectory: string;
|
|
105
102
|
finalEntryRscClientPath: string;
|
|
106
103
|
finalEntryRscPath: string;
|
|
104
|
+
finalEntryRscSsrPath: string;
|
|
107
105
|
outputClientPath: string;
|
|
108
106
|
pluginName: string;
|
|
109
107
|
serverBuildFile: string | undefined;
|
|
@@ -133,6 +131,7 @@ const createRscModePlan = async ({
|
|
|
133
131
|
customServer,
|
|
134
132
|
finalEntryRscClientPath,
|
|
135
133
|
finalEntryRscPath,
|
|
134
|
+
finalEntryRscSsrPath,
|
|
136
135
|
isBuild,
|
|
137
136
|
outputClientPath,
|
|
138
137
|
pluginName,
|
|
@@ -181,14 +180,13 @@ const createRscModePlan = async ({
|
|
|
181
180
|
layer: RSC_LAYERS.rsc,
|
|
182
181
|
},
|
|
183
182
|
},
|
|
184
|
-
createVirtualModules: (publicPath: string
|
|
183
|
+
createVirtualModules: (publicPath: string) =>
|
|
185
184
|
createReactRouterRscVirtualModules({
|
|
186
185
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
187
186
|
appDirectory,
|
|
188
187
|
basename,
|
|
189
188
|
buildDirectory,
|
|
190
189
|
isBuild,
|
|
191
|
-
jsDistPath,
|
|
192
190
|
outputClientPath,
|
|
193
191
|
publicPath,
|
|
194
192
|
routeDiscovery,
|
|
@@ -197,7 +195,9 @@ const createRscModePlan = async ({
|
|
|
197
195
|
}),
|
|
198
196
|
createResolveConfig: (rootPath: string) => ({
|
|
199
197
|
modules: [resolve(rootPath, 'node_modules'), 'node_modules'],
|
|
200
|
-
alias: createReactRouterRscResolveAliases(rootPath
|
|
198
|
+
alias: createReactRouterRscResolveAliases(rootPath, {
|
|
199
|
+
entrySsrPath: finalEntryRscSsrPath,
|
|
200
|
+
}),
|
|
201
201
|
}),
|
|
202
202
|
server: !customServer
|
|
203
203
|
? {
|
|
@@ -310,7 +310,7 @@ const createClassicModePlan = async ({
|
|
|
310
310
|
defaultEntryName,
|
|
311
311
|
serverBundleEntries: artifacts.serverBundleEntries,
|
|
312
312
|
}),
|
|
313
|
-
createVirtualModules: (publicPath: string
|
|
313
|
+
createVirtualModules: (publicPath: string) =>
|
|
314
314
|
createClassicVirtualModules({
|
|
315
315
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
316
316
|
appDirectory,
|
|
@@ -345,11 +345,9 @@ const createClassicModePlan = async ({
|
|
|
345
345
|
wasmLoading: 'fetch',
|
|
346
346
|
library: { type: 'module' },
|
|
347
347
|
module: true,
|
|
348
|
-
// Async
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
? { chunkFilename: 'static/js/async/[id]-[contenthash:16].js' }
|
|
352
|
-
: {}),
|
|
348
|
+
// Async chunk filenames are left to Rsbuild, which derives them from
|
|
349
|
+
// `output.filename.js`, `output.filenameHash`, and `distPath.jsAsync`,
|
|
350
|
+
// so user configuration governs every JavaScript filename (#129).
|
|
353
351
|
},
|
|
354
352
|
webOptimization: {
|
|
355
353
|
avoidEntryIife: true,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
generateReactRouterManifestForDev,
|
|
8
8
|
getReactRouterManifestChunkNames,
|
|
9
9
|
getReactRouterManifestPath,
|
|
10
|
+
isManifestJsAsset,
|
|
10
11
|
type ReactRouterManifestForDev as ReactRouterManifest,
|
|
11
12
|
type RouteChunkManifestOptions,
|
|
12
13
|
type RouteManifestModuleExports,
|
|
@@ -83,7 +84,7 @@ const addIntegrity = (
|
|
|
83
84
|
) => {
|
|
84
85
|
if (
|
|
85
86
|
typeof assetName !== 'string' ||
|
|
86
|
-
!assetName
|
|
87
|
+
!isManifestJsAsset(assetName) ||
|
|
87
88
|
typeof integrity !== 'string'
|
|
88
89
|
) {
|
|
89
90
|
return;
|
|
@@ -117,7 +118,7 @@ export const collectSubresourceIntegrity = (
|
|
|
117
118
|
const assets =
|
|
118
119
|
compilation.getAssets() as readonly CompilationAssetWithIntegrity[];
|
|
119
120
|
for (const asset of assets) {
|
|
120
|
-
if (!asset.name
|
|
121
|
+
if (!isManifestJsAsset(asset.name)) {
|
|
121
122
|
continue;
|
|
122
123
|
}
|
|
123
124
|
addIntegrity(
|
|
@@ -200,8 +201,7 @@ export function registerModifyBrowserManifestAssets(
|
|
|
200
201
|
|
|
201
202
|
if (isBuild) {
|
|
202
203
|
const entryAssets = stats?.assetsByChunkName?.['entry.client'];
|
|
203
|
-
const entryJsAssets =
|
|
204
|
-
entryAssets?.filter(asset => asset.endsWith('.js')) || [];
|
|
204
|
+
const entryJsAssets = entryAssets?.filter(isManifestJsAsset) || [];
|
|
205
205
|
const manifestPath = getReactRouterManifestPath({
|
|
206
206
|
version: manifest.version,
|
|
207
207
|
isBuild: true,
|
package/src/plugin-utils.ts
CHANGED
|
@@ -79,34 +79,48 @@ export function normalizeAssetPrefix(assetPrefix?: string): string {
|
|
|
79
79
|
return assetPrefix.endsWith('/') ? assetPrefix : `${assetPrefix}/`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
type AssetPrefixConfig = {
|
|
83
|
+
dev?: { assetPrefix?: unknown };
|
|
84
|
+
output?: { assetPrefix?: unknown };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const asString = (value: unknown): string | undefined =>
|
|
88
|
+
typeof value === 'string' ? value : undefined;
|
|
89
|
+
|
|
90
|
+
const pickConfiguredAssetPrefix = (
|
|
91
|
+
{ dev, output }: AssetPrefixConfig,
|
|
92
|
+
isBuild: boolean
|
|
93
|
+
): string | undefined =>
|
|
94
|
+
isBuild
|
|
95
|
+
? asString(output?.assetPrefix)
|
|
96
|
+
: (asString(dev?.assetPrefix) ?? asString(output?.assetPrefix));
|
|
97
|
+
|
|
82
98
|
/**
|
|
83
|
-
* Resolve the asset prefix
|
|
84
|
-
*
|
|
85
|
-
* defaults from `server.base`, falling back to
|
|
86
|
-
* production build it is `output.assetPrefix`.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
99
|
+
* Resolve the absolute asset prefix the server build and browser manifest use
|
|
100
|
+
* for asset URLs. In development the effective prefix is `dev.assetPrefix`
|
|
101
|
+
* (which Rsbuild defaults from `server.base`, falling back to
|
|
102
|
+
* `output.assetPrefix`); in a production build it is `output.assetPrefix`.
|
|
103
|
+
*
|
|
104
|
+
* `fallbacks` are consulted in order (e.g. the root config after the web
|
|
105
|
+
* environment) when the preceding config only offers a prefix the server
|
|
106
|
+
* cannot use: `'auto'` and empty values are browser-runtime-only, so a root
|
|
107
|
+
* CDN prefix must survive a web `'auto'`. The choice happens before
|
|
108
|
+
* normalization so that `'auto'` is not first folded into `'/'`.
|
|
89
109
|
*
|
|
90
110
|
* `dev.assetPrefix` may be a boolean on the raw config (`false` disables it);
|
|
91
|
-
* boolean/`'auto'`/empty values normalize to the root prefix `'/'`.
|
|
111
|
+
* boolean/`'auto'`/empty values ultimately normalize to the root prefix `'/'`.
|
|
92
112
|
*/
|
|
93
|
-
export function resolveEffectiveAssetPrefix(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (config.isBuild) {
|
|
103
|
-
return normalizeAssetPrefix(outputPrefix);
|
|
113
|
+
export function resolveEffectiveAssetPrefix(
|
|
114
|
+
config: AssetPrefixConfig & { isBuild: boolean },
|
|
115
|
+
...fallbacks: AssetPrefixConfig[]
|
|
116
|
+
): string {
|
|
117
|
+
for (const candidate of [config, ...fallbacks]) {
|
|
118
|
+
const prefix = pickConfiguredAssetPrefix(candidate, config.isBuild);
|
|
119
|
+
if (prefix && prefix !== 'auto') {
|
|
120
|
+
return normalizeAssetPrefix(prefix);
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
typeof config.dev?.assetPrefix === 'string'
|
|
107
|
-
? config.dev.assetPrefix
|
|
108
|
-
: undefined;
|
|
109
|
-
return normalizeAssetPrefix(devPrefix ?? outputPrefix);
|
|
123
|
+
return '/';
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
export function createRouteId(file: string): string {
|
|
@@ -623,6 +623,12 @@ const createServerRouteEntry = async (
|
|
|
623
623
|
let needsReactImport = false;
|
|
624
624
|
let needsEnsureHmrImport = false;
|
|
625
625
|
let needsStyleEntryImport = false;
|
|
626
|
+
const pushStylesheetLinks = (entryCssFilesExpression: string): void => {
|
|
627
|
+
lines.push(` ...(${entryCssFilesExpression} ?? []).map(href =>`);
|
|
628
|
+
lines.push(
|
|
629
|
+
' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
|
|
630
|
+
);
|
|
631
|
+
};
|
|
626
632
|
|
|
627
633
|
// A client route whose default component is a client reference: its bundled
|
|
628
634
|
// (non-vanilla) side-effect CSS is orphaned in the initial browser entry
|
|
@@ -651,12 +657,7 @@ const createServerRouteEntry = async (
|
|
|
651
657
|
'export default function RscClientRouteWithStyles___(props) {'
|
|
652
658
|
);
|
|
653
659
|
lines.push(' return React.createElement(React.Fragment, null,');
|
|
654
|
-
|
|
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
|
+
pushStylesheetLinks(`${RSC_ROUTE_STYLE_ENTRY_EXPORT}.entryCssFiles`);
|
|
660
661
|
lines.push(' React.createElement(RscClientRouteDefault___, props),');
|
|
661
662
|
lines.push(' );');
|
|
662
663
|
lines.push('}');
|
|
@@ -682,12 +683,7 @@ const createServerRouteEntry = async (
|
|
|
682
683
|
// graph contributes. Render those as stylesheet links at the top of the
|
|
683
684
|
// stream (mirrors upstream's `import.meta.viteRsc.loadCss()`); React's
|
|
684
685
|
// float support hoists them into `<head>`.
|
|
685
|
-
|
|
686
|
-
` ...(${exportName}WithoutClientChunk.entryCssFiles ?? []).map(href =>`
|
|
687
|
-
);
|
|
688
|
-
lines.push(
|
|
689
|
-
' React.createElement("link", { key: href, rel: "stylesheet", href: href, precedence: "default" })),'
|
|
690
|
-
);
|
|
686
|
+
pushStylesheetLinks(`${exportName}WithoutClientChunk.entryCssFiles`);
|
|
691
687
|
lines.push(
|
|
692
688
|
' React.createElement(EnsureClientRouteModuleForHMR___, null),'
|
|
693
689
|
);
|
package/src/rsc-runtime.d.ts
CHANGED
|
@@ -99,6 +99,17 @@ declare module 'virtual/react-router/unstable_rsc/react-router-serve-config' {
|
|
|
99
99
|
|
|
100
100
|
declare module 'virtual/react-router/unstable_rsc/inject-hmr-runtime' {}
|
|
101
101
|
|
|
102
|
+
declare module 'virtual/react-router/unstable_rsc/entry-ssr' {
|
|
103
|
+
export function generateHTML(
|
|
104
|
+
request: Request,
|
|
105
|
+
serverResponse: Response,
|
|
106
|
+
options?: {
|
|
107
|
+
bootstrapScripts?: string[];
|
|
108
|
+
bootstrapModules?: string[];
|
|
109
|
+
}
|
|
110
|
+
): Promise<Response>;
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
declare module 'virtual/react-router/unstable_rsc/bootstrap-scripts' {
|
|
103
114
|
const bootstrapScripts: string[];
|
|
104
115
|
export default bootstrapScripts;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, resolve } from 'pathe';
|
|
2
2
|
import type { Config } from './react-router-config.js';
|
|
3
3
|
import type { Route } from './types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeAssetPrefix } from './plugin-utils.js';
|
|
5
5
|
import { getVirtualModuleFilePath } from './virtual-modules.js';
|
|
6
6
|
import {
|
|
7
7
|
createRscInternalClientModule,
|
|
@@ -19,6 +19,7 @@ const RSC_VIRTUAL_ALIAS_IDS = [
|
|
|
19
19
|
'allowed-action-origins',
|
|
20
20
|
'client-version',
|
|
21
21
|
'react-router-serve-config',
|
|
22
|
+
'manifest-prefix',
|
|
22
23
|
'bootstrap-scripts',
|
|
23
24
|
'server-manifest',
|
|
24
25
|
] as const;
|
|
@@ -29,8 +30,6 @@ type RscVirtualModulesOptions = {
|
|
|
29
30
|
basename: string;
|
|
30
31
|
buildDirectory: string;
|
|
31
32
|
isBuild: boolean;
|
|
32
|
-
/** Resolved web `output.distPath.js` segment, e.g. `static/js`. */
|
|
33
|
-
jsDistPath: string;
|
|
34
33
|
outputClientPath: string;
|
|
35
34
|
publicPath: string;
|
|
36
35
|
routeDiscovery: Config['routeDiscovery'];
|
|
@@ -39,8 +38,18 @@ type RscVirtualModulesOptions = {
|
|
|
39
38
|
};
|
|
40
39
|
|
|
41
40
|
export const createReactRouterRscResolveAliases = (
|
|
42
|
-
rootPath: string
|
|
41
|
+
rootPath: string,
|
|
42
|
+
options: { entrySsrPath?: string } = {}
|
|
43
43
|
): Record<string, string> => ({
|
|
44
|
+
// The RSC entry template imports the SSR entry through this alias so a
|
|
45
|
+
// user-provided `app/entry.ssr.tsx` replaces the template inside the SSR
|
|
46
|
+
// layer instead of leaving the template to be compiled as server code.
|
|
47
|
+
...(options.entrySsrPath
|
|
48
|
+
? {
|
|
49
|
+
'virtual:react-router/unstable_rsc/entry-ssr': options.entrySsrPath,
|
|
50
|
+
'virtual/react-router/unstable_rsc/entry-ssr': options.entrySsrPath,
|
|
51
|
+
}
|
|
52
|
+
: {}),
|
|
44
53
|
...Object.fromEntries(
|
|
45
54
|
RSC_VIRTUAL_ALIAS_IDS.flatMap(id => {
|
|
46
55
|
const moduleId = `virtual/react-router/unstable_rsc/${id}`;
|
|
@@ -63,7 +72,6 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
63
72
|
basename,
|
|
64
73
|
buildDirectory,
|
|
65
74
|
isBuild,
|
|
66
|
-
jsDistPath,
|
|
67
75
|
outputClientPath,
|
|
68
76
|
publicPath,
|
|
69
77
|
routeDiscovery,
|
|
@@ -74,7 +82,10 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
74
82
|
resolve(buildDirectory, 'server'),
|
|
75
83
|
outputClientPath
|
|
76
84
|
);
|
|
77
|
-
|
|
85
|
+
// Absolute prefix the server must use for initial asset URLs (see
|
|
86
|
+
// `resolveEffectiveAssetPrefix`); the browser compiler may itself be on
|
|
87
|
+
// `'auto'`, which no server-rendered URL can express.
|
|
88
|
+
const serverPublicPath = normalizeAssetPrefix(publicPath);
|
|
78
89
|
|
|
79
90
|
return {
|
|
80
91
|
'virtual/react-router/unstable_rsc/routes': createRscRouteConfig({
|
|
@@ -101,9 +112,51 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
101
112
|
assetsBuildDirectory: rscAssetsBuildDirectory,
|
|
102
113
|
publicPath,
|
|
103
114
|
}),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
// Every server-facing asset URL in the rspack RSC manifest -- bootstrap
|
|
116
|
+
// `entryJsFiles`, route `entryCssFiles`, client references' `cssFiles`
|
|
117
|
+
// (react-server-dom-rspack renders those as <link>s), and Flight's
|
|
118
|
+
// `moduleLoading.prefix` for client-chunk preloads -- carries the browser
|
|
119
|
+
// compiler's public path, recorded in `moduleLoading.prefix`. With the
|
|
120
|
+
// browser compiler on `'auto'` rspack records `/`, which the server cannot
|
|
121
|
+
// serve from. `__rspack_rsc_manifest__` is this same object, so aligning
|
|
122
|
+
// it once, in place (arrays are mutated, not replaced, because
|
|
123
|
+
// `createServerEntry` has already captured references), makes every
|
|
124
|
+
// consumer agree on the server prefix without stacking a second one. The
|
|
125
|
+
// aligned manifest is exported (not imported for side effects only) so a
|
|
126
|
+
// `sideEffects: false` package cannot tree-shake the alignment away.
|
|
127
|
+
'virtual/react-router/unstable_rsc/manifest-prefix': `const manifest = __webpack_require__.rscM;
|
|
128
|
+
const serverPrefix = ${JSON.stringify(serverPublicPath)};
|
|
129
|
+
const appliedPrefix = manifest?.moduleLoading?.prefix;
|
|
130
|
+
if (appliedPrefix && appliedPrefix !== serverPrefix) {
|
|
131
|
+
const rewrite = url =>
|
|
132
|
+
typeof url === "string" && url.startsWith(appliedPrefix)
|
|
133
|
+
? serverPrefix + url.slice(appliedPrefix.length)
|
|
134
|
+
: url;
|
|
135
|
+
const rewriteAll = list => {
|
|
136
|
+
if (Array.isArray(list)) for (let i = 0; i < list.length; i++) list[i] = rewrite(list[i]);
|
|
137
|
+
};
|
|
138
|
+
manifest.moduleLoading.prefix = serverPrefix;
|
|
139
|
+
rewriteAll(manifest.entryJsFiles);
|
|
140
|
+
for (const files of Object.values(manifest.entryCssFiles ?? {})) rewriteAll(files);
|
|
141
|
+
for (const reference of Object.values(manifest.clientManifest ?? {})) rewriteAll(reference.cssFiles);
|
|
142
|
+
}
|
|
143
|
+
export const rscManifest = manifest;
|
|
144
|
+
`,
|
|
145
|
+
// The compiled browser entry scripts come from the manifest (like Next's
|
|
146
|
+
// `buildManifest` or the Vite plugin's `loadBootstrapScriptContent`), so
|
|
147
|
+
// hashed entry filenames resolve without any naming contract. The list and
|
|
148
|
+
// its order are preserved. An empty list is a build problem (rspack only
|
|
149
|
+
// records entry files named `*.js`); fail loudly instead of guessing a
|
|
150
|
+
// filename that would render a document which cannot hydrate.
|
|
151
|
+
'virtual/react-router/unstable_rsc/bootstrap-scripts': `import { rscManifest } from "virtual/react-router/unstable_rsc/manifest-prefix";
|
|
152
|
+
const entryJsFiles = rscManifest?.entryJsFiles;
|
|
153
|
+
if (!entryJsFiles?.length) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"[rsbuild-plugin-react-router] The rspack RSC manifest lists no browser entry script (entryJsFiles is empty), so the server cannot render bootstrap scripts. Rspack only records entry files whose name ends in \\".js\\"; web output.filename.js values with a query (for example \\"[name].js?v=[contenthash:8]\\") or another extension are not supported in RSC mode."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
export default entryJsFiles;
|
|
159
|
+
`,
|
|
107
160
|
'virtual/react-router/unstable_rsc/server-manifest': `export default function getServerManifest() {
|
|
108
161
|
return __webpack_require__.rscM?.serverManifest;
|
|
109
162
|
}
|
|
@@ -19,7 +19,7 @@ import clientVersion from 'virtual/react-router/unstable_rsc/client-version';
|
|
|
19
19
|
import unstable_reactRouterServeConfig from 'virtual/react-router/unstable_rsc/react-router-serve-config';
|
|
20
20
|
import bootstrapScripts from 'virtual/react-router/unstable_rsc/bootstrap-scripts';
|
|
21
21
|
import getServerManifest from 'virtual/react-router/unstable_rsc/server-manifest';
|
|
22
|
-
import { generateHTML } from '
|
|
22
|
+
import { generateHTML } from 'virtual/react-router/unstable_rsc/entry-ssr';
|
|
23
23
|
|
|
24
24
|
export { unstable_reactRouterServeConfig };
|
|
25
25
|
|