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.
- package/README.md +88 -35
- package/dist/451.js +164 -30
- package/dist/build-manifest.d.ts +6 -3
- package/dist/build-output-transforms.d.ts +2 -1
- package/dist/concurrency.d.ts +2 -1
- package/dist/config-imports.d.ts +7 -0
- package/dist/dev-background-resources.d.ts +38 -0
- package/dist/dev-generation.d.ts +2 -2
- package/dist/dev-hmr.d.ts +45 -0
- package/dist/dev-runtime-artifacts.d.ts +9 -1
- package/dist/dev-runtime-compilation.d.ts +17 -2
- package/dist/dev-runtime-controller.d.ts +6 -1
- package/dist/dev-server.d.ts +5 -0
- package/dist/effect-runtime.d.ts +18 -0
- package/dist/export-utils.d.ts +2 -2
- package/dist/index.cjs +11852 -848
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8289 -773
- package/dist/lazy-compilation-prewarm.d.ts +25 -0
- package/dist/lazy-compilation.d.ts +2 -1
- package/dist/manifest.d.ts +19 -4
- package/dist/parallel-route-transforms.d.ts +17 -2
- package/dist/prerender-build.d.ts +4 -0
- package/dist/prerender.d.ts +0 -1
- package/dist/react-router-config.d.ts +8 -5
- package/dist/route-artifacts.d.ts +10 -2
- package/dist/route-transform-tasks.d.ts +3 -0
- package/dist/server-build-resolution.d.ts +3 -0
- package/dist/ssr-externals.d.ts +1 -0
- package/dist/typegen.d.ts +14 -1
- package/dist/types.d.ts +15 -6
- package/package.json +18 -10
- package/src/build-manifest.ts +110 -73
- package/src/build-output-transforms.ts +5 -0
- package/src/concurrency.ts +3 -22
- package/src/config-imports.ts +38 -0
- package/src/dev-background-resources.ts +255 -0
- package/src/dev-generation.ts +43 -53
- package/src/dev-hmr.ts +431 -0
- package/src/dev-runtime-artifacts.ts +50 -18
- package/src/dev-runtime-compilation.ts +80 -1
- package/src/dev-runtime-controller.ts +155 -31
- package/src/dev-runtime-session.ts +18 -11
- package/src/dev-server.ts +31 -1
- package/src/effect-runtime.ts +130 -0
- package/src/export-utils.ts +82 -23
- package/src/index.ts +166 -153
- package/src/lazy-compilation-prewarm.ts +279 -0
- package/src/lazy-compilation.ts +12 -5
- package/src/manifest.ts +366 -255
- package/src/modify-browser-manifest.ts +2 -1
- package/src/parallel-route-transforms.ts +195 -69
- package/src/prerender-build.ts +147 -63
- package/src/prerender.ts +1 -19
- package/src/react-router-config.ts +98 -73
- package/src/route-artifacts.ts +122 -3
- package/src/route-export-resolution.ts +3 -3
- package/src/route-transform-tasks.ts +173 -2
- package/src/route-watch.ts +119 -84
- package/src/server-build-resolution.ts +131 -0
- package/src/server-utils.ts +7 -106
- package/src/ssr-externals.ts +1 -1
- package/src/typegen.ts +162 -33
- package/src/types.ts +16 -6
package/src/dev-hmr.ts
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import type { Rspack } from '@rsbuild/core';
|
|
4
|
+
import { dirname, join } from 'pathe';
|
|
5
|
+
|
|
6
|
+
import { HMR_PATCHABLE_ROUTE_FLAGS } from './route-artifacts.js';
|
|
7
|
+
|
|
8
|
+
export const DEV_HMR_RUNTIME_MODULE_ID = 'virtual/react-router/hmr-runtime';
|
|
9
|
+
|
|
10
|
+
type SwcLoaderOptions = {
|
|
11
|
+
jsc?: { transform?: { react?: { refresh?: boolean } } };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
15
|
+
value !== null && typeof value === 'object';
|
|
16
|
+
|
|
17
|
+
const isSwcLoader = (loader: unknown): boolean =>
|
|
18
|
+
typeof loader === 'string' && loader.includes('builtin:swc-loader');
|
|
19
|
+
|
|
20
|
+
const hasReactRefresh = (options: unknown): boolean =>
|
|
21
|
+
(options as SwcLoaderOptions | undefined)?.jsc?.transform?.react?.refresh ===
|
|
22
|
+
true;
|
|
23
|
+
|
|
24
|
+
const readSwcLoaderRefresh = (value: unknown): boolean => {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.some(readSwcLoaderRefresh);
|
|
27
|
+
}
|
|
28
|
+
if (!isObject(value)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (isSwcLoader(value.loader)) {
|
|
32
|
+
return hasReactRefresh(value.options);
|
|
33
|
+
}
|
|
34
|
+
return Object.values(value).some(readSwcLoaderRefresh);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const readRuleSwcRefresh = (rule: unknown): boolean => {
|
|
38
|
+
if (!isObject(rule)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (isSwcLoader(rule.loader)) {
|
|
42
|
+
return hasReactRefresh(rule.options);
|
|
43
|
+
}
|
|
44
|
+
return (
|
|
45
|
+
readSwcLoaderRefresh(rule.use) ||
|
|
46
|
+
readRuleSetSwcRefresh(rule.oneOf) ||
|
|
47
|
+
readRuleSetSwcRefresh(rule.rules)
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const readRuleSetSwcRefresh = (rules: unknown): boolean =>
|
|
52
|
+
Array.isArray(rules) && rules.some(readRuleSwcRefresh);
|
|
53
|
+
|
|
54
|
+
export const isRspackSwcReactRefreshEnabled = (
|
|
55
|
+
rspackConfig: Rspack.Configuration
|
|
56
|
+
): boolean => readRuleSetSwcRefresh(rspackConfig.module?.rules);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the `react-refresh/runtime` module that
|
|
60
|
+
* `@rspack/plugin-react-refresh` injects into the web bundle. The resolution
|
|
61
|
+
* walks the same dependency chain the refresh plugin uses so the returned file
|
|
62
|
+
* is the exact runtime instance already present in the browser module graph.
|
|
63
|
+
* Returns `undefined` when React Fast Refresh is unavailable, in which case
|
|
64
|
+
* dev HMR falls back to full reloads.
|
|
65
|
+
*/
|
|
66
|
+
export const resolveReactRefreshRuntimePath = (
|
|
67
|
+
rootPath: string
|
|
68
|
+
): string | undefined => {
|
|
69
|
+
const resolveFrom = (base: string, request: string): string =>
|
|
70
|
+
createRequire(base).resolve(request);
|
|
71
|
+
const rootPackageJson = join(rootPath, 'package.json');
|
|
72
|
+
try {
|
|
73
|
+
const pluginReactEntry = resolveFrom(
|
|
74
|
+
rootPackageJson,
|
|
75
|
+
'@rsbuild/plugin-react'
|
|
76
|
+
);
|
|
77
|
+
const refreshPluginEntry = resolveFrom(
|
|
78
|
+
pluginReactEntry,
|
|
79
|
+
'@rspack/plugin-react-refresh'
|
|
80
|
+
);
|
|
81
|
+
return resolveFrom(refreshPluginEntry, 'react-refresh/runtime');
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const hdrRevisionModuleContent = (revision: number): string =>
|
|
88
|
+
`export default ${revision};\n`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The HDR revision module is a real file (not a virtual module) because it
|
|
92
|
+
* must wake the web compiler through the regular file watcher: the browser
|
|
93
|
+
* HMR runtime imports it, so bumping the revision produces a web hot update
|
|
94
|
+
* whenever server code changes, which the client answers by revalidating
|
|
95
|
+
* React Router loader data.
|
|
96
|
+
*/
|
|
97
|
+
export const DEV_HDR_REVISION_RELATIVE_PATH = '.react-router/hdr-revision.mjs';
|
|
98
|
+
|
|
99
|
+
export const getDevHdrRevisionFilePath = (rootPath: string): string =>
|
|
100
|
+
join(rootPath, DEV_HDR_REVISION_RELATIVE_PATH);
|
|
101
|
+
|
|
102
|
+
export type DevHdrRevisionSignal = {
|
|
103
|
+
/** Writes the initial revision module so the first compile can resolve it. */
|
|
104
|
+
ensure: () => void;
|
|
105
|
+
/** Increments the revision, signaling hot data revalidation to the client. */
|
|
106
|
+
bump: () => void;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const createDevHdrRevisionSignal = ({
|
|
110
|
+
filePath,
|
|
111
|
+
onError,
|
|
112
|
+
}: {
|
|
113
|
+
filePath: string;
|
|
114
|
+
onError?: (error: Error) => void;
|
|
115
|
+
}): DevHdrRevisionSignal => {
|
|
116
|
+
let revision = 0;
|
|
117
|
+
let dirEnsured = false;
|
|
118
|
+
const write = (): void => {
|
|
119
|
+
try {
|
|
120
|
+
if (!dirEnsured) {
|
|
121
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
122
|
+
dirEnsured = true;
|
|
123
|
+
}
|
|
124
|
+
writeFileSync(filePath, hdrRevisionModuleContent(revision));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
ensure: write,
|
|
131
|
+
bump() {
|
|
132
|
+
revision += 1;
|
|
133
|
+
write();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Browser-side HMR runtime shared by all route client entries in development.
|
|
140
|
+
*
|
|
141
|
+
* This mirrors React Router's Vite HMR contract (see `refresh-utils.mjs` in
|
|
142
|
+
* `@react-router/dev`): route module updates are applied by patching
|
|
143
|
+
* `window.__reactRouterRouteModules` while preserving the previous component
|
|
144
|
+
* identities (React Fast Refresh swaps their implementations in place),
|
|
145
|
+
* recreating the client routes with revalidation opt-out, revalidating loader
|
|
146
|
+
* data, and finally performing a React refresh.
|
|
147
|
+
*/
|
|
148
|
+
export const generateDevHmrRuntimeModule = ({
|
|
149
|
+
reactRefreshRuntimePath,
|
|
150
|
+
hdrRevisionFilePath,
|
|
151
|
+
}: {
|
|
152
|
+
reactRefreshRuntimePath: string;
|
|
153
|
+
hdrRevisionFilePath: string;
|
|
154
|
+
}): string => `
|
|
155
|
+
import * as __refreshRuntimeModule from ${JSON.stringify(reactRefreshRuntimePath)};
|
|
156
|
+
// Read revision so the import survives sideEffects: false tree-shaking.
|
|
157
|
+
import __hdrRevision from ${JSON.stringify(hdrRevisionFilePath)};
|
|
158
|
+
|
|
159
|
+
void __hdrRevision;
|
|
160
|
+
|
|
161
|
+
const RefreshRuntime =
|
|
162
|
+
__refreshRuntimeModule && __refreshRuntimeModule.performReactRefresh
|
|
163
|
+
? __refreshRuntimeModule
|
|
164
|
+
: __refreshRuntimeModule.default;
|
|
165
|
+
|
|
166
|
+
const pendingRouteUpdates = new Map();
|
|
167
|
+
let flushTimeout;
|
|
168
|
+
let pendingRevalidation = false;
|
|
169
|
+
|
|
170
|
+
function getCurrentRouterPath(router) {
|
|
171
|
+
const basename = router.basename || '/';
|
|
172
|
+
let pathname = window.location.pathname;
|
|
173
|
+
if (basename !== '/' && pathname.startsWith(basename)) {
|
|
174
|
+
pathname = pathname.slice(basename.length) || '/';
|
|
175
|
+
// A trailing-slash basename (e.g. "/mybase/") consumes the leading slash,
|
|
176
|
+
// leaving a relative path that react-router resolves against the current
|
|
177
|
+
// location and doubles. Force it back to absolute.
|
|
178
|
+
if (pathname[0] !== '/') pathname = '/' + pathname;
|
|
179
|
+
}
|
|
180
|
+
return pathname + window.location.search + window.location.hash;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function registerReactRouterRouteExports(routeId, moduleExports) {
|
|
184
|
+
if (
|
|
185
|
+
typeof window === 'undefined' ||
|
|
186
|
+
!RefreshRuntime ||
|
|
187
|
+
typeof RefreshRuntime.register !== 'function'
|
|
188
|
+
) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const key in moduleExports) {
|
|
192
|
+
if (key === '__esModule') continue;
|
|
193
|
+
const exportValue = moduleExports[key];
|
|
194
|
+
if (RefreshRuntime.isLikelyComponentType(exportValue)) {
|
|
195
|
+
RefreshRuntime.register(exportValue, routeId + ' export ' + key);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function scheduleReactRouterRouteUpdate(
|
|
201
|
+
routeId,
|
|
202
|
+
routeFlags,
|
|
203
|
+
getRouteModuleExports
|
|
204
|
+
) {
|
|
205
|
+
pendingRouteUpdates.set(routeId, { routeFlags, getRouteModuleExports });
|
|
206
|
+
scheduleFlush();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function scheduleReactRouterRevalidation() {
|
|
210
|
+
pendingRevalidation = true;
|
|
211
|
+
scheduleFlush();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function scheduleFlush() {
|
|
215
|
+
if (typeof window === 'undefined') {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
clearTimeout(flushTimeout);
|
|
219
|
+
flushTimeout = setTimeout(flush, 16);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function takePendingRouteUpdates() {
|
|
223
|
+
const updates = Array.from(pendingRouteUpdates, ([routeId, update]) => ({
|
|
224
|
+
routeId,
|
|
225
|
+
update,
|
|
226
|
+
}));
|
|
227
|
+
pendingRouteUpdates.clear();
|
|
228
|
+
return updates;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getRouteMetadata(routeFlags) {
|
|
232
|
+
return {
|
|
233
|
+
${HMR_PATCHABLE_ROUTE_FLAGS.map(
|
|
234
|
+
(flag, index) => ` ${flag}: Boolean(routeFlags & ${1 << index}),`
|
|
235
|
+
).join('\n')}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function applyRouteModuleUpdate(routeId, update, routeEntry, routeModules) {
|
|
240
|
+
Object.assign(routeEntry, getRouteMetadata(update.routeFlags));
|
|
241
|
+
const imported = update.getRouteModuleExports();
|
|
242
|
+
registerReactRouterRouteExports(routeId, imported);
|
|
243
|
+
const current = routeModules[routeId];
|
|
244
|
+
const preserveIdentity = key =>
|
|
245
|
+
imported[key] ? (current && current[key]) || imported[key] : imported[key];
|
|
246
|
+
routeModules[routeId] = {
|
|
247
|
+
...imported,
|
|
248
|
+
default: preserveIdentity('default'),
|
|
249
|
+
ErrorBoundary: preserveIdentity('ErrorBoundary'),
|
|
250
|
+
HydrateFallback: preserveIdentity('HydrateFallback'),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getRouteById(routes, routeId) {
|
|
255
|
+
for (const route of routes) {
|
|
256
|
+
if (route.id === routeId) {
|
|
257
|
+
return route;
|
|
258
|
+
}
|
|
259
|
+
if (route.children) {
|
|
260
|
+
const child = getRouteById(route.children, routeId);
|
|
261
|
+
if (child) {
|
|
262
|
+
return child;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Deliberate coupling to React Router's private dev API: patching the live
|
|
269
|
+
// match objects is the only way to swap route implementations without a
|
|
270
|
+
// navigation. The typeof guard below degrades to a no-op if RR removes it.
|
|
271
|
+
function patchCurrentRouteMatches(router, routes) {
|
|
272
|
+
if (
|
|
273
|
+
!router.state ||
|
|
274
|
+
!Array.isArray(router.state.matches) ||
|
|
275
|
+
typeof router._internalSetStateDoNotUseOrYouWillBreakYourApp !== 'function'
|
|
276
|
+
) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let changed = false;
|
|
281
|
+
const matches = router.state.matches.map(match => {
|
|
282
|
+
const route = getRouteById(routes, match.route.id);
|
|
283
|
+
if (!route || route === match.route) {
|
|
284
|
+
return match;
|
|
285
|
+
}
|
|
286
|
+
changed = true;
|
|
287
|
+
return { ...match, route };
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (changed) {
|
|
291
|
+
router._internalSetStateDoNotUseOrYouWillBreakYourApp({ matches });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function applyPendingRouteUpdates(router, routeModules, manifest, context) {
|
|
296
|
+
if (pendingRouteUpdates.size === 0) {
|
|
297
|
+
return {
|
|
298
|
+
nextManifest: undefined,
|
|
299
|
+
shouldRefreshRouteState: false,
|
|
300
|
+
routesToRevalidate: new Set(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Clone only entries mutated before the manifest is committed in flush().
|
|
305
|
+
const nextManifest = { ...manifest, routes: { ...manifest.routes } };
|
|
306
|
+
const routesToRevalidate = new Set();
|
|
307
|
+
let shouldRefreshRouteState = false;
|
|
308
|
+
for (const { routeId, update } of takePendingRouteUpdates()) {
|
|
309
|
+
const existingEntry = nextManifest.routes[routeId];
|
|
310
|
+
if (!existingEntry) continue;
|
|
311
|
+
|
|
312
|
+
// Shallow clone is enough: only top-level flags are mutated below.
|
|
313
|
+
const routeEntry = { ...existingEntry };
|
|
314
|
+
nextManifest.routes[routeId] = routeEntry;
|
|
315
|
+
applyRouteModuleUpdate(routeId, update, routeEntry, routeModules);
|
|
316
|
+
if (
|
|
317
|
+
routeEntry.hasLoader ||
|
|
318
|
+
routeEntry.hasClientLoader ||
|
|
319
|
+
routeEntry.hasClientMiddleware
|
|
320
|
+
) {
|
|
321
|
+
routesToRevalidate.add(routeId);
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
existingEntry.hasLoader ||
|
|
325
|
+
existingEntry.hasClientLoader ||
|
|
326
|
+
existingEntry.hasClientMiddleware ||
|
|
327
|
+
routeEntry.hasLoader ||
|
|
328
|
+
routeEntry.hasClientLoader ||
|
|
329
|
+
routeEntry.hasClientMiddleware
|
|
330
|
+
) {
|
|
331
|
+
shouldRefreshRouteState = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (
|
|
336
|
+
typeof router.createRoutesForHMR === 'function' &&
|
|
337
|
+
typeof router._internalSetRoutes === 'function'
|
|
338
|
+
) {
|
|
339
|
+
const routes = router.createRoutesForHMR(
|
|
340
|
+
routesToRevalidate,
|
|
341
|
+
nextManifest.routes,
|
|
342
|
+
routeModules,
|
|
343
|
+
context.ssr,
|
|
344
|
+
context.isSpaMode
|
|
345
|
+
);
|
|
346
|
+
router._internalSetRoutes(routes);
|
|
347
|
+
patchCurrentRouteMatches(router, routes);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { nextManifest, shouldRefreshRouteState, routesToRevalidate };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function withHdrActive(fn) {
|
|
354
|
+
try {
|
|
355
|
+
window.__reactRouterHdrActive = true;
|
|
356
|
+
await fn();
|
|
357
|
+
} finally {
|
|
358
|
+
window.__reactRouterHdrActive = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function revalidateRouter(router) {
|
|
363
|
+
if (typeof router.revalidate === 'function') {
|
|
364
|
+
await withHdrActive(() => router.revalidate());
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (typeof router.navigate === 'function') {
|
|
368
|
+
await withHdrActive(() =>
|
|
369
|
+
router.navigate(getCurrentRouterPath(router), {
|
|
370
|
+
replace: true,
|
|
371
|
+
preventScrollReset: true,
|
|
372
|
+
})
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function refreshRouteState(router) {
|
|
378
|
+
if (typeof router.revalidate === 'function') {
|
|
379
|
+
await withHdrActive(() => router.revalidate());
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function performReactRefresh() {
|
|
386
|
+
if (
|
|
387
|
+
RefreshRuntime &&
|
|
388
|
+
typeof RefreshRuntime.performReactRefresh === 'function'
|
|
389
|
+
) {
|
|
390
|
+
RefreshRuntime.performReactRefresh();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function flush() {
|
|
395
|
+
const router = window.__reactRouterDataRouter;
|
|
396
|
+
const routeModules = window.__reactRouterRouteModules;
|
|
397
|
+
const manifest = window.__reactRouterManifest;
|
|
398
|
+
const context = window.__reactRouterContext;
|
|
399
|
+
if (!router || !routeModules || !manifest || !context) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let shouldRevalidate = pendingRevalidation;
|
|
404
|
+
pendingRevalidation = false;
|
|
405
|
+
const { nextManifest, shouldRefreshRouteState, routesToRevalidate } =
|
|
406
|
+
applyPendingRouteUpdates(router, routeModules, manifest, context);
|
|
407
|
+
if (nextManifest) {
|
|
408
|
+
Object.assign(manifest, nextManifest);
|
|
409
|
+
}
|
|
410
|
+
// Component-only updates do not need a full loader revalidation.
|
|
411
|
+
if (
|
|
412
|
+
shouldRefreshRouteState &&
|
|
413
|
+
(routesToRevalidate.size > 0 || shouldRevalidate)
|
|
414
|
+
) {
|
|
415
|
+
if (await refreshRouteState(router)) {
|
|
416
|
+
shouldRevalidate = false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (shouldRevalidate) {
|
|
420
|
+
await revalidateRouter(router);
|
|
421
|
+
}
|
|
422
|
+
performReactRefresh();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (typeof window !== 'undefined' && import.meta.webpackHot) {
|
|
426
|
+
import.meta.webpackHot.accept(
|
|
427
|
+
${JSON.stringify(hdrRevisionFilePath)},
|
|
428
|
+
scheduleReactRouterRevalidation
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, relative } from 'node:path';
|
|
2
2
|
import type { RsbuildDevServer, Rspack } from '@rsbuild/core';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
3
4
|
import type { ServerBuild } from 'react-router';
|
|
4
5
|
import type { ReactRouterManifestForDev } from './manifest.js';
|
|
5
|
-
import {
|
|
6
|
+
import { getCappedPluginConcurrency } from './concurrency.js';
|
|
7
|
+
import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
|
|
8
|
+
import { resolveServerBuildModuleEffect } from './server-build-resolution.js';
|
|
6
9
|
|
|
7
10
|
export type ReactRouterDevManifest = ReactRouterManifestForDev;
|
|
8
11
|
|
|
@@ -17,6 +20,13 @@ export type ReactRouterDevManifestSet = Readonly<
|
|
|
17
20
|
|
|
18
21
|
export type ReactRouterServerBuilds = Readonly<Record<string, ServerBuild>>;
|
|
19
22
|
|
|
23
|
+
export type PairedDevStats = {
|
|
24
|
+
web: Rspack.Stats;
|
|
25
|
+
node: Rspack.Stats;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type DevRuntimeStats = Rspack.Stats | Rspack.MultiStats | PairedDevStats;
|
|
29
|
+
|
|
20
30
|
export type DependencySnapshot = {
|
|
21
31
|
files: ReadonlySet<string>;
|
|
22
32
|
contexts: ReadonlySet<string>;
|
|
@@ -34,11 +44,13 @@ export type DevGraphChanges = {
|
|
|
34
44
|
};
|
|
35
45
|
|
|
36
46
|
export type DevCompilationIdentity = symbol;
|
|
47
|
+
export type DevCompileAttemptIdentity = symbol;
|
|
37
48
|
|
|
38
49
|
export type DevGraphIdentity = {
|
|
39
50
|
web: DevCompilationIdentity | undefined;
|
|
40
51
|
node: DevCompilationIdentity | undefined;
|
|
41
52
|
nodeWeb: DevCompilationIdentity | undefined;
|
|
53
|
+
attempt: DevCompileAttemptIdentity | undefined;
|
|
42
54
|
};
|
|
43
55
|
|
|
44
56
|
export type WebArtifact = {
|
|
@@ -102,10 +114,17 @@ export const isSafeOneSidedChange = (
|
|
|
102
114
|
return true;
|
|
103
115
|
};
|
|
104
116
|
|
|
117
|
+
export const isPairedDevStats = (
|
|
118
|
+
stats: DevRuntimeStats
|
|
119
|
+
): stats is PairedDevStats => 'web' in stats && 'node' in stats;
|
|
120
|
+
|
|
105
121
|
export const getEnvironmentStats = (
|
|
106
|
-
stats:
|
|
122
|
+
stats: DevRuntimeStats,
|
|
107
123
|
name: 'web' | 'node'
|
|
108
124
|
): Rspack.Stats | undefined => {
|
|
125
|
+
if (isPairedDevStats(stats)) {
|
|
126
|
+
return stats[name];
|
|
127
|
+
}
|
|
109
128
|
const children = Array.isArray((stats as Rspack.MultiStats).stats)
|
|
110
129
|
? (stats as Rspack.MultiStats).stats
|
|
111
130
|
: [stats as Rspack.Stats];
|
|
@@ -115,29 +134,42 @@ export const getEnvironmentStats = (
|
|
|
115
134
|
});
|
|
116
135
|
};
|
|
117
136
|
|
|
118
|
-
const
|
|
137
|
+
const startServerBuildEvaluationEffect = (
|
|
119
138
|
server: RsbuildDevServer,
|
|
120
139
|
entryName: string
|
|
121
|
-
):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
): Effect.Effect<ServerBuild, Error, never> =>
|
|
141
|
+
tryPluginPromise(() => server.environments.node.loadBundle(entryName)).pipe(
|
|
142
|
+
Effect.flatMap(buildModule =>
|
|
143
|
+
resolveServerBuildModuleEffect(
|
|
144
|
+
buildModule,
|
|
145
|
+
`Server entry ${JSON.stringify(entryName)}`
|
|
146
|
+
)
|
|
147
|
+
)
|
|
126
148
|
);
|
|
127
|
-
};
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
const evaluateServerBuildsEffect = (
|
|
130
151
|
server: RsbuildDevServer,
|
|
131
152
|
entryNames: readonly string[]
|
|
132
|
-
):
|
|
133
|
-
|
|
134
|
-
entryNames.map(
|
|
135
|
-
entryName
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
): Effect.Effect<ReactRouterServerBuilds, Error, never> =>
|
|
154
|
+
Effect.forEach(
|
|
155
|
+
entryNames.map(entryName =>
|
|
156
|
+
startServerBuildEvaluationEffect(server, entryName).pipe(
|
|
157
|
+
Effect.map(build => [entryName, build] as const)
|
|
158
|
+
)
|
|
159
|
+
),
|
|
160
|
+
evaluation => evaluation,
|
|
161
|
+
{ concurrency: getCappedPluginConcurrency() }
|
|
162
|
+
).pipe(
|
|
163
|
+
Effect.map(
|
|
164
|
+
evaluated => Object.fromEntries(evaluated) as Record<string, ServerBuild>
|
|
165
|
+
)
|
|
138
166
|
);
|
|
139
|
-
|
|
140
|
-
|
|
167
|
+
|
|
168
|
+
export const evaluateServerBuilds = (
|
|
169
|
+
server: RsbuildDevServer,
|
|
170
|
+
entryNames: readonly string[]
|
|
171
|
+
): Promise<ReactRouterServerBuilds> =>
|
|
172
|
+
runPluginEffect(evaluateServerBuildsEffect(server, entryNames));
|
|
141
173
|
|
|
142
174
|
const assertBuildMatchesManifest = (
|
|
143
175
|
entryName: string,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Rspack } from '@rsbuild/core';
|
|
2
2
|
import type {
|
|
3
|
+
DevCompileAttemptIdentity,
|
|
3
4
|
DevCompilationIdentity,
|
|
5
|
+
DevRuntimeStats,
|
|
4
6
|
DevGraphChanges,
|
|
5
7
|
DevGraphIdentity,
|
|
6
8
|
} from './dev-runtime-artifacts.js';
|
|
@@ -10,13 +12,16 @@ export type DevCompilerPair = {
|
|
|
10
12
|
node: Rspack.Compiler;
|
|
11
13
|
settledCompilations: WeakSet<Rspack.Compilation>;
|
|
12
14
|
pendingAttempt?: PendingDevCompilation;
|
|
15
|
+
currentAttemptIdentity?: DevCompileAttemptIdentity;
|
|
13
16
|
latestCompletedWebIdentity?: DevCompilationIdentity;
|
|
17
|
+
latestCompletedWebStats?: Rspack.Stats;
|
|
18
|
+
latestCompletedNodeStats?: Rspack.Stats;
|
|
14
19
|
latestWebStart?: CompilationStart;
|
|
15
20
|
latestNodeStart?: CompilationStart;
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
export type PendingDevCompilation = {
|
|
19
|
-
stats:
|
|
24
|
+
stats: DevRuntimeStats;
|
|
20
25
|
changes: DevGraphChanges;
|
|
21
26
|
identity: DevGraphIdentity;
|
|
22
27
|
webCompilation: Rspack.Compilation;
|
|
@@ -27,6 +32,30 @@ export type CompilationStart =
|
|
|
27
32
|
| { status: 'pending' }
|
|
28
33
|
| { status: 'started'; identity: DevCompilationIdentity };
|
|
29
34
|
|
|
35
|
+
type CompilerPairStartSide = 'latestWebStart' | 'latestNodeStart';
|
|
36
|
+
|
|
37
|
+
export const createDevCompilerPair = ({
|
|
38
|
+
web,
|
|
39
|
+
node,
|
|
40
|
+
}: {
|
|
41
|
+
web: Rspack.Compiler;
|
|
42
|
+
node: Rspack.Compiler;
|
|
43
|
+
}): DevCompilerPair => ({
|
|
44
|
+
web,
|
|
45
|
+
node,
|
|
46
|
+
settledCompilations: new WeakSet(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const resetDevCompilerPair = (pair: DevCompilerPair): void => {
|
|
50
|
+
pair.pendingAttempt = undefined;
|
|
51
|
+
pair.currentAttemptIdentity = undefined;
|
|
52
|
+
pair.latestCompletedWebIdentity = undefined;
|
|
53
|
+
pair.latestCompletedWebStats = undefined;
|
|
54
|
+
pair.latestCompletedNodeStats = undefined;
|
|
55
|
+
pair.latestWebStart = undefined;
|
|
56
|
+
pair.latestNodeStart = undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
30
59
|
export const isLatestStartedCompilation = (
|
|
31
60
|
identity: DevCompilationIdentity | undefined,
|
|
32
61
|
start: CompilationStart | undefined
|
|
@@ -37,6 +66,32 @@ export const hasPendingCompilation = (pair: DevCompilerPair): boolean =>
|
|
|
37
66
|
pair.latestWebStart?.status === 'pending' ||
|
|
38
67
|
pair.latestNodeStart?.status === 'pending';
|
|
39
68
|
|
|
69
|
+
export const beginDevCompilerAttempt = (pair: DevCompilerPair): void => {
|
|
70
|
+
pair.pendingAttempt = undefined;
|
|
71
|
+
pair.currentAttemptIdentity = Symbol();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const markDevCompilerPending = (
|
|
75
|
+
pair: DevCompilerPair,
|
|
76
|
+
side: CompilerPairStartSide
|
|
77
|
+
): boolean => {
|
|
78
|
+
const attemptAlreadyPending = hasPendingCompilation(pair);
|
|
79
|
+
pair[side] = { status: 'pending' };
|
|
80
|
+
pair.pendingAttempt = undefined;
|
|
81
|
+
if (!attemptAlreadyPending) {
|
|
82
|
+
pair.currentAttemptIdentity = Symbol();
|
|
83
|
+
}
|
|
84
|
+
return !attemptAlreadyPending;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const clearDevCompilerStart = (
|
|
88
|
+
pair: DevCompilerPair,
|
|
89
|
+
side: CompilerPairStartSide
|
|
90
|
+
): void => {
|
|
91
|
+
pair[side] = undefined;
|
|
92
|
+
pair.pendingAttempt = undefined;
|
|
93
|
+
};
|
|
94
|
+
|
|
40
95
|
export type CompilationIdentityTracker = {
|
|
41
96
|
getCompilationIdentity(
|
|
42
97
|
compilation: Rspack.Compilation
|
|
@@ -44,6 +99,13 @@ export type CompilationIdentityTracker = {
|
|
|
44
99
|
getWebIdentityForNodeCompilation(
|
|
45
100
|
compilation: Rspack.Compilation
|
|
46
101
|
): DevCompilationIdentity | undefined;
|
|
102
|
+
getAttemptIdentityForCompilation(
|
|
103
|
+
compilation: Rspack.Compilation
|
|
104
|
+
): DevCompileAttemptIdentity | undefined;
|
|
105
|
+
setAttemptIdentityForCompilation(
|
|
106
|
+
compilation: Rspack.Compilation,
|
|
107
|
+
identity: DevCompileAttemptIdentity
|
|
108
|
+
): void;
|
|
47
109
|
setWebIdentityForNodeCompilation(
|
|
48
110
|
compilation: Rspack.Compilation,
|
|
49
111
|
identity: DevCompilationIdentity
|
|
@@ -60,6 +122,10 @@ export const createCompilationIdentityTracker =
|
|
|
60
122
|
Rspack.Compilation,
|
|
61
123
|
DevCompilationIdentity
|
|
62
124
|
>();
|
|
125
|
+
const attemptIdentityByCompilation = new WeakMap<
|
|
126
|
+
Rspack.Compilation,
|
|
127
|
+
DevCompileAttemptIdentity
|
|
128
|
+
>();
|
|
63
129
|
|
|
64
130
|
return {
|
|
65
131
|
getCompilationIdentity(
|
|
@@ -82,6 +148,19 @@ export const createCompilationIdentityTracker =
|
|
|
82
148
|
return webIdentityByNodeCompilation.get(compilation);
|
|
83
149
|
},
|
|
84
150
|
|
|
151
|
+
getAttemptIdentityForCompilation(
|
|
152
|
+
compilation: Rspack.Compilation
|
|
153
|
+
): DevCompileAttemptIdentity | undefined {
|
|
154
|
+
return attemptIdentityByCompilation.get(compilation);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
setAttemptIdentityForCompilation(
|
|
158
|
+
compilation: Rspack.Compilation,
|
|
159
|
+
identity: DevCompileAttemptIdentity
|
|
160
|
+
): void {
|
|
161
|
+
attemptIdentityByCompilation.set(compilation, identity);
|
|
162
|
+
},
|
|
163
|
+
|
|
85
164
|
setWebIdentityForNodeCompilation(
|
|
86
165
|
compilation: Rspack.Compilation,
|
|
87
166
|
identity: DevCompilationIdentity
|