rsbuild-plugin-react-router 0.3.0 → 0.4.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 (64) hide show
  1. package/README.md +88 -35
  2. package/dist/451.js +164 -30
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/build-output-transforms.d.ts +2 -1
  5. package/dist/concurrency.d.ts +2 -1
  6. package/dist/config-imports.d.ts +7 -0
  7. package/dist/dev-background-resources.d.ts +38 -0
  8. package/dist/dev-generation.d.ts +2 -2
  9. package/dist/dev-hmr.d.ts +45 -0
  10. package/dist/dev-runtime-artifacts.d.ts +9 -1
  11. package/dist/dev-runtime-compilation.d.ts +17 -2
  12. package/dist/dev-runtime-controller.d.ts +6 -1
  13. package/dist/dev-server.d.ts +5 -0
  14. package/dist/effect-runtime.d.ts +18 -0
  15. package/dist/export-utils.d.ts +2 -2
  16. package/dist/index.cjs +11852 -848
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +8289 -773
  19. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  20. package/dist/lazy-compilation.d.ts +2 -1
  21. package/dist/manifest.d.ts +19 -4
  22. package/dist/parallel-route-transforms.d.ts +17 -2
  23. package/dist/prerender-build.d.ts +4 -0
  24. package/dist/prerender.d.ts +0 -1
  25. package/dist/react-router-config.d.ts +8 -5
  26. package/dist/route-artifacts.d.ts +10 -2
  27. package/dist/route-transform-tasks.d.ts +3 -0
  28. package/dist/server-build-resolution.d.ts +3 -0
  29. package/dist/ssr-externals.d.ts +1 -0
  30. package/dist/typegen.d.ts +14 -1
  31. package/dist/types.d.ts +15 -6
  32. package/package.json +18 -10
  33. package/src/build-manifest.ts +110 -73
  34. package/src/build-output-transforms.ts +5 -0
  35. package/src/concurrency.ts +3 -22
  36. package/src/config-imports.ts +38 -0
  37. package/src/dev-background-resources.ts +255 -0
  38. package/src/dev-generation.ts +43 -53
  39. package/src/dev-hmr.ts +431 -0
  40. package/src/dev-runtime-artifacts.ts +50 -18
  41. package/src/dev-runtime-compilation.ts +80 -1
  42. package/src/dev-runtime-controller.ts +155 -31
  43. package/src/dev-runtime-session.ts +18 -11
  44. package/src/dev-server.ts +31 -1
  45. package/src/effect-runtime.ts +130 -0
  46. package/src/export-utils.ts +82 -23
  47. package/src/index.ts +166 -153
  48. package/src/lazy-compilation-prewarm.ts +279 -0
  49. package/src/lazy-compilation.ts +12 -5
  50. package/src/manifest.ts +366 -255
  51. package/src/modify-browser-manifest.ts +2 -1
  52. package/src/parallel-route-transforms.ts +195 -69
  53. package/src/prerender-build.ts +147 -63
  54. package/src/prerender.ts +1 -19
  55. package/src/react-router-config.ts +98 -73
  56. package/src/route-artifacts.ts +122 -3
  57. package/src/route-export-resolution.ts +3 -3
  58. package/src/route-transform-tasks.ts +173 -2
  59. package/src/route-watch.ts +119 -84
  60. package/src/server-build-resolution.ts +131 -0
  61. package/src/server-utils.ts +7 -106
  62. package/src/ssr-externals.ts +1 -1
  63. package/src/typegen.ts +162 -33
  64. package/src/types.ts +16 -6
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import fsExtra from 'fs-extra';
5
+ import * as Effect from 'effect/Effect';
5
6
  import type { RsbuildPluginAPI } from '@rsbuild/core';
