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
@@ -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.pendingAttempt = undefined;
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 binding.runtime
145
- .finishAttempt(pending.stats, pending.changes, pending.identity)
146
- .catch(cause => {
147
- if (sessions.getActiveBinding()?.id === binding.id) {
148
- binding.runtime.failAttempt(
149
- cause instanceof Error ? cause : new Error(String(cause))
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.pendingAttempt = undefined;
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.latestWebStart = undefined;
370
+ clearDevCompilerStart(pair, 'latestWebStart');
282
371
  } else {
283
- pair.latestNodeStart = undefined;
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[side] = { status: 'pending' };
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 = getEnvironmentStats(stats, 'web');
390
- const nodeStats = getEnvironmentStats(stats, 'node');
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 identity = {
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.runtime.finishAttempt(stats, changes, identity);
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
- let closePromise: Promise<void>;
113
- try {
114
- closePromise = close();
115
- } catch (cause) {
116
- closePromise = Promise.reject(cause);
117
- }
118
- observation.promise = closePromise;
119
- void closePromise.then(
120
- () => applyCloseOutcome(observation, { ok: true }),
121
- cause => applyCloseOutcome(observation, { ok: false, cause })
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 closePromise;
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
+ };
@@ -1,5 +1,6 @@
1
1
  import { readFile, stat } from 'node:fs/promises';
2
- import { langFromPath, parse } from 'yuku-parser';
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 cached = exportInfoCache.get(code);
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(code) === trackedExportInfo) {
167
- exportInfoCache.delete(code);
225
+ if (exportInfoCache.get(cacheKey) === trackedExportInfo) {
226
+ exportInfoCache.delete(cacheKey);
168
227
  }
169
228
  });
170
229
 
171
230
  setBoundedCacheEntry(
172
231
  exportInfoCache,
173
- code,
232
+ cacheKey,
174
233
  trackedExportInfo,
175
234
  MAX_EXPORT_UTILS_CACHE_ENTRIES
176
235
  );