rsbuild-plugin-react-router 0.2.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 (105) hide show
  1. package/README.md +213 -201
  2. package/dist/451.js +1103 -0
  3. package/dist/bounded-cache.d.ts +1 -0
  4. package/dist/build-manifest.d.ts +7 -4
  5. package/dist/build-output-transforms.d.ts +30 -0
  6. package/dist/concurrency.d.ts +3 -0
  7. package/dist/config-imports.d.ts +10 -0
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/dev-background-resources.d.ts +38 -0
  10. package/dist/dev-generation.d.ts +25 -0
  11. package/dist/dev-runtime-artifacts.d.ts +47 -0
  12. package/dist/dev-runtime-compilation.d.ts +47 -0
  13. package/dist/dev-runtime-controller.d.ts +14 -0
  14. package/dist/dev-runtime-session.d.ts +34 -0
  15. package/dist/dev-server.d.ts +16 -2
  16. package/dist/effect-runtime.d.ts +18 -0
  17. package/dist/export-utils.d.ts +15 -7
  18. package/dist/index.cjs +13775 -1412
  19. package/dist/index.d.ts +8 -1
  20. package/dist/index.js +9429 -1501
  21. package/dist/lazy-compilation-prewarm.d.ts +25 -0
  22. package/dist/lazy-compilation.d.ts +6 -0
  23. package/dist/manifest.d.ts +48 -10
  24. package/dist/modify-browser-manifest.d.ts +30 -13
  25. package/dist/parallel-route-transform-protocol.d.ts +25 -0
  26. package/dist/parallel-route-transform-worker.d.ts +1 -0
  27. package/dist/parallel-route-transform-worker.js +38 -0
  28. package/dist/parallel-route-transforms.d.ts +29 -0
  29. package/dist/performance.d.ts +26 -0
  30. package/dist/plugin-utils.d.ts +2 -12
  31. package/dist/prerender-build.d.ts +36 -0
  32. package/dist/prerender.d.ts +27 -2
  33. package/dist/react-router-config.d.ts +15 -5
  34. package/dist/route-artifacts.d.ts +33 -0
  35. package/dist/route-ast.d.ts +21 -0
  36. package/dist/route-chunks.d.ts +9 -1
  37. package/dist/route-component-transform.d.ts +5 -0
  38. package/dist/route-export-pruning.d.ts +6 -0
  39. package/dist/route-export-resolution.d.ts +5 -0
  40. package/dist/route-transform-tasks.d.ts +47 -0
  41. package/dist/route-watch.d.ts +27 -0
  42. package/dist/server-build-plan.d.ts +22 -0
  43. package/dist/server-build-resolution.d.ts +3 -0
  44. package/dist/server-utils.d.ts +3 -2
  45. package/dist/ssr-externals.d.ts +1 -0
  46. package/dist/templates/entry.server.cjs +3 -3
  47. package/dist/templates/entry.server.js +3 -3
  48. package/dist/typegen.d.ts +15 -0
  49. package/dist/types.d.ts +41 -14
  50. package/dist/virtual-modules.d.ts +2 -0
  51. package/dist/warnings/warn-on-client-source-maps.d.ts +1 -0
  52. package/dist/yuku.d.ts +15 -0
  53. package/package.json +24 -19
  54. package/src/bounded-cache.ts +18 -0
  55. package/src/build-manifest.ts +205 -0
  56. package/src/build-output-transforms.ts +273 -0
  57. package/src/concurrency.ts +15 -0
  58. package/src/config-imports.ts +83 -0
  59. package/src/constants.ts +91 -0
  60. package/src/dev-background-resources.ts +255 -0
  61. package/src/dev-generation.ts +690 -0
  62. package/src/dev-runtime-artifacts.ts +239 -0
  63. package/src/dev-runtime-compilation.ts +171 -0
  64. package/src/dev-runtime-controller.ts +542 -0
  65. package/src/dev-runtime-session.ts +191 -0
  66. package/src/dev-server.ts +88 -0
  67. package/src/effect-runtime.ts +130 -0
  68. package/src/export-utils.ts +278 -0
  69. package/src/index.ts +990 -0
  70. package/src/lazy-compilation-prewarm.ts +279 -0
  71. package/src/lazy-compilation.ts +101 -0
  72. package/src/manifest.ts +591 -0
  73. package/src/modify-browser-manifest.ts +246 -0
  74. package/src/parallel-route-transform-protocol.ts +35 -0
  75. package/src/parallel-route-transform-worker.ts +82 -0
  76. package/src/parallel-route-transforms.ts +458 -0
  77. package/src/performance.ts +254 -0
  78. package/src/plugin-utils.ts +82 -0
  79. package/src/prerender-build.ts +687 -0
  80. package/src/prerender.ts +349 -0
  81. package/src/react-router-config.ts +245 -0
  82. package/src/route-artifacts.ts +155 -0
  83. package/src/route-ast.ts +163 -0
  84. package/src/route-chunks.ts +857 -0
  85. package/src/route-component-transform.ts +314 -0
  86. package/src/route-config.ts +106 -0
  87. package/src/route-export-pruning.ts +668 -0
  88. package/src/route-export-resolution.ts +329 -0
  89. package/src/route-transform-tasks.ts +249 -0
  90. package/src/route-watch.ts +357 -0
  91. package/src/server-build-plan.ts +91 -0
  92. package/src/server-build-resolution.ts +131 -0
  93. package/src/server-utils.ts +111 -0
  94. package/src/ssr-externals.ts +59 -0
  95. package/src/templates/context.ts +12 -0
  96. package/src/templates/entry.client.tsx +12 -0
  97. package/src/templates/entry.server.tsx +76 -0
  98. package/src/typegen.ts +178 -0
  99. package/src/types.ts +92 -0
  100. package/src/validation/validate-plugin-order.ts +76 -0
  101. package/src/virtual-modules.ts +30 -0
  102. package/src/warnings/warn-on-client-source-maps.ts +96 -0
  103. package/src/yuku.ts +67 -0
  104. package/dist/0~rslib-runtime.js +0 -16
  105. package/dist/babel.d.ts +0 -8
