rsbuild-plugin-react-router 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +14 -2
  2. package/dist/468.js +48 -0
  3. package/dist/511.js +60 -86
  4. package/dist/819.js +64 -0
  5. package/dist/build-output-transforms.d.ts +3 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/dev-hdr-channel.d.ts +9 -0
  8. package/dist/dev-hmr.d.ts +1 -22
  9. package/dist/dev-runtime-controller.d.ts +1 -6
  10. package/dist/dev-server.d.ts +3 -1
  11. package/dist/index.cjs +5417 -5080
  12. package/dist/index.js +1701 -1441
  13. package/dist/manifest-assets.d.ts +34 -0
  14. package/dist/manifest-snapshot.d.ts +17 -0
  15. package/dist/manifest-state.d.ts +12 -0
  16. package/dist/manifest.d.ts +2 -22
  17. package/dist/modify-browser-manifest.d.ts +2 -0
  18. package/dist/node-only-manifest.d.ts +8 -0
  19. package/dist/plugin-utils.d.ts +1 -1
  20. package/dist/rsc-prerender.d.ts +3 -2
  21. package/dist/server-build-worker-client.d.ts +20 -0
  22. package/dist/server-build-worker-protocol.d.ts +84 -0
  23. package/dist/server-build-worker.d.ts +1 -0
  24. package/dist/server-build-worker.js +123 -0
  25. package/dist/server-utils.d.ts +1 -2
  26. package/dist/types.d.ts +6 -0
  27. package/package.json +4 -4
  28. package/src/build-output-transforms.ts +22 -1
  29. package/src/classic-mode.ts +0 -1
  30. package/src/constants.ts +3 -0
  31. package/src/dev-hdr-channel.ts +38 -0
  32. package/src/dev-hmr.ts +57 -100
  33. package/src/dev-runtime-controller.ts +23 -18
  34. package/src/dev-server.ts +24 -4
  35. package/src/index.ts +229 -144
  36. package/src/lazy-compilation.ts +7 -2
  37. package/src/manifest-assets.ts +228 -0
  38. package/src/manifest-snapshot.ts +80 -0
  39. package/src/manifest-state.ts +71 -0
  40. package/src/manifest.ts +23 -161
  41. package/src/mode-plan.ts +12 -4
  42. package/src/modify-browser-manifest.ts +47 -18
  43. package/src/node-only-manifest.ts +52 -0
  44. package/src/plugin-utils.ts +6 -2
  45. package/src/prerender-build.ts +76 -86
  46. package/src/route-chunks.ts +152 -63
  47. package/src/rsc-prerender.ts +7 -35
  48. package/src/server-build-resolution.ts +1 -2
  49. package/src/server-build-worker-client.ts +221 -0
  50. package/src/server-build-worker-protocol.ts +69 -0
  51. package/src/server-build-worker.ts +192 -0
  52. package/src/server-utils.ts +0 -2
  53. package/src/types.ts +7 -0
@@ -1,3 +1,5 @@
1
+ import { BROWSER_MANIFEST_ENTRY_NAME } from './constants.js';
2
+ import { getManifestAssetType, stripAssetQuery } from './manifest-assets.js';
1
3
  import { createHash } from 'node:crypto';
2
4
  import type { Route, PluginOptions } from './types.js';
3
5
  import type { RsbuildPluginAPI, Rspack } from '@rsbuild/core';
@@ -30,6 +32,8 @@ type CompilationAssetWithIntegrity = {
30
32
  source?: { source(): string | Buffer };
31
33
  info?: {
32
34
  integrity?: unknown;
35
+ assetType?: string;
36
+ javascriptModule?: boolean;
33
37
  };
34
38
  };
35
39
 
