rsbuild-plugin-react-router 0.4.0 → 0.5.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.
@@ -0,0 +1,149 @@
1
+ import { threadId } from 'node:worker_threads';
2
+ import remapping, { type SourceMapInput } from '@jridgewell/remapping';
3
+ import type { LoaderDefinition } from '@rspack/core';
4
+ import { createReactRouterPerformanceProfiler } from './performance.js';
5
+ import { executeRouteTransformTask } from './route-transform-tasks.js';
6
+
7
+ export type RouteModuleTransformLoaderOptions = {
8
+ environmentName: string;
9
+ performanceScopeId: string;
10
+ logPerformance: boolean;
11
+ ssr: boolean;
12
+ isBuild: boolean;
13
+ isSpaMode: boolean;
14
+ rootRoutePath: string | null;
15
+ devHmr: boolean;
16
+ };
17
+
18
+ type LoaderContextWithSourceMap = ThisParameterType<
19
+ LoaderDefinition<RouteModuleTransformLoaderOptions>
20
+ > & {
21
+ sourceMap?: boolean;
22
+ };
23
+ type LoaderInputSourceMap = Parameters<
24
+ LoaderDefinition<RouteModuleTransformLoaderOptions>
25
+ >[1];
26
+ type RawLoaderSourceMap = Exclude<LoaderInputSourceMap, string | undefined>;
27
+
28
+ const LOADER_PERFORMANCE_FLUSH_DELAY_MS = 50;
29
+ const performanceWorkerId = `process-${process.pid}:thread-${threadId}`;
30
+
31
+ const composeSourceMaps = (
32
+ generatedMap: SourceMapInput,
33
+ inputSourceMap: SourceMapInput
34
+ ): RawLoaderSourceMap =>
35
+ JSON.parse(
36
+ remapping([generatedMap, inputSourceMap], () => null).toString()
37
+ ) as RawLoaderSourceMap;
38
+
39
+ const performanceProfilersByScope = new Map<
40
+ string,
41
+ Map<string, ReturnType<typeof createReactRouterPerformanceProfiler>>
42
+ >();
43
+ let performanceFlushTimer: ReturnType<typeof setTimeout> | undefined;
44
+
45
+ const getPerformanceProfiler = (
46
+ scopeId: string,
47
+ environment: string | undefined
48
+ ): ReturnType<typeof createReactRouterPerformanceProfiler> => {
49
+ let profilers = performanceProfilersByScope.get(scopeId);
50
+ if (!profilers) {
51
+ profilers = new Map();
52
+ performanceProfilersByScope.set(scopeId, profilers);
53
+ }
54
+ const environmentKey = environment ?? 'unknown';
55
+ let profiler = profilers.get(environmentKey);
56
+ if (!profiler) {
57
+ profiler = createReactRouterPerformanceProfiler({
58
+ enabled: true,
59
+ log: message => console.info(message),
60
+ });
61
+ profilers.set(environmentKey, profiler);
62
+ }
63
+ return profiler;
64
+ };
65
+
66
+ export const flushRouteModuleTransformLoaderPerformance = (): void => {
67
+ if (performanceFlushTimer) {
68
+ clearTimeout(performanceFlushTimer);
69
+ performanceFlushTimer = undefined;
70
+ }
71
+
72
+ for (const profilers of performanceProfilersByScope.values()) {
73
+ for (const [environment, profiler] of profilers) {
74
+ profiler.flush(environment === 'unknown' ? undefined : environment, {
75
+ partial: true,
76
+ workerId: performanceWorkerId,
77
+ });
78
+ }
79
+ }
80
+ performanceProfilersByScope.clear();
81
+ };
82
+
83
+ const schedulePerformanceFlush = (): void => {
84
+ if (performanceFlushTimer) {
85
+ clearTimeout(performanceFlushTimer);
86
+ }
87
+ performanceFlushTimer = setTimeout(
88
+ flushRouteModuleTransformLoaderPerformance,
89
+ LOADER_PERFORMANCE_FLUSH_DELAY_MS
90
+ );
91
+ };
92
+
93
+ process.once('beforeExit', flushRouteModuleTransformLoaderPerformance);
94
+
95
+ const routeModuleTransformLoader: LoaderDefinition<RouteModuleTransformLoaderOptions> =
96
+ async function routeModuleTransformLoader(
97
+ this: LoaderContextWithSourceMap,
98
+ source,
99
+ inputSourceMap
100
+ ) {
101
+ const callback = this.async();
102
+ const options = this.getOptions();
103
+
104
+ const transform = () =>
105
+ executeRouteTransformTask({
106
+ kind: 'routeModule',
107
+ code: String(source),
108
+ resource: this.resource,
109
+ resourcePath: this.resourcePath,
110
+ environmentName: options.environmentName,
111
+ sourceMaps: Boolean(this.sourceMap),
112
+ ssr: options.ssr,
113
+ isBuild: options.isBuild,
114
+ isSpaMode: options.isSpaMode,
115
+ rootRoutePath: options.rootRoutePath,
116
+ devHmr: options.devHmr,
117
+ });
118
+
119
+ try {
120
+ const result = options.logPerformance
121
+ ? await getPerformanceProfiler(
122
+ options.performanceScopeId,
123
+ options.environmentName
124
+ ).record(
125
+ options.environmentName,
126
+ 'route:module',
127
+ this.resource,
128
+ transform
129
+ )
130
+ : await transform();
131
+
132
+ const outputSourceMap =
133
+ result.map && inputSourceMap
134
+ ? composeSourceMaps(
135
+ result.map as SourceMapInput,
136
+ inputSourceMap as SourceMapInput
137
+ )
138
+ : result.map;
139
+ callback(null, result.code, outputSourceMap ?? undefined);
140
+ } catch (error) {
141
+ callback(error instanceof Error ? error : new Error(String(error)));
142
+ } finally {
143
+ if (options.logPerformance) {
144
+ schedulePerformanceFlush();
145
+ }
146
+ }
147
+ };
148
+
149
+ export default routeModuleTransformLoader;
@@ -0,0 +1,115 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import type { Rspack } from '@rsbuild/core';
3
+ import type { RouteModuleTransformLoaderOptions } from './route-module-transform-loader.js';
4
+ import type { PluginOptions, Route } from './types.js';
5
+
6
+ const routeModuleTransformLoaderPath = fileURLToPath(
7
+ new URL('./route-module-transform-loader.js', import.meta.url)
8
+ );
9
+
10
+ const getRouteModuleTransformParallel = (
11
+ parallelRouteTransform: PluginOptions['parallelRouteTransform']
12
+ ): Rspack.RuleSetLoaderWithOptions['parallel'] => {
13
+ if (
14
+ parallelRouteTransform === undefined ||
15
+ parallelRouteTransform === false
16
+ ) {
17
+ return undefined;
18
+ }
19
+ if (parallelRouteTransform === true) {
20
+ return true;
21
+ }
22
+ if (typeof parallelRouteTransform === 'number') {
23
+ if (
24
+ !Number.isInteger(parallelRouteTransform) ||
25
+ parallelRouteTransform < 1
26
+ ) {
27
+ throw new Error(
28
+ '[react-router] parallelRouteTransform must be true, false, or a positive integer.'
29
+ );
30
+ }
31
+ return { maxWorkers: parallelRouteTransform };
32
+ }
33
+ throw new Error(
34
+ '[react-router] parallelRouteTransform must be true, false, or a positive integer.'
35
+ );
36
+ };
37
+
38
+ export const shouldUseRouteModuleTransformLoader = (
39
+ parallelRouteTransform: PluginOptions['parallelRouteTransform']
40
+ ): boolean =>
41
+ getRouteModuleTransformParallel(parallelRouteTransform) !== undefined;
42
+
43
+ const createRouteModuleTransformUse = (
44
+ options: RouteModuleTransformLoaderOptions,
45
+ parallelRouteTransform: PluginOptions['parallelRouteTransform']
46
+ ): Rspack.RuleSetLoaderWithOptions => {
47
+ const parallel = getRouteModuleTransformParallel(parallelRouteTransform);
48
+ return {
49
+ loader: routeModuleTransformLoaderPath,
50
+ options,
51
+ ...(parallel ? { parallel } : {}),
52
+ };
53
+ };
54
+
55
+ export const registerRouteModuleTransformRules = (
56
+ rspackConfig: Rspack.Configuration,
57
+ {
58
+ environmentName,
59
+ ssr,
60
+ isBuild,
61
+ isSpaMode,
62
+ rootRoutePath,
63
+ devHmr,
64
+ logPerformance,
65
+ routeByFilePath,
66
+ parallelRouteTransform,
67
+ }: {
68
+ environmentName: string;
69
+ ssr: boolean;
70
+ isBuild: boolean;
71
+ isSpaMode: boolean;
72
+ rootRoutePath: string | null;
73
+ devHmr: boolean;
74
+ logPerformance: boolean;
75
+ routeByFilePath: Map<string, Route>;
76
+ parallelRouteTransform: PluginOptions['parallelRouteTransform'];
77
+ }
78
+ ): void => {
79
+ const routeModuleTransformUse = createRouteModuleTransformUse(
80
+ {
81
+ environmentName,
82
+ performanceScopeId: [
83
+ environmentName,
84
+ isBuild ? 'build' : 'dev',
85
+ ssr ? 'ssr' : 'spa',
86
+ rootRoutePath ?? 'rootless',
87
+ ].join(':'),
88
+ logPerformance,
89
+ ssr,
90
+ isBuild,
91
+ isSpaMode,
92
+ rootRoutePath,
93
+ devHmr,
94
+ },
95
+ parallelRouteTransform
96
+ );
97
+
98
+ rspackConfig.module ??= {};
99
+ rspackConfig.module.rules ??= [];
100
+ rspackConfig.module.rules.push(
101
+ {
102
+ resourceQuery: /\?react-router-route/,
103
+ enforce: 'post',
104
+ use: [routeModuleTransformUse],
105
+ },
106
+ {
107
+ test: path => routeByFilePath.has(path),
108
+ resourceQuery: {
109
+ not: /__react-router-build-client-route|react-router-route|route-chunk=/,
110
+ },
111
+ enforce: 'post',
112
+ use: [routeModuleTransformUse],
113
+ }
114
+ );
115
+ };
@@ -24,10 +24,7 @@ import {
24
24
  type RouteChunkCache,
25
25
  type RouteChunkConfig,
26
26
  } from './route-chunks.js';
