rsbuild-plugin-react-router 0.6.6 → 0.7.1
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 +42 -2
- package/dist/511.js +10 -3
- package/dist/environment-output.d.ts +16 -2
- package/dist/federation.d.ts +26 -0
- package/dist/index.cjs +160 -76
- package/dist/index.js +149 -70
- package/dist/manifest.d.ts +31 -0
- package/dist/mode-plan.d.ts +1 -1
- package/dist/plugin-utils.d.ts +19 -13
- package/dist/rsc-virtual-modules.d.ts +1 -3
- package/package.json +1 -1
- package/src/environment-output.ts +60 -2
- package/src/federation.ts +93 -14
- package/src/index.ts +75 -62
- package/src/manifest.ts +96 -17
- package/src/mode-plan.ts +6 -12
- 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-virtual-modules.ts +59 -8
package/src/federation.ts
CHANGED
|
@@ -1,9 +1,73 @@
|
|
|
1
1
|
import type { Rspack } from '@rsbuild/core';
|
|
2
2
|
|
|
3
|
+
type ModuleFederationPluginOptionsLike = {
|
|
4
|
+
name?: string;
|
|
5
|
+
experiments?: { asyncStartup?: boolean };
|
|
6
|
+
};
|
|
7
|
+
|
|
3
8
|
type ModuleFederationPluginLike = {
|
|
4
9
|
name?: string;
|
|
5
|
-
_options?:
|
|
6
|
-
options?:
|
|
10
|
+
_options?: ModuleFederationPluginOptionsLike;
|
|
11
|
+
options?: ModuleFederationPluginOptionsLike;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getModuleFederationOptions = (
|
|
15
|
+
plugin: unknown
|
|
16
|
+
): ModuleFederationPluginOptionsLike | undefined => {
|
|
17
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const federationPlugin = plugin as ModuleFederationPluginLike;
|
|
21
|
+
if (
|
|
22
|
+
federationPlugin.name !== 'ModuleFederationPlugin' &&
|
|
23
|
+
federationPlugin.name !== 'RspackModuleFederationPlugin'
|
|
24
|
+
) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return federationPlugin._options ?? federationPlugin.options;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The Module Federation container name(s) configured on this compiler, i.e.
|
|
32
|
+
* the entry names of the remote containers it emits.
|
|
33
|
+
*/
|
|
34
|
+
export const getFederationContainerNames = (
|
|
35
|
+
rspackConfig: Rspack.Configuration | undefined
|
|
36
|
+
): string[] =>
|
|
37
|
+
(rspackConfig?.plugins ?? [])
|
|
38
|
+
.map(getModuleFederationOptions)
|
|
39
|
+
.map(options => options?.name)
|
|
40
|
+
.filter((name): name is string => typeof name === 'string');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Classic mode shares one runtime chunk across every browser entry so route
|
|
44
|
+
* module entries share a module registry. A federation container must not
|
|
45
|
+
* share it: importing the container would run the app entries' async startup
|
|
46
|
+
* (share-scope consumes) before the host has initialized the share scope,
|
|
47
|
+
* yielding duplicate singletons (a second React). Give containers their own
|
|
48
|
+
* runtime chunk.
|
|
49
|
+
*/
|
|
50
|
+
export const isolateFederationContainerRuntime = (
|
|
51
|
+
rspackConfig: Rspack.Configuration | undefined
|
|
52
|
+
): void => {
|
|
53
|
+
const containers = new Set(getFederationContainerNames(rspackConfig));
|
|
54
|
+
if (!rspackConfig || containers.size === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const current = rspackConfig.optimization?.runtimeChunk;
|
|
58
|
+
const appRuntimeName =
|
|
59
|
+
typeof current === 'object' && typeof current?.name === 'string'
|
|
60
|
+
? current.name
|
|
61
|
+
: 'runtime';
|
|
62
|
+
rspackConfig.optimization = {
|
|
63
|
+
...rspackConfig.optimization,
|
|
64
|
+
runtimeChunk: {
|
|
65
|
+
name: (entrypoint: { name: string }) =>
|
|
66
|
+
containers.has(entrypoint.name)
|
|
67
|
+
? `runtime-${entrypoint.name}`
|
|
68
|
+
: appRuntimeName,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
7
71
|
};
|
|
8
72
|
|
|
9
73
|
export const ensureFederationAsyncStartup = (
|
|
@@ -14,18 +78,7 @@ export const ensureFederationAsyncStartup = (
|
|
|
14
78
|
}
|
|
15
79
|
|
|
16
80
|
for (const plugin of rspackConfig.plugins) {
|
|
17
|
-
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const federationPlugin = plugin as ModuleFederationPluginLike;
|
|
21
|
-
if (
|
|
22
|
-
federationPlugin.name !== 'ModuleFederationPlugin' &&
|
|
23
|
-
federationPlugin.name !== 'RspackModuleFederationPlugin'
|
|
24
|
-
) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const pluginOptions = federationPlugin._options ?? federationPlugin.options;
|
|
81
|
+
const pluginOptions = getModuleFederationOptions(plugin);
|
|
29
82
|
if (!pluginOptions) {
|
|
30
83
|
continue;
|
|
31
84
|
}
|
|
@@ -36,3 +89,29 @@ export const ensureFederationAsyncStartup = (
|
|
|
36
89
|
};
|
|
37
90
|
}
|
|
38
91
|
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `@module-federation/node` replaces Rspack's `readFileVm` chunk loader with one
|
|
95
|
+
* that tracks loaded chunks privately, so initial chunks split off a
|
|
96
|
+
* multi-entry server build never satisfy Rspack's startup gate
|
|
97
|
+
* (`__webpack_require__.O`) and the async startup resolves to `undefined`
|
|
98
|
+
* instead of the server build's exports. Keep server code splitting to async
|
|
99
|
+
* chunks only. Runs at the final `tools.rspack` boundary so a user
|
|
100
|
+
* `optimization.splitChunks` override or a preset whose cache group selects
|
|
101
|
+
* `chunks: 'all'` (e.g. Rsbuild's `single-vendor`, `enforce: true`) cannot
|
|
102
|
+
* reintroduce initial chunk dependencies. A disabled `splitChunks` is kept.
|
|
103
|
+
*/
|
|
104
|
+
export const enforceAsyncOnlyServerSplitChunks = (
|
|
105
|
+
rspackConfig: Rspack.Configuration | undefined
|
|
106
|
+
): void => {
|
|
107
|
+
const splitChunks = rspackConfig?.optimization?.splitChunks;
|
|
108
|
+
if (!splitChunks) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
splitChunks.chunks = 'async';
|
|
112
|
+
for (const group of Object.values(splitChunks.cacheGroups ?? {})) {
|
|
113
|
+
if (group && typeof group === 'object' && 'chunks' in group) {
|
|
114
|
+
group.chunks = 'async';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
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,
|
|
@@ -30,6 +26,7 @@ import {
|
|
|
30
26
|
type ResolvedReactRouterConfig,
|
|
31
27
|
} from './react-router-config.js';
|
|
32
28
|
import {
|
|
29
|
+
collectUnsupportedRscScriptAssets,
|
|
33
30
|
configRoutesToRouteManifest,
|
|
34
31
|
createReactRouterManifestStats,
|
|
35
32
|
type ReactRouterManifestForDev as ReactRouterManifest,
|
|
@@ -222,13 +219,25 @@ export const pluginReactRouter = (
|
|
|
222
219
|
warnOnClientSourceMaps(normalized, msg => api.logger.warn(msg), 'web');
|
|
223
220
|
});
|
|
224
221
|
|
|
222
|
+
// The manifest / server `publicPath` follows the web environment's asset
|
|
223
|
+
// prefix because that is where the browser assets are served from. A web
|
|
224
|
+
// prefix the server cannot use (`'auto'`) falls back to the root prefix,
|
|
225
|
+
// so `output.assetPrefix: 'https://cdn/'` + web `'auto'` still emits CDN
|
|
226
|
+
// URLs from the server.
|
|
225
227
|
api.onBeforeCreateCompiler(() => {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
const root = api.getNormalizedConfig();
|
|
229
|
+
// `getNormalizedConfig({ environment: 'web' })` throws when the build was
|
|
230
|
+
// narrowed to other environments (`--environment node`), so look the web
|
|
231
|
+
// environment up on the root config instead.
|
|
232
|
+
const web = root.environments.web;
|
|
233
|
+
assetPrefix = resolveEffectiveAssetPrefix(
|
|
234
|
+
{
|
|
235
|
+
dev: web?.dev,
|
|
236
|
+
output: web?.output,
|
|
237
|
+
isBuild: api.context.action === 'build',
|
|
238
|
+
},
|
|
239
|
+
{ dev: root.dev, output: root.output }
|
|
240
|
+
);
|
|
232
241
|
});
|
|
233
242
|
|
|
234
243
|
const configPath = findEntryFile(resolve('react-router.config'));
|
|
@@ -765,6 +774,27 @@ export const pluginReactRouter = (
|
|
|
765
774
|
stats?.compilation,
|
|
766
775
|
manifestChunkNames
|
|
767
776
|
);
|
|
777
|
+
if (isRscMode && stats) {
|
|
778
|
+
// Rspack's RSC manifest only records browser scripts whose emitted
|
|
779
|
+
// name ends in ".js" (entry files and client-reference chunks
|
|
780
|
+
// alike); anything else silently disappears from `entryJsFiles` and
|
|
781
|
+
// the client manifest, and the server cannot bootstrap or preload
|
|
782
|
+
// it. Check the emitted output, which is what the manifest saw, so
|
|
783
|
+
// function filenames and `tools.rspack` overrides are covered too.
|
|
784
|
+
const unsupported = collectUnsupportedRscScriptAssets(
|
|
785
|
+
stats.compilation
|
|
786
|
+
);
|
|
787
|
+
if (unsupported.length > 0) {
|
|
788
|
+
throw new Error(
|
|
789
|
+
`[${PLUGIN_NAME}] RSC mode requires every browser JavaScript asset to be named "*.js" (no query, no other extension): rspack's RSC manifest omits ${unsupported
|
|
790
|
+
.slice(0, 5)
|
|
791
|
+
.map(asset => JSON.stringify(asset))
|
|
792
|
+
.join(
|
|
793
|
+
', '
|
|
794
|
+
)}${unsupported.length > 5 ? ` and ${unsupported.length - 5} more` : ''}. Adjust web \`output.filename.js\` / \`chunkFilename\`.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
768
798
|
}
|
|
769
799
|
if (pluginOptions.federation && ssr) {
|
|
770
800
|
const serverBuildDir = resolve(buildDirectory, 'server');
|
|
@@ -832,39 +862,19 @@ export const pluginReactRouter = (
|
|
|
832
862
|
}
|
|
833
863
|
|
|
834
864
|
// Public requests stay bare while Rspack resolves seeded virtual files.
|
|
835
|
-
const createVirtualModulePlugin = (
|
|
836
|
-
publicPath: string,
|
|
837
|
-
jsDistPath: string
|
|
838
|
-
) => {
|
|
865
|
+
const createVirtualModulePlugin = (publicPath: string) => {
|
|
839
866
|
return new rspack.experiments.VirtualModulesPlugin(
|
|
840
|
-
mapVirtualModules(modePlan.createVirtualModules(publicPath
|
|
867
|
+
mapVirtualModules(modePlan.createVirtualModules(publicPath))
|
|
841
868
|
);
|
|
842
869
|
};
|
|
843
870
|
|
|
844
871
|
api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
(typeof webDistPath === 'object' ? webDistPath.js : undefined) ??
|
|
852
|
-
(typeof rootDistPath === 'object' ? rootDistPath.js : undefined) ??
|
|
853
|
-
DEFAULT_JS_DIST_PATH;
|
|
854
|
-
const assetPrefix = resolveEffectiveAssetPrefix({
|
|
855
|
-
dev: config.dev,
|
|
856
|
-
output: config.output,
|
|
857
|
-
isBuild,
|
|
858
|
-
});
|
|
859
|
-
const vmodPlugin = createVirtualModulePlugin(assetPrefix, jsDistPath);
|
|
860
|
-
const useAsyncNodeChunkLoading =
|
|
861
|
-
options.federation && resolvedServerOutput === 'commonjs';
|
|
862
|
-
let nodeChunkLoading: 'import' | 'async-node' | 'require' = 'require';
|
|
863
|
-
if (resolvedServerOutput === 'module') {
|
|
864
|
-
nodeChunkLoading = 'import';
|
|
865
|
-
} else if (useAsyncNodeChunkLoading) {
|
|
866
|
-
nodeChunkLoading = 'async-node';
|
|
867
|
-
}
|
|
872
|
+
const webConfig = config.environments?.web;
|
|
873
|
+
const assetPrefix = resolveEffectiveAssetPrefix(
|
|
874
|
+
{ dev: webConfig?.dev, output: webConfig?.output, isBuild },
|
|
875
|
+
{ dev: config.dev, output: config.output }
|
|
876
|
+
);
|
|
877
|
+
const vmodPlugin = createVirtualModulePlugin(assetPrefix);
|
|
868
878
|
const configuredLazyCompilation = Object.prototype.hasOwnProperty.call(
|
|
869
879
|
options,
|
|
870
880
|
'lazyCompilation'
|
|
@@ -964,9 +974,6 @@ export const pluginReactRouter = (
|
|
|
964
974
|
}),
|
|
965
975
|
},
|
|
966
976
|
output: {
|
|
967
|
-
filename: {
|
|
968
|
-
js: '[name].js',
|
|
969
|
-
},
|
|
970
977
|
distPath: {
|
|
971
978
|
root: outputClientPath,
|
|
972
979
|
},
|
|
@@ -985,15 +992,6 @@ export const pluginReactRouter = (
|
|
|
985
992
|
],
|
|
986
993
|
},
|
|
987
994
|
externalsType: modePlan.webExternalsType,
|
|
988
|
-
output: {
|
|
989
|
-
...modePlan.webOutput,
|
|
990
|
-
publicPath: assetPrefix,
|
|
991
|
-
...(options.federation
|
|
992
|
-
? {
|
|
993
|
-
chunkLoading: 'import',
|
|
994
|
-
}
|
|
995
|
-
: {}),
|
|
996
|
-
},
|
|
997
995
|
optimization: modePlan.webOptimization,
|
|
998
996
|
},
|
|
999
997
|
},
|
|
@@ -1038,17 +1036,6 @@ export const pluginReactRouter = (
|
|
|
1038
1036
|
externals: modePlan.nodeExternals,
|
|
1039
1037
|
...modePlan.nodeDependencies,
|
|
1040
1038
|
externalsType: resolvedServerOutput,
|
|
1041
|
-
output: {
|
|
1042
|
-
chunkFormat: resolvedServerOutput,
|
|
1043
|
-
chunkLoading: nodeChunkLoading,
|
|
1044
|
-
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
|
1045
|
-
devtoolFallbackModuleFilenameTemplate:
|
|
1046
|
-
'[absolute-resource-path]?[hash]',
|
|
1047
|
-
workerChunkLoading: nodeChunkLoading,
|
|
1048
|
-
wasmLoading: 'fetch',
|
|
1049
|
-
module: resolvedServerOutput === 'module',
|
|
1050
|
-
chunkFilename: 'static/js/async/[name].js',
|
|
1051
|
-
},
|
|
1052
1039
|
},
|
|
1053
1040
|
},
|
|
1054
1041
|
},
|
|
@@ -1060,8 +1047,34 @@ export const pluginReactRouter = (
|
|
|
1060
1047
|
api,
|
|
1061
1048
|
federation: pluginOptions.federation,
|
|
1062
1049
|
resolvedServerOutput,
|
|
1050
|
+
webOutput: modePlan.webOutput,
|
|
1063
1051
|
});
|
|
1064
1052
|
|
|
1053
|
+
if (pluginOptions.federation && modePlan.kind === 'classic') {
|
|
1054
|
+
// Module Federation's async startup makes every entry's startup a
|
|
1055
|
+
// promise. React Router imports each browser route-module entry
|
|
1056
|
+
// synchronously (`import * as route0 from ".../root.js"`) and reads its
|
|
1057
|
+
// exports right away, and `import()`s split route chunks the same way.
|
|
1058
|
+
// Making those entry modules async (top-level await) turns Rspack's
|
|
1059
|
+
// module-library export into `(await startup).default`, so importers
|
|
1060
|
+
// wait for the awaited startup instead of reading a snapshot of the
|
|
1061
|
+
// promise (#132). Runs after SWC so it applies to the final module code.
|
|
1062
|
+
const browserEntryModules = new Set([
|
|
1063
|
+
finalEntryClientPath,
|
|
1064
|
+
...routeByFilePath.keys(),
|
|
1065
|
+
]);
|
|
1066
|
+
api.transform(
|
|
1067
|
+
{
|
|
1068
|
+
environments: ['web'],
|
|
1069
|
+
order: 'post',
|
|
1070
|
+
test: (resourcePath: string) => browserEntryModules.has(resourcePath),
|
|
1071
|
+
},
|
|
1072
|
+
// `export {}` keeps an otherwise-empty client module (a route with only
|
|
1073
|
+
// server exports) parsed as ESM, which top-level await requires.
|
|
1074
|
+
({ code }) => `${code}\nexport {};\nawait Promise.resolve();\n`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1065
1078
|
if (modePlan.kind === 'classic' && useRouteModuleTransformLoader) {
|
|
1066
1079
|
api.modifyEnvironmentConfig(
|
|
1067
1080
|
async (config, { name, mergeEnvironmentConfig }) => {
|
package/src/manifest.ts
CHANGED
|
@@ -167,18 +167,89 @@ type ReactRouterManifestStatsCompilation = {
|
|
|
167
167
|
entrypoints?: ReactRouterManifestStatsLookup<ReactRouterManifestStatsEntrypoint>;
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The minimal compilation surface for `collectUnsupportedRscScriptAssets`.
|
|
184
|
+
* Chunks are classified as JavaScript-emitting from compilation metadata (their
|
|
185
|
+
* `javascript` content hash / modules), never from a filename.
|
|
186
|
+
*/
|
|
187
|
+
export type RscScriptAssetCompilation = {
|
|
188
|
+
chunks: Iterable<RscScriptAssetChunk>;
|
|
189
|
+
chunkGraph: {
|
|
190
|
+
getChunkModulesIterableBySourceType(
|
|
191
|
+
chunk: RscScriptAssetChunk,
|
|
192
|
+
sourceType: string
|
|
193
|
+
): Iterable<unknown>;
|
|
194
|
+
};
|
|
195
|
+
outputOptions: {
|
|
196
|
+
filename?: unknown;
|
|
197
|
+
chunkFilename?: unknown;
|
|
198
|
+
};
|
|
199
|
+
getPath(filename: string, data: Record<string, unknown>): string;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export type RscScriptAssetChunk = {
|
|
203
|
+
contentHash?: Record<string, string>;
|
|
204
|
+
canBeInitial(): boolean;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const hasSome = (iterable: Iterable<unknown>): boolean => {
|
|
208
|
+
for (const _ of iterable) {
|
|
209
|
+
return true;
|
|
175
210
|
}
|
|
211
|
+
return false;
|
|
212
|
+
};
|
|
176
213
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Browser JavaScript assets rspack's RSC manifest would drop: it only records
|
|
216
|
+
* chunk files whose emitted name ends in ".js", so `.mjs` names, query-hash
|
|
217
|
+
* names (`[name].js?v=...`), or any other extension vanish from
|
|
218
|
+
* `entryJsFiles` and the client manifest. The emitted script name is derived
|
|
219
|
+
* from the chunk's own filename template (entry or async) the same way rspack
|
|
220
|
+
* emits it, so function templates and `tools.rspack` overrides are covered.
|
|
221
|
+
*/
|
|
222
|
+
export const collectUnsupportedRscScriptAssets = (
|
|
223
|
+
compilation: RscScriptAssetCompilation
|
|
224
|
+
): string[] => {
|
|
225
|
+
const unsupported = new Set<string>();
|
|
226
|
+
for (const chunk of compilation.chunks) {
|
|
227
|
+
const emitsJavaScript =
|
|
228
|
+
chunk.contentHash?.javascript !== undefined ||
|
|
229
|
+
hasSome(
|
|
230
|
+
compilation.chunkGraph.getChunkModulesIterableBySourceType(
|
|
231
|
+
chunk,
|
|
232
|
+
'javascript'
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
if (!emitsJavaScript) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const pathData = { chunk, contentHashType: 'javascript' };
|
|
239
|
+
const template = chunk.canBeInitial()
|
|
240
|
+
? compilation.outputOptions.filename
|
|
241
|
+
: compilation.outputOptions.chunkFilename;
|
|
242
|
+
const resolvedTemplate =
|
|
243
|
+
typeof template === 'function' ? template(pathData) : template;
|
|
244
|
+
if (typeof resolvedTemplate !== 'string') {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const file = compilation.getPath(resolvedTemplate, pathData);
|
|
248
|
+
if (!file.endsWith('.js')) {
|
|
249
|
+
unsupported.add(file);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return [...unsupported];
|
|
182
253
|
};
|
|
183
254
|
|
|
184
255
|
const collectManifestFilesByName = <T>(
|
|
@@ -241,8 +312,7 @@ export const createReactRouterManifestStats = (
|
|
|
241
312
|
const assetsByChunkName = collectManifestFilesByName(
|
|
242
313
|
compilation.namedChunks,
|
|
243
314
|
chunkNames,
|
|
244
|
-
(
|
|
245
|
-
orderChunkFiles(chunkName, Array.from(chunk.files ?? []))
|
|
315
|
+
(_chunkName, chunk) => Array.from(chunk.files ?? [])
|
|
246
316
|
);
|
|
247
317
|
const entrypointFilesByName = compilation.entrypoints
|
|
248
318
|
? collectManifestFilesByName(
|
|
@@ -303,22 +373,31 @@ const createChunkAssetResolver = (
|
|
|
303
373
|
|
|
304
374
|
const cssAssets = new Set<string>();
|
|
305
375
|
const jsAssets = new Set<string>();
|
|
376
|
+
// The chunk's own files come first so `js[0]` is the route module itself;
|
|
377
|
+
// entrypoint files (runtime, shared vendor chunks) follow as `imports`.
|
|
306
378
|
for (const asset of assets) {
|
|
307
|
-
if (asset
|
|
379
|
+
if (isManifestCssAsset(asset)) {
|
|
308
380
|
cssAssets.add(asset);
|
|
309
|
-
} else if (asset
|
|
381
|
+
} else if (isManifestJsAsset(asset)) {
|
|
310
382
|
jsAssets.add(asset);
|
|
311
383
|
}
|
|
312
384
|
}
|
|
313
385
|
for (const asset of clientStats?.entrypointFilesByName?.[chunkName] ?? []) {
|
|
314
|
-
if (asset
|
|
386
|
+
if (isManifestCssAsset(asset)) {
|
|
315
387
|
cssAssets.add(asset);
|
|
316
|
-
} else if (includeEntrypointJs && asset
|
|
388
|
+
} else if (includeEntrypointJs && isManifestJsAsset(asset)) {
|
|
317
389
|
jsAssets.add(asset);
|
|
318
390
|
}
|
|
319
391
|
}
|
|
320
392
|
if (jsAssets.size === 0) {
|
|
321
|
-
|
|
393
|
+
// Compilation metadata exists for this chunk but names no module script.
|
|
394
|
+
// Guessing `<dir>/<chunk>.js` here would turn an identifiable build
|
|
395
|
+
// problem into a browser 404, so surface it at build time instead.
|
|
396
|
+
throw new Error(
|
|
397
|
+
`[react-router] Chunk "${chunkName}" emitted no JavaScript asset the browser manifest can reference (files: ${
|
|
398
|
+
assets.join(', ') || 'none'
|
|
399
|
+
}). Check the web \`output.filename.js\` scheme.`
|
|
400
|
+
);
|
|
322
401
|
}
|
|
323
402
|
|
|
324
403
|
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;
|
|
@@ -183,14 +180,13 @@ const createRscModePlan = async ({
|
|
|
183
180
|
layer: RSC_LAYERS.rsc,
|
|
184
181
|
},
|
|
185
182
|
},
|
|
186
|
-
createVirtualModules: (publicPath: string
|
|
183
|
+
createVirtualModules: (publicPath: string) =>
|
|
187
184
|
createReactRouterRscVirtualModules({
|
|
188
185
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
189
186
|
appDirectory,
|
|
190
187
|
basename,
|
|
191
188
|
buildDirectory,
|
|
192
189
|
isBuild,
|
|
193
|
-
jsDistPath,
|
|
194
190
|
outputClientPath,
|
|
195
191
|
publicPath,
|
|
196
192
|
routeDiscovery,
|
|
@@ -314,7 +310,7 @@ const createClassicModePlan = async ({
|
|
|
314
310
|
defaultEntryName,
|
|
315
311
|
serverBundleEntries: artifacts.serverBundleEntries,
|
|
316
312
|
}),
|
|
317
|
-
createVirtualModules: (publicPath: string
|
|
313
|
+
createVirtualModules: (publicPath: string) =>
|
|
318
314
|
createClassicVirtualModules({
|
|
319
315
|
allowedActionOrigins: allowedActionOriginsForBuild,
|
|
320
316
|
appDirectory,
|
|
@@ -349,11 +345,9 @@ const createClassicModePlan = async ({
|
|
|
349
345
|
wasmLoading: 'fetch',
|
|
350
346
|
library: { type: 'module' },
|
|
351
347
|
module: true,
|
|
352
|
-
// Async
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
? { chunkFilename: 'static/js/async/[id]-[contenthash:16].js' }
|
|
356
|
-
: {}),
|
|
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).
|
|
357
351
|
},
|
|
358
352
|
webOptimization: {
|
|
359
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
|
);
|