@@ -79,14 +83,10 @@ const toManifestAssetUrl = (assetPrefix: string, assetName: string) => {
79
83
  const addIntegrity = (
80
84
  sri: Record<string, string>,
81
85
  assetPrefix: string,
82
- assetName: unknown,
86
+ assetName: string,
83
87
  integrity: unknown
84
88
  ) => {
85
- if (
86
- typeof assetName !== 'string' ||
87
- !isManifestJsAsset(assetName) ||
88
- typeof integrity !== 'string'
89
- ) {
89
+ if (typeof integrity !== 'string') {
90
90
  return;
91
91
  }
92
92
  sri[toManifestAssetUrl(assetPrefix, assetName)] = integrity;
@@ -111,6 +111,9 @@ export const collectSubresourceIntegrity = (
111
111
  const sri: Record<string, string> = {};
112
112
 
113
113
  for (const asset of stats?.assets ?? []) {
114
+ if (typeof asset.name !== 'string' || !isManifestJsAsset(asset.name)) {
115
+ continue;
116
+ }
114
117
  addIntegrity(sri, assetPrefix, asset.name, asset.integrity);
115
118
  }
116
119
 
@@ -118,7 +121,17 @@ export const collectSubresourceIntegrity = (
118
121
  const assets =
119
122
  compilation.getAssets() as readonly CompilationAssetWithIntegrity[];
120
123
  for (const asset of assets) {
121
- if (!isManifestJsAsset(asset.name)) {
124
+ const assetType =
125
+ asset.info?.assetType ??
126
+ (typeof asset.info?.javascriptModule === 'boolean'
127
+ ? 'javascript'
128
+ : undefined);
129
+ if (
130
+ getManifestAssetType(
131
+ asset.name,
132
+ assetType === undefined ? undefined : { [asset.name]: assetType }
133
+ ) !== 'javascript'
134
+ ) {
122
135
  continue;
123
136
  }
124
137
  addIntegrity(
@@ -144,13 +157,15 @@ export function registerModifyBrowserManifestAssets(
144
157
  ): void {
145
158
  const getAssetPrefix =
146
159
  typeof assetPrefix === 'function' ? assetPrefix : () => assetPrefix;
147
- const manifestChunkNames =
160
+ const manifestChunkNames = new Set(
148
161
  options?.manifestChunkNames ??
149
- getReactRouterManifestChunkNames(
150
- routes,
151
- appDirectory,
152
- routeChunkOptions?.splitRouteModules
153
- );
162
+ getReactRouterManifestChunkNames(
163
+ routes,
164
+ appDirectory,
165
+ routeChunkOptions?.splitRouteModules
166
+ )
167
+ );
168
+ manifestChunkNames.add(BROWSER_MANIFEST_ENTRY_NAME);
154
169
  const isBuild = Boolean(routeChunkOptions?.isBuild);
155
170
  const finalizeSri = Boolean(
156
171
  isBuild &&
@@ -165,6 +180,7 @@ export function registerModifyBrowserManifestAssets(
165
180
  { assets, sources, compilation }: ManifestProcessAssetsContext,
166
181
  { withSri }: { withSri: boolean }
167
182
  ): Promise<void> => {
183
+ if (compilation.errors?.length) return;
168
184
  const currentAssetPrefix = getAssetPrefix();
169
185
  const stats = createReactRouterManifestStats(
170
186
  compilation,
@@ -185,8 +201,17 @@ export function registerModifyBrowserManifestAssets(
185
201
  const browserManifest = { ...manifest };
186
202
  delete browserManifest.sri;
187
203
 
188
- const browserManifestAsset = assets[BROWSER_MANIFEST_ASSET];
189
- if (browserManifestAsset) {
204
+ const browserManifestPaths = stats?.assetsByChunkName?.[
205
+ BROWSER_MANIFEST_ENTRY_NAME
206
+ ]?.filter(
207
+ name =>
208
+ getManifestAssetType(name, stats.assetTypesByName) === 'javascript'
209
+ ) ?? [BROWSER_MANIFEST_ASSET];
210
+ // Production consumes the separately emitted versioned manifest. Leave the
211
+ // placeholder chunk unchanged: its content hash has already been finalized.
212
+ for (const browserManifestPath of isBuild ? [] : browserManifestPaths) {
213
+ const browserManifestAsset = assets[browserManifestPath];
214
+ if (!browserManifestAsset) continue;
190
215
  const originalSource = browserManifestAsset.source().toString();
191
216
  const serializedManifest = jsesc(browserManifest, { es6: true });
192
217
  const newSource = originalSource.replace(
@@ -194,18 +219,22 @@ export function registerModifyBrowserManifestAssets(
194
219
  () => serializedManifest
195
220
  );
196
221
  compilation.updateAsset(
197
- BROWSER_MANIFEST_ASSET,
222
+ browserManifestPath,
198
223
  new sources.RawSource(newSource)
199
224
  );
200
225
  }
201
226
 
202
227
  if (isBuild) {
203
228
  const entryAssets = stats?.assetsByChunkName?.['entry.client'];
204
- const entryJsAssets = entryAssets?.filter(isManifestJsAsset) || [];
229
+ const entryJsAssets =
230
+ entryAssets?.filter(
231
+ name =>
232
+ getManifestAssetType(name, stats?.assetTypesByName) === 'javascript'
233
+ ) || [];
205
234
  const manifestPath = getReactRouterManifestPath({
206
235
  version: manifest.version,
207
236
  isBuild: true,
208
- entryModulePath: entryJsAssets[0],
237
+ entryModulePath: stripAssetQuery(entryJsAssets[0] ?? ''),
209
238
  });
210
239
  const manifestSource = `window.__reactRouterManifest=${jsesc(
211
240
  browserManifest,
@@ -0,0 +1,52 @@
1
+ import { rspack, type RsbuildPluginAPI, type Rspack } from '@rsbuild/core';
2
+ import { resolve } from 'pathe';
3
+ import type { ReactRouterManifestSnapshot } from './manifest-snapshot.js';
4
+ import type { Route } from './types.js';
5
+
6
+ // Inspect the compiled graph rather than parsing source: loaders, re-exports,
7
+ // and cached modules must all participate in the compatibility check.
8
+ export const registerNodeOnlyManifestValidation = ({
9
+ api,
10
+ routeByFilePath,
11
+ getSnapshot,
12
+ }: {
13
+ api: RsbuildPluginAPI;
14
+ routeByFilePath: ReadonlyMap<string, Route>;
15
+ getSnapshot: () => ReactRouterManifestSnapshot | null;
16
+ }): void => {
17
+ api.modifyRspackConfig((config, { environment }) => {
18
+ if (environment.name !== 'node') return;
19
+ config.plugins ??= [];
20
+ config.plugins.push({
21
+ apply(compiler: Rspack.Compiler) {
22
+ const name = 'ReactRouterNodeOnlyManifestValidation';
23
+ compiler.hooks.thisCompilation.tap(name, compilation => {
24
+ compilation.hooks.afterOptimizeModules.tap(name, modules => {
25
+ const snapshot = getSnapshot();
26
+ if (!snapshot) return;
27
+ for (const module of modules) {
28
+ if (!(module instanceof rspack.NormalModule)) continue;
29
+ const route = routeByFilePath.get(
30
+ resolve(module.resource.split('?')[0])
31
+ );
32
+ if (!route) continue;
33
+ const exports =
34
+ compilation.moduleGraph.getProvidedExports(module);
35
+ const saved = snapshot.browser.routes[route.id];
36
+ if (
37
+ !Array.isArray(exports) ||
38
+ !saved ||
39
+ exports.includes('loader') !== saved.hasLoader ||
40
+ exports.includes('action') !== saved.hasAction
41
+ ) {
42
+ throw new Error(
43
+ `Run a full build before building only the node environment: loader/action exports changed for route "${route.id}".`
44
+ );
45
+ }
46
+ }
47
+ });
48
+ });
49
+ },
50
+ });
51
+ });
52
+ };
@@ -6,10 +6,14 @@ import { JS_EXTENSIONS } from './constants.js';
6
6
  const requireFromApp = createRequire(resolve(process.cwd(), 'package.json'));
7
7
 
8
8
  export const resolveAppPackagePath = (
9
- specifier: string
9
+ specifier: string,
10
+ rootPath?: string
10
11
  ): string | undefined => {
11
12
  try {
12
- return requireFromApp.resolve(specifier);
13
+ const require = rootPath
14
+ ? createRequire(resolve(rootPath, 'package.json'))
15
+ : requireFromApp;
16
+ return require.resolve(specifier);
13
17
  } catch {
14
18
  return undefined;
15
19
  }
@@ -1,14 +1,9 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
- import { pathToFileURL } from 'node:url';
4
3
  import fsExtra from 'fs-extra';
5
4
  import * as Effect from 'effect/Effect';
6
5
  import type { RsbuildPluginAPI } from '@rsbuild/core';
7
- import {
8
- createRequestHandler,
9
- matchRoutes,
10
- type ServerBuild,
11
- } from 'react-router';
6
+ import { matchRoutes } from 'react-router';
12
7
  import { dirname, relative, resolve } from 'pathe';
13
8
  import { PLUGIN_NAME, SPA_FALLBACK_HTML_FILE } from './constants.js';
14
9
  import { getBuildManifest } from './build-manifest.js';
@@ -31,24 +26,11 @@ import type {
31
26
  Config,
32
27
  ResolvedReactRouterConfig,
33
28
  } from './react-router-config.js';
34
- import { resolveServerBuildModule } from './server-utils.js';
29
+ import { startServerBuildWorker } from './server-build-worker-client.js';
30
+ import type { ServerBuildDescription } from './server-build-worker-protocol.js';
35
31
  import type { PluginOptions, Route } from './types.js';
36
32
  import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
37
33
 
38
- type BuildRouteModule = {
39
- loader?: unknown;
40
- default?: unknown;
41
- ErrorBoundary?: unknown;
42
- };
43
-
44
- type PrerenderServerBuild = ServerBuild & {
45
- routes: Record<string, { id?: string; module?: BuildRouteModule }>;
46
- assets?: {
47
- routes?: Record<string, { hasLoader?: boolean }>;
48
- };
49
- prerender?: string[];
50
- };
51
-
52
34
  type PrerenderBuildApi = Pick<
53
35
  RsbuildPluginAPI,
54
36
  'logger' | 'getNormalizedConfig'
@@ -344,7 +326,7 @@ const handleSpaMode = async ({
344
326
  api,
345
327
  }: {
346
328
  handler: (request: Request) => Promise<Response>;
347
- build: PrerenderServerBuild;
329
+ build: ServerBuildDescription;
348
330
  clientBuildDir: string;
349
331
  basename: string;
350
332
  api: PrerenderBuildApi;
@@ -459,7 +441,7 @@ const createPrerenderPathEffect = ({
459
441
  options,
460
442
  }: {
461
443
  path: string;
462
- build: PrerenderServerBuild;
444
+ build: ServerBuildDescription;
463
445
  buildRoutes: ReturnType<typeof createPrerenderRoutes>;
464
446
  requestHandler: (request: Request) => Promise<Response>;
465
447
  clientBuildDir: string;
@@ -596,75 +578,83 @@ export const runReactRouterPrerenderBuild = async (
596
578
  await mkdir(clientBuildDir, { recursive: true });
597
579
 
598
580
  if (!ssr || isPrerenderEnabled) {
599
- process.env.IS_RR_BUILD_REQUEST = 'yes';
600
- const buildModule = await import(pathToFileURL(serverBuildPath).toString());
601
- const build = (await resolveServerBuildModule(
602
- buildModule,
603
- `Server build ${JSON.stringify(serverBuildPath)}`
604
- )) as PrerenderServerBuild;
605
- const requestHandler = createRequestHandler(build, 'production');
606
-
607
- if (isPrerenderEnabled) {
608
- if (!ssr) {
609
- const generated = latestBrowserManifest
610
- ? {
611
- manifest: latestBrowserManifest,
612
- moduleExportsByRouteId: latestBrowserManifestModuleExports,
613
- }
614
- : await generateReactRouterManifestForDev(
615
- routes,
616
- pluginOptions,
617
- clientStats,
618
- appDirectory,
619
- assetPrefix,
620
- createReactRouterManifestOptions({
621
- routeChunks: routeChunkOptions,
622
- routeModuleAnalysis,
623
- })
624
- );
625
- assertValidSsrFalsePrerenderExports({
626
- routes,
627
- manifestRoutes: generated.manifest.routes,
628
- routeExports: generated.moduleExportsByRouteId,
629
- prerenderPaths,
630
- api,
631
- });
581
+ const worker = await startServerBuildWorker({
582
+ serverBuildPath,
583
+ mode: 'classic',
584
+ });
585
+ try {
586
+ const build = worker.description;
587
+ if (!build) {
588
+ throw new Error(
589
+ `[${PLUGIN_NAME}] Server build worker returned no build description`
590
+ );
632
591
  }
592
+ const requestHandler = worker.handler;
593
+
594
+ if (isPrerenderEnabled) {
595
+ if (!ssr) {
596
+ const generated = latestBrowserManifest
597
+ ? {
598
+ manifest: latestBrowserManifest,
599
+ moduleExportsByRouteId: latestBrowserManifestModuleExports,
600
+ }
601
+ : await generateReactRouterManifestForDev(
602
+ routes,
603
+ pluginOptions,
604
+ clientStats,
605
+ appDirectory,
606
+ assetPrefix,
607
+ createReactRouterManifestOptions({
608
+ routeChunks: routeChunkOptions,
609
+ routeModuleAnalysis,
610
+ })
611
+ );
612
+ assertValidSsrFalsePrerenderExports({
613
+ routes,
614
+ manifestRoutes: generated.manifest.routes,
615
+ routeExports: generated.moduleExportsByRouteId,
616
+ prerenderPaths,
617
+ api,
618
+ });
619
+ }
620
+
621
+ validatePrerenderPathMatches(routes, prerenderPaths);
633
622
 
634
- validatePrerenderPathMatches(routes, prerenderPaths);
623
+ if (prerenderPaths.length > 0) {
624
+ api.logger.info(
625
+ `Prerender (html): ${prerenderPaths.length} path(s)...`
626
+ );
627
+ }
635
628
 
636
- if (prerenderPaths.length > 0) {
637
- api.logger.info(
638
- `Prerender (html): ${prerenderPaths.length} path(s)...`
629
+ const buildRoutes = createPrerenderRoutes(build.routes);
630
+ await runPluginEffect(
631
+ createBoundedPrerenderTasksEffect(
632
+ prerenderPaths,
633
+ getPrerenderConcurrency(prerenderConfig),
634
+ path =>
635
+ createPrerenderPathEffect({
636
+ path,
637
+ build,
638
+ buildRoutes,
639
+ requestHandler,
640
+ clientBuildDir,
641
+ options,
642
+ })
643
+ )
639
644
  );
640
645
  }
641
646
 
642
- const buildRoutes = createPrerenderRoutes(build.routes);
643
- await runPluginEffect(
644
- createBoundedPrerenderTasksEffect(
645
- prerenderPaths,
646
- getPrerenderConcurrency(prerenderConfig),
647
- path =>
648
- createPrerenderPathEffect({
649
- path,
650
- build,
651
- buildRoutes,
652
- requestHandler,
653
- clientBuildDir,
654
- options,
655
- })
656
- )
657
- );
658
- }
659
-
660
- if (!ssr) {
661
- await handleSpaMode({
662
- handler: requestHandler,
663
- build,
664
- clientBuildDir,
665
- basename,
666
- api,
667
- });
647
+ if (!ssr) {
648
+ await handleSpaMode({
649
+ handler: requestHandler,
650
+ build,
651
+ clientBuildDir,
652
+ basename,
653
+ api,
654
+ });
655
+ }
656
+ } finally {
657
+ await worker.close();
668
658
  }
669
659
  }
670
660