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,22 @@
1
+ import type { ReactRouterDevBuildPlan } from './dev-runtime-artifacts.js';
2
+ import type { Route } from './types.js';
3
+ export type ReactRouterServerBundleEntry = {
4
+ bundleId: string;
5
+ entryName: string;
6
+ };
7
+ export type ReactRouterServerBuildPlan = ReactRouterDevBuildPlan & {
8
+ serverBundleEntries: ReactRouterServerBundleEntry[];
9
+ };
10
+ export declare const createReactRouterServerBuildPlan: ({ routesByServerBundleId, serverBuildFile, defaultEntryName, }: {
11
+ routesByServerBundleId: Record<string, Record<string, Route>>;
12
+ serverBuildFile: string | undefined;
13
+ defaultEntryName: string;
14
+ }) => ReactRouterServerBuildPlan;
15
+ export declare const createReactRouterNodeEntries: ({ hasServerApp, isBuild, serverAppPath, entryServerPath, defaultEntryName, serverBundleEntries, }: {
16
+ hasServerApp: boolean;
17
+ isBuild: boolean;
18
+ serverAppPath: string;
19
+ entryServerPath: string;
20
+ defaultEntryName: string;
21
+ serverBundleEntries: readonly ReactRouterServerBundleEntry[];
22
+ }) => Record<string, string>;
@@ -0,0 +1,3 @@
1
+ import * as Effect from 'effect/Effect';
2
+ import type { ServerBuild } from 'react-router';
3
+ export declare function resolveServerBuildModuleEffect(buildModule: unknown, source: string): Effect.Effect<ServerBuild, Error, never>;
@@ -1,3 +1,4 @@
1
+ import type { ServerBuild } from 'react-router';
1
2
  import type { Route } from './types.js';
