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
package/src/dev-hmr.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
1
  import { createRequire } from 'node:module';
3
2
  import type { Rspack } from '@rsbuild/core';
4
- import { dirname, join } from 'pathe';
3
+ import { join } from 'pathe';
4
+ import { DEV_HDR_UPDATE_EVENT } from './dev-hdr-channel.js';
5
5
 
6
6
  export const DEV_HMR_RUNTIME_MODULE_ID = 'virtual/react-router/hmr-runtime';
7
7
  export const DEV_MANIFEST_UPDATE_EVENT = 'react-router:manifest-update';
@@ -9,7 +9,6 @@ export const DEV_MANIFEST_UPDATE_EVENT = 'react-router:manifest-update';
9
9
  export type DevHmrPlanOptions = {
10
10
  isEnabled: () => boolean;
11
11
  runtimeModule: string;
12
- onNodeRebuildCommitted: () => void;
13
12
  };
14
13
 
15
14
  type SwcLoaderOptions = {
@@ -80,57 +79,6 @@ export const resolveReactRefreshRuntimePath = (
80
79
  }
81
80
  };
82
81
 
83
- const hdrRevisionModuleContent = (revision: number): string =>
84
- `export default ${revision};\n`;
85
-
86
- /**
87
- * The HDR revision module is a real file (not a virtual module) because it
88
- * must wake the web compiler through the regular file watcher: the browser
89
- * HMR runtime imports it, so bumping the revision produces a web hot update
90
- * whenever server code changes, which the client answers by revalidating
91
- * React Router loader data.
92
- */
93
- export const getDevHdrRevisionFilePath = (rootPath: string): string =>
94
- join(rootPath, '.react-router', 'hdr-revision.mjs');
95
-
96
- export type DevHdrRevisionSignal = {
97
- filePath: string;
98
- /** Writes the initial revision module so the first compile can resolve it. */
99
- ensure: () => void;
100
- /** Increments the revision, signaling hot data revalidation to the client. */
101
- bump: () => void;
102
- };
103
-
104
- export const createDevHdrRevisionSignal = ({
105
- filePath,
106
- onError,
107
- }: {
108
- filePath: string;
109
- onError?: (error: Error) => void;
110
- }): DevHdrRevisionSignal => {
111
- let revision = 0;
112
- let dirEnsured = false;
113
- const write = (): void => {
114
- try {
115
- if (!dirEnsured) {
116
- mkdirSync(dirname(filePath), { recursive: true });
117
- dirEnsured = true;
118
- }
119
- writeFileSync(filePath, hdrRevisionModuleContent(revision));
120
- } catch (error) {
121
- onError?.(error instanceof Error ? error : new Error(String(error)));
122
- }
123
- };
124
- return {
125
- filePath,
126
- ensure: write,
127
- bump() {
128
- revision += 1;
129
- write();
130
- },
131
- };
132
- };
133
-
134
82
  /**
135
83
  * Browser-side HMR runtime shared by all route client entries in development.
136
84
  *
@@ -143,20 +91,10 @@ export const createDevHdrRevisionSignal = ({
143
91
  */