6
7
  import {
7
8
  createRequestHandler,
@@ -22,7 +23,6 @@ import {
22
23
  getPrerenderConcurrency,
23
24
  getSsrFalsePrerenderExportErrors,
24
25
  normalizePrerenderMatchPath,
25
- withBuildRequest,
26
26
  } from './prerender.js';
27
27
  import type {
28
28
  Config,
@@ -30,6 +30,7 @@ import type {
30
30
  } from './react-router-config.js';
31
31
  import { resolveServerBuildModule } from './server-utils.js';
32
32
  import type { PluginOptions, Route } from './types.js';
33
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
33
34
 
34
35
  type ReactRouterManifest = Awaited<
35
36
  ReturnType<typeof getReactRouterManifestForDev>
@@ -131,6 +132,34 @@ const createDataRequestPath = (
131
132
  : `${prerenderPath.replace(/\/$/, '')}.data`;
132
133
  };
133
134
 
135
+ export const createBuildRequestEffect = <T>(
136
+ input: string | URL,
137
+ init: RequestInit | undefined,
138
+ handle: (request: Request) => Promise<T>
139
+ ): Effect.Effect<T, Error, never> =>
140
+ Effect.acquireUseRelease(
141
+ Effect.sync(() => new AbortController()),
142
+ controller =>
143
+ tryPluginPromise(() =>
144
+ handle(
145
+ new Request(input, {
146
+ ...init,
147
+ signal: controller.signal,
148
+ })
149
+ )
150
+ ),
151
+ controller =>
152
+ Effect.sync(() => {
153
+ controller.abort();
154
+ })
155
+ );
156
+
157
+ export const withBuildRequest = <T>(
158
+ input: string | URL,
159
+ init: RequestInit | undefined,
160
+ handle: (request: Request) => Promise<T>
161
+ ): Promise<T> => runPluginEffect(createBuildRequestEffect(input, init, handle));
162
+
134
163
  const prerenderData = async ({
135
164
  handler,
136
165
  prerenderPath,
@@ -150,11 +179,21 @@ const prerenderData = async ({
150
179
  api: PrerenderBuildApi;
151
180
  requestInit?: RequestInit;
152
181
  }): Promise<string> => {
153
- const dataRequestPath = createDataRequestPath(
182
+ const dataOutputPath = createDataRequestPath(
154
183
  prerenderPath,
155
184
  trailingSlashAwareDataRequests
156
185
  );
186
+ // The handler always serves the root route's data at /_root.data, even when
187
+ // trailing-slash-aware naming writes the root output to /_.data.
188
+ const dataRequestPath =
189
+ trailingSlashAwareDataRequests && prerenderPath === '/'
190
+ ? '/_root.data'
191
+ : dataOutputPath;
157
192
  const normalizedPath = `${basename}${dataRequestPath}`.replace(/\/\/+/g, '/');
193
+ const outputNormalizedPath =
194
+ dataOutputPath === dataRequestPath
195
+ ? normalizedPath
196
+ : `${basename}${dataOutputPath}`.replace(/\/\/+/g, '/');
158
197
  const url = new URL(`http://localhost${normalizedPath}`);
159
198
  if (onlyRoutes?.length) {
160
199
  url.searchParams.set('_routes', onlyRoutes.join(','));
@@ -172,7 +211,10 @@ const prerenderData = async ({
172
211
  );
173
212
  }
174
213
 
175
- const outputPath = resolve(clientBuildDir, ...normalizedPath.split('/'));
214
+ const outputPath = resolve(
215
+ clientBuildDir,
216
+ ...outputNormalizedPath.split('/')
217
+ );
176
218
  await mkdir(dirname(outputPath), { recursive: true });
177
219
  await writeFile(outputPath, data);
178
220
  api.logger.info(
@@ -401,23 +443,30 @@ const validatePrerenderPathMatches = (
401
443
  }
402
444
  };
403
445
 
404
- const runPrerenderPaths = async ({
446
+ export const createBoundedPrerenderTasksEffect = (
447
+ prerenderPaths: string[],
448
+ concurrency: number,
449
+ renderPath: (path: string) => Effect.Effect<void, Error, never>
450
+ ): Effect.Effect<void, Error, never> =>
451
+ Effect.forEach(prerenderPaths, renderPath, { concurrency, discard: true });
452
+
453
+ const createPrerenderPathEffect = ({
454
+ path,
405
455
  build,
456
+ buildRoutes,
406
457
  requestHandler,
407
458
  clientBuildDir,
408
459
  options,
409
460
  }: {
461
+ path: string;
410
462
  build: PrerenderServerBuild;
463
+ buildRoutes: ReturnType<typeof createPrerenderRoutes>;
411
464
  requestHandler: (request: Request) => Promise<Response>;
412
465
  clientBuildDir: string;
413
466
  options: RunReactRouterPrerenderBuildOptions;
414
- }): Promise<void> => {
415
- const { api, basename, future, prerenderConfig, prerenderPaths } = options;
416
- const buildRoutes = createPrerenderRoutes(build.routes);
417
- const concurrency = getPrerenderConcurrency(prerenderConfig);
418
- const pending = new Set<Promise<void>>();
419
-
420
- const enqueue = async (path: string) => {
467
+ }): Effect.Effect<void, Error, never> =>
468
+ Effect.gen(function* () {
469
+ const { api, basename, future } = options;
421
470
  const matches = matchRoutes(buildRoutes, normalizePrerenderMatchPath(path));
422
471
  if (!matches) {
423
472
  return;
@@ -431,27 +480,33 @@ const runPrerenderPaths = async ({
431
480
 
432
481
  if (isResourceRoute) {
433
482
  if (manifestRoute.loader && routeId) {
434
- await prerenderData({
435
- handler: requestHandler,
436
- prerenderPath: path,
437
- onlyRoutes: [routeId],
438
- clientBuildDir,
439
- basename,
440
- trailingSlashAwareDataRequests:
441
- future.unstable_trailingSlashAwareDataRequests,
442
- api,
443
- });
444
- await prerenderResourceRoute({
445
- handler: requestHandler,
446
- prerenderPath: path,
447
- clientBuildDir,
448
- basename,
449
- api,
450
- });
451
- } else {
452
- api.logger.warn(
453
- `⚠️ Skipping prerendering for resource route without a loader: ${routeId}`
483
+ yield* tryPluginPromise(() =>
484
+ prerenderData({
485
+ handler: requestHandler,
486
+ prerenderPath: path,
487
+ onlyRoutes: [routeId],
488
+ clientBuildDir,
489
+ basename,
490
+ trailingSlashAwareDataRequests:
491
+ future.unstable_trailingSlashAwareDataRequests,
492
+ api,
493
+ })
494
+ );
495
+ yield* tryPluginPromise(() =>
496
+ prerenderResourceRoute({
497
+ handler: requestHandler,
498
+ prerenderPath: path,
499
+ clientBuildDir,
500
+ basename,
501
+ api,
502
+ })
454
503
  );
504
+ } else {
505
+ yield* Effect.sync(() => {
506
+ api.logger.warn(
507
+ `⚠️ Skipping prerendering for resource route without a loader: ${routeId}`
508
+ );
509
+ });
455
510
  }
456
511
  return;
457
512
  }
@@ -463,44 +518,73 @@ const runPrerenderPaths = async ({
463
518
  }
464
519
  return build.assets?.routes?.[matchedRouteId]?.hasLoader;
465
520
  });
466
- let data: string | undefined;
467
- if (hasLoaders) {
468
- data = await prerenderData({
521
+ const data = hasLoaders
522
+ ? yield* tryPluginPromise(() =>
523
+ prerenderData({
524
+ handler: requestHandler,
525
+ prerenderPath: path,
526
+ onlyRoutes: null,
527
+ clientBuildDir,
528
+ basename,
529
+ trailingSlashAwareDataRequests:
530
+ future.unstable_trailingSlashAwareDataRequests,
531
+ api,
532
+ })
533
+ )
534
+ : undefined;
535
+
536
+ yield* tryPluginPromise(() =>
537
+ prerenderRoute({
469
538
  handler: requestHandler,
470
539
  prerenderPath: path,
471
- onlyRoutes: null,
472
540
  clientBuildDir,
473
541
  basename,
474
- trailingSlashAwareDataRequests:
475
- future.unstable_trailingSlashAwareDataRequests,
476
542
  api,
477
- });
478
- }
479
- await prerenderRoute({
480
- handler: requestHandler,
481
- prerenderPath: path,
482
- clientBuildDir,
483
- basename,
484
- api,
485
- requestInit: data
486
- ? {
487
- headers: {
488
- 'X-React-Router-Prerender-Data': encodeURI(data),
489
- },
490
- }
491
- : undefined,
492
- });
493
- };
543
+ requestInit: data ? createPrerenderDataRequestInit(data) : undefined,
544
+ })
545
+ );
546
+ });
494
547
 
495
- for (const path of prerenderPaths) {
496
- const task = enqueue(path);
497
- pending.add(task);
498
- task.finally(() => pending.delete(task));
499
- if (pending.size >= concurrency) {
500
- await Promise.race(pending);
501
- }
502
- }
503
- await Promise.all(pending);
548
+ const createPrerenderDataRequestInit = (
549
+ data: string
550
+ ): RequestInit | undefined => {
551
+ const encodedData = encodeURI(data);
552
+ return encodedData.length < 8 * 1024
553
+ ? {
554
+ headers: {
555
+ 'X-React-Router-Prerender-Data': encodedData,
556
+ },
557
+ }
558
+ : undefined;
559
+ };
560
+
561
+ const runPrerenderPaths = async ({
562
+ build,
563
+ requestHandler,
564
+ clientBuildDir,
565
+ options,
566
+ }: {
567
+ build: PrerenderServerBuild;
568
+ requestHandler: (request: Request) => Promise<Response>;
569
+ clientBuildDir: string;
570
+ options: RunReactRouterPrerenderBuildOptions;
571
+ }): Promise<void> => {
572
+ const { prerenderConfig, prerenderPaths } = options;
573
+ const buildRoutes = createPrerenderRoutes(build.routes);
574
+ const concurrency = getPrerenderConcurrency(prerenderConfig);
575
+
576
+ await runPluginEffect(
577
+ createBoundedPrerenderTasksEffect(prerenderPaths, concurrency, path =>
578
+ createPrerenderPathEffect({
579
+ path,
580
+ build,
581
+ buildRoutes,
582
+ requestHandler,
583
+ clientBuildDir,
584
+ options,
585
+ })
586
+ )
587
+ );
504
588
  };
505
589
 
506
590
  export const runReactRouterPrerenderBuild = async (
package/src/prerender.ts CHANGED
@@ -92,24 +92,6 @@ export const createPrerenderRoutes = (
92
92
  export const normalizePrerenderMatchPath = (path: string): string =>
93
93
  `/${path}/`.replace(/^\/\/+/, '/');
94
94
 
95
- export const withBuildRequest = async <T>(
96
- input: string | URL,
97
- init: RequestInit | undefined,
98
- handle: (request: Request) => Promise<T>
99
- ): Promise<T> => {
100
- const controller = new AbortController();
101
- try {
102
- return await handle(
103
- new Request(input, {
104
- ...init,
105
- signal: controller.signal,
106
- })
107
- );
108
- } finally {
109
- controller.abort();
110
- }
111
- };
112
-
113
95
  export const getSsrFalsePrerenderExportErrors = ({
114
96
  routes,
115
97
  manifestRoutes,
@@ -267,7 +249,7 @@ export const resolvePrerenderPaths = async (
267
249
  [
268
250
  'Warning: Paths with dynamic/splat params cannot be prerendered when using `prerender: true`.',
269
251
  'You may want to use the `prerender()` API to prerender the following paths:',
270
- ...paramRoutes.map(path => ` - ${path}`),
252
+ ...paramRoutes.map(path => ` - ${path.replace(/^\/(?=[:*])/, '')}`),
271
253
  ].join('\n')
272
254
  );
273
255
  }
@@ -4,6 +4,9 @@ import type {
4
4
  } from '@react-router/dev/config';
5
5
  import type { NormalizedConfig } from '@rsbuild/core';
6
6
  import type { RouteConfigEntry } from '@react-router/dev/routes';
7
+ import * as Effect from 'effect/Effect';
8
+ import { getCappedPluginConcurrency } from './concurrency.js';
9
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
7
10
 
8
11
  export type BuildEndHook = {
9
12
  bivarianceHack(args: {
@@ -45,6 +48,12 @@ type RouteManifestEntry = {
45
48
 
46
49
  type RouteManifest = Record<string, RouteManifestEntry>;
47
50
 
51
+ type ResolveReactRouterConfigResult = {
52
+ resolved: ResolvedReactRouterConfig;
53
+ presets: NonNullable<Config['presets']>;
54
+ hasConfiguredServerModuleFormat: boolean;
55
+ };
56
+
48
57
  export type ResolvedReactRouterConfig = Readonly<{
49
58
  appDirectory: string;
50
59
  basename: string;
@@ -102,10 +111,15 @@ const mergeReactRouterConfig = (...configs: Config[]): Config => {
102
111
  buildEnd: async (
103
112
  ...args: Parameters<NonNullable<Config['buildEnd']>>
104
113
  ) => {
105
- await Promise.all([
106
- configA.buildEnd?.(...args),
107
- configB.buildEnd?.(...args),
108
- ]);
114
+ await runPluginEffect(
115
+ Effect.all(
116
+ [
117
+ tryPluginPromise(() => configA.buildEnd?.(...args)),
118
+ tryPluginPromise(() => configB.buildEnd?.(...args)),
119
+ ],
120
+ { discard: true }
121
+ )
122
+ );
109
123
  },
110
124
  }
111
125
  : {}),
@@ -145,76 +159,87 @@ const normalizeSubResourceIntegrity = (config: Config): Config => {
145
159
  };
146
160
  };
147
161
 
148
- export const resolveReactRouterConfig = async (
162
+ export const resolveReactRouterConfigEffect = (
149
163
  reactRouterUserConfig: Config
150
- ): Promise<{
151
- resolved: ResolvedReactRouterConfig;
152
- presets: NonNullable<Config['presets']>;
153
- hasConfiguredServerModuleFormat: boolean;
154
- }> => {
155
- const presets = await Promise.all(
156
- (reactRouterUserConfig.presets ?? []).map(async preset => {
157
- if (!preset.name) {
158
- throw new Error(
159
- 'React Router presets must have a `name` property defined.'
160
- );
161
- }
162
- if (!preset.reactRouterConfig) {
163
- return null;
164
- }
165
- const { buildEnd: _buildEnd, ...reactRouterUserConfigForPreset } =
166
- reactRouterUserConfig;
167
- const presetConfig = await preset.reactRouterConfig({
168
- reactRouterUserConfig: reactRouterUserConfigForPreset,
169
- });
170
- if (!presetConfig) return null;
171
- const { presets: _presets, ...rest } = presetConfig as Config;
172
- return rest;
173
- })
174
- );
175
-
176
- const userAndPresetConfigs = mergeReactRouterConfig(
177
- ...(presets.filter(Boolean) as Config[]).map(normalizeSubResourceIntegrity),
178
- normalizeSubResourceIntegrity(reactRouterUserConfig)
179
- );
164
+ ): Effect.Effect<ResolveReactRouterConfigResult, Error, never> =>
165
+ Effect.gen(function* () {
166
+ const presets = yield* Effect.forEach(
167
+ reactRouterUserConfig.presets ?? [],
168
+ preset =>
169
+ Effect.gen(function* () {
170
+ if (!preset.name) {
171
+ return yield* Effect.fail(
172
+ new Error(
173
+ 'React Router presets must have a `name` property defined.'
174
+ )
175
+ );
176
+ }
177
+ if (!preset.reactRouterConfig) {
178
+ return null;
179
+ }
180
+ const { buildEnd: _buildEnd, ...reactRouterUserConfigForPreset } =
181
+ reactRouterUserConfig;
182
+ const presetConfig = yield* tryPluginPromise(() =>
183
+ preset.reactRouterConfig?.({
184
+ reactRouterUserConfig: reactRouterUserConfigForPreset,
185
+ })
186
+ );
187
+ if (!presetConfig) return null;
188
+ const { presets: _presets, ...rest } = presetConfig as Config;
189
+ return rest;
190
+ }),
191
+ { concurrency: getCappedPluginConcurrency() }
192
+ );
193
+
194
+ const userAndPresetConfigs = mergeReactRouterConfig(
195
+ ...(presets.filter(Boolean) as Config[]).map(
196
+ normalizeSubResourceIntegrity
197
+ ),
198
+ normalizeSubResourceIntegrity(reactRouterUserConfig)
199
+ );
200
+
201
+ const subResourceIntegrity =
202
+ userAndPresetConfigs.subResourceIntegrity ??
203
+ userAndPresetConfigs.future?.unstable_subResourceIntegrity ??
204
+ DEFAULT_CONFIG.subResourceIntegrity;
205
+ const resolvedFuture: FutureConfig = {
206
+ ...DEFAULT_CONFIG.future,
207
+ ...(userAndPresetConfigs.future ?? {}),
208
+ unstable_subResourceIntegrity: subResourceIntegrity,
209
+ };
210
+ const splitRouteModules =
211
+ userAndPresetConfigs.splitRouteModules ??
212
+ userAndPresetConfigs.future?.v8_splitRouteModules ??
213
+ DEFAULT_CONFIG.splitRouteModules;
214
+
215
+ let resolved: ResolvedReactRouterConfig = {
216
+ ...DEFAULT_CONFIG,
217
+ ...userAndPresetConfigs,
218
+ future: resolvedFuture,
219
+ splitRouteModules,
220
+ subResourceIntegrity,
221
+ allowedActionOrigins:
222
+ userAndPresetConfigs.allowedActionOrigins ??
223
+ DEFAULT_CONFIG.allowedActionOrigins,
224
+ routes: DEFAULT_CONFIG.routes,
225
+ unstable_routeConfig: DEFAULT_CONFIG.unstable_routeConfig,
226
+ };
227
+ if (!resolved.ssr) {
228
+ resolved = {
229
+ ...resolved,
230
+ serverBundles: undefined,
231
+ };
232
+ }
180
233
 
181
- const subResourceIntegrity =
182
- userAndPresetConfigs.subResourceIntegrity ??
183
- userAndPresetConfigs.future?.unstable_subResourceIntegrity ??
184
- DEFAULT_CONFIG.subResourceIntegrity;
185
- const resolvedFuture: FutureConfig = {
186
- ...DEFAULT_CONFIG.future,
187
- ...(userAndPresetConfigs.future ?? {}),
188
- unstable_subResourceIntegrity: subResourceIntegrity,
189
- };
190
- const splitRouteModules =
191
- userAndPresetConfigs.splitRouteModules ??
192
- userAndPresetConfigs.future?.v8_splitRouteModules ??
193
- DEFAULT_CONFIG.splitRouteModules;
194
-
195
- let resolved: ResolvedReactRouterConfig = {
196
- ...DEFAULT_CONFIG,
197
- ...userAndPresetConfigs,
198
- future: resolvedFuture,
199
- splitRouteModules,
200
- subResourceIntegrity,
201
- allowedActionOrigins:
202
- userAndPresetConfigs.allowedActionOrigins ??
203
- DEFAULT_CONFIG.allowedActionOrigins,
204
- routes: DEFAULT_CONFIG.routes,
205
- unstable_routeConfig: DEFAULT_CONFIG.unstable_routeConfig,
206
- };
207
- if (!resolved.ssr) {
208
- resolved = {
209
- ...resolved,
210
- serverBundles: undefined,
234
+ return {
235
+ resolved,
236
+ presets: reactRouterUserConfig.presets ?? [],
237
+ hasConfiguredServerModuleFormat:
238
+ userAndPresetConfigs.serverModuleFormat !== undefined,
211
239
  };
212
- }
240
+ });
213
241
 
214
- return {
215
- resolved,
216
- presets: reactRouterUserConfig.presets ?? [],
217
- hasConfiguredServerModuleFormat:
218
- userAndPresetConfigs.serverModuleFormat !== undefined,
219
- };
220
- };
242
+ export const resolveReactRouterConfig = (
243
+ reactRouterUserConfig: Config
244
+ ): Promise<ResolveReactRouterConfigResult> =>
245
+ runPluginEffect(resolveReactRouterConfigEffect(reactRouterUserConfig));