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
package/src/typegen.ts CHANGED
@@ -1,49 +1,178 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
1
3
  import type { RsbuildPluginAPI } from '@rsbuild/core';
2
4
  import type { ResultPromise } from 'execa';
5
+ import * as Effect from 'effect/Effect';
6
+ import { createDelayedPluginTask, tryPluginPromise } from './effect-runtime.js';
7
+ import { resolvePackageJson } from './ssr-externals.js';
3
8
 
4
- export const registerReactRouterTypegen = (api: RsbuildPluginAPI): void => {
5
- let typegenProcess: ResultPromise | undefined;
9
+ // Quiet period with no dev compiles before the typegen watch starts. Long
10
+ // enough that route-load and HMR compile bursts (each rescheduling the task)
11
+ // finish before the typegen process competes for CPU on small machines.
12
+ const TYPEGEN_IDLE_DELAY_MS = 10_000;
13
+
14
+ type Execa = typeof import('execa').execa;
15
+ type LoadExeca = () => Promise<Execa>;
16
+
17
+ export type ReactRouterTypegenRunner = {
18
+ startWatch(): Promise<void>;
19
+ closeWatch(): Promise<void>;
20
+ runBuild(): Promise<void>;
21
+ };
22
+
23
+ const loadDefaultExeca: LoadExeca = async () => {
24
+ const { execa } = await import('execa');
25
+ return execa;
26
+ };
27
+
28
+ type TypegenCommand = {
29
+ command: string;
30
+ args: string[];
31
+ };
6
32
 
7
- api.onBeforeStartDevServer(async () => {
8
- if (typegenProcess) {
9
- return;
33
+ // The `react-router` CLI bin is provided by `@react-router/dev`. Spawning it
34
+ // directly through `process.execPath` skips the npx bootstrap (npm config
35
+ // load and package resolution), which otherwise costs several hundred ms of
36
+ // CPU during dev-server startup and every production build.
37
+ const resolveDirectTypegenCommand = (
38
+ appDirectory: string
39
+ ): TypegenCommand | undefined => {
40
+ const packageJsonPath = resolvePackageJson('@react-router/dev', appDirectory);
41
+ if (!packageJsonPath) {
42
+ // `@react-router/dev` is not resolvable from the app directory; the
43
+ // caller falls back to spawning through npx.
44
+ return undefined;
45
+ }
46
+ try {
47
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
48
+ bin?: string | Record<string, string>;
49
+ };
50
+ const binRelativePath =
51
+ typeof packageJson.bin === 'string'
52
+ ? packageJson.bin
53
+ : packageJson.bin?.['react-router'];
54
+ if (!binRelativePath) {
55
+ return undefined;
10
56
  }
11
- const { execa } = await import('execa');
12
- const process = execa(
13
- 'npx',
14
- ['--yes', 'react-router', 'typegen', '--watch'],
15
- {
16
- stdio: 'inherit',
17
- detached: false,
18
- cleanup: true,
19
- }
20
- );
21
- typegenProcess = process;
22
- process
23
- .catch(() => {
24
- // Ignore errors when the process is killed on server shutdown.
25
- })
57
+ return {
58
+ command: process.execPath,
59
+ args: [resolve(dirname(packageJsonPath), binRelativePath)],
60
+ };
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ };
65
+
66
+ export const createReactRouterTypegenRunner = (
67
+ loadExeca: LoadExeca = loadDefaultExeca,
68
+ appDirectory?: string
69
+ ): ReactRouterTypegenRunner => {
70
+ let typegenProcess: ResultPromise | undefined;
71
+ let typegenCommand: TypegenCommand | undefined;
72
+
73
+ const getTypegenCommand = (): TypegenCommand => {
74
+ typegenCommand ??= (appDirectory
75
+ ? resolveDirectTypegenCommand(appDirectory)
76
+ : undefined) ?? {
77
+ command: 'npx',
78
+ args: ['--yes', 'react-router'],
79
+ };
80
+ return typegenCommand;
81
+ };
82
+
83
+ const observeWatchExit = (process: ResultPromise): void => {
84
+ void process
85
+ .catch(() => undefined)
26
86
  .finally(() => {
27
87
  if (typegenProcess === process) {
28
88
  typegenProcess = undefined;
29
89
  }
30
90
  });
31
- });
91
+ };
32
92
 
33
- api.onCloseDevServer(async () => {
34
- const process = typegenProcess;
35
- typegenProcess = undefined;
36
- if (!process) {
37
- return;
38
- }
39
- process.kill('SIGTERM');
40
- await process.catch(() => undefined);
93
+ return {
94
+ async startWatch(): Promise<void> {
95
+ if (typegenProcess) {
96
+ return;
97
+ }
98
+
99
+ const execa = await loadExeca();
100
+ const { command, args } = getTypegenCommand();
101
+ const process = execa(command, [...args, 'typegen', '--watch'], {
102
+ stdio: 'inherit',
103
+ detached: false,
104
+ cleanup: true,
105
+ });
106
+ typegenProcess = process;
107
+ observeWatchExit(process);
108
+ },
109
+
110
+ async closeWatch(): Promise<void> {
111
+ const process = typegenProcess;
112
+ typegenProcess = undefined;
113
+ if (!process) {
114
+ return;
115
+ }
116
+
117
+ process.kill('SIGTERM');
118
+ await process.catch(() => undefined);
119
+ },
120
+
121
+ async runBuild(): Promise<void> {
122
+ const execa = await loadExeca();
123
+ const { command, args } = getTypegenCommand();
124
+ await execa(command, [...args, 'typegen'], {
125
+ stdio: 'inherit',
126
+ });
127
+ },
128
+ };
129
+ };
130
+
131
+ export const registerReactRouterTypegen = (
132
+ api: RsbuildPluginAPI,
133
+ {
134
+ runner,
135
+ devWatchDelayMs = TYPEGEN_IDLE_DELAY_MS,
136
+ appDirectory,
137
+ }: {
138
+ runner?: ReactRouterTypegenRunner;
139
+ devWatchDelayMs?: number;
140
+ appDirectory?: string;
141
+ } = {}
142
+ ): void => {
143
+ const resolvedRunner =
144
+ runner ?? createReactRouterTypegenRunner(loadDefaultExeca, appDirectory);
145
+ let devWatchStarted = false;
146
+ const devWatchTask = createDelayedPluginTask({
147
+ delayMs: devWatchDelayMs,
148
+ run: () =>
149
+ tryPluginPromise(() => {
150
+ devWatchStarted = true;
151
+ return resolvedRunner.startWatch();
152
+ }).pipe(Effect.asVoid),
153
+ onError(error) {
154
+ api.logger.warn(
155
+ `[react-router] Failed to start React Router typegen watch: ${error}`
156
+ );
157
+ },
41
158
  });
42
159
 
43
- api.onBeforeBuild(async () => {
44
- const { execa } = await import('execa');
45
- await execa('npx', ['--yes', 'react-router', 'typegen'], {
46
- stdio: 'inherit',
160
+ if (api.context.action !== 'build') {
161
+ // Reschedule on every compile so the typegen watch only starts after a
162
+ // quiet period with no compiles. Starting it during the initial compile
163
+ // burst competes with HMR rebuilds for CPU on small machines.
164
+ api.onAfterDevCompile(() => {
165
+ if (devWatchStarted) {
166
+ return;
167
+ }
168
+ devWatchTask.reschedule();
47
169
  });
170
+ }
171
+
172
+ api.onCloseDevServer(async () => {
173
+ await devWatchTask.cancel();
174
+ await resolvedRunner.closeWatch();
48
175
  });
176
+
177
+ api.onBeforeBuild(() => resolvedRunner.runBuild());
49
178
  };
package/src/types.ts CHANGED
@@ -20,8 +20,8 @@ export type PluginOptions = {
20
20
 
21
21
  /**
22
22
  * The output format for server builds.
23
- * When set to "module", no package.json will be emitted.
24
- * @default "module"
23
+ * When omitted, React Router's `serverModuleFormat` selects the emitted
24
+ * Rsbuild format (`"esm"` -> `"module"`, `"cjs"` -> `"commonjs"`).
25
25
  */
26
26
  serverOutput?: 'module' | 'commonjs';
27
27
 
@@ -31,15 +31,25 @@ export type PluginOptions = {
31
31
  federation?: boolean;
32
32
 
33
33
  /**
34
- * Opt in to Rsbuild's dev-only lazy compilation behavior.
34
+ * Rsbuild dev-only lazy compilation behavior.
35
35
  *
36
- * React Router hydration modules remain eager so initial dev requests can
37
- * load the browser manifest and route modules without lazy proxy delays.
36
+ * React Router's browser manifest remains eager so initial dev requests can
37
+ * discover browser assets without lazy proxy delays.
38
38
  *
39
- * @default undefined
39
+ * Pass `false` to disable.
40
+ * @default true
40
41
  */
41
42
  lazyCompilation?: NonNullable<RsbuildConfig['dev']>['lazyCompilation'];
42
43
 
44
+ /**
45
+ * Prewarm Rspack lazy-compilation proxy modules after dev compiles.
46
+ * This depends on Rspack's generated lazy-compilation client shape and should
47
+ * be treated as experimental.
48
+ *
49
+ * @default false
50
+ */
51
+ unstableLazyCompilationPrewarm?: boolean;
52
+
43
53
  /**
44
54
  * Emit structured React Router plugin timing logs.
45
55
  * @default false