144
92
  export const generateDevHmrRuntimeModule = ({
145
93
  reactRefreshRuntimePath,
146
- hdrRevisionFilePath,
147
94
  }: {
148
95
  reactRefreshRuntimePath: string;
149
- hdrRevisionFilePath: string;
150
96
  }): string => `
151
97
  import * as __refreshRuntimeModule from ${JSON.stringify(reactRefreshRuntimePath)};
152
- // Read revision so the import survives sideEffects: false tree-shaking.
153
- import __hdrRevision from ${JSON.stringify(hdrRevisionFilePath)};
154
-
155
- let latestHdrRevision = __hdrRevision;
156
-
157
- export function getReactRouterHdrRevision() {
158
- return latestHdrRevision;
159
- }
160
98
 
161
99
  const RefreshRuntime =
162
100
  __refreshRuntimeModule && __refreshRuntimeModule.performReactRefresh
@@ -166,7 +104,24 @@ const RefreshRuntime =
166
104
  const pendingRouteUpdates = new Map();
167
105
  let flushTimeout;
168
106
  let pendingRevalidation = false;
169
- let pendingComponentRouteRevalidation = false;
107
+ let flushing = false;
108
+ let hdrSession;
109
+ let latestHdrRevision = 0;
110
+ const retiredHdrSessions = new Set();
111
+
112
+ function receiveHdrRevision(message) {
113
+ if (!message || typeof message.sessionId !== 'string' ||
114
+ !Number.isSafeInteger(message.revision) || message.revision < 1 ||
115
+ retiredHdrSessions.has(message.sessionId)) return;
116
+ if (hdrSession !== message.sessionId) {
117
+ if (hdrSession) retiredHdrSessions.add(hdrSession);
118
+ hdrSession = message.sessionId;
119
+ latestHdrRevision = 0;
120
+ }
121
+ if (message.revision <= latestHdrRevision) return;
122
+ latestHdrRevision = message.revision;
123
+ scheduleReactRouterRevalidation();
124
+ }
170
125
 
171
126
  function getCurrentRouterPath(router) {
172
127
  const basename = router.basename || '/';
@@ -207,7 +162,7 @@ export function scheduleReactRouterRouteUpdate(
207
162
  scheduleFlush();
208
163
  }
209
164
 
210
- export function scheduleReactRouterRevalidation() {
165
+ function scheduleReactRouterRevalidation() {
211
166
  pendingRevalidation = true;
212
167
  scheduleFlush();
213
168
  }
@@ -406,45 +361,50 @@ function applyManifestUpdate(nextRoutes) {
406
361
  }
407
362
 
408
363
  async function flush() {
364
+ if (flushing) return;
365
+ if (import.meta.webpackHot && import.meta.webpackHot.status() !== 'idle') {
366
+ scheduleFlush();
367
+ return;
368
+ }
409
369
  const router = window.__reactRouterDataRouter;
410
370
  const routeModules = window.__reactRouterRouteModules;
411
371
  const manifest = window.__reactRouterManifest;
412
372
  const context = window.__reactRouterContext;
413
373
  if (!router || !routeModules || !manifest || !context) {
374
+ scheduleFlush();
414
375
  return;
415
376
  }
416
-
417
- let shouldRevalidate = pendingRevalidation;
418
- pendingRevalidation = false;
419
- const { nextManifest, hmrRoutes, shouldRefreshRouteState } =
420
- applyPendingRouteUpdates(router, routeModules, manifest, context);
421
- // Loader updates must be visible during revalidation. Component-only routes
422
- // stay staged until revalidation completes, matching React Router's HMR flow.
423
- if (nextManifest && shouldRefreshRouteState) {
424
- pendingComponentRouteRevalidation = false;
425
- if (hmrRoutes) {
426
- patchCurrentRouteMatches(router, hmrRoutes);
427
- }
428
- Object.assign(manifest, nextManifest);
429
- await refreshRouteState(router);
430
- shouldRevalidate = false;
431
- } else if (nextManifest) {
432
- if (hmrRoutes) {
433
- patchCurrentRouteMatches(router, hmrRoutes);
377
+ flushing = true;
378
+ try {
379
+ let shouldRevalidate = pendingRevalidation;
380
+ pendingRevalidation = false;
381
+ const { nextManifest, hmrRoutes, shouldRefreshRouteState } =
382
+ applyPendingRouteUpdates(router, routeModules, manifest, context);
383
+ // Loader updates must be visible during revalidation. Component-only routes
384
+ // stay staged until revalidation completes, matching React Router's HMR flow.
385
+ if (nextManifest && shouldRefreshRouteState) {
386
+ if (hmrRoutes) {
387
+ patchCurrentRouteMatches(router, hmrRoutes);
388
+ }
389
+ Object.assign(manifest, nextManifest);
390
+ await refreshRouteState(router);
391
+ shouldRevalidate = false;
392
+ } else if (nextManifest) {
393
+ if (hmrRoutes) {
394
+ patchCurrentRouteMatches(router, hmrRoutes);
395
+ }
396
+ Object.assign(manifest, nextManifest);
434
397
  }
435
- Object.assign(manifest, nextManifest);
436
- // The node compiler also emits an HDR revision for this route edit. If it
437
- // did not arrive in this flush, consume that redundant revalidation later.
438
- pendingComponentRouteRevalidation = !shouldRevalidate;
439
- shouldRevalidate = false;
440
- } else if (shouldRevalidate) {
441
- if (pendingComponentRouteRevalidation) {
442
- pendingComponentRouteRevalidation = false;
443
- } else {
398
+ // A replay may represent several server edits, not just the component
399
+ // update in this batch. Only an actual data revalidation can consume it.
400
+ if (shouldRevalidate) {
444
401
  await revalidateRouter(router);
445
402
  }
403
+ performReactRefresh();
404
+ } finally {
405
+ flushing = false;
406
+ if (pendingRevalidation || pendingRouteUpdates.size > 0) scheduleFlush();
446
407
  }
447
- performReactRefresh();
448
408
  }
449
409
 
450
410
  if (typeof window !== 'undefined' && import.meta.webpackHot) {
@@ -452,12 +412,9 @@ if (typeof window !== 'undefined' && import.meta.webpackHot) {
452
412
  ${JSON.stringify(DEV_MANIFEST_UPDATE_EVENT)},
453
413
  applyManifestUpdate
454
414
  );
455
- import.meta.webpackHot.accept(
456
- ${JSON.stringify(hdrRevisionFilePath)},
457
- () => {
458
- latestHdrRevision = __hdrRevision;
459
- scheduleReactRouterRevalidation();
460
- }
415
+ import.meta.webpackHot.on(
416
+ ${JSON.stringify(DEV_HDR_UPDATE_EVENT)},
417
+ receiveHdrRevision
461
418
  );
462
419
  }
463
420
  `;
