rsbuild-plugin-react-router 0.3.0 → 0.3.1

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 (56) hide show
  1. package/README.md +55 -24
  2. package/dist/451.js +36 -20
  3. package/dist/build-manifest.d.ts +6 -3
  4. package/dist/concurrency.d.ts +2 -1
  5. package/dist/config-imports.d.ts +7 -0
  6. package/dist/dev-background-resources.d.ts +38 -0
  7. package/dist/dev-generation.d.ts +2 -2
  8. package/dist/dev-runtime-artifacts.d.ts +9 -1
  9. package/dist/dev-runtime-compilation.d.ts +17 -2
  10. package/dist/dev-server.d.ts +5 -0
  11. package/dist/effect-runtime.d.ts +18 -0
  12. package/dist/export-utils.d.ts +2 -2
  13. package/dist/index.cjs +11448 -891
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.js +7950 -758
  16. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  17. package/dist/lazy-compilation.d.ts +2 -1
  18. package/dist/manifest.d.ts +19 -4
  19. package/dist/parallel-route-transforms.d.ts +17 -2
  20. package/dist/prerender-build.d.ts +4 -0
  21. package/dist/prerender.d.ts +0 -1
  22. package/dist/react-router-config.d.ts +8 -5
  23. package/dist/server-build-resolution.d.ts +3 -0
  24. package/dist/ssr-externals.d.ts +1 -0
  25. package/dist/typegen.d.ts +14 -1
  26. package/dist/types.d.ts +15 -6
  27. package/package.json +11 -9
  28. package/src/build-manifest.ts +110 -73
  29. package/src/concurrency.ts +3 -22
  30. package/src/config-imports.ts +38 -0
  31. package/src/dev-background-resources.ts +255 -0
  32. package/src/dev-generation.ts +43 -53
  33. package/src/dev-runtime-artifacts.ts +50 -18
  34. package/src/dev-runtime-compilation.ts +80 -1
  35. package/src/dev-runtime-controller.ts +111 -31
  36. package/src/dev-runtime-session.ts +18 -11
  37. package/src/dev-server.ts +31 -1
  38. package/src/effect-runtime.ts +130 -0
  39. package/src/export-utils.ts +82 -23
  40. package/src/index.ts +118 -153
  41. package/src/lazy-compilation-prewarm.ts +279 -0
  42. package/src/lazy-compilation.ts +12 -5
  43. package/src/manifest.ts +366 -255
  44. package/src/modify-browser-manifest.ts +2 -1
  45. package/src/parallel-route-transforms.ts +195 -69
  46. package/src/prerender-build.ts +125 -61
  47. package/src/prerender.ts +0 -18
  48. package/src/react-router-config.ts +98 -73
  49. package/src/route-artifacts.ts +2 -2
  50. package/src/route-export-resolution.ts +3 -3
  51. package/src/route-watch.ts +119 -84
  52. package/src/server-build-resolution.ts +131 -0
  53. package/src/server-utils.ts +7 -106
  54. package/src/ssr-externals.ts +1 -1
  55. package/src/typegen.ts +162 -33
  56. package/src/types.ts +16 -6
@@ -1,5 +1,7 @@
1
1
  import { resolve } from 'pathe';
2
2
  import type { ServerBuild } from 'react-router';
3
+ import { runPluginEffect } from './effect-runtime.js';
4
+ import { resolveServerBuildModuleEffect } from './server-build-resolution.js';
3
5
  import type { Route } from './types.js';
4
6
 
