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
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { RsbuildConfig, RsbuildPluginAPI, Rspack } from '@rsbuild/core';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
2
3
|
import type { ServerBuild } from 'react-router';
|
|
3
4
|
import { PLUGIN_NAME } from './constants.js';
|
|
4
5
|
import {
|
|
6
|
+
beginDevCompilerAttempt,
|
|
7
|
+
clearDevCompilerStart,
|
|
5
8
|
createCompilationIdentityTracker,
|
|
9
|
+
createDevCompilerPair,
|
|
6
10
|
hasPendingCompilation,
|
|
7
11
|
isLatestStartedCompilation,
|
|
12
|
+
markDevCompilerPending,
|
|
13
|
+
resetDevCompilerPair,
|
|
8
14
|
type DevCompilerPair,
|
|
9
15
|
} from './dev-runtime-compilation.js';
|
|
10
16
|
import {
|
|
@@ -16,13 +22,22 @@ import {
|
|
|
16
22
|
import {
|
|
17
23
|
getEnvironmentStats,
|
|
18
24
|
snapshotDevChangedFiles,
|
|
25
|
+
type DevGraphIdentity,
|
|
26
|
+
type DevRuntimeStats,
|
|
19
27
|
type ReactRouterDevBuildPlan,
|
|
20
28
|
type ReactRouterDevManifestSet,
|
|
21
29
|
} from './dev-runtime-artifacts.js';
|
|
30
|
+
import { DEV_HDR_REVISION_RELATIVE_PATH } from './dev-hmr.js';
|
|
22
31
|
import {
|
|
23
32
|
createDevRuntimeSessionManager,
|
|
24
33
|
type RuntimeBinding,
|
|
25
34
|
} from './dev-runtime-session.js';
|
|
35
|
+
import {
|
|
36
|
+
normalizeEffectError,
|
|
37
|
+
runPluginEffect,
|
|
38
|
+
tryPluginPromise,
|
|
39
|
+
tryPluginSync,
|
|
40
|
+
} from './effect-runtime.js';
|
|
26
41
|
|
|
27
42
|
type ServerSetup = Exclude<
|
|
28
43
|
NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
|
|
@@ -41,6 +56,11 @@ type CreateControllerOptions = {
|
|
|
41
56
|
api: RsbuildPluginAPI;
|
|
42
57
|
isBuild: boolean;
|
|
43
58
|
buildPlan: ReactRouterDevBuildPlan;
|
|
59
|
+
/**
|
|
60
|
+
* Invoked after a development attempt commits a re-evaluated node build for
|
|
61
|
+
* changed server files. Used to signal hot data revalidation to the client.
|
|
62
|
+
*/
|
|
63
|
+
onNodeRebuildCommitted?: () => void;
|
|
44
64
|
};
|
|
45
65
|
|
|
46
66
|
const escapeHtml = (value: string): string =>
|
|
@@ -51,10 +71,29 @@ const escapeHtml = (value: string): string =>
|
|
|
51
71
|
|
|
52
72
|
const CSS_SOURCE_RELOAD_DELAY_MS = 1000;
|
|
53
73
|
|
|
74
|
+
const isHdrRevisionFile = (file: string): boolean =>
|
|
75
|
+
file.includes(DEV_HDR_REVISION_RELATIVE_PATH);
|
|
76
|
+
|
|
77
|
+
const isCssSourceFile = (file: string): boolean =>
|
|
78
|
+
/\.css(?:\.[cm]?[jt]s)?$/.test(file);
|
|
79
|
+
|
|
80
|
+
// A change that should bump the HDR revision: anything except the revision
|
|
81
|
+
// file itself (the bump's own echo) and CSS sources (styling never changes
|
|
82
|
+
// loader data).
|
|
83
|
+
const hasHdrTriggeringChange = (files: Iterable<string>): boolean => {
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
if (!isHdrRevisionFile(file) && !isCssSourceFile(file)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
|
|
54
92
|
export const createReactRouterDevRuntimeController = ({
|
|
55
93
|
api,
|
|
56
94
|
isBuild,
|
|
57
95
|
buildPlan,
|
|
96
|
+
onNodeRebuildCommitted,
|
|
58
97
|
}: CreateControllerOptions): ReactRouterDevRuntimeController => {
|
|
59
98
|
if (isBuild) {
|
|
60
99
|
return {
|
|
@@ -107,10 +146,7 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
107
146
|
reloadAfterCssAssetOwnershipRemoval = false;
|
|
108
147
|
const pair = binding.compilers;
|
|
109
148
|
if (pair) {
|
|
110
|
-
pair
|
|
111
|
-
pair.latestCompletedWebIdentity = undefined;
|
|
112
|
-
pair.latestWebStart = undefined;
|
|
113
|
-
pair.latestNodeStart = undefined;
|
|
149
|
+
resetDevCompilerPair(pair);
|
|
114
150
|
}
|
|
115
151
|
binding.compilers = undefined;
|
|
116
152
|
binding.runtime.close(error);
|
|
@@ -121,6 +157,65 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
121
157
|
const compilationIdentities = createCompilationIdentityTracker();
|
|
122
158
|
const { getCompilationIdentity } = compilationIdentities;
|
|
123
159
|
|
|
160
|
+
// Web-only commits reuse the node compiler's stale `modifiedFiles`
|
|
161
|
+
// snapshot, and every HDR bump itself triggers a web rebuild — so signal
|
|
162
|
+
// once per node compilation identity or the bump loop self-sustains.
|
|
163
|
+
const hdrSignaledNodeIdentity = new WeakMap<
|
|
164
|
+
DevCompilerPair,
|
|
165
|
+
NonNullable<DevGraphIdentity['node']>
|
|
166
|
+
>();
|
|
167
|
+
|
|
168
|
+
const finishRuntimeAttemptEffect = (
|
|
169
|
+
binding: RuntimeBinding,
|
|
170
|
+
pair: DevCompilerPair,
|
|
171
|
+
stats: DevRuntimeStats,
|
|
172
|
+
changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
|
|
173
|
+
identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
|
|
174
|
+
): Effect.Effect<void, Error, never> =>
|
|
175
|
+
tryPluginPromise(() =>
|
|
176
|
+
binding.runtime.finishAttempt(stats, changes, identity)
|
|
177
|
+
).pipe(
|
|
178
|
+
Effect.flatMap(result =>
|
|
179
|
+
tryPluginSync(() => {
|
|
180
|
+
if (sessions.getActiveBinding()?.id !== binding.id) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (result === 'retry-node') {
|
|
184
|
+
pair.node.watching?.invalidate();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
result === 'committed' &&
|
|
189
|
+
changes.node.known &&
|
|
190
|
+
identity.node !== undefined &&
|
|
191
|
+
hdrSignaledNodeIdentity.get(pair) !== identity.node &&
|
|
192
|
+
hasHdrTriggeringChange(changes.node.files)
|
|
193
|
+
) {
|
|
194
|
+
hdrSignaledNodeIdentity.set(pair, identity.node);
|
|
195
|
+
onNodeRebuildCommitted?.();
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
),
|
|
199
|
+
Effect.catchAll(cause =>
|
|
200
|
+
tryPluginSync(() => {
|
|
201
|
+
if (sessions.getActiveBinding()?.id === binding.id) {
|
|
202
|
+
binding.runtime.failAttempt(normalizeEffectError(cause));
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const finishRuntimeAttempt = (
|
|
209
|
+
binding: RuntimeBinding,
|
|
210
|
+
pair: DevCompilerPair,
|
|
211
|
+
stats: DevRuntimeStats,
|
|
212
|
+
changes: Parameters<RuntimeBinding['runtime']['finishAttempt']>[1],
|
|
213
|
+
identity: Parameters<RuntimeBinding['runtime']['finishAttempt']>[2]
|
|
214
|
+
): Promise<void> =>
|
|
215
|
+
runPluginEffect(
|
|
216
|
+
finishRuntimeAttemptEffect(binding, pair, stats, changes, identity)
|
|
217
|
+
);
|
|
218
|
+
|
|
124
219
|
const flushSettledAttempt = (
|
|
125
220
|
binding: RuntimeBinding,
|
|
126
221
|
pair: DevCompilerPair
|
|
@@ -141,15 +236,13 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
141
236
|
) {
|
|
142
237
|
return;
|
|
143
238
|
}
|
|
144
|
-
void
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
});
|
|
239
|
+
void finishRuntimeAttempt(
|
|
240
|
+
binding,
|
|
241
|
+
pair,
|
|
242
|
+
pending.stats,
|
|
243
|
+
pending.changes,
|
|
244
|
+
pending.identity
|
|
245
|
+
);
|
|
153
246
|
};
|
|
154
247
|
|
|
155
248
|
const rejectUnsupportedCompiler = (reason: string): void => {
|
|
@@ -247,7 +340,7 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
247
340
|
if (!binding || !pair || hasPendingCompilation(pair)) {
|
|
248
341
|
return;
|
|
249
342
|
}
|
|
250
|
-
pair
|
|
343
|
+
beginDevCompilerAttempt(pair);
|
|
251
344
|
binding.runtime.beginAttempt();
|
|
252
345
|
},
|
|
253
346
|
});
|
|
@@ -267,22 +360,17 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
267
360
|
if (!binding) {
|
|
268
361
|
return;
|
|
269
362
|
}
|
|
270
|
-
const pair: DevCompilerPair = {
|
|
271
|
-
web,
|
|
272
|
-
node,
|
|
273
|
-
settledCompilations: new WeakSet(),
|
|
274
|
-
};
|
|
363
|
+
const pair: DevCompilerPair = createDevCompilerPair({ web, node });
|
|
275
364
|
binding.compilers = pair;
|
|
276
365
|
const sessionId = binding.id;
|
|
277
366
|
const runtime = binding.runtime;
|
|
278
367
|
const failCurrentAttempt = (side: 'web' | 'node', error: Error): void => {
|
|
279
368
|
if (sessions.getActiveBinding()?.id === sessionId) {
|
|
280
369
|
if (side === 'web') {
|
|
281
|
-
pair
|
|
370
|
+
clearDevCompilerStart(pair, 'latestWebStart');
|
|
282
371
|
} else {
|
|
283
|
-
pair
|
|
372
|
+
clearDevCompilerStart(pair, 'latestNodeStart');
|
|
284
373
|
}
|
|
285
|
-
pair.pendingAttempt = undefined;
|
|
286
374
|
runtime.failAttempt(error);
|
|
287
375
|
}
|
|
288
376
|
};
|
|
@@ -293,12 +381,9 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
293
381
|
sessions.getActiveBinding()?.id === sessionId &&
|
|
294
382
|
pair[side]?.status !== 'pending'
|
|
295
383
|
) {
|
|
296
|
-
const attemptAlreadyPending = hasPendingCompilation(pair);
|
|
297
384
|
// Invalidation can arrive before the aggregate before-compile hook.
|
|
298
385
|
// Supersede any evaluation that could resolve in that gap immediately.
|
|
299
|
-
pair
|
|
300
|
-
pair.pendingAttempt = undefined;
|
|
301
|
-
if (!attemptAlreadyPending) {
|
|
386
|
+
if (markDevCompilerPending(pair, side)) {
|
|
302
387
|
runtime.beginAttempt();
|
|
303
388
|
}
|
|
304
389
|
if (side === 'latestWebStart' && reloadAfterCssAssetOwnershipRemoval) {
|
|
@@ -322,12 +407,27 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
322
407
|
pair.latestCompletedWebIdentity = getCompilationIdentity(
|
|
323
408
|
stats.compilation
|
|
324
409
|
);
|
|
410
|
+
pair.latestCompletedWebStats = stats;
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
node.hooks.done.tap(
|
|
414
|
+
{ name: `${PLUGIN_NAME}:dev-node-complete`, stage: -1000 },
|
|
415
|
+
stats => {
|
|
416
|
+
if (sessions.getActiveBinding()?.id === sessionId) {
|
|
417
|
+
pair.latestCompletedNodeStats = stats;
|
|
418
|
+
}
|
|
325
419
|
}
|
|
326
420
|
);
|
|
327
421
|
web.hooks.thisCompilation.tap(
|
|
328
422
|
`${PLUGIN_NAME}:dev-web-compilation`,
|
|
329
423
|
compilation => {
|
|
330
424
|
if (sessions.getActiveBinding()?.id === sessionId) {
|
|
425
|
+
if (pair.currentAttemptIdentity) {
|
|
426
|
+
compilationIdentities.setAttemptIdentityForCompilation(
|
|
427
|
+
compilation,
|
|
428
|
+
pair.currentAttemptIdentity
|
|
429
|
+
);
|
|
430
|
+
}
|
|
331
431
|
pair.latestWebStart = {
|
|
332
432
|
status: 'started',
|
|
333
433
|
identity: getCompilationIdentity(compilation),
|
|
@@ -349,6 +449,12 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
349
449
|
status: 'started',
|
|
350
450
|
identity: getCompilationIdentity(compilation),
|
|
351
451
|
};
|
|
452
|
+
if (pair.currentAttemptIdentity) {
|
|
453
|
+
compilationIdentities.setAttemptIdentityForCompilation(
|
|
454
|
+
compilation,
|
|
455
|
+
pair.currentAttemptIdentity
|
|
456
|
+
);
|
|
457
|
+
}
|
|
352
458
|
if (pair.latestCompletedWebIdentity) {
|
|
353
459
|
compilationIdentities.setWebIdentityForNodeCompilation(
|
|
354
460
|
compilation,
|
|
@@ -386,8 +492,10 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
386
492
|
if (!binding || !pair) {
|
|
387
493
|
return;
|
|
388
494
|
}
|
|
389
|
-
const webStats =
|
|
390
|
-
|
|
495
|
+
const webStats =
|
|
496
|
+
getEnvironmentStats(stats, 'web') ?? pair.latestCompletedWebStats;
|
|
497
|
+
const nodeStats =
|
|
498
|
+
getEnvironmentStats(stats, 'node') ?? pair.latestCompletedNodeStats;
|
|
391
499
|
if (
|
|
392
500
|
(webStats && webStats.compilation.compiler !== pair.web) ||
|
|
393
501
|
(nodeStats && nodeStats.compilation.compiler !== pair.node)
|
|
@@ -410,7 +518,17 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
410
518
|
web: snapshotDevChangedFiles(pair.web),
|
|
411
519
|
node: snapshotDevChangedFiles(pair.node),
|
|
412
520
|
};
|
|
413
|
-
const
|
|
521
|
+
const webAttempt = webStats
|
|
522
|
+
? compilationIdentities.getAttemptIdentityForCompilation(
|
|
523
|
+
webStats.compilation
|
|
524
|
+
)
|
|
525
|
+
: undefined;
|
|
526
|
+
const nodeAttempt = nodeStats
|
|
527
|
+
? compilationIdentities.getAttemptIdentityForCompilation(
|
|
528
|
+
nodeStats.compilation
|
|
529
|
+
)
|
|
530
|
+
: undefined;
|
|
531
|
+
const identity: DevGraphIdentity = {
|
|
414
532
|
web: webIdentity,
|
|
415
533
|
node: nodeIdentity,
|
|
416
534
|
nodeWeb: nodeStats
|
|
@@ -418,13 +536,19 @@ export const createReactRouterDevRuntimeController = ({
|
|
|
418
536
|
nodeStats.compilation
|
|
419
537
|
)
|
|
420
538
|
: undefined,
|
|
539
|
+
attempt:
|
|
540
|
+
webAttempt && nodeAttempt && webAttempt === nodeAttempt
|
|
541
|
+
? webAttempt
|
|
542
|
+
: undefined,
|
|
421
543
|
};
|
|
544
|
+
const finishStats: DevRuntimeStats =
|
|
545
|
+
webStats && nodeStats ? { web: webStats, node: nodeStats } : stats;
|
|
422
546
|
if (!webStats || !nodeStats) {
|
|
423
|
-
await binding
|
|
547
|
+
await finishRuntimeAttempt(binding, pair, finishStats, changes, identity);
|
|
424
548
|
return;
|
|
425
549
|
}
|
|
426
550
|
pair.pendingAttempt = {
|
|
427
|
-
stats,
|
|
551
|
+
stats: finishStats,
|
|
428
552
|
changes,
|
|
429
553
|
identity,
|
|
430
554
|
webCompilation: webStats.compilation,
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { RsbuildDevServer } from '@rsbuild/core';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
2
3
|
import { PLUGIN_NAME } from './constants.js';
|
|
3
4
|
import type { ReactRouterDevRuntime } from './dev-generation.js';
|
|
4
5
|
import type { DevCompilerPair } from './dev-runtime-compilation.js';
|
|
6
|
+
import {
|
|
7
|
+
runPluginEffect,
|
|
8
|
+
tryPluginPromise,
|
|
9
|
+
tryPluginSync,
|
|
10
|
+
} from './effect-runtime.js';
|
|
5
11
|
|
|
6
12
|
export type RuntimeBinding = {
|
|
7
13
|
id: number;
|
|
@@ -109,18 +115,19 @@ export const createDevRuntimeSessionManager = (
|
|
|
109
115
|
if (observation.promise) {
|
|
110
116
|
return observation.promise;
|
|
111
117
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
observation.promise = runPluginEffect(
|
|
119
|
+
tryPluginPromise(close).pipe(
|
|
120
|
+
Effect.tap(() =>
|
|
121
|
+
tryPluginSync(() => applyCloseOutcome(observation, { ok: true }))
|
|
122
|
+
),
|
|
123
|
+
Effect.catchAll(cause =>
|
|
124
|
+
tryPluginSync(() =>
|
|
125
|
+
applyCloseOutcome(observation, { ok: false, cause })
|
|
126
|
+
).pipe(Effect.zipRight(Effect.fail(cause)))
|
|
127
|
+
)
|
|
128
|
+
)
|
|
122
129
|
);
|
|
123
|
-
return
|
|
130
|
+
return observation.promise;
|
|
124
131
|
};
|
|
125
132
|
closeObservationByServer.set(server, observation);
|
|
126
133
|
return observation;
|
package/src/dev-server.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { RsbuildConfig } from '@rsbuild/core';
|
|
2
3
|
import type { ServerBuild } from 'react-router';
|
|
3
4
|
|
|
5
|
+
export type ServerSetup = Exclude<
|
|
6
|
+
NonNullable<NonNullable<RsbuildConfig['server']>['setup']>,
|
|
7
|
+
unknown[]
|
|
8
|
+
>;
|
|
9
|
+
|
|
4
10
|
export type DevServerMiddleware = (
|
|
5
11
|
req: IncomingMessage,
|
|
6
12
|
res: ServerResponse,
|
|
@@ -42,11 +48,18 @@ export const createDevServerMiddleware = (
|
|
|
42
48
|
dependencies.loadBuild,
|
|
43
49
|
'development'
|
|
44
50
|
);
|
|
45
|
-
return createRequestListener(requestHandler);
|
|
51
|
+
return createRequestListener(request => requestHandler(request));
|
|
46
52
|
})();
|
|
47
53
|
return listenerPromise;
|
|
48
54
|
};
|
|
49
55
|
|
|
56
|
+
// Warm the handler imports now so the first request does not pay them.
|
|
57
|
+
// On failure, reset so the first real request retries and surfaces the
|
|
58
|
+
// error through the middleware's own error path.
|
|
59
|
+
void getListener().catch(() => {
|
|
60
|
+
listenerPromise = undefined;
|
|
61
|
+
});
|
|
62
|
+
|
|
50
63
|
return async (req, res, next): Promise<void> => {
|
|
51
64
|
try {
|
|
52
65
|
const listener = await getListener();
|
|
@@ -56,3 +69,20 @@ export const createDevServerMiddleware = (
|
|
|
56
69
|
}
|
|
57
70
|
};
|
|
58
71
|
};
|
|
72
|
+
|
|
73
|
+
export const createReactRouterDevServerSetup = ({
|
|
74
|
+
loadBuild,
|
|
75
|
+
}: {
|
|
76
|
+
loadBuild: BuildProvider;
|
|
77
|
+
}): ServerSetup =>
|
|
78
|
+
function reactRouterDevServerSetup(context) {
|
|
79
|
+
if (context.action !== 'dev') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// The returned callback runs after Rsbuild registers its built-in
|
|
83
|
+
// middlewares, so static assets are served before the React Router
|
|
84
|
+
// request handler.
|
|
85
|
+
return () => {
|
|
86
|
+
context.server.middlewares.use(createDevServerMiddleware({ loadBuild }));
|
|
87
|
+
};
|
|
88
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as Cause from 'effect/Cause';
|
|
2
|
+
import * as Duration from 'effect/Duration';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import * as Exit from 'effect/Exit';
|
|
5
|
+
import * as Fiber from 'effect/Fiber';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
|
|
8
|
+
export const DEV_BACKGROUND_STARTUP_DELAY_MS = 3_000;
|
|
9
|
+
|
|
10
|
+
export const normalizeEffectError = (cause: unknown): Error =>
|
|
11
|
+
cause instanceof Error ? cause : new Error(String(cause));
|
|
12
|
+
|
|
13
|
+
const normalizeEffectCause = <E>(cause: Cause.Cause<E>): Error => {
|
|
14
|
+
const failure = Cause.failureOption(cause);
|
|
15
|
+
return normalizeEffectError(
|
|
16
|
+
Option.isSome(failure) ? failure.value : Cause.squash(cause)
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const runPluginEffect = async <A, E>(
|
|
21
|
+
effect: Effect.Effect<A, E, never>
|
|
22
|
+
): Promise<A> => {
|
|
23
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
24
|
+
if (Exit.isSuccess(exit)) {
|
|
25
|
+
return exit.value;
|
|
26
|
+
}
|
|
27
|
+
throw normalizeEffectCause(exit.cause);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const tryPluginSync = <A>(
|
|
31
|
+
evaluate: () => A
|
|
32
|
+
): Effect.Effect<A, Error, never> =>
|
|
33
|
+
Effect.try({
|
|
34
|
+
try: evaluate,
|
|
35
|
+
catch: normalizeEffectError,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const tryPluginPromise = <A>(
|
|
39
|
+
evaluate: () => PromiseLike<A> | A
|
|
40
|
+
): Effect.Effect<A, Error, never> =>
|
|
41
|
+
Effect.tryPromise({
|
|
42
|
+
try: () => Promise.resolve(evaluate()),
|
|
43
|
+
catch: normalizeEffectError,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type DelayedPluginTask = {
|
|
47
|
+
schedule(): void;
|
|
48
|
+
reschedule(): void;
|
|
49
|
+
cancelEffect(): Effect.Effect<void, Error, never>;
|
|
50
|
+
cancel(): Promise<void>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const createDelayedPluginTask = ({
|
|
54
|
+
delayMs,
|
|
55
|
+
run,
|
|
56
|
+
onError,
|
|
57
|
+
}: {
|
|
58
|
+
delayMs: number;
|
|
59
|
+
run: () => Effect.Effect<void, Error, never>;
|
|
60
|
+
onError: (error: Error) => void;
|
|
61
|
+
}): DelayedPluginTask => {
|
|
62
|
+
let activeFiber: ReturnType<typeof Effect.runFork> | undefined;
|
|
63
|
+
let version = 0;
|
|
64
|
+
|
|
65
|
+
const cancelActiveEffect = (): Effect.Effect<void, Error, never> =>
|
|
66
|
+
Effect.sync(() => {
|
|
67
|
+
const fiber = activeFiber;
|
|
68
|
+
activeFiber = undefined;
|
|
69
|
+
return fiber;
|
|
70
|
+
}).pipe(
|
|
71
|
+
Effect.flatMap(fiber =>
|
|
72
|
+
fiber ? Fiber.interrupt(fiber).pipe(Effect.asVoid) : Effect.void
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const cancelEffect = (): Effect.Effect<void, Error, never> =>
|
|
77
|
+
Effect.sync(() => {
|
|
78
|
+
version += 1;
|
|
79
|
+
}).pipe(Effect.zipRight(cancelActiveEffect()));
|
|
80
|
+
|
|
81
|
+
const start = (taskVersion: number): void => {
|
|
82
|
+
if (activeFiber || version !== taskVersion) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let fiber: ReturnType<typeof Effect.runFork>;
|
|
87
|
+
fiber = Effect.runFork(
|
|
88
|
+
Effect.sleep(Duration.millis(delayMs)).pipe(
|
|
89
|
+
Effect.zipRight(Effect.suspend(run)),
|
|
90
|
+
Effect.catchAll(error =>
|
|
91
|
+
Effect.sync(() => {
|
|
92
|
+
onError(error);
|
|
93
|
+
})
|
|
94
|
+
),
|
|
95
|
+
Effect.ensuring(
|
|
96
|
+
Effect.sync(() => {
|
|
97
|
+
if (activeFiber === fiber) {
|
|
98
|
+
activeFiber = undefined;
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
activeFiber = fiber;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
schedule(): void {
|
|
109
|
+
if (activeFiber) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
version += 1;
|
|
113
|
+
start(version);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
reschedule(): void {
|
|
117
|
+
version += 1;
|
|
118
|
+
const taskVersion = version;
|
|
119
|
+
void runPluginEffect(cancelActiveEffect())
|
|
120
|
+
.then(() => start(taskVersion))
|
|
121
|
+
.catch(onError);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
cancelEffect,
|
|
125
|
+
|
|
126
|
+
async cancel(): Promise<void> {
|
|
127
|
+
await runPluginEffect(cancelEffect());
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
package/src/export-utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { rspack } from '@rsbuild/core';
|
|
3
|
+
import { langFromPath, parse, type ParseOptions } from 'yuku-parser';
|
|
3
4
|
import { setBoundedCacheEntry } from './bounded-cache.js';
|
|
4
5
|
import {
|
|
5
6
|
getExportedName,
|
|
@@ -34,6 +35,75 @@ const routeModuleAnalysisCache = new Map<
|
|
|
34
35
|
|
|
35
36
|
const MAX_EXPORT_UTILS_CACHE_ENTRIES = 2048;
|
|
36
37
|
|
|
38
|
+
const stripResourcePathQuery = (resourcePath: string): string =>
|
|
39
|
+
resourcePath.replace(/[?#].*$/, '');
|
|
40
|
+
|
|
41
|
+
type TypeScriptParseLang = Extract<
|
|
42
|
+
NonNullable<ParseOptions['lang']>,
|
|
43
|
+
'ts' | 'tsx'
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
const getExportAnalysisCode = (
|
|
47
|
+
code: string,
|
|
48
|
+
resourcePath: string,
|
|
49
|
+
lang: TypeScriptParseLang
|
|
50
|
+
): string =>
|
|
51
|
+
rspack.experiments.swc.transformSync(code, {
|
|
52
|
+
filename: resourcePath,
|
|
53
|
+
jsc: {
|
|
54
|
+
parser: {
|
|
55
|
+
syntax: 'typescript',
|
|
56
|
+
tsx: lang === 'tsx',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}).code;
|
|
60
|
+
|
|
61
|
+
const getParseErrors = (result: ReturnType<typeof parse>) =>
|
|
62
|
+
result.diagnostics.filter(diagnostic => diagnostic.severity === 'error');
|
|
63
|
+
|
|
64
|
+
const getParseErrorMessage = (
|
|
65
|
+
errors: ReturnType<typeof getParseErrors>
|
|
66
|
+
): string => errors.map(error => error.message).join('\n');
|
|
67
|
+
|
|
68
|
+
const parseProgram = (code: string, resourcePath?: string): ProgramNode => {
|
|
69
|
+
const sourcePath = resourcePath
|
|
70
|
+
? stripResourcePathQuery(resourcePath)
|
|
71
|
+
: undefined;
|
|
72
|
+
const lang = sourcePath ? langFromPath(sourcePath) : 'tsx';
|
|
73
|
+
const result = parse(code, {
|
|
74
|
+
sourceType: 'module',
|
|
75
|
+
lang,
|
|
76
|
+
});
|
|
77
|
+
const errors = getParseErrors(result);
|
|
78
|
+
if (errors.length === 0) {
|
|
79
|
+
return getProgram(result);
|
|
80
|
+
}
|
|
81
|
+
if (!sourcePath || (lang !== 'ts' && lang !== 'tsx')) {
|
|
82
|
+
throw new Error(getParseErrorMessage(errors));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const normalizedCode = getExportAnalysisCode(code, sourcePath, lang);
|
|
86
|
+
const normalizedResult = parse(normalizedCode, {
|
|
87
|
+
sourceType: 'module',
|
|
88
|
+
lang: 'js',
|
|
89
|
+
});
|
|
90
|
+
const normalizedErrors = getParseErrors(normalizedResult);
|
|
91
|
+
if (normalizedErrors.length > 0) {
|
|
92
|
+
throw new Error(getParseErrorMessage(normalizedErrors));
|
|
93
|
+
}
|
|
94
|
+
return getProgram(normalizedResult);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getExportInfoCacheKey = (
|
|
98
|
+
code: string,
|
|
99
|
+
resourcePath?: string
|
|
100
|
+
): string => {
|
|
101
|
+
const lang = resourcePath
|
|
102
|
+
? langFromPath(stripResourcePathQuery(resourcePath))
|
|
103
|
+
: 'inline';
|
|
104
|
+
return `${lang}\0${code}`;
|
|
105
|
+
};
|
|
106
|
+
|
|
37
107
|
const cachePromiseOnReject = <T>(
|
|
38
108
|
promise: Promise<T>,
|
|
39
109
|
invalidate: () => void
|
|
@@ -43,20 +113,6 @@ const cachePromiseOnReject = <T>(
|
|
|
43
113
|
throw error;
|
|
44
114
|
});
|
|
45
115
|
|
|
46
|
-
const parseProgram = (code: string, resourcePath?: string): ProgramNode => {
|
|
47
|
-
const result = parse(code, {
|
|
48
|
-
sourceType: 'module',
|
|
49
|
-
lang: resourcePath ? langFromPath(resourcePath) : 'tsx',
|
|
50
|
-
});
|
|
51
|
-
const errors = result.diagnostics.filter(
|
|
52
|
-
diagnostic => diagnostic.severity === 'error'
|
|
53
|
-
);
|
|
54
|
-
if (errors.length > 0) {
|
|
55
|
-
throw new Error(errors.map(error => error.message).join('\n'));
|
|
56
|
-
}
|
|
57
|
-
return getProgram(result);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
116
|
const isTypeOnlyExport = (node: AnyNode): boolean =>
|
|
61
117
|
node.exportKind === 'type' ||
|
|
62
118
|
node.type === 'TSExportAssignment' ||
|
|
@@ -140,21 +196,24 @@ const collectExportAllModules = (program: AnyNode): string[] => {
|
|
|
140
196
|
};
|
|
141
197
|
|
|
142
198
|
export const getExportNames = async (
|
|
143
|
-
code: string
|
|
199
|
+
code: string,
|
|
200
|
+
resourcePath?: string
|
|
144
201
|
): Promise<readonly string[]> => {
|
|
145
|
-
return (await getExportNamesAndExportAll(code)).exportNames;
|
|
202
|
+
return (await getExportNamesAndExportAll(code, resourcePath)).exportNames;
|
|
146
203
|
};
|
|
147
204
|
|
|
148
205
|
export const getExportNamesAndExportAll = async (
|
|
149
|
-
code: string
|
|
206
|
+
code: string,
|
|
207
|
+
resourcePath?: string
|
|
150
208
|
): Promise<ExportInfo> => {
|
|
151
|
-
const
|
|
209
|
+
const cacheKey = getExportInfoCacheKey(code, resourcePath);
|
|
210
|
+
const cached = exportInfoCache.get(cacheKey);
|
|
152
211
|
if (cached) {
|
|
153
212
|
return cached;
|
|
154
213
|
}
|
|
155
214
|
|
|
156
215
|
const exportInfo = (async () => {
|
|
157
|
-
const program = parseProgram(code);
|
|
216
|
+
const program = parseProgram(code, resourcePath);
|
|
158
217
|
return {
|
|
159
218
|
exportNames: collectProgramExportNames(program),
|
|
160
219
|
exportAllModules: collectExportAllModules(program),
|
|
@@ -163,14 +222,14 @@ export const getExportNamesAndExportAll = async (
|
|
|
163
222
|
|
|
164
223
|
let trackedExportInfo: Promise<ExportInfo>;
|
|
165
224
|
trackedExportInfo = cachePromiseOnReject(exportInfo, () => {
|
|
166
|
-
if (exportInfoCache.get(
|
|
167
|
-
exportInfoCache.delete(
|
|
225
|
+
if (exportInfoCache.get(cacheKey) === trackedExportInfo) {
|
|
226
|
+
exportInfoCache.delete(cacheKey);
|
|
168
227
|
}
|
|
169
228
|
});
|
|
170
229
|
|
|
171
230
|
setBoundedCacheEntry(
|
|
172
231
|
exportInfoCache,
|
|
173
|
-
|
|
232
|
+
cacheKey,
|
|
174
233
|
trackedExportInfo,
|
|
175
234
|
MAX_EXPORT_UTILS_CACHE_ENTRIES
|
|
176
235
|
);
|