@@ -20,6 +20,7 @@ import {
20
20
  unregisterReactRouterDevRuntime,
21
21
  } from './dev-generation.js';
22
22
  import { createDevHdrIntentTracker } from './dev-hdr-intent.js';
23
+ import { createDevHdrChannel } from './dev-hdr-channel.js';
23
24
  import { DEV_MANIFEST_UPDATE_EVENT } from './dev-hmr.js';
24
25
  import {
25
26
  getEnvironmentStats,
@@ -57,18 +58,10 @@ type CreateControllerOptions = {
57
58
  * flags) in place, so metadata-only changes no longer need a full reload.
58
59
  */
59
60
  clientPatchesRouteMetadata?: boolean | (() => boolean);
60
- /**
61
- * Invoked after a development attempt commits a re-evaluated node build for
62
- * changed server files. Used to signal hot data revalidation to the client.
63
- */
64
- onNodeRebuildCommitted?: () => void;
65
61
  };
66
62
 
67
63
  const CSS_SOURCE_RELOAD_DELAY_MS = 1000;
68
64
 
69
- const isHdrRevisionFile = (file: string): boolean =>
70
- file.includes('.react-router/hdr-revision.mjs');
71
-
72
65
  const isCssSourceFile = (file: string): boolean =>
73
66
  /\.css(?:\.[cm]?[jt]s)?$/.test(file);
74
67
 
