rsbuild-plugin-react-router 0.7.0 → 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 +20 -2
- package/dist/federation.d.ts +26 -0
- package/dist/index.cjs +70 -15
- package/dist/index.js +73 -15
- package/dist/manifest.d.ts +29 -0
- package/package.json +1 -1
- package/src/environment-output.ts +10 -1
- package/src/federation.ts +93 -14
- package/src/index.ts +47 -16
- package/src/manifest.ts +73 -0
- package/src/rsc-virtual-modules.ts +12 -4
package/README.md
CHANGED
|
@@ -103,9 +103,17 @@ pluginReactRouter({
|
|
|
103
103
|
| `federation` | `false` | Enables the plugin's experimental Module Federation integration. |
|
|
104
104
|
|
|
105
105
|
When `federation` is enabled, configure the Module Federation plugin with
|
|
106
|
-
`experiments.asyncStartup: true
|
|
106
|
+
`experiments.asyncStartup: true` on every compiler (the plugin enforces it) and
|
|
107
|
+
keep shared dependencies non-eager. The dev server resolves async server build
|
|
107
108
|
exports automatically; production custom servers or adapters should resolve
|
|
108
|
-
async exports before passing the build to React Router's request handler
|
|
109
|
+
async exports before passing the build to React Router's request handler
|
|
110
|
+
(`resolveReactRouterServerBuild`). Give remote containers an explicit
|
|
111
|
+
`filename` (for example `static/js/remote.js`); every other browser chunk keeps
|
|
112
|
+
Rsbuild's content hash. Under the hood the plugin gives each container its own
|
|
113
|
+
runtime chunk (so importing a container does not start the app's own share
|
|
114
|
+
consumes), makes browser route-module entries async so their exports resolve
|
|
115
|
+
through the async startup, and keeps server code splitting async-only so the
|
|
116
|
+
`@module-federation/node` chunk loader can satisfy the server build's startup.
|
|
109
117
|
|
|
110
118
|
### React Router Config
|
|
111
119
|
|
|
@@ -365,6 +373,16 @@ content-hashed filenames, and `output.filename`, `output.filenameHash`,
|
|
|
365
373
|
`output.distPath`, and `tools.rspack` output settings you configure govern
|
|
366
374
|
the emitted files and the manifest URLs that reference them.
|
|
367
375
|
|
|
376
|
+
Supported browser filename forms differ by mode:
|
|
377
|
+
|
|
378
|
+
- **Classic mode** accepts any scheme, including `.mjs`/`.cjs` and query-hash
|
|
379
|
+
names such as `[name].js?v=[contenthash:8]`; the browser manifest classifies
|
|
380
|
+
emitted assets by pathname and keeps the full reference.
|
|
381
|
+
- **RSC mode** requires every browser JavaScript asset to be named `*.js`
|
|
382
|
+
(hashes are fine, e.g. `[contenthash:8]-[name].js`): rspack's RSC manifest
|
|
383
|
+
only records `.js` files, so the build fails with a clear error for query
|
|
384
|
+
hashes or other extensions.
|
|
385
|
+
|
|
368
386
|
## Custom Server Setup
|
|
369
387
|
|
|
370
388
|
The plugin supports two ways to handle server-side rendering:
|
package/dist/federation.d.ts
CHANGED
|
@@ -1,2 +1,28 @@
|
|
|
1
1
|
import type { Rspack } from '@rsbuild/core';
|
|
2
|
+
/**
|
|
3
|
+
* The Module Federation container name(s) configured on this compiler, i.e.
|
|
4
|
+
* the entry names of the remote containers it emits.
|
|
5
|
+
*/
|
|
6
|
+
export declare const getFederationContainerNames: (rspackConfig: Rspack.Configuration | undefined) => string[];
|
|
7
|
+
/**
|
|
8
|
+
* Classic mode shares one runtime chunk across every browser entry so route
|
|
9
|
+
* module entries share a module registry. A federation container must not
|
|
10
|
+
* share it: importing the container would run the app entries' async startup
|
|
11
|
+
* (share-scope consumes) before the host has initialized the share scope,
|
|
12
|
+
* yielding duplicate singletons (a second React). Give containers their own
|
|
13
|
+
* runtime chunk.
|
|
14
|
+
*/
|
|
15
|
+
export declare const isolateFederationContainerRuntime: (rspackConfig: Rspack.Configuration | undefined) => void;
|
|
2
16
|
export declare const ensureFederationAsyncStartup: (rspackConfig: Rspack.Configuration | undefined) => void;
|
|
17
|
+
/**
|
|
18
|
+
* `@module-federation/node` replaces Rspack's `readFileVm` chunk loader with one
|
|
19
|
+
* that tracks loaded chunks privately, so initial chunks split off a
|
|
20
|
+
* multi-entry server build never satisfy Rspack's startup gate
|
|
21
|
+
* (`__webpack_require__.O`) and the async startup resolves to `undefined`
|
|
22
|
+
* instead of the server build's exports. Keep server code splitting to async
|
|
23
|
+
* chunks only. Runs at the final `tools.rspack` boundary so a user
|
|
24
|
+
* `optimization.splitChunks` override or a preset whose cache group selects
|
|
25
|
+
* `chunks: 'all'` (e.g. Rsbuild's `single-vendor`, `enforce: true`) cannot
|
|
26
|
+
* reintroduce initial chunk dependencies. A disabled `splitChunks` is kept.
|
|
27
|
+
*/
|
|
28
|
+
export declare const enforceAsyncOnlyServerSplitChunks: (rspackConfig: Rspack.Configuration | undefined) => void;
|
package/dist/index.cjs
CHANGED
|
@@ -617,15 +617,29 @@ const resolveEntryWithTemplate = ({ appDirectory, entryName, templateName, templ
|
|
|
617
617
|
hasServerApp,
|
|
618
618
|
serverAppPath
|
|
619
619
|
};
|
|
620
|
+
}, getModuleFederationOptions = (plugin)=>{
|
|
621
|
+
if (plugin && 'object' == typeof plugin && ('ModuleFederationPlugin' === plugin.name || 'RspackModuleFederationPlugin' === plugin.name)) return plugin._options ?? plugin.options;
|
|
622
|
+
}, getFederationContainerNames = (rspackConfig)=>(rspackConfig?.plugins ?? []).map(getModuleFederationOptions).map((options)=>options?.name).filter((name)=>'string' == typeof name), isolateFederationContainerRuntime = (rspackConfig)=>{
|
|
623
|
+
let containers = new Set(getFederationContainerNames(rspackConfig));
|
|
624
|
+
if (!rspackConfig || 0 === containers.size) return;
|
|
625
|
+
let current = rspackConfig.optimization?.runtimeChunk, appRuntimeName = 'object' == typeof current && 'string' == typeof current?.name ? current.name : 'runtime';
|
|
626
|
+
rspackConfig.optimization = {
|
|
627
|
+
...rspackConfig.optimization,
|
|
628
|
+
runtimeChunk: {
|
|
629
|
+
name: (entrypoint)=>containers.has(entrypoint.name) ? `runtime-${entrypoint.name}` : appRuntimeName
|
|
630
|
+
}
|
|
631
|
+
};
|
|
620
632
|
}, ensureFederationAsyncStartup = (rspackConfig)=>{
|
|
621
633
|
if (rspackConfig?.plugins?.length) for (let plugin of rspackConfig.plugins){
|
|
622
|
-
|
|
623
|
-
let pluginOptions = plugin._options ?? plugin.options;
|
|
634
|
+
let pluginOptions = getModuleFederationOptions(plugin);
|
|
624
635
|
pluginOptions && (pluginOptions.experiments = {
|
|
625
636
|
...pluginOptions.experiments,
|
|
626
637
|
asyncStartup: !0
|
|
627
638
|
});
|
|
628
639
|
}
|
|
640
|
+
}, enforceAsyncOnlyServerSplitChunks = (rspackConfig)=>{
|
|
641
|
+
let splitChunks = rspackConfig?.optimization?.splitChunks;
|
|
642
|
+
if (splitChunks) for (let group of (splitChunks.chunks = 'async', Object.values(splitChunks.cacheGroups ?? {})))group && 'object' == typeof group && 'chunks' in group && (group.chunks = 'async');
|
|
629
643
|
}, registerReactRouterEnvironmentOutput = ({ api, federation, resolvedServerOutput, webOutput })=>{
|
|
630
644
|
let nodeChunkLoading = 'module' === resolvedServerOutput ? 'import' : federation ? 'async-node' : 'require';
|
|
631
645
|
api.modifyRspackConfig((rspackConfig, { environment, mergeConfig })=>'web' === environment.name ? mergeConfig(rspackConfig, {
|
|
@@ -649,7 +663,7 @@ const resolveEntryWithTemplate = ({ appDirectory, entryName, templateName, templ
|
|
|
649
663
|
}) : rspackConfig), api.modifyEnvironmentConfig(async (config, { name, mergeEnvironmentConfig })=>'web' !== name && 'node' !== name ? config : mergeEnvironmentConfig(config, {
|
|
650
664
|
tools: {
|
|
651
665
|
rspack: (rspackConfig)=>{
|
|
652
|
-
if (federation && ensureFederationAsyncStartup(rspackConfig), 'node' === name) {
|
|
666
|
+
if (federation && (ensureFederationAsyncStartup(rspackConfig), 'web' === name ? isolateFederationContainerRuntime(rspackConfig) : enforceAsyncOnlyServerSplitChunks(rspackConfig)), 'node' === name) {
|
|
653
667
|
let output = rspackConfig.output;
|
|
654
668
|
if (output) {
|
|
655
669
|
let library = output.library, libraryOptions = library && 'object' == typeof library && !Array.isArray(library) ? library : {};
|
|
@@ -1470,7 +1484,7 @@ const getCurrentVersion = ()=>moduleVersion, setCurrentVersion = (version)=>{
|
|
|
1470
1484
|
}), findLast = null, zip = null, Iterable_zipWith = null, intersperse = null, Iterable_containsWith = (isEquivalent)=>dual(2, (self, a)=>{
|
|
1471
1485
|
for (let i of self)if (isEquivalent(a, i)) return !0;
|
|
1472
1486
|
return !1;
|
|
1473
|
-
}), Iterable_equivalence = null, Iterable_contains = null, chunksOf = null, groupWith = null,
|
|
1487
|
+
}), Iterable_equivalence = null, Iterable_contains = null, chunksOf = null, groupWith = null, Iterable_group = null, groupBy = null, constEmpty = {
|
|
1474
1488
|
[Symbol.iterator]: ()=>constEmptyIterator
|
|
1475
1489
|
}, constEmptyIterator = {
|
|
1476
1490
|
next: ()=>({
|
|
@@ -11816,7 +11830,25 @@ const createReactRouterManifestOptions = ({ routeChunks, routeModuleAnalysis })=
|
|
|
11816
11830
|
routeModuleAnalysis
|
|
11817
11831
|
} : {}
|
|
11818
11832
|
};
|
|
11819
|
-
}, isManifestJsAsset = (asset)=>/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset), isManifestCssAsset = (asset)=>/\.css(?:\?.*)?$/.test(asset),
|
|
11833
|
+
}, isManifestJsAsset = (asset)=>/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset), isManifestCssAsset = (asset)=>/\.css(?:\?.*)?$/.test(asset), hasSome = (iterable)=>{
|
|
11834
|
+
for (let _ of iterable)return !0;
|
|
11835
|
+
return !1;
|
|
11836
|
+
}, collectUnsupportedRscScriptAssets = (compilation)=>{
|
|
11837
|
+
let unsupported = new Set();
|
|
11838
|
+
for (let chunk of compilation.chunks){
|
|
11839
|
+
if (!(chunk.contentHash?.javascript !== void 0 || hasSome(compilation.chunkGraph.getChunkModulesIterableBySourceType(chunk, "javascript")))) continue;
|
|
11840
|
+
let pathData = {
|
|
11841
|
+
chunk,
|
|
11842
|
+
contentHashType: "javascript"
|
|
11843
|
+
}, template = chunk.canBeInitial() ? compilation.outputOptions.filename : compilation.outputOptions.chunkFilename, resolvedTemplate = 'function' == typeof template ? template(pathData) : template;
|
|
11844
|
+
if ('string' != typeof resolvedTemplate) continue;
|
|
11845
|
+
let file = compilation.getPath(resolvedTemplate, pathData);
|
|
11846
|
+
file.endsWith('.js') || unsupported.add(file);
|
|
11847
|
+
}
|
|
11848
|
+
return [
|
|
11849
|
+
...unsupported
|
|
11850
|
+
];
|
|
11851
|
+
}, collectManifestFilesByName = (items, names, getFiles)=>{
|
|
11820
11852
|
let filesByName = {};
|
|
11821
11853
|
if (!names) {
|
|
11822
11854
|
for (let [name, item] of items)null != item && (filesByName[name] = getFiles(name, item));
|
|
@@ -14758,11 +14790,19 @@ export {
|
|
|
14758
14790
|
'virtual/react-router/unstable_rsc/manifest-prefix': `const manifest = __webpack_require__.rscM;
|
|
14759
14791
|
const serverPrefix = ${JSON.stringify(serverPublicPath)};
|
|
14760
14792
|
const appliedPrefix = manifest?.moduleLoading?.prefix;
|
|
14761
|
-
|
|
14793
|
+
// An empty applied prefix (web \`assetPrefix: ''\`) yields relative references
|
|
14794
|
+
// such as "static/js/index.js"; those are rebased too, while absolute and
|
|
14795
|
+
// protocol-relative URLs are left alone.
|
|
14796
|
+
const isAbsoluteUrl = url => /^(?:[a-z][a-z\\d+.-]*:|\\/\\/|\\/)/i.test(url);
|
|
14797
|
+
if (typeof appliedPrefix === "string" && appliedPrefix !== serverPrefix) {
|
|
14762
14798
|
const rewrite = url =>
|
|
14763
|
-
typeof url
|
|
14764
|
-
?
|
|
14765
|
-
: url
|
|
14799
|
+
typeof url !== "string"
|
|
14800
|
+
? url
|
|
14801
|
+
: appliedPrefix !== "" && url.startsWith(appliedPrefix)
|
|
14802
|
+
? serverPrefix + url.slice(appliedPrefix.length)
|
|
14803
|
+
: appliedPrefix === "" && !isAbsoluteUrl(url)
|
|
14804
|
+
? serverPrefix + url
|
|
14805
|
+
: url;
|
|
14766
14806
|
const rewriteAll = list => {
|
|
14767
14807
|
if (Array.isArray(list)) for (let i = 0; i < list.length; i++) list[i] = rewrite(list[i]);
|
|
14768
14808
|
};
|
|
@@ -16084,8 +16124,12 @@ export default entryJsFiles;
|
|
|
16084
16124
|
initialRouteTopology: routeTopology.initialRouteTopology,
|
|
16085
16125
|
onRouteTopologyChange: pluginOptions.onRouteTopologyChange
|
|
16086
16126
|
});
|
|
16087
|
-
api.onAfterEnvironmentCompile(({ stats, environment })=>{
|
|
16088
|
-
if ('web' === environment.name && (clientStats = createReactRouterManifestStats(stats?.compilation, manifestChunkNames)
|
|
16127
|
+
if (api.onAfterEnvironmentCompile(({ stats, environment })=>{
|
|
16128
|
+
if ('web' === environment.name && (clientStats = createReactRouterManifestStats(stats?.compilation, manifestChunkNames), isRscMode && stats)) {
|
|
16129
|
+
let unsupported = collectUnsupportedRscScriptAssets(stats.compilation);
|
|
16130
|
+
if (unsupported.length > 0) throw Error(`[${PLUGIN_NAME}] RSC mode requires every browser JavaScript asset to be named "*.js" (no query, no other extension): rspack's RSC manifest omits ${unsupported.slice(0, 5).map((asset)=>JSON.stringify(asset)).join(', ')}${unsupported.length > 5 ? ` and ${unsupported.length - 5} more` : ''}. Adjust web \`output.filename.js\` / \`chunkFilename\`.`);
|
|
16131
|
+
}
|
|
16132
|
+
if (pluginOptions.federation && ssr) {
|
|
16089
16133
|
let serverBuildDir = (0, external_pathe_namespaceObject.resolve)(buildDirectory, 'server'), clientBuildDir = (0, external_pathe_namespaceObject.resolve)(buildDirectory, 'client');
|
|
16090
16134
|
if ((0, external_node_fs_namespaceObject.existsSync)(serverBuildDir)) {
|
|
16091
16135
|
let ssrDir = (0, external_pathe_namespaceObject.resolve)(clientBuildDir, 'static');
|
|
@@ -16128,9 +16172,7 @@ export default entryJsFiles;
|
|
|
16128
16172
|
prerenderPaths: modePlan.prerenderPaths,
|
|
16129
16173
|
basename
|
|
16130
16174
|
})))), api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig })=>{
|
|
16131
|
-
let publicPath, webConfig = config.environments?.web,
|
|
16132
|
-
if (isRscMode && 'string' == typeof webJsFilename && !/\.js$/.test(webJsFilename)) throw Error(`[${PLUGIN_NAME}] RSC mode requires web \`output.filename.js\` to end in ".js" (got ${JSON.stringify(webJsFilename)}): rspack's RSC manifest omits entry files with a query or another extension, so the server could not render bootstrap scripts.`);
|
|
16133
|
-
let vmodPlugin = (publicPath = resolveEffectiveAssetPrefix({
|
|
16175
|
+
let publicPath, webConfig = config.environments?.web, vmodPlugin = (publicPath = resolveEffectiveAssetPrefix({
|
|
16134
16176
|
dev: webConfig?.dev,
|
|
16135
16177
|
output: webConfig?.output,
|
|
16136
16178
|
isBuild
|
|
@@ -16257,7 +16299,20 @@ export default entryJsFiles;
|
|
|
16257
16299
|
federation: pluginOptions.federation,
|
|
16258
16300
|
resolvedServerOutput,
|
|
16259
16301
|
webOutput: modePlan.webOutput
|
|
16260
|
-
}), 'classic' === modePlan.kind
|
|
16302
|
+
}), pluginOptions.federation && 'classic' === modePlan.kind) {
|
|
16303
|
+
let browserEntryModules = new Set([
|
|
16304
|
+
finalEntryClientPath,
|
|
16305
|
+
...routeByFilePath.keys()
|
|
16306
|
+
]);
|
|
16307
|
+
api.transform({
|
|
16308
|
+
environments: [
|
|
16309
|
+
'web'
|
|
16310
|
+
],
|
|
16311
|
+
order: 'post',
|
|
16312
|
+
test: (resourcePath)=>browserEntryModules.has(resourcePath)
|
|
16313
|
+
}, ({ code })=>`${code}\nexport {};\nawait Promise.resolve();\n`);
|
|
16314
|
+
}
|
|
16315
|
+
'classic' === modePlan.kind && useRouteModuleTransformLoader && api.modifyEnvironmentConfig(async (config, { name, mergeEnvironmentConfig })=>'web' !== name && 'node' !== name ? config : mergeEnvironmentConfig(config, {
|
|
16261
16316
|
tools: {
|
|
16262
16317
|
rspack: (rspackConfig)=>{
|
|
16263
16318
|
let environmentDevHmrEnabled = 'web' === name && !isBuild && void 0 !== devHmrRefreshRuntimePath && 'development' === config.mode && config.dev?.hmr !== !1 && isRspackSwcReactRefreshEnabled(rspackConfig);
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,8 @@ let getAvailableCpuCount = ()=>'function' == typeof availableParallelism ? avail
|
|
|
38
38
|
}, resolveEntryWithTemplate = ({ appDirectory, entryName, templateName, templatesDirectory })=>{
|
|
39
39
|
let userEntryPath = findEntryFile(external_pathe_resolve(appDirectory, entryName));
|
|
40
40
|
return existsSync(userEntryPath) ? userEntryPath : external_pathe_resolve(templatesDirectory, templateName);
|
|
41
|
+
}, getModuleFederationOptions = (plugin)=>{
|
|
42
|
+
if (plugin && 'object' == typeof plugin && ('ModuleFederationPlugin' === plugin.name || 'RspackModuleFederationPlugin' === plugin.name)) return plugin._options ?? plugin.options;
|
|
41
43
|
}, Function_dual = function(arity, body) {
|
|
42
44
|
if ("function" == typeof arity) return function() {
|
|
43
45
|
return arity(arguments) ? body.apply(this, arguments) : (self)=>body(self, ...arguments);
|
|
@@ -7356,7 +7358,10 @@ let createReactRouterManifestOptions = ({ routeChunks, routeModuleAnalysis })=>{
|
|
|
7356
7358
|
routeModuleAnalysis
|
|
7357
7359
|
} : {}
|
|
7358
7360
|
};
|
|
7359
|
-
}, isManifestJsAsset = (asset)=>/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset), isManifestCssAsset = (asset)=>/\.css(?:\?.*)?$/.test(asset),
|
|
7361
|
+
}, isManifestJsAsset = (asset)=>/(?<!\.hot-update)\.[cm]?js(?:\?.*)?$/.test(asset), isManifestCssAsset = (asset)=>/\.css(?:\?.*)?$/.test(asset), hasSome = (iterable)=>{
|
|
7362
|
+
for (let _ of iterable)return !0;
|
|
7363
|
+
return !1;
|
|
7364
|
+
}, collectManifestFilesByName = (items, names, getFiles)=>{
|
|
7360
7365
|
let filesByName = {};
|
|
7361
7366
|
if (!names) {
|
|
7362
7367
|
for (let [name, item] of items)null != item && (filesByName[name] = getFiles(name, item));
|
|
@@ -9623,11 +9628,19 @@ export default [`;
|
|
|
9623
9628
|
'virtual/react-router/unstable_rsc/manifest-prefix': `const manifest = __webpack_require__.rscM;
|
|
9624
9629
|
const serverPrefix = ${JSON.stringify(serverPublicPath)};
|
|
9625
9630
|
const appliedPrefix = manifest?.moduleLoading?.prefix;
|
|
9626
|
-
|
|
9631
|
+
// An empty applied prefix (web \`assetPrefix: ''\`) yields relative references
|
|
9632
|
+
// such as "static/js/index.js"; those are rebased too, while absolute and
|
|
9633
|
+
// protocol-relative URLs are left alone.
|
|
9634
|
+
const isAbsoluteUrl = url => /^(?:[a-z][a-z\\d+.-]*:|\\/\\/|\\/)/i.test(url);
|
|
9635
|
+
if (typeof appliedPrefix === "string" && appliedPrefix !== serverPrefix) {
|
|
9627
9636
|
const rewrite = url =>
|
|
9628
|
-
typeof url
|
|
9629
|
-
?
|
|
9630
|
-
: url
|
|
9637
|
+
typeof url !== "string"
|
|
9638
|
+
? url
|
|
9639
|
+
: appliedPrefix !== "" && url.startsWith(appliedPrefix)
|
|
9640
|
+
? serverPrefix + url.slice(appliedPrefix.length)
|
|
9641
|
+
: appliedPrefix === "" && !isAbsoluteUrl(url)
|
|
9642
|
+
? serverPrefix + url
|
|
9643
|
+
: url;
|
|
9631
9644
|
const rewriteAll = list => {
|
|
9632
9645
|
if (Array.isArray(list)) for (let i = 0; i < list.length; i++) list[i] = rewrite(list[i]);
|
|
9633
9646
|
};
|
|
@@ -10907,8 +10920,27 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
|
|
|
10907
10920
|
initialRouteTopology: routeTopology.initialRouteTopology,
|
|
10908
10921
|
onRouteTopologyChange: pluginOptions.onRouteTopologyChange
|
|
10909
10922
|
});
|
|
10910
|
-
api.onAfterEnvironmentCompile(({ stats, environment })=>{
|
|
10911
|
-
if ('web' === environment.name && (clientStats = createReactRouterManifestStats(stats?.compilation, manifestChunkNames)
|
|
10923
|
+
if (api.onAfterEnvironmentCompile(({ stats, environment })=>{
|
|
10924
|
+
if ('web' === environment.name && (clientStats = createReactRouterManifestStats(stats?.compilation, manifestChunkNames), isRscMode && stats)) {
|
|
10925
|
+
let unsupported = ((compilation)=>{
|
|
10926
|
+
let unsupported = new Set();
|
|
10927
|
+
for (let chunk of compilation.chunks){
|
|
10928
|
+
if (!(chunk.contentHash?.javascript !== void 0 || hasSome(compilation.chunkGraph.getChunkModulesIterableBySourceType(chunk, "javascript")))) continue;
|
|
10929
|
+
let pathData = {
|
|
10930
|
+
chunk,
|
|
10931
|
+
contentHashType: "javascript"
|
|
10932
|
+
}, template = chunk.canBeInitial() ? compilation.outputOptions.filename : compilation.outputOptions.chunkFilename, resolvedTemplate = 'function' == typeof template ? template(pathData) : template;
|
|
10933
|
+
if ('string' != typeof resolvedTemplate) continue;
|
|
10934
|
+
let file = compilation.getPath(resolvedTemplate, pathData);
|
|
10935
|
+
file.endsWith('.js') || unsupported.add(file);
|
|
10936
|
+
}
|
|
10937
|
+
return [
|
|
10938
|
+
...unsupported
|
|
10939
|
+
];
|
|
10940
|
+
})(stats.compilation);
|
|
10941
|
+
if (unsupported.length > 0) throw Error(`[${PLUGIN_NAME}] RSC mode requires every browser JavaScript asset to be named "*.js" (no query, no other extension): rspack's RSC manifest omits ${unsupported.slice(0, 5).map((asset)=>JSON.stringify(asset)).join(', ')}${unsupported.length > 5 ? ` and ${unsupported.length - 5} more` : ''}. Adjust web \`output.filename.js\` / \`chunkFilename\`.`);
|
|
10942
|
+
}
|
|
10943
|
+
if (pluginOptions.federation && ssr) {
|
|
10912
10944
|
let serverBuildDir = external_pathe_resolve(buildDirectory, 'server'), clientBuildDir = external_pathe_resolve(buildDirectory, 'client');
|
|
10913
10945
|
if (existsSync(serverBuildDir)) {
|
|
10914
10946
|
let ssrDir = external_pathe_resolve(clientBuildDir, 'static');
|
|
@@ -10952,9 +10984,7 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
|
|
|
10952
10984
|
basename
|
|
10953
10985
|
})))), api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig })=>{
|
|
10954
10986
|
var existing;
|
|
10955
|
-
let publicPath, webConfig = config.environments?.web,
|
|
10956
|
-
if (isRscMode && 'string' == typeof webJsFilename && !/\.js$/.test(webJsFilename)) throw Error(`[${PLUGIN_NAME}] RSC mode requires web \`output.filename.js\` to end in ".js" (got ${JSON.stringify(webJsFilename)}): rspack's RSC manifest omits entry files with a query or another extension, so the server could not render bootstrap scripts.`);
|
|
10957
|
-
let vmodPlugin = (publicPath = resolveEffectiveAssetPrefix({
|
|
10987
|
+
let publicPath, webConfig = config.environments?.web, vmodPlugin = (publicPath = resolveEffectiveAssetPrefix({
|
|
10958
10988
|
dev: webConfig?.dev,
|
|
10959
10989
|
output: webConfig?.output,
|
|
10960
10990
|
isBuild
|
|
@@ -11140,16 +11170,31 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
|
|
|
11140
11170
|
}) : rspackConfig), api.modifyEnvironmentConfig(async (config, { name, mergeEnvironmentConfig })=>'web' !== name && 'node' !== name ? config : mergeEnvironmentConfig(config, {
|
|
11141
11171
|
tools: {
|
|
11142
11172
|
rspack: (rspackConfig)=>{
|
|
11143
|
-
if (federation
|
|
11173
|
+
if (federation) {
|
|
11144
11174
|
if (rspackConfig?.plugins?.length) for (let plugin of rspackConfig.plugins){
|
|
11145
|
-
|
|
11146
|
-
let pluginOptions = plugin._options ?? plugin.options;
|
|
11175
|
+
let pluginOptions = getModuleFederationOptions(plugin);
|
|
11147
11176
|
pluginOptions && (pluginOptions.experiments = {
|
|
11148
11177
|
...pluginOptions.experiments,
|
|
11149
11178
|
asyncStartup: !0
|
|
11150
11179
|
});
|
|
11151
11180
|
}
|
|
11152
|
-
|
|
11181
|
+
if ('web' === name) ((rspackConfig)=>{
|
|
11182
|
+
let containers = new Set((rspackConfig?.plugins ?? []).map(getModuleFederationOptions).map((options)=>options?.name).filter((name)=>'string' == typeof name));
|
|
11183
|
+
if (!rspackConfig || 0 === containers.size) return;
|
|
11184
|
+
let current = rspackConfig.optimization?.runtimeChunk, appRuntimeName = 'object' == typeof current && 'string' == typeof current?.name ? current.name : 'runtime';
|
|
11185
|
+
rspackConfig.optimization = {
|
|
11186
|
+
...rspackConfig.optimization,
|
|
11187
|
+
runtimeChunk: {
|
|
11188
|
+
name: (entrypoint)=>containers.has(entrypoint.name) ? `runtime-${entrypoint.name}` : appRuntimeName
|
|
11189
|
+
}
|
|
11190
|
+
};
|
|
11191
|
+
})(rspackConfig);
|
|
11192
|
+
else {
|
|
11193
|
+
let splitChunks = rspackConfig?.optimization?.splitChunks;
|
|
11194
|
+
if (splitChunks) for (let group of (splitChunks.chunks = 'async', Object.values(splitChunks.cacheGroups ?? {})))group && 'object' == typeof group && 'chunks' in group && (group.chunks = 'async');
|
|
11195
|
+
}
|
|
11196
|
+
}
|
|
11197
|
+
if ('node' === name) {
|
|
11153
11198
|
let output = rspackConfig.output;
|
|
11154
11199
|
if (output) {
|
|
11155
11200
|
let library = output.library, libraryOptions = library && 'object' == typeof library && !Array.isArray(library) ? library : {};
|
|
@@ -11171,7 +11216,20 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
|
|
|
11171
11216
|
federation: pluginOptions.federation,
|
|
11172
11217
|
resolvedServerOutput,
|
|
11173
11218
|
webOutput: modePlan.webOutput
|
|
11174
|
-
}), 'classic' === modePlan.kind
|
|
11219
|
+
}), pluginOptions.federation && 'classic' === modePlan.kind) {
|
|
11220
|
+
let browserEntryModules = new Set([
|
|
11221
|
+
finalEntryClientPath,
|
|
11222
|
+
...routeByFilePath.keys()
|
|
11223
|
+
]);
|
|
11224
|
+
api.transform({
|
|
11225
|
+
environments: [
|
|
11226
|
+
'web'
|
|
11227
|
+
],
|
|
11228
|
+
order: 'post',
|
|
11229
|
+
test: (resourcePath)=>browserEntryModules.has(resourcePath)
|
|
11230
|
+
}, ({ code })=>`${code}\nexport {};\nawait Promise.resolve();\n`);
|
|
11231
|
+
}
|
|
11232
|
+
'classic' === modePlan.kind && useRouteModuleTransformLoader && api.modifyEnvironmentConfig(async (config, { name, mergeEnvironmentConfig })=>'web' !== name && 'node' !== name ? config : mergeEnvironmentConfig(config, {
|
|
11175
11233
|
tools: {
|
|
11176
11234
|
rspack: (rspackConfig)=>{
|
|
11177
11235
|
let environmentDevHmrEnabled = 'web' === name && !isBuild && void 0 !== devHmrRefreshRuntimePath && 'development' === config.mode && config.dev?.hmr !== !1 && isRspackSwcReactRefreshEnabled(rspackConfig);
|
package/dist/manifest.d.ts
CHANGED
|
@@ -66,6 +66,35 @@ type ReactRouterManifestStatsCompilation = {
|
|
|
66
66
|
};
|
|
67
67
|
export declare const isManifestJsAsset: (asset: string) => boolean;
|
|
68
68
|
export declare const isManifestCssAsset: (asset: string) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The minimal compilation surface for `collectUnsupportedRscScriptAssets`.
|
|
71
|
+
* Chunks are classified as JavaScript-emitting from compilation metadata (their
|
|
72
|
+
* `javascript` content hash / modules), never from a filename.
|
|
73
|
+
*/
|
|
74
|
+
export type RscScriptAssetCompilation = {
|
|
75
|
+
chunks: Iterable<RscScriptAssetChunk>;
|
|
76
|
+
chunkGraph: {
|
|
77
|
+
getChunkModulesIterableBySourceType(chunk: RscScriptAssetChunk, sourceType: string): Iterable<unknown>;
|
|
78
|
+
};
|
|
79
|
+
outputOptions: {
|
|
80
|
+
filename?: unknown;
|
|
81
|
+
chunkFilename?: unknown;
|
|
82
|
+
};
|
|
83
|
+
getPath(filename: string, data: Record<string, unknown>): string;
|
|
84
|
+
};
|
|
85
|
+
export type RscScriptAssetChunk = {
|
|
86
|
+
contentHash?: Record<string, string>;
|
|
87
|
+
canBeInitial(): boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Browser JavaScript assets rspack's RSC manifest would drop: it only records
|
|
91
|
+
* chunk files whose emitted name ends in ".js", so `.mjs` names, query-hash
|
|
92
|
+
* names (`[name].js?v=...`), or any other extension vanish from
|
|
93
|
+
* `entryJsFiles` and the client manifest. The emitted script name is derived
|
|
94
|
+
* from the chunk's own filename template (entry or async) the same way rspack
|
|
95
|
+
* emits it, so function templates and `tools.rspack` overrides are covered.
|
|
96
|
+
*/
|
|
97
|
+
export declare const collectUnsupportedRscScriptAssets: (compilation: RscScriptAssetCompilation) => string[];
|
|
69
98
|
export declare const createReactRouterManifestStats: (compilation: ReactRouterManifestStatsCompilation | undefined, chunkNames?: ReadonlySet<string>) => ReactRouterManifestStats | undefined;
|
|
70
99
|
export type RouteManifestModuleExports = Record<string, readonly string[]>;
|
|
71
100
|
export type ReactRouterManifestGenerationResult = {
|
package/package.json
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
enforceAsyncOnlyServerSplitChunks,
|
|
4
|
+
ensureFederationAsyncStartup,
|
|
5
|
+
isolateFederationContainerRuntime,
|
|
6
|
+
} from './federation.js';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Rspack `output` policy for the web and node environments, in two tiers:
|
|
@@ -70,6 +74,11 @@ export const registerReactRouterEnvironmentOutput = ({
|
|
|
70
74
|
rspack: rspackConfig => {
|
|
71
75
|
if (federation) {
|
|
72
76
|
ensureFederationAsyncStartup(rspackConfig);
|
|
77
|
+
if (name === 'web') {
|
|
78
|
+
isolateFederationContainerRuntime(rspackConfig);
|
|
79
|
+
} else {
|
|
80
|
+
enforceAsyncOnlyServerSplitChunks(rspackConfig);
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
if (name === 'node') {
|
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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type ResolvedReactRouterConfig,
|
|
27
27
|
} from './react-router-config.js';
|
|
28
28
|
import {
|
|
29
|
+
collectUnsupportedRscScriptAssets,
|
|
29
30
|
configRoutesToRouteManifest,
|
|
30
31
|
createReactRouterManifestStats,
|
|
31
32
|
type ReactRouterManifestForDev as ReactRouterManifest,
|
|
@@ -773,6 +774,27 @@ export const pluginReactRouter = (
|
|
|
773
774
|
stats?.compilation,
|
|
774
775
|
manifestChunkNames
|
|
775
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
|
+
}
|
|
776
798
|
}
|
|
777
799
|
if (pluginOptions.federation && ssr) {
|
|
778
800
|
const serverBuildDir = resolve(buildDirectory, 'server');
|
|
@@ -848,22 +870,6 @@ export const pluginReactRouter = (
|
|
|
848
870
|
|
|
849
871
|
api.modifyRsbuildConfig(async (config, { mergeRsbuildConfig }) => {
|
|
850
872
|
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
|
-
}
|
|
867
873
|
const assetPrefix = resolveEffectiveAssetPrefix(
|
|
868
874
|
{ dev: webConfig?.dev, output: webConfig?.output, isBuild },
|
|
869
875
|
{ dev: config.dev, output: config.output }
|
|
@@ -1044,6 +1050,31 @@ export const pluginReactRouter = (
|
|
|
1044
1050
|
webOutput: modePlan.webOutput,
|
|
1045
1051
|
});
|
|
1046
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
|
+
|
|
1047
1078
|
if (modePlan.kind === 'classic' && useRouteModuleTransformLoader) {
|
|
1048
1079
|
api.modifyEnvironmentConfig(
|
|
1049
1080
|
async (config, { name, mergeEnvironmentConfig }) => {
|
package/src/manifest.ts
CHANGED
|
@@ -179,6 +179,79 @@ export const isManifestJsAsset = (asset: string): boolean =>
|
|
|
179
179
|
export const isManifestCssAsset = (asset: string): boolean =>
|
|
180
180
|
/\.css(?:\?.*)?$/.test(asset);
|
|
181
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;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
};
|
|
213
|
+
|
|
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];
|
|
253
|
+
};
|
|
254
|
+
|
|
182
255
|
const collectManifestFilesByName = <T>(
|
|
183
256
|
items: ReactRouterManifestStatsLookup<T>,
|
|
184
257
|
names: ReadonlySet<string> | undefined,
|
|
@@ -127,11 +127,19 @@ export const createReactRouterRscVirtualModules = ({
|
|
|
127
127
|
'virtual/react-router/unstable_rsc/manifest-prefix': `const manifest = __webpack_require__.rscM;
|
|
128
128
|
const serverPrefix = ${JSON.stringify(serverPublicPath)};
|
|
129
129
|
const appliedPrefix = manifest?.moduleLoading?.prefix;
|
|
130
|
-
|
|
130
|
+
// An empty applied prefix (web \`assetPrefix: ''\`) yields relative references
|
|
131
|
+
// such as "static/js/index.js"; those are rebased too, while absolute and
|
|
132
|
+
// protocol-relative URLs are left alone.
|
|
133
|
+
const isAbsoluteUrl = url => /^(?:[a-z][a-z\\d+.-]*:|\\/\\/|\\/)/i.test(url);
|
|
134
|
+
if (typeof appliedPrefix === "string" && appliedPrefix !== serverPrefix) {
|
|
131
135
|
const rewrite = url =>
|
|
132
|
-
typeof url
|
|
133
|
-
?
|
|
134
|
-
: url
|
|
136
|
+
typeof url !== "string"
|
|
137
|
+
? url
|
|
138
|
+
: appliedPrefix !== "" && url.startsWith(appliedPrefix)
|
|
139
|
+
? serverPrefix + url.slice(appliedPrefix.length)
|
|
140
|
+
: appliedPrefix === "" && !isAbsoluteUrl(url)
|
|
141
|
+
? serverPrefix + url
|
|
142
|
+
: url;
|
|
135
143
|
const rewriteAll = list => {
|
|
136
144
|
if (Array.isArray(list)) for (let i = 0; i < list.length; i++) list[i] = rewrite(list[i]);
|
|
137
145
|
};
|