5
7
  /**
@@ -91,120 +93,19 @@ function generateServerBuild(
91
93
  return generateStaticTemplate(routes, options);
92
94
  }
93
95
 
94
- const RESOLVABLE_BUILD_EXPORTS = new Set([
95
- 'allowedActionOrigins',
96
- 'assets',
97
- 'assetsBuildDirectory',
98
- 'basename',
99
- 'entry',
100
- 'future',
101
- 'isSpaMode',
102
- 'prerender',
103
- 'publicPath',
104
- 'routeDiscovery',
105
- 'routes',
106
- 'ssr',
107
- ]);
108
-
109
- function isRecord(value: unknown): value is Record<string, unknown> {
110
- return typeof value === 'object' && value !== null && !Array.isArray(value);
111
- }
112
-
113
- function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
114
- return isRecord(value) && typeof value.then === 'function';
115
- }
116
-
117
- function isRouteDiscovery(value: unknown): boolean {
118
- return (
119
- isRecord(value) &&
120
- (value.mode === 'initial' ||
121
- (value.mode === 'lazy' &&
122
- (value.manifestPath === undefined ||
123
- typeof value.manifestPath === 'string')))
124
- );
125
- }
126
-
127
- async function resolveBuildExports(
128
- build: Record<string, unknown>
129
- ): Promise<Record<string, unknown>> {
130
- const resolved = { ...build };
131
- for (const key of Object.keys(build)) {
132
- if (!RESOLVABLE_BUILD_EXPORTS.has(key)) {
133
- continue;
134
- }
135
- const value = build[key];
136
- if (typeof value === 'function' && value.length === 0) {
137
- const result = value();
138
- resolved[key] = isPromiseLike(result) ? await result : result;
139
- continue;
140
- }
141
- if (isPromiseLike(value)) {
142
- resolved[key] = await value;
143
- }
144
- }
145
- return resolved;
146
- }
147
-
148
- function isServerBuild(value: unknown): value is ServerBuild {
149
- return Boolean(
150
- isRecord(value) &&
151
- isRecord(value.entry) &&
152
- isRecord(value.entry.module) &&
153
- typeof value.entry.module.default === 'function' &&
154
- isRecord(value.routes) &&
155
- isRecord(value.assets) &&
156
- typeof value.assetsBuildDirectory === 'string' &&
157
- (value.basename === undefined || typeof value.basename === 'string') &&
158
- isRecord(value.future) &&
159
- typeof value.isSpaMode === 'boolean' &&
160
- Array.isArray(value.prerender) &&
161
- typeof value.publicPath === 'string' &&
162
- isRouteDiscovery(value.routeDiscovery) &&
163
- typeof value.ssr === 'boolean'
164
- );
165
- }
166
-
167
- async function resolveServerBuildCandidate(
168
- candidate: unknown
169
- ): Promise<ServerBuild | undefined> {
170
- if (!isRecord(candidate)) {
171
- return undefined;
172
- }
173
- const resolved = await resolveBuildExports(candidate);
174
- return isServerBuild(resolved) ? resolved : undefined;
175
- }
176
-
177
- export async function resolveServerBuildModule(
96
+ export function resolveServerBuildModule(
178
97
  buildModule: unknown,
179
98
  source: string
180
99
  ): Promise<ServerBuild> {
181
- const moduleValue = await buildModule;
182
- const candidates = [() => moduleValue];
183
- if (isRecord(moduleValue)) {
184
- if ('default' in moduleValue) {
185
- candidates.push(() => moduleValue.default);
186
- }
187
- if ('module.exports' in moduleValue) {
188
- candidates.push(() => moduleValue['module.exports']);
189
- }
190
- }
191
-
192
- for (const getCandidate of candidates) {
193
- const candidate = await getCandidate();
194
- const serverBuild = await resolveServerBuildCandidate(candidate);
195
- if (serverBuild) {
196
- return serverBuild;
197
- }
198
- }
199
- throw new Error(
200
- `[rsbuild-plugin-react-router] ${source} did not contain a valid React Router ServerBuild.`
201
- );
100
+ return runPluginEffect(resolveServerBuildModuleEffect(buildModule, source));
202
101
  }
203
102
 
204
103
  export function resolveReactRouterServerBuild(
205
104
  buildModule: unknown
206
105
  ): Promise<ServerBuild> {
207
- return resolveServerBuildModule(buildModule, 'Imported module');
106
+ return runPluginEffect(
107
+ resolveServerBuildModuleEffect(buildModule, 'Imported module')
108
+ );
208
109
  }
209
110
 
210
111
  export { generateServerBuild };
@@ -15,7 +15,7 @@ const REACT_ROUTER_EXTERNALS = [
15
15
 
16
16
  const requireFromHere = createRequire(import.meta.url);
17
17
 
18
- function resolvePackageJson(
18
+ export function resolvePackageJson(
19
19
  name: string,
20
20
  rootDirectory: string
21
21
  ): string | null {
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