@@ -77,7 +70,6 @@ export const createReactRouterDevRuntimeController = ({
77
70
  isBuild,
78
71
  buildPlan,
79
72
  clientPatchesRouteMetadata,
80
- onNodeRebuildCommitted,
81
73
  }: CreateControllerOptions): ReactRouterDevRuntimeController => {
82
74
  if (isBuild) {
83
75
  return {
@@ -122,7 +114,18 @@ export const createReactRouterDevRuntimeController = ({
122
114
  }, CSS_SOURCE_RELOAD_DELAY_MS);
123
115
  };
124
116
 
117
+ const hdrChannels = new WeakMap<
118
+ RuntimeBinding,
119
+ ReturnType<typeof createDevHdrChannel>
120
+ >();
121
+ const isHmrEnabled = () =>
122
+ typeof clientPatchesRouteMetadata === 'function'
123
+ ? clientPatchesRouteMetadata()
124
+ : clientPatchesRouteMetadata === true;
125
+
125
126
  const closeBinding = (binding: RuntimeBinding, error?: Error): void => {
127
+ hdrChannels.get(binding)?.close();
128
+ hdrChannels.delete(binding);
126
129
  if (scheduledCssAssetOwnershipReload) {
127
130
  clearTimeout(scheduledCssAssetOwnershipReload);
128
131
  scheduledCssAssetOwnershipReload = undefined;
@@ -174,7 +177,7 @@ export const createReactRouterDevRuntimeController = ({
174
177
  identity.node === binding.runtime.getCommittedNodeIdentity()
175
178
  ) {
176
179
  hdrIntentsByPair.get(pair)?.signalCommitted(nodeCompilation, () => {
177
- onNodeRebuildCommitted?.();
180
+ hdrChannels.get(binding)?.publish();
178
181
  });
179
182
  }
180
183
  } catch (cause) {
@@ -278,11 +281,7 @@ export const createReactRouterDevRuntimeController = ({
278
281
  if (sessions.getActiveBinding()?.runtime !== runtime) {
279
282
  return;
280
283
  }
281
- const patchesRouteMetadata =
282
- typeof clientPatchesRouteMetadata === 'function'
283
- ? clientPatchesRouteMetadata()
284
- : clientPatchesRouteMetadata;
285
- if (patchesRouteMetadata) {
284
+ if (isHmrEnabled()) {
286
285
  server.sockWrite('custom', {
287
286
  event: DEV_MANIFEST_UPDATE_EVENT,
288
287
  data: manifest.routes,
@@ -294,6 +293,14 @@ export const createReactRouterDevRuntimeController = ({
294
293
  onWarning: message => api.logger.warn(message),
295
294
  });
296
295
  const binding = sessions.createBinding(server, runtime);
296
+ hdrChannels.set(
297
+ binding,
298
+ createDevHdrChannel({
299
+ hot: server.environments.web.hot,
300
+ isEnabled: () =>
301
+ sessions.getActiveBinding() === binding && isHmrEnabled(),
302
+ })
303
+ );
297
304
  registerReactRouterDevRuntime(server, runtime);
298
305
  sessions.bindCloseObservation(binding);
299
306
  },
@@ -430,9 +437,7 @@ export const createReactRouterDevRuntimeController = ({
430
437
  hdrIntents.capture(
431
438
  compilation,
432
439
  changes.known &&
433
- Array.from(changes.files).some(
434
- file => !isHdrRevisionFile(file) && !isCssSourceFile(file)
435
- )
440
+ Array.from(changes.files).some(file => !isCssSourceFile(file))
436
441
  );
437
442
  pair.latestNodeStart = {
438
443
  status: 'started',
package/src/dev-server.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { pathToFileURL } from 'node:url';
2
3
  import type { RsbuildConfig } from '@rsbuild/core';
3
4
  import type { ServerBuild } from 'react-router';
4
5
  import { installDevServerSourceMapSupport } from './dev-source-maps.js';
6
+ import { resolveAppPackagePath } from './plugin-utils.js';
5
7
 
6
8
  export type ServerSetup = Exclude<
7
9
  NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
@@ -18,6 +20,7 @@ type RequestHandler = (request: Request) => Response | Promise<Response>;
18
20
  type BuildProvider = () => Promise<ServerBuild>;
19
21
 
20
22
  export type DevServerMiddlewareDependencies = {
23
+ rootPath: string;
21
24
  loadBuild: BuildProvider;
22
25
  createRequestHandler?: (
23
26
  build: BuildProvider,
@@ -39,9 +42,22 @@ export const createDevServerMiddleware = (
39
42
 
40
43
  const getListener = () => {
41
44
  listenerPromise ??= (async () => {
42
- const createRequestHandler =
43
- dependencies.createRequestHandler ??
44
- (await import('react-router')).createRequestHandler;
45
+ let createRequestHandler = dependencies.createRequestHandler;
46
+ if (!createRequestHandler) {
47
+ const reactRouterPath = resolveAppPackagePath(
48
+ 'react-router',
49
+ dependencies.rootPath
50
+ );
51
+ if (!reactRouterPath) {
52
+ throw new Error('Cannot resolve react-router from the application.');
53
+ }
54
+ // Match the application's browser/server bundles, including when the
55
+ // plugin is linked from a workspace with a different Router version.
56
+ const routerModule: typeof import('react-router') = await import(
57
+ pathToFileURL(reactRouterPath).href
58
+ );
59
+ createRequestHandler = routerModule.createRequestHandler;
60
+ }
45
61
  const createRequestListener =
46
62
  dependencies.createRequestListener ??
47
63
  (await import('@remix-run/node-fetch-server')).createRequestListener;
@@ -73,8 +89,10 @@ export const createDevServerMiddleware = (
73
89
 
74
90
  export const createReactRouterDevServerSetup = ({
75
91
  loadBuild,
92
+ rootPath,
76
93
  }: {
77
94
  loadBuild: BuildProvider;
95
+ rootPath: string;
78
96
  }): ServerSetup =>
79
97
  function reactRouterDevServerSetup(context) {
80
98
  if (context.action !== 'dev') {
@@ -85,6 +103,8 @@ export const createReactRouterDevServerSetup = ({
85
103
  // request handler.
86
104
  return () => {
87
105
  installDevServerSourceMapSupport();
88
- context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
106
+ context.server.middlewares.use(
107
+ createDevServerMiddleware({ loadBuild, rootPath })
108
+ );
89
109
  };
90
110
  };