@@ -0,0 +1,59 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { sep } from 'node:path';
4
+
5
+ const REACT_ROUTER_EXTERNALS = [
6
+ 'react-router',
7
+ 'react-router-dom',
8
+ '@react-router/architect',
9
+ '@react-router/cloudflare',
10
+ '@react-router/dev',
11
+ '@react-router/express',
12
+ '@react-router/node',
13
+ '@react-router/serve',
14
+ ];
15
+
16
+ const requireFromHere = createRequire(import.meta.url);
17
+
18
+ export function resolvePackageJson(
19
+ name: string,
20
+ rootDirectory: string
21
+ ): string | null {
22
+ try {
23
+ return requireFromHere.resolve(`${name}/package.json`, {
24
+ paths: [rootDirectory],
25
+ });
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function safeRealpath(pathname: string): string {
32
+ try {
33
+ return realpathSync(pathname);
34
+ } catch {
35
+ return pathname;
36
+ }
37
+ }
38
+
39
+ function isPathInNodeModules(pathname: string): boolean {
40
+ return pathname.split(sep).includes('node_modules');
41
+ }
42
+
43
+ export function getSsrExternals(rootDirectory: string): string[] {
44
+ const externals: string[] = [];
45
+
46
+ for (const name of REACT_ROUTER_EXTERNALS) {
47
+ const resolved = resolvePackageJson(name, rootDirectory);
48
+ if (!resolved) {
49
+ continue;
50
+ }
51
+
52
+ const realPath = safeRealpath(resolved);
53
+ if (!isPathInNodeModules(realPath)) {
54
+ externals.push(name);
55
+ }
56
+ }
57
+
58
+ return externals;
59
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext } from 'react';
2
+ import type { Context } from 'react';
3
+
4
+ export interface Assets {
5
+ scriptTags: string[];
6
+ styleTags: string[];
7
+ }
8
+
9
+ export const AssetsContext: Context<Assets> = createContext<Assets>({
10
+ scriptTags: [],
11
+ styleTags: [],
12
+ });
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+ import { hydrateRoot } from 'react-dom/client';
3
+ import { HydratedRouter } from 'react-router/dom';
4
+
5
+ React.startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <React.StrictMode>
9
+ <HydratedRouter />
10
+ </React.StrictMode>
11
+ );
12
+ });
@@ -0,0 +1,76 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { createReadableStreamFromReadable } from '@react-router/node';
3
+ import { isbot } from 'isbot';
4
+ import * as React from 'react';
5
+ import type { RenderToPipeableStreamOptions } from 'react-dom/server';
6
+ import { renderToPipeableStream } from 'react-dom/server';
7
+ import type {
8
+ // AppLoadContext,
9
+ EntryContext,
10
+ } from 'react-router';
11
+ import { ServerRouter } from 'react-router';
12
+
13
+ const ABORT_DELAY = 5_000;
14
+
15
+ export default function handleRequest(
16
+ request: Request,
17
+ responseStatusCode: number,
18
+ responseHeaders: Headers,
19
+ routerContext: EntryContext
20
+ // loadContext: AppLoadContext
21
+ ): Promise<Response> {
22
+ return new Promise<Response>((resolve, reject) => {
23
+ let shellRendered = false;
24
+ let statusCode = responseStatusCode;
25
+ const userAgent = request.headers.get('user-agent');
26
+
27
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
28
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
29
+ const readyOption: keyof RenderToPipeableStreamOptions =
30
+ (userAgent && isbot(userAgent)) || routerContext.isSpaMode
31
+ ? 'onAllReady'
32
+ : 'onShellReady';
33
+ let abortDelay: ReturnType<typeof setTimeout> | undefined;
34
+ let ready = false;
35
+
36
+ const { pipe, abort } = renderToPipeableStream(
37
+ <ServerRouter context={routerContext} url={request.url} />,
38
+ {
39
+ [readyOption]() {
40
+ ready = true;
41
+ if (readyOption === 'onAllReady' && abortDelay) {
42
+ clearTimeout(abortDelay);
43
+ }
44
+ shellRendered = true;
45
+ const body = new PassThrough();
46
+ const stream = createReadableStreamFromReadable(body);
47
+
48
+ responseHeaders.set('Content-Type', 'text/html');
49
+
50
+ resolve(
51
+ new Response(stream, {
52
+ headers: responseHeaders,
53
+ status: statusCode,
54
+ })
55
+ );
56
+
57
+ pipe(body);
58
+ },
59
+ onShellError(error: unknown) {
60
+ reject(error);
61
+ },
62
+ onError(error: unknown) {
63
+ statusCode = 500;
64
+ if (shellRendered) {
65
+ console.error(error);
66
+ }
67
+ },
68
+ }
69
+ );
70
+
71
+ abortDelay = setTimeout(abort, ABORT_DELAY);
72
+ if (readyOption === 'onAllReady' && ready) {
73
+ clearTimeout(abortDelay);
74
+ }
75
+ });
76
+ }
package/src/typegen.ts ADDED
@@ -0,0 +1,178 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import type { RsbuildPluginAPI } from '@rsbuild/core';
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';
8
+
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
+ };
32
+
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;
56
+ }
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)
86
+ .finally(() => {
87
+ if (typegenProcess === process) {
88
+ typegenProcess = undefined;
89
+ }
90
+ });
91
+ };
92
+
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
+ },
158
+ });
159
+
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();
169
+ });
170
+ }
171
+
172
+ api.onCloseDevServer(async () => {
173
+ await devWatchTask.cancel();
174
+ await resolvedRunner.closeWatch();
175
+ });
176
+
177
+ api.onBeforeBuild(() => resolvedRunner.runBuild());
178
+ };
package/src/types.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { RsbuildConfig } from '@rsbuild/core';
2
+
3
+ export type Route = {
4
+ id: string;
5
+ parentId?: string;
6
+ file: string;
7
+ path?: string;
8
+ index?: boolean;
9
+ caseSensitive?: boolean;
10
+ children?: Route[];
11
+ };
12
+
13
+ export type PluginOptions = {
14
+ /**
15
+ * Whether to disable automatic middleware setup for custom server implementation.
16
+ * Use this when you want to handle server setup manually.
17
+ * @default false
18
+ */
19
+ customServer?: boolean;
20
+
21
+ /**
22
+ * The output format for server builds.
23
+ * When omitted, React Router's `serverModuleFormat` selects the emitted
24
+ * Rsbuild format (`"esm"` -> `"module"`, `"cjs"` -> `"commonjs"`).
25
+ */
26
+ serverOutput?: 'module' | 'commonjs';
27
+
28
+ /**
29
+ * Federation mode configuration
30
+ */
31
+ federation?: boolean;
32
+
33
+ /**
34
+ * Rsbuild dev-only lazy compilation behavior.
35
+ *
36
+ * React Router's browser manifest remains eager so initial dev requests can
37
+ * discover browser assets without lazy proxy delays.
38
+ *
39
+ * Pass `false` to disable.
40
+ * @default true
41
+ */
42
+ lazyCompilation?: NonNullable<RsbuildConfig['dev']>['lazyCompilation'];
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
+
53
+ /**
54
+ * Emit structured React Router plugin timing logs.
55
+ * @default false
56
+ */
57
+ logPerformance?: boolean;
58
+
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.
65
+ */
66
+ parallelRouteTransform?: boolean | number;
67
+
68
+ /**
69
+ * Called when the route graph changes during development.
70
+ * Programmatic/custom servers can use this to recreate their Rsbuild server;
71
+ * the CLI uses its built-in reload-server watcher when this is omitted. This
72
+ * notification is not awaited, so it may safely close the current server.
73
+ */
74
+ onRouteTopologyChange?: () => void | Promise<void>;
75
+ };
76
+
77
+ export type RouteManifestItem = Omit<Route, 'file' | 'children'> & {
78
+ module: string;
79
+ clientActionModule?: string;
80
+ clientLoaderModule?: string;
81
+ clientMiddlewareModule?: string;
82
+ hydrateFallbackModule?: string;
83
+ hasAction: boolean;
84
+ hasLoader: boolean;
85
+ hasClientAction: boolean;
86
+ hasClientLoader: boolean;
87
+ hasClientMiddleware: boolean;
88
+ hasDefaultExport: boolean;
89
+ hasErrorBoundary: boolean;
90
+ imports: string[];
91
+ css: string[];
92
+ };
@@ -0,0 +1,76 @@
1
+ import type { RsbuildConfig } from '@rsbuild/core';
2
+
3
+ function pluginId(name: string) {
4
+ return `rsbuild:${name}`;
5
+ }
6
+
7
+ function flattenPlugins(input: unknown): Array<{ name?: string }> {
8
+ if (!Array.isArray(input)) return [];
9
+ const out: Array<{ name?: string }> = [];
10
+ for (const item of input) {
11
+ if (!item) continue;
12
+ if (Array.isArray(item)) {
13
+ out.push(...flattenPlugins(item));
14
+ continue;
15
+ }
16
+ if (typeof (item as any)?.then === 'function') {
17
+ // Skip promises - normalized config should already be resolved.
18
+ continue;
19
+ }
20
+ if (typeof item === 'object') {
21
+ out.push(item as any);
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function indexOfPlugin(plugins: Array<{ name?: string }>, names: string[]) {
28
+ return plugins.findIndex(p => (p?.name ? names.includes(p.name) : false));
29
+ }
30
+
31
+ export type PluginOrderIssue =
32
+ | {
33
+ kind: 'error';
34
+ message: string;
35
+ }
36
+ | {
37
+ kind: 'warn';
38
+ message: string;
39
+ };
40
+
41
+ export function validatePluginOrderFromConfig(
42
+ config: RsbuildConfig
43
+ ): PluginOrderIssue[] {
44
+ const plugins = flattenPlugins(config.plugins);
45
+ const issues: PluginOrderIssue[] = [];
46
+
47
+ // Best-effort port of upstream validate-plugin-order:
48
+ // - `rsbuild:mdx` must be before `rsbuild:react-router` so that route files
49
+ // can be authored in MDX and still have exports parsed correctly.
50
+ const rrIndex = indexOfPlugin(plugins, ['rsbuild:react-router']);
51
+ const mdxIndex = indexOfPlugin(plugins, [pluginId('mdx')]);
52
+ if (rrIndex >= 0 && mdxIndex >= 0 && mdxIndex > rrIndex) {
53
+ issues.push({
54
+ kind: 'error',
55
+ message:
56
+ `The \"rsbuild:mdx\" plugin should be placed before the React Router plugin ` +
57
+ `in your Rsbuild config. This ensures route modules authored in MDX are ` +
58
+ `compiled before React Router inspects route exports.`,
59
+ });
60
+ }
61
+
62
+ // Rsbuild/React Router expectation: user should also include React support
63
+ // (JSX transform + refresh). We can only warn since Rsbuild configs may not
64
+ // use React at all (or may provide a custom JSX pipeline).
65
+ const hasReact = plugins.some(p => p?.name === pluginId('react'));
66
+ if (rrIndex >= 0 && !hasReact) {
67
+ issues.push({
68
+ kind: 'warn',
69
+ message:
70
+ `React Router plugin detected without \"rsbuild:react\". If you are using ` +
71
+ `JSX/TSX route modules, add \"@rsbuild/plugin-react\" to your plugins list.`,
72
+ });
73
+ }
74
+
75
+ return issues;
76
+ }
@@ -0,0 +1,30 @@
1
+ const VIRTUAL_MODULE_PREFIX = 'virtual/react-router/';
2
+
3
+ export const getVirtualModuleFilePath = (moduleId: string): string => {
4
+ if (!moduleId.startsWith(VIRTUAL_MODULE_PREFIX)) {
5
+ throw new Error(
6
+ `Virtual module id must start with ${JSON.stringify(VIRTUAL_MODULE_PREFIX)}: ${moduleId}`
7
+ );
8
+ }
9
+
10
+ const relativeId = moduleId.slice(VIRTUAL_MODULE_PREFIX.length);
11
+ const segments = relativeId.split('/');
12
+ if (
13
+ !relativeId ||
14
+ segments.some(segment => !segment || segment === '.' || segment === '..')
15
+ ) {
16
+ throw new Error(`Invalid virtual module id: ${moduleId}`);
17
+ }
18
+
19
+ return `node_modules/${moduleId}.js`;
20
+ };
21
+
22
+ export const mapVirtualModules = (
23
+ modules: Record<string, string>
24
+ ): Record<string, string> =>
25
+ Object.fromEntries(
26
+ Object.entries(modules).map(([moduleId, contents]) => [
27
+ getVirtualModuleFilePath(moduleId),
28
+ contents,
29
+ ])
30
+ );
@@ -0,0 +1,96 @@
1
+ import type { NormalizedConfig } from '@rsbuild/core';
2
+
3
+ type Warn = (message: string) => void;
4
+ type ToolsRspackConfig = NonNullable<NormalizedConfig['tools']>['rspack'];
5
+ type SourceMapConfigObject = { js?: unknown };
6
+
7
+ function isProdBuild(mode?: string): boolean {
8
+ // Prefer Rsbuild's normalized `mode` (explicit) and fall back to NODE_ENV.
9
+ return mode === 'production' || process.env.NODE_ENV === 'production';
10
+ }
11
+
12
+ export function isSourceMapEnabled(value: unknown): boolean {
13
+ // Rsbuild normalizes `output.sourceMap` into either:
14
+ // - boolean
15
+ // - { js?: devtool; css: boolean }
16
+ if (value === true) return true;
17
+ if (value === false || value == null) return false;
18
+ if (typeof value === 'string') return true;
19
+ if (typeof value === 'object') {
20
+ const js = (value as SourceMapConfigObject).js;
21
+ // Any truthy devtool string/object means source maps are on for JS.
22
+ return Boolean(js);
23
+ }
24
+ return false;
25
+ }
26
+
27
+ function isDevtoolSourceMap(value: unknown): boolean {
28
+ if (value === true) return true;
29
+ if (value == null || value === false) return false;
30
+ if (typeof value === 'string') {
31
+ return value.includes('source-map');
32
+ }
33
+ // Unknown object shape - treat as enabled to be safe.
34
+ return typeof value === 'object';
35
+ }
36
+
37
+ export function getClientSourceMapSetting(
38
+ normalized: NormalizedConfig,
39
+ clientEnvName = 'web'
40
+ ): unknown {
41
+ // Prefer environment setting, fallback to global.
42
+ return (
43
+ normalized.environments?.[clientEnvName]?.output?.sourceMap ??
44
+ normalized.output?.sourceMap
45
+ );
46
+ }
47
+
48
+ export function getClientDevtoolSetting(
49
+ normalized: NormalizedConfig,
50
+ clientEnvName = 'web'
51
+ ): unknown {
52
+ const envTools = normalized.environments?.[clientEnvName]?.tools?.rspack;
53
+ const rootTools = normalized.tools?.rspack;
54
+ return (
55
+ getDevtoolFromRspackConfig(envTools) ??
56
+ getDevtoolFromRspackConfig(rootTools)
57
+ );
58
+ }
59
+
60
+ function getDevtoolFromRspackConfig(config?: ToolsRspackConfig): unknown {
61
+ if (!config) return undefined;
62
+ if (typeof config === 'function') return undefined;
63
+ if (typeof config !== 'object') return undefined;
64
+ return (config as { devtool?: unknown }).devtool;
65
+ }
66
+
67
+ export function warnOnClientSourceMaps(
68
+ normalized: NormalizedConfig,
69
+ warn: Warn,
70
+ clientEnvName = 'web'
71
+ ): void {
72
+ // Only warn on production builds.
73
+ if (!isProdBuild(normalized.mode)) {
74
+ return;
75
+ }
76
+
77
+ const sourceMapSetting = getClientSourceMapSetting(normalized, clientEnvName);
78
+ const devtoolSetting = getClientDevtoolSetting(normalized, clientEnvName);
79
+ if (
80
+ !isSourceMapEnabled(sourceMapSetting) &&
81
+ !isDevtoolSourceMap(devtoolSetting)
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ warn(
87
+ [
88
+ '',
89
+ ' WARNING: Source maps are enabled in production',
90
+ ' This makes your server code publicly visible in the browser.',
91
+ ' This is highly discouraged! If you insist, ensure that you are using',
92
+ ' environment variables for secrets and not hard-coding them in your source code.',
93
+ '',
94
+ ].join('\n')
95
+ );
96
+ }
package/src/yuku.ts ADDED
@@ -0,0 +1,67 @@
1
+ import {
2
+ parse as yukuParse,
3
+ walk,
4
+ type ParseOptions,
5
+ type ParseResult,
6
+ } from 'yuku-parser';
7
+ import type { Rspack } from '@rsbuild/core';
8
+ import { print } from 'yuku-codegen';
9
+
10
+ export const parse = (
11
+ code: string,
12
+ options: ParseOptions = {}
13
+ ): ParseResult => {
14
+ const result = yukuParse(code, {
15
+ ...options,
16
+ sourceType: options.sourceType ?? 'module',
17
+ lang: options.lang ?? 'tsx',
18
+ attachComments: options.attachComments ?? true,
19
+ });
20
+ const errors = result.diagnostics.filter(
21
+ diagnostic => diagnostic.severity === 'error'
22
+ );
23
+ if (errors.length > 0) {
24
+ throw new Error(errors.map(error => error.message).join('\n'));
25
+ }
26
+ return result;
27
+ };
28
+
29
+ export const traverse: typeof walk = walk;
30
+
31
+ export const generate = (
32
+ ast: ParseResult | { type: 'Program' },
33
+ options: {
34
+ sourceMaps?: boolean;
35
+ filename?: string;
36
+ sourceFileName?: string;
37
+ } = {}
38
+ ): { code: string; map: Rspack.RawSourceMap | null } => {
39
+ const result = 'program' in ast ? ast : { program: ast, lineStarts: [] };
40
+ const generated = print(result.program as Parameters<typeof print>[0], {
41
+ comments: true,
42
+ sourceMaps: options.sourceMaps
43
+ ? {
44
+ lineStarts: result.lineStarts,
45
+ file: options.filename,
46
+ sourceFileName: options.sourceFileName,
47
+ }
48
+ : undefined,
49
+ });
50
+ if (generated.errors.length > 0) {
51
+ throw new Error(generated.errors.map(error => error.message).join('\n'));
52
+ }
53
+ const map = generated.map
54
+ ? {
55
+ ...generated.map,
56
+ file: generated.map.file ?? options.filename ?? '',
57
+ sourceRoot: generated.map.sourceRoot ?? undefined,
58
+ sourcesContent:
59
+ generated.map.sourcesContent?.map(source => source ?? '') ??
60
+ undefined,
61
+ }
62
+ : null;
63
+
64
+ return { code: generated.code, map };
65
+ };
66
+
67
+ export type { ParseResult };