27
- import {
28
- getProgram,
29
- type AnyNode,
30
- } from './route-ast.js';
27
+ import { getProgram, type AnyNode } from './route-ast.js';
31
28
 
32
29
  export type RouteTransformResult = {
33
30
  code: string;
@@ -88,6 +85,10 @@ export type RouteTransformTaskOptions = {
88
85
  routeChunkCache?: RouteChunkCache;
89
86
  };
90
87
 
88
+ export type RouteTransformRunner = (
89
+ task: RouteTransformTask
90
+ ) => Promise<RouteTransformResult>;
91
+
91
92
  const defaultRouteChunkCache: RouteChunkCache = new Map();
92
93
 
93
94
  const getRouteChunkCache = (options?: RouteTransformTaskOptions) =>
package/src/types.ts CHANGED
@@ -57,11 +57,10 @@ export type PluginOptions = {
57
57
  logPerformance?: boolean;
58
58
 
59
59
  /**
60
- * Run route transforms in a worker-thread pool.
61
- * Pass `true` to force the default worker count, a positive integer to set
62
- * the worker count, or `false` to disable.
63
- * @default Automatically enabled for 256+ resolved routes. The automatic
64
- * pool uses available CPU cores minus 2.
60
+ * Run route module transforms with Rspack's parallel loader workers.
61
+ * Pass `true` to use Rspack's default worker count, a positive integer to set
62
+ * the maximum worker count, or `false` to disable.
63
+ * @default false
65
64
  */
66
65
  parallelRouteTransform?: boolean | number;
67
66
 
@@ -1,25 +0,0 @@
1
- import type { RouteTransformResult, RouteTransformTask } from './route-transform-tasks.js';
2
- type WithoutRequiredSource<Task> = Task extends RouteTransformTask ? Omit<Task, 'code'> & {
3
- code?: string;
4
- } : never;
5
- export type CachedRouteTransformTask = WithoutRequiredSource<RouteTransformTask>;
6
- export type WorkerRequest = {
7
- id: number;
8
- task: RouteTransformTask | CachedRouteTransformTask;
9
- sourceCacheKey?: string;
10
- };
11
- export type WorkerErrorPayload = {
12
- name?: string;
13
- message: string;
14
- stack?: string;
15
- };
16
- export type WorkerResponse = {
17
- id: number;
18
- ok: true;
19
- result: RouteTransformResult;
20
- } | {
21
- id: number;
22
- ok: false;
23
- error: WorkerErrorPayload;
24
- };
25
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,38 +0,0 @@
1
- import { parentPort } from "node:worker_threads";
2
- import { setBoundedCacheEntry, executeRouteTransformTask } from "./451.js";
3
- if (!parentPort) throw Error('parallel route transform worker requires parentPort');
4
- let sourceCache = new Map();
5
- parentPort.on('message', async ({ id, task, sourceCacheKey })=>{
6
- try {
7
- let hydratedTask = (({ task, sourceCacheKey })=>{
8
- if (!sourceCacheKey) return task;
9
- if ('string' == typeof task.code) return setBoundedCacheEntry(sourceCache, sourceCacheKey, task.code, 2048), task;
10
- let code = sourceCache.get(sourceCacheKey);
11
- if (void 0 === code) throw Error(`Missing cached route transform source for ${sourceCacheKey}.`);
12
- return {
13
- ...task,
14
- code
15
- };
16
- })({
17
- task,
18
- sourceCacheKey
19
- }), result = await executeRouteTransformTask(hydratedTask);
20
- parentPort?.postMessage({
21
- id,
22
- ok: !0,
23
- result
24
- });
25
- } catch (error) {
26
- parentPort?.postMessage({
27
- id,
28
- ok: !1,
29
- error: error instanceof Error ? {
30
- name: error.name,
31
- message: error.message,
32
- stack: error.stack
33
- } : {
34
- message: String(error)
35
- }
36
- });
37
- }
38
- });
@@ -1,29 +0,0 @@
1
- import { type RouteTransformResult, type RouteTransformTask, type RouteTransformTaskOptions } from './route-transform-tasks.js';
2
- import type { PluginOptions } from './types.js';
3
- import type { WorkerRequest, WorkerResponse } from './parallel-route-transform-protocol.js';
4
- export type ParallelRouteTransformConfig = NonNullable<PluginOptions['parallelRouteTransform']> extends infer Config ? Exclude<Config, false> : never;
5
- export type RouteTransformExecutorOptions = RouteTransformTaskOptions & {
6
- parallelRouteTransform?: PluginOptions['parallelRouteTransform'];
7
- splitRouteModules?: boolean;
8
- isBuild?: boolean;
9
- };
10
- export type RouteTransformExecutor = {
11
- run: (task: RouteTransformTask) => Promise<RouteTransformResult>;
12
- prewarm: () => void;
13
- close: () => Promise<void>;
14
- };
15
- type RouteTransformWorker = {
16
- on(event: 'message', handler: (response: WorkerResponse) => void): unknown;
17
- on(event: 'error', handler: (error: Error) => void): unknown;
18
- on(event: 'exit', handler: (code: number) => void): unknown;
19
- postMessage(message: WorkerRequest): void;
20
- terminate(): Promise<number> | number;
21
- };
22
- type RouteTransformWorkerFactory = () => RouteTransformWorker;
23
- export declare const getDefaultWorkerCount: (cpuCount?: number, { isBuild }?: {
24
- isBuild?: boolean;
25
- }) => number;
26
- export declare const shouldParallelizeRouteTransforms: (routeCount: number) => boolean;
27
- export declare const createRouteTransformExecutor: ({ parallelRouteTransform, routeChunkCache, splitRouteModules, isBuild, }?: RouteTransformExecutorOptions) => RouteTransformExecutor;
28
- export declare const createRouteTransformExecutorForTesting: (options: RouteTransformExecutorOptions, createWorker: RouteTransformWorkerFactory) => RouteTransformExecutor;
29
- export {};
@@ -1,35 +0,0 @@
1
- import type {
2
- RouteTransformResult,
3
- RouteTransformTask,
4
- } from './route-transform-tasks.js';
5
-
6
- type WithoutRequiredSource<Task> = Task extends RouteTransformTask
7
- ? Omit<Task, 'code'> & { code?: string }
8
- : never;
9
-
10
- export type CachedRouteTransformTask =
11
- WithoutRequiredSource<RouteTransformTask>;
12
-
13
- export type WorkerRequest = {
14
- id: number;
15
- task: RouteTransformTask | CachedRouteTransformTask;
16
- sourceCacheKey?: string;
17
- };
18
-
19
- export type WorkerErrorPayload = {
20
- name?: string;
21
- message: string;
22
- stack?: string;
23
- };
24
-
25
- export type WorkerResponse =
26
- | {
27
- id: number;
28
- ok: true;
29
- result: RouteTransformResult;
30
- }
31
- | {
32
- id: number;
33
- ok: false;
34
- error: WorkerErrorPayload;
35
- };
@@ -1,82 +0,0 @@
1
- import { parentPort } from 'node:worker_threads';
2
- import { setBoundedCacheEntry } from './bounded-cache.js';
3
- import {
4
- executeRouteTransformTask,
5
- type RouteTransformTask,
6
- } from './route-transform-tasks.js';
7
- import type {
8
- WorkerErrorPayload,
9
- WorkerRequest,
10
- WorkerResponse,
11
- } from './parallel-route-transform-protocol.js';
12
-
13
- const serializeError = (error: unknown): WorkerErrorPayload => {
14
- if (error instanceof Error) {
15
- return {
16
- name: error.name,
17
- message: error.message,
18
- stack: error.stack,
19
- };
20
- }
21
- return {
22
- message: String(error),
23
- };
24
- };
25
-
26
- if (!parentPort) {
27
- throw new Error('parallel route transform worker requires parentPort');
28
- }
29
-
30
- const MAX_SOURCE_CACHE_ENTRIES = 2048;
31
- const sourceCache = new Map<string, string>();
32
-
33
- const hydrateTaskSource = ({
34
- task,
35
- sourceCacheKey,
36
- }: Pick<WorkerRequest, 'task' | 'sourceCacheKey'>): RouteTransformTask => {
37
- if (!sourceCacheKey) {
38
- return task as RouteTransformTask;
39
- }
40
-
41
- if (typeof task.code === 'string') {
42
- setBoundedCacheEntry(
43
- sourceCache,
44
- sourceCacheKey,
45
- task.code,
46
- MAX_SOURCE_CACHE_ENTRIES
47
- );
48
- return task as RouteTransformTask;
49
- }
50
-
51
- const code = sourceCache.get(sourceCacheKey);
52
- if (code === undefined) {
53
- throw new Error(
54
- `Missing cached route transform source for ${sourceCacheKey}.`
55
- );
56
- }
57
- return {
58
- ...task,
59
- code,
60
- } as RouteTransformTask;
61
- };
62
-
63
- parentPort.on(
64
- 'message',
65
- async ({ id, task, sourceCacheKey }: WorkerRequest) => {
66
- try {
67
- const hydratedTask = hydrateTaskSource({ task, sourceCacheKey });
68
- const result = await executeRouteTransformTask(hydratedTask);
69
- parentPort?.postMessage({
70
- id,
71
- ok: true,
72
- result,
73
- } satisfies WorkerResponse);
74
- } catch (error) {
75
- parentPort?.postMessage({
76
- id,
77
- ok: false,
78
- error: serializeError(error),
79
- } satisfies WorkerResponse);
80
- }
81
- }
82
- );