rsbuild-plugin-react-router 0.4.1 → 0.6.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 (126) hide show
  1. package/README.md +72 -240
  2. package/dist/{451.js → 511.js} +461 -331
  3. package/dist/build-output-transforms.d.ts +32 -7
  4. package/dist/classic-mode.d.ts +56 -0
  5. package/dist/config-imports.d.ts +8 -2
  6. package/dist/constants.d.ts +3 -0
  7. package/dist/dev-background-resources.d.ts +3 -3
  8. package/dist/dev-generation.d.ts +2 -3
  9. package/dist/dev-hmr.d.ts +8 -2
  10. package/dist/dev-runtime-controller.d.ts +6 -1
  11. package/dist/dev-source-maps.d.ts +4 -0
  12. package/dist/effect-runtime.d.ts +17 -5
  13. package/dist/entry-paths.d.ts +16 -0
  14. package/dist/environment-output.d.ts +6 -0
  15. package/dist/export-utils.d.ts +2 -1
  16. package/dist/federation.d.ts +2 -0
  17. package/dist/index.cjs +3675 -2107
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +3614 -2214
  20. package/dist/lazy-compilation-prewarm.d.ts +8 -5
  21. package/dist/manifest.d.ts +16 -4
  22. package/dist/mode-plan.d.ts +82 -0
  23. package/dist/modify-browser-manifest.d.ts +8 -4
  24. package/dist/performance.d.ts +4 -1
  25. package/dist/plugin-utils.d.ts +27 -1
  26. package/dist/prerender-build.d.ts +5 -5
  27. package/dist/prerender.d.ts +1 -5
  28. package/dist/react-router-config.d.ts +11 -3
  29. package/dist/route-artifacts.d.ts +4 -2
  30. package/dist/route-chunks.d.ts +2 -1
  31. package/dist/route-component-transform.d.ts +0 -2
  32. package/dist/route-export-pruning.d.ts +4 -1
  33. package/dist/route-imports.d.ts +18 -0
  34. package/dist/route-module-transform-loader.d.ts +14 -0
  35. package/dist/route-module-transform-loader.js +44 -0
  36. package/dist/route-module-transform-rules.d.ts +14 -0
  37. package/dist/route-transform-tasks.d.ts +6 -0
  38. package/dist/route-watch.d.ts +11 -6
  39. package/dist/rsc-dev-server.d.ts +26 -0
  40. package/dist/rsc-prerender.d.ts +54 -0
  41. package/dist/rsc-route-config.d.ts +6 -0
  42. package/dist/rsc-route-exports.d.ts +15 -0
  43. package/dist/rsc-route-transform-loader.cjs +43 -0
  44. package/dist/rsc-route-transform-loader.d.ts +30 -0
  45. package/dist/rsc-route-transform-loader.js +16 -0
  46. package/dist/rsc-route-transform-registration.d.ts +12 -0
  47. package/dist/rsc-route-transforms.d.ts +21 -0
  48. package/dist/rsc-support.d.ts +23 -0
  49. package/dist/rsc-virtual-modules.d.ts +19 -0
  50. package/dist/server-build-plan.d.ts +2 -1
  51. package/dist/server-build-resolution.d.ts +1 -2
  52. package/dist/server-utils.d.ts +4 -5
  53. package/dist/ssr-asset-relocation.d.ts +98 -0
  54. package/dist/templates/entry.rsc.client.d.ts +1 -0
  55. package/dist/templates/entry.rsc.client.js +61 -0
  56. package/dist/templates/entry.rsc.d.ts +9 -0
  57. package/dist/templates/entry.rsc.js +38 -0
  58. package/dist/templates/entry.rsc.ssr.d.ts +4 -0
  59. package/dist/templates/entry.rsc.ssr.js +24 -0
  60. package/dist/typegen.d.ts +4 -2
  61. package/dist/types.d.ts +34 -6
  62. package/package.json +76 -22
  63. package/src/build-output-transforms.ts +181 -43
  64. package/src/classic-mode.ts +253 -0
  65. package/src/config-imports.ts +153 -6
  66. package/src/constants.ts +6 -2
  67. package/src/dev-background-resources.ts +46 -100
  68. package/src/dev-generation.ts +52 -33
  69. package/src/dev-hmr.ts +112 -80
  70. package/src/dev-runtime-artifacts.ts +11 -12
  71. package/src/dev-runtime-controller.ts +75 -85
  72. package/src/dev-runtime-session.ts +14 -18
  73. package/src/dev-server.ts +2 -0
  74. package/src/dev-source-maps.ts +257 -0
  75. package/src/effect-runtime.ts +105 -57
  76. package/src/entry-paths.ts +80 -0
  77. package/src/environment-output.ts +55 -0
  78. package/src/export-utils.ts +15 -11
  79. package/src/federation.ts +38 -0
  80. package/src/index.ts +666 -519
  81. package/src/lazy-compilation-prewarm.ts +20 -4
  82. package/src/manifest.ts +157 -82
  83. package/src/mode-plan.ts +367 -0
  84. package/src/modify-browser-manifest.ts +130 -124
  85. package/src/performance.ts +11 -2
  86. package/src/plugin-utils.ts +103 -39
  87. package/src/prerender-build.ts +82 -104
  88. package/src/prerender.ts +23 -24
  89. package/src/react-router-config.ts +72 -26
  90. package/src/route-artifacts.ts +96 -66
  91. package/src/route-chunks.ts +167 -51
  92. package/src/route-component-transform.ts +14 -21
  93. package/src/route-export-pruning.ts +4 -3
  94. package/src/route-imports.ts +100 -0
  95. package/src/route-module-transform-loader.ts +149 -0
  96. package/src/route-module-transform-rules.ts +115 -0
  97. package/src/route-transform-tasks.ts +44 -29
  98. package/src/route-watch.ts +166 -188
  99. package/src/rsc-dev-server.ts +112 -0
  100. package/src/rsc-prerender.ts +362 -0
  101. package/src/rsc-route-config.ts +175 -0
  102. package/src/rsc-route-exports.ts +67 -0
  103. package/src/rsc-route-transform-loader.ts +65 -0
  104. package/src/rsc-route-transform-registration.ts +145 -0
  105. package/src/rsc-route-transforms.ts +995 -0
  106. package/src/rsc-runtime.d.ts +143 -0
  107. package/src/rsc-support.ts +116 -0
  108. package/src/rsc-virtual-modules.ts +113 -0
  109. package/src/server-build-plan.ts +14 -3
  110. package/src/server-build-resolution.ts +37 -47
  111. package/src/server-utils.ts +21 -35
  112. package/src/ssr-asset-relocation.ts +183 -0
  113. package/src/ssr-externals.ts +8 -26
  114. package/src/templates/entry.rsc.client.tsx +168 -0
  115. package/src/templates/entry.rsc.ssr.tsx +45 -0
  116. package/src/templates/entry.rsc.tsx +80 -0
  117. package/src/typegen.ts +40 -23
  118. package/src/types.ts +43 -6
  119. package/src/warnings/warn-on-client-source-maps.ts +6 -10
  120. package/dist/parallel-route-transform-protocol.d.ts +0 -25
  121. package/dist/parallel-route-transform-worker.d.ts +0 -1
  122. package/dist/parallel-route-transform-worker.js +0 -38
  123. package/dist/parallel-route-transforms.d.ts +0 -29
  124. package/src/parallel-route-transform-protocol.ts +0 -35
  125. package/src/parallel-route-transform-worker.ts +0 -82
  126. package/src/parallel-route-transforms.ts +0 -458