2
3
  /**
3
4
  * Generates the server build template string.
@@ -33,6 +34,6 @@ interface ServerBuildOptions {
33
34
  declare function generateServerBuild(routes: Record<string, Route>, options: ServerBuildOptions & {
34
35
  federation?: boolean;
35
36
  }): string;
36
- export declare const normalizeBuildModule: <T extends Record<string, any>>(build: T) => T;
37
- export declare const resolveBuildExports: <T extends Record<string, any>>(build: T) => Promise<T>;
37
+ export declare function resolveServerBuildModule(buildModule: unknown, source: string): Promise<ServerBuild>;
38
+ export declare function resolveReactRouterServerBuild(buildModule: unknown): Promise<ServerBuild>;
38
39
  export { generateServerBuild };
@@ -1 +1,2 @@
1
+ export declare function resolvePackageJson(name: string, rootDirectory: string): string | null;
1
2
  export declare function getSsrExternals(rootDirectory: string): string[];
@@ -23,12 +23,12 @@ __webpack_require__.r(__webpack_exports__), __webpack_require__.d(__webpack_expo
23
23
  const external_node_stream_namespaceObject = require("node:stream"), node_namespaceObject = require("@react-router/node"), external_isbot_namespaceObject = require("isbot"), external_react_namespaceObject = require("react"), server_namespaceObject = require("react-dom/server"), external_react_router_namespaceObject = require("react-router"), ABORT_DELAY = 5000;
24
24
  function handleRequest(request, responseStatusCode, responseHeaders, routerContext) {
25
25
  return new Promise((resolve, reject)=>{
26
- let shellRendered = !1, statusCode = responseStatusCode, userAgent = request.headers.get('user-agent'), readyOption = userAgent && (0, external_isbot_namespaceObject.isbot)(userAgent) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady', { pipe, abort } = (0, server_namespaceObject.renderToPipeableStream)(external_react_namespaceObject.createElement(external_react_router_namespaceObject.ServerRouter, {
26
+ let abortDelay, shellRendered = !1, statusCode = responseStatusCode, userAgent = request.headers.get('user-agent'), readyOption = userAgent && (0, external_isbot_namespaceObject.isbot)(userAgent) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady', ready = !1, { pipe, abort } = (0, server_namespaceObject.renderToPipeableStream)(external_react_namespaceObject.createElement(external_react_router_namespaceObject.ServerRouter, {
27
27
  context: routerContext,
28
28
  url: request.url
29
29
  }), {
30
30
  [readyOption] () {
31
- shellRendered = !0;
31
+ ready = !0, 'onAllReady' === readyOption && abortDelay && clearTimeout(abortDelay), shellRendered = !0;
32
32
  let body = new external_node_stream_namespaceObject.PassThrough(), stream = (0, node_namespaceObject.createReadableStreamFromReadable)(body);
33
33
  responseHeaders.set('Content-Type', 'text/html'), resolve(new Response(stream, {
34
34
  headers: responseHeaders,
@@ -42,7 +42,7 @@ function handleRequest(request, responseStatusCode, responseHeaders, routerConte
42
42
  statusCode = 500, shellRendered && console.error(error);
43
43
  }
44
44
  });
45
- setTimeout(abort, 5000);
45
+ abortDelay = setTimeout(abort, 5000), 'onAllReady' === readyOption && ready && clearTimeout(abortDelay);
46
46
  });
47
47
  }
48
48
  for(var __rspack_i in exports.default = __webpack_exports__.default, __webpack_exports__)-1 === [
@@ -6,12 +6,12 @@ import { ServerRouter } from "react-router";
6
6
  import * as __rspack_external_react from "react";
7
7
  export default function(request, responseStatusCode, responseHeaders, routerContext) {
8
8
  return new Promise((resolve, reject)=>{
9
- let shellRendered = !1, statusCode = responseStatusCode, userAgent = request.headers.get('user-agent'), readyOption = userAgent && isbot(userAgent) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady', { pipe, abort } = renderToPipeableStream(__rspack_external_react.createElement(ServerRouter, {
9
+ let abortDelay, shellRendered = !1, statusCode = responseStatusCode, userAgent = request.headers.get('user-agent'), readyOption = userAgent && isbot(userAgent) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady', ready = !1, { pipe, abort } = renderToPipeableStream(__rspack_external_react.createElement(ServerRouter, {
10
10
  context: routerContext,
11
11
  url: request.url
12
12
  }), {
13
13
  [readyOption] () {
14
- shellRendered = !0;
14
+ ready = !0, 'onAllReady' === readyOption && abortDelay && clearTimeout(abortDelay), shellRendered = !0;
15
15
  let body = new PassThrough(), stream = createReadableStreamFromReadable(body);
16
16
  responseHeaders.set('Content-Type', 'text/html'), resolve(new Response(stream, {
17
17
  headers: responseHeaders,
@@ -25,6 +25,6 @@ export default function(request, responseStatusCode, responseHeaders, routerCont
25
25
  statusCode = 500, shellRendered && console.error(error);
26
26
  }
27
27
  });
28
- setTimeout(abort, 5000);
28
+ abortDelay = setTimeout(abort, 5000), 'onAllReady' === readyOption && ready && clearTimeout(abortDelay);
29
29
  });
30
30
  };
@@ -0,0 +1,15 @@
1
+ import type { RsbuildPluginAPI } from '@rsbuild/core';
2
+ type Execa = typeof import('execa').execa;
3
+ type LoadExeca = () => Promise<Execa>;
4
+ export type ReactRouterTypegenRunner = {
5
+ startWatch(): Promise<void>;
6
+ closeWatch(): Promise<void>;
7
+ runBuild(): Promise<void>;
8
+ };
9
+ export declare const createReactRouterTypegenRunner: (loadExeca?: LoadExeca, appDirectory?: string) => ReactRouterTypegenRunner;
10
+ export declare const registerReactRouterTypegen: (api: RsbuildPluginAPI, { runner, devWatchDelayMs, appDirectory, }?: {
11
+ runner?: ReactRouterTypegenRunner;
12
+ devWatchDelayMs?: number;
13
+ appDirectory?: string;
14
+ }) => void;
15
+ export {};
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { RsbuildConfig } from '@rsbuild/core';
1
2
  export type Route = {
2
3
  id: string;
3
4
  parentId?: string;
@@ -16,26 +17,52 @@ export type PluginOptions = {
16
17
  customServer?: boolean;
17
18
  /**
18
19
  * The output format for server builds.
19
- * When set to "module", no package.json will be emitted.
20
- * @default "module"
20
+ * When omitted, React Router's `serverModuleFormat` selects the emitted
21
+ * Rsbuild format (`"esm"` -> `"module"`, `"cjs"` -> `"commonjs"`).
21
22
  */