@@ -0,0 +1,253 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import type { RsbuildEntryDescription, RsbuildPluginAPI } from '@rsbuild/core';
3
+ import type { RouteConfigEntry } from '@react-router/dev/routes';
4
+ import { resolve } from 'pathe';
5
+ import {
6
+ getBuildManifest,
7
+ getRoutesByServerBundleId,
8
+ } from './build-manifest.js';
9
+ import { BUILD_CLIENT_ROUTE_QUERY_STRING } from './constants.js';
10
+ import {
11
+ DEV_HMR_RUNTIME_MODULE_ID,
12
+ type DevHmrPlanOptions,
13
+ } from './dev-hmr.js';
14
+ import {
15
+ createReactRouterDevRuntimeController,
16
+ type ReactRouterDevRuntimeController,
17
+ } from './dev-runtime-controller.js';
18
+ import { generateWithProps } from './plugin-utils.js';
19
+ import { resolvePrerenderPaths } from './prerender.js';
20
+ import { generateServerBuild } from './server-utils.js';
21
+ import {
22
+ createReactRouterServerBuildPlan,
23
+ type ReactRouterServerBundleEntry,
24
+ } from './server-build-plan.js';
25
+ import {
26
+ getRouteChunkEntryName,
27
+ getRouteChunkModuleId,
28
+ routeChunkExportNames,
29
+ } from './route-chunks.js';
30
+ import type { Config } from './react-router-config.js';
31
+ import type { Route } from './types.js';
32
+
33
+ type CreateClassicWebRouteEntriesOptions = {
34
+ appDirectory: string;
35
+ isBuild: boolean;
36
+ routes: Record<string, Route>;
37
+ splitRouteModules: boolean;
38
+ };
39
+
40
+ type CreateClassicVirtualModulesOptions = {
41
+ allowedActionOrigins: string[] | undefined;
42
+ appDirectory: string;
43
+ assetsBuildDirectory: string;
44
+ basename: string;
45
+ devHmrRuntimeModule?: string;
46
+ entryServerPath: string;
47
+ future: Config['future'];
48
+ isSpaMode: boolean;
49
+ prerenderPaths: string[];
50
+ publicPath: string;
51
+ routeDiscovery: Config['routeDiscovery'];
52
+ routes: Record<string, Route>;
53
+ routesByServerBundleId: Record<string, Record<string, Route>>;
54
+ ssr: boolean;
55
+ };
56
+
57
+ type CreateClassicBuildArtifactsOptions = {
58
+ api: RsbuildPluginAPI;
59
+ defaultEntryName: string;
60
+ isBuild: boolean;
61
+ prerenderConfig: Config['prerender'];
62
+ reactRouterConfig: Required<
63
+ Pick<
64
+ Config,
65
+ 'appDirectory' | 'buildDirectory' | 'serverBuildFile' | 'future'
66
+ >
67
+ > &
68
+ Pick<Config, 'serverBundles'>;
69
+ routeConfig: RouteConfigEntry[];
70
+ routes: Record<string, Route>;
71
+ rootDirectory: string;
72
+ ssr: boolean;
73
+ devHmr?: DevHmrPlanOptions;
74
+ };
75
+
76
+ export type ClassicBuildArtifacts = {
77
+ buildManifest: Awaited<ReturnType<typeof getBuildManifest>>;
78
+ devRuntime: ReactRouterDevRuntimeController;
79
+ prerenderPaths: string[];
80
+ routesByServerBundleId: Record<string, Record<string, Route>>;
81
+ serverBundleEntries: ReactRouterServerBundleEntry[];
82
+ };
83
+
84
+ export const createClassicBuildArtifacts = async ({
85
+ api,
86
+ defaultEntryName,
87
+ isBuild,
88
+ prerenderConfig,
89
+ reactRouterConfig,
90
+ routeConfig,
91
+ routes,
92
+ rootDirectory,
93
+ ssr,
94
+ devHmr,
95
+ }: CreateClassicBuildArtifactsOptions): Promise<ClassicBuildArtifacts> => {
96
+ const buildManifest = await getBuildManifest({
97
+ reactRouterConfig,
98
+ routes,
99
+ rootDirectory,
100
+ });
101
+ const routesByServerBundleId = getRoutesByServerBundleId(
102
+ buildManifest,
103
+ routes
104
+ );
105
+ const serverBuildPlan = createReactRouterServerBuildPlan({
106
+ routesByServerBundleId,
107
+ serverBuildFile: reactRouterConfig.serverBuildFile,
108
+ defaultEntryName,
109
+ });
110
+ const prerenderPaths = await resolvePrerenderPaths(
111
+ prerenderConfig,
112
+ ssr,
113
+ routeConfig,
114
+ {
115
+ logWarning: true,
116
+ warn: message => api.logger.warn(message),
117
+ }
118
+ );
119
+
120
+ return {
121
+ buildManifest,
122
+ devRuntime: createReactRouterDevRuntimeController({
123
+ api,
124
+ isBuild,
125
+ buildPlan: serverBuildPlan,
126
+ clientPatchesRouteMetadata: devHmr?.isEnabled,
127
+ onNodeRebuildCommitted: devHmr?.onNodeRebuildCommitted,
128
+ }),
129
+ prerenderPaths,
130
+ routesByServerBundleId,
131
+ serverBundleEntries: serverBuildPlan.serverBundleEntries,
132
+ };
133
+ };
134
+
135
+ export const createClassicWebRouteEntries = ({
136
+ appDirectory,
137
+ isBuild,
138
+ routes,
139
+ splitRouteModules,
140
+ }: CreateClassicWebRouteEntriesOptions): {
141
+ manifestChunkNames: Set<string>;
142
+ webRouteEntries: Record<string, RsbuildEntryDescription>;
143
+ } => {
144
+ const manifestChunkNames = new Set<string>(['entry.client']);
145
+ const webRouteEntries = Object.values(routes).reduce(
146
+ (acc, route) => {
147
+ const entryName = route.file.slice(0, route.file.lastIndexOf('.'));
148
+ const routeFilePath = resolve(appDirectory, route.file);
149
+ manifestChunkNames.add(entryName);
150
+ acc[entryName] = {
151
+ import: `${routeFilePath}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
152
+ html: false,
153
+ };
154
+
155
+ if (isBuild && splitRouteModules && route.id !== 'root') {
156
+ let source: string;
157
+ try {
158
+ source = readFileSync(routeFilePath, 'utf8');
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
161
+ return acc;
162
+ }
163
+ throw error;
164
+ }
165
+ for (const exportName of routeChunkExportNames) {
166
+ if (!source.includes(exportName)) {
167
+ continue;
168
+ }
169
+ const chunkEntryName = getRouteChunkEntryName(route.id, exportName);
170
+ manifestChunkNames.add(chunkEntryName);
171
+ acc[chunkEntryName] = {
172
+ import: getRouteChunkModuleId(routeFilePath, exportName),
173
+ html: false,
174
+ };
175
+ }
176
+ }
177
+
178
+ return acc;
179
+ },
180
+ {} as Record<string, RsbuildEntryDescription>
181
+ );
182
+
183
+ return {
184
+ manifestChunkNames,
185
+ webRouteEntries,
186
+ };
187
+ };
188
+
189
+ export const createClassicVirtualModules = ({
190
+ allowedActionOrigins,
191
+ appDirectory,
192
+ assetsBuildDirectory,
193
+ basename,
194
+ devHmrRuntimeModule,
195
+ entryServerPath,
196
+ future,
197
+ isSpaMode,
198
+ prerenderPaths,
199
+ publicPath,
200
+ routeDiscovery,
201
+ routes,
202
+ routesByServerBundleId,
203
+ ssr,
204
+ }: CreateClassicVirtualModulesOptions): Record<string, string> => {
205
+ const serverBuildOptions = {
206
+ entryServerPath,
207
+ assetsBuildDirectory,
208
+ basename,
209
+ appDirectory,
210
+ ssr,
211
+ isSpaMode,
212
+ future,
213
+ allowedActionOrigins,
214
+ prerender: prerenderPaths,
215
+ routeDiscovery,
216
+ publicPath,
217
+ };
218
+ const bundleVirtualModules = Object.fromEntries(
219
+ Object.entries(routesByServerBundleId).map(([bundleId, bundleRoutes]) => [
220
+ `virtual/react-router/server-build-${bundleId}`,
221
+ generateServerBuild(bundleRoutes, {
222
+ ...serverBuildOptions,
223
+ serverManifestId: `virtual/react-router/server-manifest-${bundleId}`,
224
+ }),
225
+ ])
226
+ );
227
+ const bundleManifestModules = Object.fromEntries(
228
+ Object.entries(routesByServerBundleId)
229
+ .filter(
230
+ ([, bundleRoutes]) =>
231
+ bundleRoutes && Object.keys(bundleRoutes).length > 0
232
+ )
233
+ .map(([bundleId]) => [
234
+ `virtual/react-router/server-manifest-${bundleId}`,
235
+ 'export default {};',
236
+ ])
237
+ );
238
+
239
+ return {
240
+ 'virtual/react-router/browser-manifest': 'export default {};',
241
+ 'virtual/react-router/server-manifest': 'export default {};',
242
+ 'virtual/react-router/server-build': generateServerBuild(
243
+ routes,
244
+ serverBuildOptions
245
+ ),
246
+ ...bundleVirtualModules,
247
+ ...bundleManifestModules,
248
+ 'virtual/react-router/with-props': generateWithProps(),
249
+ ...(devHmrRuntimeModule !== undefined
250
+ ? { [DEV_HMR_RUNTIME_MODULE_ID]: devHmrRuntimeModule }
251
+ : {}),
252
+ };
253
+ };
@@ -1,14 +1,162 @@
1
- import type { ModuleCache } from 'jiti';
1
+ import type { JitiOptions, ModuleCache, TransformOptions } from 'jiti';
2
2
  import { createJiti } from 'jiti';
3
3
  import { resolve } from 'pathe';
4
+ import { parse } from './yuku.js';
4
5
 
5
- type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
6
+ export type ConfigImporter = Pick<ReturnType<typeof createJiti>, 'import'>;
7
+
8
+ type ConfigImportOptions = {
9
+ define?: Record<string, unknown>;
10
+ moduleCache?: boolean;
11
+ };
6
12
 
7
13
  const normalizePath = (filePath: string): string => resolve(filePath);
8
14
 
9
15
  const isNodeModulePath = (filePath: string): boolean =>
10
16
  filePath.split(/[\\/]/).includes('node_modules');
11
17
 
18
+ type AstNode = {
19
+ type?: string;
20
+ start?: number;
21
+ end?: number;
22
+ object?: AstNode;
23
+ property?: AstNode;
24
+ meta?: AstNode;
25
+ name?: string;
26
+ value?: unknown;
27
+ computed?: boolean;
28
+ };
29
+
30
+ const normalizeDefineValue = (value: unknown): string | undefined => {
31
+ if (value === undefined) {
32
+ return undefined;
33
+ }
34
+ if (typeof value === 'string') {
35
+ return value;
36
+ }
37
+ return JSON.stringify(value);
38
+ };
39
+
40
+ const getMemberExpressionName = (node: AstNode | undefined): string | null => {
41
+ if (!node) {
42
+ return null;
43
+ }
44
+ if (node.type === 'Identifier') {
45
+ return node.name ?? null;
46
+ }
47
+ if (node.type === 'MetaProperty') {
48
+ const meta = getMemberExpressionName(node.meta);
49
+ const property = getMemberExpressionName(node.property);
50
+ return meta && property ? `${meta}.${property}` : null;
51
+ }
52
+ if (node.type !== 'MemberExpression') {
53
+ return null;
54
+ }
55
+ const object = getMemberExpressionName(node.object);
56
+ const property =
57
+ node.computed && typeof node.property?.value === 'string'
58
+ ? node.property.value
59
+ : getMemberExpressionName(node.property);
60
+ return object && property ? `${object}.${property}` : null;
61
+ };
62
+
63
+ const collectDefineReplacements = (
64
+ node: unknown,
65
+ define: ReadonlyMap<string, string>,
66
+ replacements: Array<{ start: number; end: number; value: string }>
67
+ ): void => {
68
+ if (!node || typeof node !== 'object') {
69
+ return;
70
+ }
71
+
72
+ const astNode = node as AstNode;
73
+ const memberName = getMemberExpressionName(astNode);
74
+ const value = memberName ? define.get(memberName) : undefined;
75
+ if (
76
+ value !== undefined &&
77
+ typeof astNode.start === 'number' &&
78
+ typeof astNode.end === 'number'
79
+ ) {
80
+ replacements.push({
81
+ start: astNode.start,
82
+ end: astNode.end,
83
+ value,
84
+ });
85
+ return;
86
+ }
87
+
88
+ for (const child of Object.values(node)) {
89
+ if (Array.isArray(child)) {
90
+ for (const item of child) {
91
+ collectDefineReplacements(item, define, replacements);
92
+ }
93
+ continue;
94
+ }
95
+ collectDefineReplacements(child, define, replacements);
96
+ }
97
+ };
98
+
99
+ export const applyConfigDefines = (
100
+ source: string,
101
+ define: Record<string, unknown> | undefined
102
+ ): string => {
103
+ if (!define) {
104
+ return source;
105
+ }
106
+
107
+ const normalizedDefine = new Map(
108
+ Object.entries(define).flatMap(([key, value]) => {
109
+ const normalized = normalizeDefineValue(value);
110
+ return normalized === undefined ? [] : [[key, normalized]];
111
+ })
112
+ );
113
+ if (normalizedDefine.size === 0) {
114
+ return source;
115
+ }
116
+
117
+ const replacements: Array<{ start: number; end: number; value: string }> = [];
118
+ collectDefineReplacements(
119
+ parse(source, { sourceType: 'module' }),
120
+ normalizedDefine,
121
+ replacements
122
+ );
123
+
124
+ return replacements
125
+ .sort((left, right) => right.start - left.start)
126
+ .reduce(
127
+ (contents, replacement) =>
128
+ `${contents.slice(0, replacement.start)}${replacement.value}${contents.slice(
129
+ replacement.end
130
+ )}`,
131
+ source
132
+ );
133
+ };
134
+
135
+ export const createConfigImporter = ({
136
+ define,
137
+ moduleCache = true,
138
+ }: ConfigImportOptions = {}): ReturnType<typeof createJiti> => {
139
+ const defaultJiti = createJiti(process.cwd(), { moduleCache });
140
+ if (!define || Object.keys(define).length === 0) {
141
+ return defaultJiti;
142
+ }
143
+
144
+ const defaultTransform = defaultJiti.transform;
145
+ const options = {
146
+ moduleCache,
147
+ transform(transformOptions: TransformOptions) {
148
+ return {
149
+ code: defaultTransform({
150
+ ...transformOptions,
151
+ source: applyConfigDefines(transformOptions.source, define),
152
+ }),
153
+ };
154
+ },
155
+ } satisfies JitiOptions;
156
+
157
+ return createJiti(process.cwd(), options);
158
+ };
159
+
12
160
  export const collectConfigImportWatchPaths = (
13
161
  configPath: string,
14
162
  moduleCache: ModuleCache,
@@ -50,11 +198,10 @@ export const clearConfigImportCache = (
50
198
  export const importConfigWithWatchPaths = async <T>(
51
199
  configPath: string,
52
200
  load: (importer: ConfigImporter) => PromiseLike<T> | T = async importer =>
53
- importer.import<T>(configPath, { default: true })
201
+ importer.import<T>(configPath, { default: true }),
202
+ options?: ConfigImportOptions
54
203
  ): Promise<{ value: Awaited<T>; watchPaths: string | string[] }> => {
55
- const jiti = createJiti(process.cwd(), {
56
- moduleCache: true,
57
- });
204
+ const jiti = createConfigImporter({ moduleCache: true, ...options });
58
205
  const previousCacheKeys = new Set(Object.keys(jiti.cache));
59
206
  let importPaths: string[] = [];
60
207
 
package/src/constants.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export const PLUGIN_NAME = 'rsbuild:react-router';
2
2
 
3
+ /** Default web `output.distPath.js` segment when the user has not customized it. */
4
+ export const DEFAULT_JS_DIST_PATH = 'static/js';
5
+
3
6
  export const JS_EXTENSIONS = [
4
7
  '.tsx',
5
8
  '.ts',
@@ -32,8 +35,6 @@ export const SERVER_ONLY_ROUTE_EXPORTS_SET: ReadonlySet<string> = new Set(
32
35
  SERVER_ONLY_ROUTE_EXPORTS
33
36
  );
34
37
 
35
- // Client route exports are split into non-component exports and component exports.
36
- // This mirrors upstream React Router Vite plugin intent and is used for export filtering.
37
38
  export const CLIENT_NON_COMPONENT_EXPORTS = [
38
39
  'clientAction',
39
40
  'clientLoader',
@@ -89,3 +90,6 @@ export const CLIENT_EXPORTS = {
89
90
  meta: 'meta',
90
91
  shouldRevalidate: 'shouldRevalidate',
91
92
  } as const;
93
+
94
+ // SPA-mode prerender fallback document, served when no prerendered page matches.
95
+ export const SPA_FALLBACK_HTML_FILE = '__spa-fallback.html';
@@ -7,21 +7,19 @@ import {
7
7
  createDelayedPluginTask,
8
8
  DEV_BACKGROUND_STARTUP_DELAY_MS,
9
9
  normalizeEffectError,
10
- runPluginEffect,
11
- tryPluginPromise,
10
+ type PluginEffectRuntime,
12
11
  } from './effect-runtime.js';
13
12
  import {
14
- createLazyCompilationPrewarmController,
13
+ acquireLazyCompilationPrewarm,
15
14
  normalizeLazyCompilationPrewarmOptions,
16
15
  } from './lazy-compilation-prewarm.js';
17
16
  import {
18
17
  configRoutesToRouteManifestEntries,
19
18
  type ReactRouterManifestForDev,
20
19
  } from './manifest.js';
21
- import type { RouteTransformExecutor } from './parallel-route-transforms.js';
22
20
  import {
21
+ acquireRouteTopologyWatcher,
23
22
  createRouteManifestSnapshot,
24
- createRouteTopologyWatcher,
25
23
  ensureDevRestartMarker,
26
24
  type WatchFileConfig,
27
25
  } from './route-watch.js';
@@ -29,9 +27,9 @@ import type { PluginOptions } from './types.js';
29
27
 
30
28
  type RegisterReactRouterDevBackgroundResourcesOptions = {
31
29
  api: RsbuildPluginAPI;
30
+ runtime: PluginEffectRuntime;
32
31
  isBuild: boolean;
33
32
  lazyCompilationPrewarm: PluginOptions['unstableLazyCompilationPrewarm'];
34
- routeTransformExecutor: RouteTransformExecutor;
35
33
  routeRestartMarkerPath: string;
36
34
  watchDirectory: string;
37
35
  getRouteTopology: () => Promise<Set<string>>;
@@ -100,7 +98,10 @@ export const createReactRouterRouteWatchFiles = ({
100
98
  watchFiles.push(
101
99
  {
102
100
  paths: routeConfigWatchPaths,
103
- type: 'reload-server',
101
+ // Keep the current compiler alive while a route config is temporarily
102
+ // invalid. The topology watcher touches the dedicated restart marker
103
+ // once a valid edit actually changes the route graph.
104
+ type: 'reload-page',
104
105
  },
105
106
  {
106
107
  paths: routeRestartMarkerPath,
@@ -112,38 +113,18 @@ export const createReactRouterRouteWatchFiles = ({
112
113
  return watchFiles;
113
114
  };
114
115
 
115
- const closeAll = async (
116
- message: string,
117
- closers: Array<() => Promise<void>>
118
- ): Promise<void> => {
119
- const results = await Promise.allSettled(closers.map(closer => closer()));
120
- const errors = results
121
- .filter(
122
- (result): result is PromiseRejectedResult => result.status === 'rejected'
123
- )
124
- .map(result => normalizeEffectError(result.reason));
125
-
126
- if (errors.length === 1) {
127
- throw errors[0];
128
- }
129
- if (errors.length > 1) {
130
- throw new AggregateError(errors, message);
131
- }
132
- };
133
-
134
- export const registerReactRouterDevBackgroundResources = ({
116
+ export const registerReactRouterDevBackgroundResources = async ({
135
117
  api,
118
+ runtime,
136
119
  isBuild,
137
120
  lazyCompilationPrewarm,
138
- routeTransformExecutor,
139
121
  routeRestartMarkerPath,
140
122
  watchDirectory,
141
123
  getRouteTopology,
142
124
  initialRouteTopology,
143
125
  onRouteTopologyChange,
144
- }: RegisterReactRouterDevBackgroundResourcesOptions): ReactRouterDevBackgroundResources => {
145
- let closeActiveRouteTopologyWatcher: (() => Promise<void>) | undefined;
146
- let routeTopologyWatcherClosed = false;
126
+ }: RegisterReactRouterDevBackgroundResourcesOptions): Promise<ReactRouterDevBackgroundResources> => {
127
+ let routeTopologyWatcherStarted = false;
147
128
 
148
129
  const reportRouteTopologyWatcherError = (error: unknown): void => {
149
130
  api.logger.warn(
@@ -152,54 +133,49 @@ export const registerReactRouterDevBackgroundResources = ({
152
133
  };
153
134
 
154
135
  const routeTopologyWatcherTask = createDelayedPluginTask({
136
+ runtime,
155
137
  delayMs: DEV_BACKGROUND_STARTUP_DELAY_MS,
156
138
  run: () =>
157
- Effect.gen(function* () {
158
- yield* tryPluginPromise(() =>
159
- ensureDevRestartMarker(routeRestartMarkerPath)
160
- );
161
- const closeWatcher = yield* tryPluginPromise(() =>
162
- createRouteTopologyWatcher({
163
- watchDirectory,
164
- getRouteTopology,
165
- initialRouteTopology,
166
- restartMarkerPath: routeRestartMarkerPath,
167
- onRouteTopologyChange,
168
- onError: reportRouteTopologyWatcherError,
169
- })
170
- );
171
- if (routeTopologyWatcherClosed) {
172
- yield* tryPluginPromise(() => closeWatcher());
173
- return;
174
- }
175
- closeActiveRouteTopologyWatcher = closeWatcher;
176
- }),
139
+ acquireRouteTopologyWatcher({
140
+ runtime,
141
+ watchDirectory,
142
+ getRouteTopology,
143
+ initialRouteTopology,
144
+ restartMarkerPath: routeRestartMarkerPath,
145
+ onRouteTopologyChange,
146
+ onError: reportRouteTopologyWatcherError,
147
+ }).pipe(
148
+ Effect.tap(() =>
149
+ Effect.sync(() => (routeTopologyWatcherStarted = true))
150
+ ),
151
+ Effect.asVoid
152
+ ),
177
153
  onError: reportRouteTopologyWatcherError,
178
154
  });
179
155
 
180
- const scheduleRouteTopologyWatcher = (): void => {
181
- if (routeTopologyWatcherClosed || closeActiveRouteTopologyWatcher) {
182
- return;
183
- }
184
- routeTopologyWatcherTask.schedule();
185
- };
186
-
187
156
  const lazyCompilationPrewarmConfig = normalizeLazyCompilationPrewarmOptions(
188
157
  lazyCompilationPrewarm
189
158
  );
190
159
  const lazyCompilationPrewarmController = lazyCompilationPrewarmConfig
191
- ? createLazyCompilationPrewarmController({
192
- config: lazyCompilationPrewarmConfig,
193
- onError: error =>
194
- api.logger.warn(
195
- `[${PLUGIN_NAME}] Lazy compilation prewarm skipped: ${error.message}`
196
- ),
197
- })
160
+ ? await runtime.runPromise(
161
+ acquireLazyCompilationPrewarm({
162
+ runtime,
163
+ config: lazyCompilationPrewarmConfig,
164
+ onError: error =>
165
+ api.logger.warn(
166
+ `[${PLUGIN_NAME}] Lazy compilation prewarm skipped: ${error.message}`
167
+ ),
168
+ })
169
+ )
198
170
  : null;
199
171
 
200
172
  if (!isBuild) {
201
- api.onBeforeStartDevServer(() => {
202
- routeTopologyWatcherClosed = false;
173
+ await runtime.runPromise(
174
+ Effect.acquireRelease(Effect.void, routeTopologyWatcherTask.cancelEffect)
175
+ );
176
+
177
+ api.onBeforeStartDevServer(async () => {
178
+ await ensureDevRestartMarker(routeRestartMarkerPath);
203
179
  });
204
180
 
205
181
  api.onAfterStartDevServer(({ port }) => {
@@ -210,43 +186,13 @@ export const registerReactRouterDevBackgroundResources = ({
210
186
  });
211
187
 
212
188
  api.onAfterDevCompile(() => {
213
- scheduleRouteTopologyWatcher();
189
+ if (!routeTopologyWatcherStarted) {
190
+ routeTopologyWatcherTask.schedule();
191
+ }
214
192
  lazyCompilationPrewarmController?.schedule();
215
193
  });
216
-
217
- // Spawn transform workers now so thread startup overlaps Rsbuild's own
218
- // compiler creation instead of delaying the first route transform.
219
- routeTransformExecutor.prewarm();
220
194
  }
221
195
 
222
- const closeRouteTopologyWatcher = async (): Promise<void> => {
223
- routeTopologyWatcherClosed = true;
224
- await runPluginEffect(routeTopologyWatcherTask.cancelEffect());
225
- await closeActiveRouteTopologyWatcher?.();
226
- closeActiveRouteTopologyWatcher = undefined;
227
- };
228
-
229
- const closeLazyCompilationPrewarm = async (): Promise<void> => {
230
- await runPluginEffect(
231
- lazyCompilationPrewarmController?.cancelEffect() ?? Effect.void
232
- );
233
- };
234
-
235
- const closeRouteTransformExecutor = (): Promise<void> =>
236
- routeTransformExecutor.close();
237
-
238
- api.onCloseDevServer(() =>
239
- closeAll(
240
- '[rsbuild-plugin-react-router] Failed to close dev server resources.',
241
- [
242
- closeRouteTopologyWatcher,
243
- closeLazyCompilationPrewarm,
244
- closeRouteTransformExecutor,
245
- ]
246
- )
247
- );
248
- api.onCloseBuild(closeRouteTransformExecutor);
249
-
250
196
  return {
251
197
  setManifest(manifest) {
252
198
  lazyCompilationPrewarmController?.setManifest(manifest);