22
23
  serverOutput?: 'module' | 'commonjs';
23
24
  /**
24
25
  * Federation mode configuration
25
26
  */
26
27
  federation?: boolean;
27
- };
28
- /**
29
- * Arguments passed to transform functions
30
- */
31
- export type TransformArgs = {
32
- code: string;
33
- resource: string;
34
- resourcePath: string;
35
- context?: string | null;
36
- environment?: {
37
- name: string;
38
- };
28
+ /**
29
+ * Rsbuild dev-only lazy compilation behavior.
30
+ *
31
+ * React Router's browser manifest remains eager so initial dev requests can
32
+ * discover browser assets without lazy proxy delays.
33
+ *
34
+ * Pass `false` to disable.
35
+ * @default true
36
+ */
37
+ lazyCompilation?: NonNullable<RsbuildConfig['dev']>['lazyCompilation'];
38
+ /**
39
+ * Prewarm Rspack lazy-compilation proxy modules after dev compiles.
40
+ * This depends on Rspack's generated lazy-compilation client shape and should
41
+ * be treated as experimental.
42
+ *
43
+ * @default false
44
+ */
45
+ unstableLazyCompilationPrewarm?: boolean;
46
+ /**
47
+ * Emit structured React Router plugin timing logs.
48
+ * @default false
49
+ */
50
+ logPerformance?: boolean;
51
+ /**
52
+ * Run route transforms in a worker-thread pool.
53
+ * Pass `true` to force the default worker count, a positive integer to set
54
+ * the worker count, or `false` to disable.
55
+ * @default Automatically enabled for 256+ resolved routes. The automatic
56
+ * pool uses available CPU cores minus 2.
57
+ */
58
+ parallelRouteTransform?: boolean | number;
59
+ /**
60
+ * Called when the route graph changes during development.
61
+ * Programmatic/custom servers can use this to recreate their Rsbuild server;
62
+ * the CLI uses its built-in reload-server watcher when this is omitted. This
63
+ * notification is not awaited, so it may safely close the current server.
64
+ */
65
+ onRouteTopologyChange?: () => void | Promise<void>;
39
66
  };
40
67
  export type RouteManifestItem = Omit<Route, 'file' | 'children'> & {
41
68
  module: string;
@@ -0,0 +1,2 @@
1
+ export declare const getVirtualModuleFilePath: (moduleId: string) => string;
2
+ export declare const mapVirtualModules: (modules: Record<string, string>) => Record<string, string>;
@@ -1,5 +1,6 @@
1
1
  import type { NormalizedConfig } from '@rsbuild/core';
2
2
  type Warn = (message: string) => void;
3
+ export declare function isSourceMapEnabled(value: unknown): boolean;
3
4
  export declare function getClientSourceMapSetting(normalized: NormalizedConfig, clientEnvName?: string): unknown;
4
5
  export declare function getClientDevtoolSetting(normalized: NormalizedConfig, clientEnvName?: string): unknown;
5
6
  export declare function warnOnClientSourceMaps(normalized: NormalizedConfig, warn: Warn, clientEnvName?: string): void;
package/dist/yuku.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { walk, type ParseOptions, type ParseResult } from 'yuku-parser';
2
+ import type { Rspack } from '@rsbuild/core';
3
+ export declare const parse: (code: string, options?: ParseOptions) => ParseResult;
4
+ export declare const traverse: typeof walk;
5
+ export declare const generate: (ast: ParseResult | {
6
+ type: "Program";
7
+ }, options?: {
8
+ sourceMaps?: boolean;
9
+ filename?: string;
10
+ sourceFileName?: string;
11
+ }) => {
12
+ code: string;
13
+ map: Rspack.RawSourceMap | null;
14
+ };
15
+ export type { ParseResult };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rsbuild-plugin-react-router",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "React Router plugin for Rsbuild",
5
5
  "repository": {
6
6
  "type": "git",
@@ -42,22 +42,16 @@
42
42
  "main": "./dist/index.cjs",
43
43
  "types": "./dist/index.d.ts",
44
44
  "files": [
45
- "dist"
45
+ "dist",
46
+ "src"
46
47
  ],
47
48
  "publint": {
48
49
  "ignoreMissingDts": true
49
50
  },
50
51
  "dependencies": {
51
- "@babel/core": "^7.28.6",
52
- "@babel/generator": "^7.28.6",
53
- "@babel/parser": "^7.28.6",
54
- "@babel/traverse": "^7.28.6",
55
- "@babel/types": "^7.28.6",
56
52
  "@react-router/node": "^7.13.0",
57
53
  "@remix-run/node-fetch-server": "^0.13.0",
58
54
  "@rspack/plugin-react-refresh": "^2.0.2",
59
- "babel-dead-code-elimination": "^1.0.12",
60
- "esbuild": "^0.27.2",
61
55
  "execa": "^9.6.1",
62
56
  "fs-extra": "11.3.3",
63
57
  "isbot": "5.1.34",
@@ -65,28 +59,30 @@
65
59
  "jsesc": "^3.1.0",
66
60
  "pathe": "^2.0.3",
67
61
  "react-refresh": "^0.18.0",
68
- "rspack-plugin-virtual-module": "^1.0.1"
62
+ "yuku-analyzer": "0.5.39",
63
+ "yuku-codegen": "0.5.39",
64
+ "yuku-parser": "0.5.39"
69
65
  },
70
66
  "devDependencies": {
71
67
  "@changesets/cli": "^2.29.8",
72
- "@react-router/dev": "^7.13.0",
73
- "@rsbuild/core": "2.0.15",
68
+ "@react-router/dev": "^8.0.1",
69
+ "@rsbuild/core": "2.1.0",
70
+ "@rsbuild/plugin-react": "2.1.0",
74
71
  "@rslib/core": "^0.22.1",
75
- "@rspack/core": "2.0.8",
76
- "@swc/helpers": "^0.5.23",
72
+ "@rspack/core": "2.1.0",
77
73
  "@rstest/core": "^0.8.1",
78
74
  "@rstest/coverage-istanbul": "^0.2.0",
79
- "@types/babel__core": "^7.20.5",
80
- "@types/babel__generator": "^7.27.0",
81
- "@types/babel__traverse": "^7.28.0",
75
+ "@swc/helpers": "^0.5.23",
82
76
  "@types/fs-extra": "11.0.4",
83
77
  "@types/jsesc": "^3.0.3",
84
78
  "@types/node": "^25.0.10",
85
79
  "@types/react": "^19.2.10",
86
80
  "@types/react-dom": "^19.2.3",
81
+ "effect": "^3.21.4",
87
82
  "es-module-lexer": "1.7.0",
88
83
  "kill-port": "^2.0.1",
89
- "playwright": "^1.58.0",
84
+ "pkg-pr-new": "^0.0.75",
85
+ "playwright": "1.58.0",
90
86
  "prettier": "3.8.1",
91
87
  "react": "^19.2.4",
92
88
  "react-dom": "^19.2.4",
@@ -105,13 +101,22 @@
105
101
  },
106
102
  "scripts": {
107
103
  "build": "rslib build",
108
- "e2e": "pnpm build && pnpm --filter './examples/{default-template,spa-mode,prerender,custom-node-server,cloudflare,client-only}' test:e2e",
104
+ "bench": "node scripts/bench-builds.mts",
105
+ "bench:ci-report": "node scripts/report-benchmark-ci.mts",
106
+ "bench:compare": "node scripts/compare-benchmarks.mts",
107
+ "bench:smoke": "node scripts/bench-builds.mts --profile smoke --iterations 1 --warmup 0 --format both --out .benchmark/results/smoke",
108
+ "bench:baseline": "node scripts/bench-builds.mts --profile default --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/baseline",
109
+ "bench:full": "node scripts/bench-builds.mts --profile full --iterations 5 --warmup 1 --clean build --format both --out .benchmark/results/full",
110
+ "bench:large": "node scripts/bench-builds.mts --profile large --iterations 1 --warmup 0 --clean cold --format both --out .benchmark/results/large",
111
+ "bench:synthetic-app": "node scripts/bench-synthetic-app.mjs",
112
+ "e2e": "pnpm build && pnpm test:package-interop && pnpm --filter './examples/{default-template,spa-mode,prerender,custom-node-server,cloudflare,client-only}' test:e2e",
109
113
  "dev": "rslib build --watch",
110
114
  "test": "rstest run",
111
115
  "test:watch": "rstest watch",
112
116
  "test:coverage": "rstest run --coverage",
113
117
  "test:core": "rstest run -c ./rstest.config.ts",
114
118
  "test:core:watch": "rstest watch -c ./rstest.config.ts",
119
+ "test:package-interop": "node scripts/test-package-interop.mts",
115
120
  "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
116
121
  "format:check": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
117
122
  "changeset": "changeset",
@@ -0,0 +1,18 @@
1
+ export const setBoundedCacheEntry = <Key, Value>(
2
+ cache: Map<Key, Value>,
3
+ key: Key,
4
+ value: Value,
5
+ maxEntries: number
6
+ ): void => {
7
+ if (maxEntries <= 0) {
8
+ cache.clear();
9
+ return;
10
+ }
11
+ if (!cache.has(key) && cache.size >= maxEntries) {
12
+ const oldestEntry = cache.keys().next();
13
+ if (!oldestEntry.done) {
14
+ cache.delete(oldestEntry.value);
15
+ }
16
+ }
17
+ cache.set(key, value);
18
+ };
@@ -0,0 +1,205 @@
1
+ import { relative, resolve } from 'pathe';
2
+ import * as Effect from 'effect/Effect';
3
+ import { getCappedPluginConcurrency } from './concurrency.js';
4
+ import { runPluginEffect, tryPluginPromise } from './effect-runtime.js';
5
+ import type { Config } from './react-router-config.js';
6
+ import type { Route } from './types.js';
7
+
8
+ type BuildManifest =
9
+ | {
10
+ routes: Record<string, Route>;
11
+ }
12
+ | {
13
+ routes: Record<string, Route>;
14
+ serverBundles: Record<string, { id: string; file: string }>;
15
+ routeIdToServerBundleId: Record<string, string>;
16
+ };
17
+
18
+ const normalizePath = (value: string): string => value.replace(/\\/g, '/');
19
+
20
+ const getAddressableRoutes = (routes: Record<string, Route>): Route[] => {
21
+ const nonAddressableIds = new Set<string>();
22
+ for (const id in routes) {
23
+ const route = routes[id];
24
+ if (route.index) {
25
+ if (!route.parentId) {
26
+ continue;
27
+ }
28
+ nonAddressableIds.add(route.parentId);
29
+ }
30
+ if (typeof route.path !== 'string' && !route.index) {
31
+ nonAddressableIds.add(id);
32
+ }
33
+ }
34
+ return Object.values(routes).filter(
35
+ route => !nonAddressableIds.has(route.id)
36
+ );
37
+ };
38
+
39
+ const getRouteBranch = (
40
+ routes: Record<string, Route>,
41
+ routeId: string
42
+ ): Route[] => {
43
+ const branch: Route[] = [];
44
+ let current = routeId;
45
+ while (current) {
46
+ const route = routes[current];
47
+ if (!route) {
48
+ throw new Error(`Missing route for ${current}`);
49
+ }
50
+ branch.push(route);
51
+ current = route.parentId || '';
52
+ }
53
+ return branch.reverse();
54
+ };
55
+
56
+ const configRouteToBranchRoute = (route: Route) => ({
57
+ id: route.id,
58
+ path: route.path,
59
+ file: route.file,
60
+ index: route.index,
61
+ });
62
+
63
+ const validateServerBundleId = (
64
+ serverBundleId: string,
65
+ future: Required<Config>['future']
66
+ ): Error | undefined => {
67
+ if (future?.v8_viteEnvironmentApi) {
68
+ return /^[a-zA-Z0-9_]+$/.test(serverBundleId)
69
+ ? undefined
70
+ : new Error(
71
+ 'The "serverBundles" function must only return strings containing alphanumeric characters and underscores.'
72
+ );
73
+ }
74
+
75
+ return /^[a-zA-Z0-9-_]+$/.test(serverBundleId)
76
+ ? undefined
77
+ : new Error(
78
+ 'The "serverBundles" function must only return strings containing alphanumeric characters, hyphens and underscores.'
79
+ );
80
+ };
81
+
82
+ type GetBuildManifestOptions = {
83
+ reactRouterConfig: Required<
84
+ Pick<
85
+ Config,
86
+ 'appDirectory' | 'buildDirectory' | 'serverBuildFile' | 'future'
87
+ >
88
+ > &
89
+ Pick<Config, 'serverBundles'>;
90
+ routes: Record<string, Route>;
91
+ rootDirectory: string;
92
+ };
93
+
94
+ export const getBuildManifestEffect = ({
95
+ reactRouterConfig,
96
+ routes,
97
+ rootDirectory,
98
+ }: GetBuildManifestOptions): Effect.Effect<
99
+ BuildManifest | undefined,
100
+ Error,
101
+ never
102
+ > =>
103
+ Effect.gen(function* () {
104
+ const {
105
+ serverBundles,
106
+ appDirectory,
107
+ buildDirectory,
108
+ serverBuildFile,
109
+ future,
110
+ } = reactRouterConfig;
111
+
112
+ if (!serverBundles) {
113
+ return { routes };
114
+ }
115
+
116
+ const rootRelativeRoutes = Object.fromEntries(
117
+ Object.entries(routes).map(([id, route]) => {
118
+ const filePath = resolve(appDirectory, route.file);
119
+ return [
120
+ id,
121
+ { ...route, file: normalizePath(relative(rootDirectory, filePath)) },
122
+ ];
123
+ })
124
+ );
125
+
126
+ const serverBuildDirectory = resolve(buildDirectory, 'server');
127
+
128
+ const buildManifest: BuildManifest = {
129
+ routes: rootRelativeRoutes,
130
+ serverBundles: {},
131
+ routeIdToServerBundleId: {},
132
+ };
133
+
134
+ yield* Effect.forEach(
135
+ getAddressableRoutes(routes),
136
+ route =>
137
+ Effect.gen(function* () {
138
+ const branch = getRouteBranch(routes, route.id);
139
+ const serverBundleId = yield* tryPluginPromise(() =>
140
+ serverBundles({
141
+ branch: branch.map(branchRoute =>
142
+ configRouteToBranchRoute({
143
+ ...branchRoute,
144
+ file: resolve(appDirectory, branchRoute.file),
145
+ })
146
+ ),
147
+ })
148
+ );
149
+
150
+ if (typeof serverBundleId !== 'string') {
151
+ return yield* Effect.fail(
152
+ new Error('The "serverBundles" function must return a string')
153
+ );
154
+ }
155
+
156
+ const validationError = validateServerBundleId(
157
+ serverBundleId,
158
+ future
159
+ );
160
+ if (validationError) {
161
+ return yield* Effect.fail(validationError);
162
+ }
163
+
164
+ buildManifest.routeIdToServerBundleId![route.id] = serverBundleId;
165
+ buildManifest.serverBundles![serverBundleId] ??= {
166
+ id: serverBundleId,
167
+ file: normalizePath(
168
+ relative(
169
+ rootDirectory,
170
+ resolve(serverBuildDirectory, serverBundleId, serverBuildFile)
171
+ )
172
+ ),
173
+ };
174
+ }),
175
+ { concurrency: getCappedPluginConcurrency(), discard: true }
176
+ );
177
+
178
+ return buildManifest;
179
+ });
180
+
181
+ export const getBuildManifest = (
182
+ options: GetBuildManifestOptions
183
+ ): Promise<BuildManifest | undefined> =>
184
+ runPluginEffect(getBuildManifestEffect(options));
185
+
186
+ export const getRoutesByServerBundleId = (
187
+ buildManifest: BuildManifest | undefined,
188
+ sourceRoutes: Record<string, Route>
189
+ ): Record<string, Record<string, Route>> => {
190
+ if (!buildManifest || !('routeIdToServerBundleId' in buildManifest)) {
191
+ return {};
192
+ }
193
+
194
+ const routesByServerBundleId: Record<string, Record<string, Route>> = {};
195
+ for (const [routeId, serverBundleId] of Object.entries(
196
+ buildManifest.routeIdToServerBundleId
197
+ )) {
198
+ routesByServerBundleId[serverBundleId] ??= {};
199
+ const branch = getRouteBranch(sourceRoutes, routeId);
200
+ for (const route of branch) {
201
+ routesByServerBundleId[serverBundleId][route.id] = route;
202
+ }
203
+ }
204
+ return routesByServerBundleId;
205
+ };