@react-router/dev 8.4.0 → 8.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # `@react-router/dev`
2
2
 
3
+ ## v8.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Use resolved Vite environment output directories when reading and writing build artifacts ([#15480](https://github.com/remix-run/react-router/pull/15480))
8
+ - Finalize server-bundle manifest paths after asynchronous Vite configuration hooks complete
9
+ - Preserve output-directory overrides applied when creating a Vite build environment
10
+ - Run prerendering after other Vite plugins finish their application build hooks ([#15481](https://github.com/remix-run/react-router/pull/15481))
11
+ - Close React Router's build resources when another plugin's application build hook fails during `react-router build`
12
+ - Updated dependencies:
13
+ - [`react-router@8.5.1`](https://github.com/remix-run/react-router/releases/tag/react-router@8.5.1)
14
+ - [`@react-router/node@8.5.1`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@8.5.1)
15
+ - [`@react-router/serve@8.5.1`](https://github.com/remix-run/react-router/releases/tag/@react-router/serve@8.5.1)
16
+
17
+ ## v8.5.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Fix critical CSS collection in dev registering server-only route imports as client dependencies, which triggered dependency re-optimization and full page reloads ([#15595](https://github.com/remix-run/react-router/pull/15595))
22
+ - Fix intermittent `Prerender: Request failed for ... connect ECONNREFUSED` errors during prerendering ([#15325](https://github.com/remix-run/react-router/pull/15325))
23
+ - Send requests to the address the Vite preview server bound to
24
+ - Avoid re-resolving `localhost` for each request, which could select a different address family (`127.0.0.1` vs `::1`)
25
+
26
+ ### Unstable Changes
27
+
28
+ ⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_
29
+
30
+ - Add `ssr: "unstable_api-only"` to retain a runtime server for loaders, actions, and route discovery while disabling production document rendering ([#15601](https://github.com/remix-run/react-router/pull/15601))
31
+ - Generate a root-only SPA shell when prerendering is disabled
32
+ - Support prerendered pages and data files alongside runtime loaders and actions, with a SPA fallback for non-prerendered paths
33
+ - Default API-only builds to lazy route discovery
34
+ - Reject API-only configuration in RSC Framework Mode
35
+ - Preserve server-only route exports in additional Vite server environments in both API-only and SSR modes
36
+ - Serve prerendered pages and the SPA fallback in Vite preview while retaining live data requests and route discovery
37
+ - Add the `future.unstable_customRouteDiscovery` flag to opt into application-controlled route discovery in conventional Framework Mode ([#15602](https://github.com/remix-run/react-router/pull/15602))
38
+ - Fix source maps for route modules in unstable RSC Framework Mode ([#15479](https://github.com/remix-run/react-router/pull/15479))
39
+ - Add `unstable_apiServerOrigin` to configure the origin for API-only loader, action, and route discovery requests ([#15601](https://github.com/remix-run/react-router/pull/15601))
40
+ - Updated dependencies:
41
+ - [`react-router@8.5.0`](https://github.com/remix-run/react-router/releases/tag/react-router@8.5.0)
42
+ - [`@react-router/node@8.5.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@8.5.0)
43
+ - [`@react-router/serve@8.5.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/serve@8.5.0)
44
+
3
45
  ## v8.4.0
4
46
 
5
47
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,12 +8,12 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { _ as getVite, v as preloadVite } from "./typegen-WywajSVn.js";
11
+ import { _ as getVite, v as preloadVite } from "./typegen-CqlAVM2O.js";
12
12
  //#region vite/build.ts
13
13
  async function build(root, viteBuildOptions) {
14
14
  await preloadVite();
15
15
  let { assetsInlineLimit, clearScreen, config: configFile, emptyOutDir, force, logLevel, minify, mode, sourcemapClient, sourcemapServer } = viteBuildOptions;
16
- await (await getVite().createBuilder({
16
+ let builder = await getVite().createBuilder({
17
17
  root,
18
18
  mode,
19
19
  configFile,
@@ -35,7 +35,13 @@ async function build(root, viteBuildOptions) {
35
35
  if (!config.plugins.find((plugin) => plugin.name === "react-router" || plugin.name === "react-router/rsc")) throw new Error("React Router Vite plugin not found in Vite config");
36
36
  }
37
37
  }]
38
- })).buildApp();
38
+ });
39
+ try {
40
+ await builder.buildApp();
41
+ } finally {
42
+ let { __reactRouterBuildContext: buildContext } = builder.config;
43
+ await buildContext?.close();
44
+ }
39
45
  }
40
46
  //#endregion
41
47
  export { build };
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @react-router/dev v8.4.0
3
+ * @react-router/dev v8.5.1
4
4
  *
5
5
  * Copyright (c) Remix Software Inc.
6
6
  *
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @license MIT
11
11
  */
12
- import { _ as getVite, l as hasNodeDependency, n as watch, t as run$1, u as loadConfig, v as preloadVite } from "../typegen-WywajSVn.js";
12
+ import { _ as getVite, l as hasNodeDependency, n as watch, t as run$1, u as loadConfig, v as preloadVite } from "../typegen-CqlAVM2O.js";
13
13
  import { createRequire } from "node:module";
14
14
  import { parseArgs } from "node:util";
15
15
  import semver from "semver";
@@ -172,7 +172,7 @@ async function routes(rootDirectory, flags = {}) {
172
172
  }
173
173
  async function build(root, options = {}) {
174
174
  root = resolveRootDirectory(root, options);
175
- let { build } = await import("../build-CsJF5XUO.js");
175
+ let { build } = await import("../build-DjEus3HZ.js");
176
176
  if (options.profile) await start();
177
177
  try {
178
178
  await build(root, options);
@@ -182,7 +182,7 @@ async function build(root, options = {}) {
182
182
  }
183
183
  async function dev(root, options = {}) {
184
184
  if (developmentConditionEnabled) {
185
- let { dev } = await import("../dev-_GaivbeG.js");
185
+ let { dev } = await import("../dev-LVfdDvQH.js");
186
186
  if (options.profile) await start();
187
187
  exitHook(() => stop(console.info));
188
188
  root = resolveRootDirectory(root, options);
@@ -38,6 +38,7 @@ type ServerBundlesBuildManifest = BaseBuildManifest & {
38
38
  };
39
39
  type ServerModuleFormat = "esm" | "cjs";
40
40
  interface FutureConfig {
41
+ unstable_customRouteDiscovery: boolean;
41
42
  unstable_enableNodeReadableStream: boolean;
42
43
  unstable_optimizeDeps: boolean;
43
44
  }
@@ -140,12 +141,30 @@ type ReactRouterConfig = {
140
141
  */
141
142
  serverBundles?: ServerBundlesFunction;
142
143
  /**
143
- * Enable server-side rendering for your application. Disable to use "SPA
144
- * Mode", which will request the `/` path at build-time and save it as an
145
- * `index.html` file with your assets so your application can be deployed as a
146
- * SPA without server-rendering. Default's to `true`.
144
+ * Control runtime server-side document rendering. Defaults to `true`.
145
+ *
146
+ * Set `false` to generate a static client build without a runtime server.
147
+ * Without prerendering, this generates a root-only SPA shell. With
148
+ * prerendering, it generates HTML and data files for selected paths plus
149
+ * a SPA fallback for non-prerendered paths.
150
+ *
151
+ * Set `"unstable_api-only"` to retain a runtime server for loaders, actions,
152
+ * and route discovery while disabling production document rendering.
153
+ * Documents follow the same prerendering and SPA fallback behavior as
154
+ * `ssr:false`, with runtime loaders and actions available for all routes.
155
+ * API-only mode is unstable and is not supported in RSC Framework Mode.
156
+ */
157
+ ssr?: boolean | "unstable_api-only";
158
+ /**
159
+ * The origin to use for server requests when `ssr` is `"unstable_api-only"`.
160
+ * This is ignored when `ssr` is `true` or `false`. Defaults to the same
161
+ * origin that serves the client build. Applies to loader, action, and route
162
+ * discovery requests.
163
+ *
164
+ * **Note:** The server origin will need to be configured appropriately for
165
+ * Cross-Origin Request Sharing (CORS).
147
166
  */
148
- ssr?: boolean;
167
+ unstable_apiServerOrigin?: string;
149
168
  /**
150
169
  * Enable subresource integrity hashes on asset script tags. Defaults to
151
170
  * `false`.
@@ -256,12 +275,24 @@ type ResolvedReactRouterConfig = Readonly<{
256
275
  */
257
276
  serverModuleFormat: ServerModuleFormat;
258
277
  /**
259
- * Enable server-side rendering for your application. Disable to use "SPA
260
- * Mode", which will request the `/` path at build-time and save it as an
261
- * `index.html` file with your assets so your application can be deployed as a
262
- * SPA without server-rendering. Default's to `true`.
278
+ * Control runtime server-side document rendering. Defaults to `true`.
279
+ *
280
+ * Set `false` to generate a static client build without a runtime server.
281
+ * Without prerendering, this generates a root-only SPA shell. With
282
+ * prerendering, it generates HTML and data files for selected paths plus
283
+ * a SPA fallback for non-prerendered paths.
284
+ *
285
+ * Set `"unstable_api-only"` to retain a runtime server for loaders, actions,
286
+ * and route discovery while disabling production document rendering.
287
+ * Documents follow the same prerendering and SPA fallback behavior as
288
+ * `ssr:false`, with runtime loaders and actions available for all routes.
289
+ * API-only mode is unstable and is not supported in RSC Framework Mode.
290
+ */
291
+ ssr: boolean | "unstable_api-only";
292
+ /**
293
+ * The origin to use for server requests in API-only mode.
263
294
  */
264
- ssr: boolean;
295
+ unstable_apiServerOrigin?: string;
265
296
  /**
266
297
  * Whether to generate subresource integrity hashes for asset script tags.
267
298
  */
package/dist/config.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
 
2
- import { i as ServerBundlesFunction, n as Preset, r as ReactRouterConfig, t as BuildManifest } from "./config-t89niiFv.js";
2
+ import { i as ServerBundlesFunction, n as Preset, r as ReactRouterConfig, t as BuildManifest } from "./config-DP95WFpo.js";
3
3
  export type { BuildManifest, ReactRouterConfig as Config, Preset, ServerBundlesFunction };
package/dist/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { _ as getVite, v as preloadVite } from "./typegen-WywajSVn.js";
11
+ import { _ as getVite, v as preloadVite } from "./typegen-CqlAVM2O.js";
12
12
  import { n as start, r as stop, t as getSession } from "./cli/index.js";
13
13
  import colors from "picocolors";
14
14
  //#region vite/dev.ts
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
package/dist/routes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,5 +8,5 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { a as prefix, i as layout, n as getAppDirectory, o as relative, r as index, s as route } from "./routes-BFdkiVnp.js";
11
+ import { a as prefix, i as layout, n as getAppDirectory, o as relative, r as index, s as route } from "./routes-DAb9gz9-.js";
12
12
  export { getAppDirectory, index, layout, prefix, relative, route };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { c as setAppDirectory, l as validateRouteConfig, t as configRoutesToRouteManifest, u as invariant } from "./routes-BFdkiVnp.js";
11
+ import { c as setAppDirectory, l as validateRouteConfig, t as configRoutesToRouteManifest, u as invariant } from "./routes-DAb9gz9-.js";
12
12
  import { createRequire } from "node:module";
13
13
  import colors from "picocolors";
14
14
  import fs from "node:fs";
@@ -228,10 +228,18 @@ async function resolveConfig({ root, viteRunnerContext, reactRouterConfigFile, s
228
228
  ssr: true
229
229
  };
230
230
  let userAndPresetConfigs = mergeReactRouterConfig(...presets, reactRouterUserConfig);
231
- let { appDirectory: userAppDirectory, basename, buildDirectory: userBuildDirectory, buildEnd, prerender, routeDiscovery: userRouteDiscovery, serverBuildFile, serverBundles, serverModuleFormat, ssr } = {
231
+ let { appDirectory: userAppDirectory, basename, buildDirectory: userBuildDirectory, buildEnd, prerender, routeDiscovery: userRouteDiscovery, serverBuildFile, serverBundles, serverModuleFormat, ssr, unstable_apiServerOrigin } = {
232
232
  ...defaults,
233
233
  ...userAndPresetConfigs
234
234
  };
235
+ if (ssr === "unstable_api-only" && unstable_apiServerOrigin != null) try {
236
+ let url = new URL(unstable_apiServerOrigin);
237
+ if (url.pathname !== "/" || url.search || url.hash) return err("The `unstable_apiServerOrigin` config must be an origin without a path, query, or hash.");
238
+ unstable_apiServerOrigin = url.origin;
239
+ } catch {
240
+ return err("The `unstable_apiServerOrigin` config must be a valid absolute URL.");
241
+ }
242
+ else unstable_apiServerOrigin = void 0;
235
243
  if (!ssr && serverBundles) serverBundles = void 0;
236
244
  if (prerender) {
237
245
  let isValidPrerenderPathsConfig = (p) => typeof p === "boolean" || typeof p === "function" || Array.isArray(p);
@@ -295,6 +303,7 @@ async function resolveConfig({ root, viteRunnerContext, reactRouterConfigFile, s
295
303
  if ("unstable_subResourceIntegrity" in futureConfig) return err("The `future.unstable_subResourceIntegrity` flag has been stabilized and moved to a top-level `config.subResourceIntegrity` field");
296
304
  }
297
305
  let future = {
306
+ unstable_customRouteDiscovery: userAndPresetConfigs.future?.unstable_customRouteDiscovery ?? false,
298
307
  unstable_enableNodeReadableStream: userAndPresetConfigs.future?.unstable_enableNodeReadableStream ?? false,
299
308
  unstable_optimizeDeps: userAndPresetConfigs.future?.unstable_optimizeDeps ?? false
300
309
  };
@@ -314,6 +323,7 @@ async function resolveConfig({ root, viteRunnerContext, reactRouterConfigFile, s
314
323
  serverBundles,
315
324
  serverModuleFormat,
316
325
  ssr,
326
+ unstable_apiServerOrigin,
317
327
  splitRouteModules,
318
328
  subResourceIntegrity,
319
329
  allowedActionOrigins,
@@ -624,6 +634,8 @@ function generateServerBuild(ctx) {
624
634
  export const routeDiscovery: ServerBuild["routeDiscovery"];
625
635
  export const routes: ServerBuild["routes"];
626
636
  export const ssr: ServerBuild["ssr"];
637
+ export const unstable_apiOnly: ServerBuild["unstable_apiOnly"];
638
+ export const unstable_apiServerOrigin: ServerBuild["unstable_apiServerOrigin"];
627
639
  export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
628
640
  export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
629
641
  }
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @react-router/dev v8.4.0
2
+ * @react-router/dev v8.5.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * @license MIT
10
10
  */
11
- import { _ as getVite, a as t, c as createConfigLoader, d as resolveEntryFiles, f as resolveRSCEntryFiles, g as getUserBuildRollupOptions, h as defineOptimizeDepsCompilerOptions, i as parse$1, l as hasNodeDependency, m as defineCompilerOptions, n as watch, o as traverse, p as ssrExternals, r as generate, s as configRouteToBranchRoute, v as preloadVite } from "./typegen-WywajSVn.js";
12
- import { u as invariant } from "./routes-BFdkiVnp.js";
11
+ import { _ as getVite, a as t, c as createConfigLoader, d as resolveEntryFiles, f as resolveRSCEntryFiles, g as getUserBuildRollupOptions, h as defineOptimizeDepsCompilerOptions, i as parse$1, l as hasNodeDependency, m as defineCompilerOptions, n as watch, o as traverse, p as ssrExternals, r as generate, s as configRouteToBranchRoute, v as preloadVite } from "./typegen-CqlAVM2O.js";
12
+ import { u as invariant } from "./routes-DAb9gz9-.js";
13
13
  import { createRequire } from "node:module";
14
14
  import colors from "picocolors";
15
15
  import { existsSync, readFileSync, readdirSync } from "node:fs";
@@ -114,7 +114,8 @@ const findDeps = async (vite, node, deps) => {
114
114
  }
115
115
  }
116
116
  async function addFromUrl(url) {
117
- let node = await vite.moduleGraph.getModuleByUrl(url);
117
+ let ssrNode = await vite.environments.ssr.moduleGraph.getModuleByUrl(url);
118
+ let node = ssrNode?.id ? vite.moduleGraph.getModuleById(ssrNode.id) : void 0;
118
119
  if (node) await addFromNode(node);
119
120
  }
120
121
  if (node.ssrTransformResult) {
@@ -788,90 +789,91 @@ function normalizePostProcessResult(result) {
788
789
  * Vite plugin for prerendering using the preview server
789
790
  */
790
791
  function prerender(options) {
791
- const { config, requests, postProcess = defaultPostProcess, handleError = defaultHandleError, logFile, finalize } = options;
792
+ const { config, requests, postProcess = defaultPostProcess, handleError = defaultHandleError, logFile, finalize, cleanup } = options;
792
793
  let viteConfig;
793
794
  return {
794
795
  name: "prerender",
796
+ enforce: "post",
795
797
  sharedDuringBuild: true,
796
- config: {
798
+ buildApp: {
797
799
  order: "post",
798
- handler({ builder: { buildApp } = {} }) {
799
- return { builder: { async buildApp(builder) {
800
- await buildApp?.(builder);
800
+ async handler() {
801
+ let ogIsBuildRequest = process.env.IS_RR_BUILD_REQUEST;
802
+ try {
801
803
  const prerenderRequests = (typeof requests === "function" ? await requests() : requests).map(normalizePrerenderRequest);
802
804
  if (prerenderRequests.length === 0) return;
803
- const { buildDirectory = viteConfig.environments.client.build.outDir, concurrency = 1, retryCount = 0, retryDelay = 500, maxRedirects = 0, timeout = 1e4 } = (typeof config === "function" ? await config() : config) ?? {};
804
- let ogIsBuildRequest = process.env.IS_RR_BUILD_REQUEST;
805
+ const { buildDirectory = path.resolve(viteConfig.root, viteConfig.environments.client.build.outDir), concurrency = 1, retryCount = 0, retryDelay = 500, maxRedirects = 0, timeout = 1e4 } = (typeof config === "function" ? await config() : config) ?? {};
805
806
  process.env.IS_RR_BUILD_REQUEST = "yes";
807
+ const previewServer = await startPreviewServer(viteConfig);
806
808
  try {
807
- const previewServer = await startPreviewServer(viteConfig);
808
- try {
809
- const baseUrl = getResolvedUrl(previewServer);
810
- async function prerenderRequest(input, metadata) {
811
- let attemptCount = 0;
812
- let redirectCount = 0;
813
- const request = new Request(input);
814
- const url = new URL(request.url);
815
- if (url.origin !== baseUrl.origin) {
816
- url.hostname = baseUrl.hostname;
817
- url.protocol = baseUrl.protocol;
818
- url.port = baseUrl.port;
819
- }
820
- async function attempt(url) {
821
- try {
822
- const signal = AbortSignal.timeout(timeout);
823
- const response = await nodeHttpFetch(new Request(url, request), { signal });
824
- if (response.status >= 300 && response.status < 400 && response.headers.has("location") && ++redirectCount <= maxRedirects) {
825
- const location = response.headers.get("location");
826
- const responseURL = new URL(response.url);
827
- const locationUrl = new URL(location, response.url);
828
- if (responseURL.origin !== locationUrl.origin) return await postProcess(request, response, metadata);
829
- return await attempt(new URL(location, url));
830
- }
831
- if (response.status >= 500 && ++attemptCount <= retryCount) {
832
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
833
- return attempt(url);
834
- }
835
- return await postProcess(request, response, metadata);
836
- } catch (error) {
837
- if (++attemptCount <= retryCount) {
838
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
839
- return attempt(url);
840
- }
841
- handleError(request, error instanceof Error ? error : new Error(error?.toString() ?? "Unknown error"), metadata);
842
- return [];
843
- }
844
- }
845
- return attempt(url);
809
+ const baseUrl = getResolvedUrl(previewServer);
810
+ async function prerenderRequest(input, metadata) {
811
+ let attemptCount = 0;
812
+ let redirectCount = 0;
813
+ const request = new Request(input);
814
+ const url = new URL(request.url);
815
+ if (url.origin !== baseUrl.origin) {
816
+ url.hostname = baseUrl.hostname;
817
+ url.protocol = baseUrl.protocol;
818
+ url.port = baseUrl.port;
846
819
  }
847
- async function prerender(input, metadata) {
848
- const { files, requests } = normalizePostProcessResult(await prerenderRequest(input, metadata));
849
- for (const file of files) await writePrerenderFile(file, metadata);
850
- for (const followUp of requests) {
851
- const normalized = normalizePrerenderRequest(followUp);
852
- await prerender(normalized.request, normalized.metadata);
820
+ async function attempt(url) {
821
+ try {
822
+ const signal = AbortSignal.timeout(timeout);
823
+ const response = await nodeHttpFetch(new Request(url, request), { signal });
824
+ if (response.status >= 300 && response.status < 400 && response.headers.has("location") && ++redirectCount <= maxRedirects) {
825
+ const location = response.headers.get("location");
826
+ const responseURL = new URL(response.url);
827
+ const locationUrl = new URL(location, response.url);
828
+ if (responseURL.origin !== locationUrl.origin) return await postProcess(request, response, metadata);
829
+ return await attempt(new URL(location, url));
830
+ }
831
+ if (response.status >= 500 && ++attemptCount <= retryCount) {
832
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
833
+ return attempt(url);
834
+ }
835
+ return await postProcess(request, response, metadata);
836
+ } catch (error) {
837
+ if (++attemptCount <= retryCount) {
838
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
839
+ return attempt(url);
840
+ }
841
+ handleError(request, error instanceof Error ? error : new Error(error?.toString() ?? "Unknown error"), metadata);
842
+ return [];
853
843
  }
854
844
  }
855
- async function writePrerenderFile(file, metadata) {
856
- const normalizedPath = file.path.startsWith("/") ? file.path.slice(1) : file.path;
857
- const outputPath = path.join(buildDirectory, ...normalizedPath.split("/"));
858
- await mkdir(path.dirname(outputPath), { recursive: true });
859
- await writeFile(outputPath, file.contents);
860
- const relativePath = path.relative(viteConfig.root, outputPath);
861
- if (logFile) logFile(relativePath, metadata);
862
- return relativePath;
845
+ return attempt(url);
846
+ }
847
+ async function prerender(input, metadata) {
848
+ const { files, requests } = normalizePostProcessResult(await prerenderRequest(input, metadata));
849
+ for (const file of files) await writePrerenderFile(file, metadata);
850
+ for (const followUp of requests) {
851
+ const normalized = normalizePrerenderRequest(followUp);
852
+ await prerender(normalized.request, normalized.metadata);
863
853
  }
864
- await (await import("p-map")).default(prerenderRequests, async ({ request, metadata }) => {
865
- await prerender(request, metadata);
866
- }, { concurrency });
867
- if (finalize) await finalize(buildDirectory);
868
- } finally {
869
- await previewServer.close();
870
854
  }
855
+ async function writePrerenderFile(file, metadata) {
856
+ const normalizedPath = file.path.startsWith("/") ? file.path.slice(1) : file.path;
857
+ const outputPath = path.join(buildDirectory, ...normalizedPath.split("/"));
858
+ await mkdir(path.dirname(outputPath), { recursive: true });
859
+ await writeFile(outputPath, file.contents);
860
+ const relativePath = path.relative(viteConfig.root, outputPath);
861
+ if (logFile) logFile(relativePath, metadata);
862
+ return relativePath;
863
+ }
864
+ await (await import("p-map")).default(prerenderRequests, async ({ request, metadata }) => {
865
+ await prerender(request, metadata);
866
+ }, { concurrency });
867
+ if (finalize) await finalize(buildDirectory);
871
868
  } finally {
872
- process.env.IS_RR_BUILD_REQUEST = ogIsBuildRequest;
869
+ await previewServer.close();
873
870
  }
874
- } } };
871
+ } catch (error) {
872
+ await cleanup?.();
873
+ throw error;
874
+ } finally {
875
+ process.env.IS_RR_BUILD_REQUEST = ogIsBuildRequest;
876
+ }
875
877
  }
876
878
  },
877
879
  configResolved(resolvedConfig) {
@@ -916,7 +918,7 @@ async function nodeHttpFetch(request, { signal }) {
916
918
  return new Promise((resolve, reject) => {
917
919
  const req = http.request({
918
920
  protocol: url.protocol,
919
- hostname: url.hostname,
921
+ hostname: url.hostname.replace(/^\[|\]$/g, ""),
920
922
  port: url.port || void 0,
921
923
  path: url.pathname + url.search,
922
924
  method: request.method,
@@ -966,9 +968,15 @@ async function startPreviewServer(viteConfig) {
966
968
  }
967
969
  }
968
970
  function getResolvedUrl(previewServer) {
969
- const baseUrl = previewServer.resolvedUrls?.local[0];
970
- if (!baseUrl) throw new Error("Prerender: No resolved URL is available from the Vite preview server");
971
- return new URL(baseUrl);
971
+ const printableUrl = previewServer.resolvedUrls?.local[0];
972
+ const address = previewServer.httpServer?.address();
973
+ if (address && typeof address === "object" && address.address !== "::" && address.address !== "0.0.0.0") {
974
+ const protocol = printableUrl ? new URL(printableUrl).protocol : "http:";
975
+ const hostname = address.family === "IPv6" ? `[${address.address}]` : address.address;
976
+ return new URL(`${protocol}//${hostname}:${address.port}/`);
977
+ }
978
+ if (!printableUrl) throw new Error("Prerender: No resolved URL is available from the Vite preview server");
979
+ return new URL(printableUrl);
972
980
  }
973
981
  //#endregion
974
982
  //#region vite/plugin.ts
@@ -1151,8 +1159,14 @@ const getRouteModuleExports = async (viteChildCompiler, ctx, routeFile, readRout
1151
1159
  if (!viteChildCompiler) throw new Error("Vite child compiler not found");
1152
1160
  return getExportNames(await compileRouteFile(viteChildCompiler, ctx, routeFile, readRouteFile));
1153
1161
  };
1154
- let getServerBuildDirectory = (reactRouterConfig, { serverBundleId } = {}) => path$2.join(reactRouterConfig.buildDirectory, "server", ...serverBundleId ? [serverBundleId] : []);
1155
- let getClientBuildDirectory$1 = (reactRouterConfig) => path$2.join(reactRouterConfig.buildDirectory, "client");
1162
+ let getDefaultServerBuildDirectory = (reactRouterConfig, { serverBundleId } = {}) => path$2.join(reactRouterConfig.buildDirectory, "server", ...serverBundleId ? [serverBundleId] : []);
1163
+ let getDefaultClientBuildDirectory = (reactRouterConfig) => path$2.join(reactRouterConfig.buildDirectory, "client");
1164
+ let getClientBuildDirectory$1 = (viteConfig) => path$2.resolve(viteConfig.root, viteConfig.environments.client.build.outDir);
1165
+ let getServerBuildDirectory = (viteConfig, environmentName = "ssr") => {
1166
+ let environment = viteConfig.environments[environmentName];
1167
+ invariant(environment, `Vite environment "${environmentName}" not found`);
1168
+ return path$2.resolve(viteConfig.root, environment.build.outDir);
1169
+ };
1156
1170
  let getServerBundleRouteIds = (vitePluginContext, ctx) => {
1157
1171
  if (!ctx.buildManifest) return;
1158
1172
  let environmentName = vitePluginContext.environment.name;
@@ -1182,9 +1196,12 @@ const reactRouterVitePlugin = () => {
1182
1196
  let logger;
1183
1197
  let firstLoad = true;
1184
1198
  let ctx;
1185
- let closePluginResources = async () => {
1199
+ let closeChildCompiler = async () => {
1186
1200
  await viteChildCompiler?.close();
1187
1201
  viteChildCompiler = null;
1202
+ };
1203
+ let closePluginResources = async () => {
1204
+ await closeChildCompiler();
1188
1205
  await reactRouterConfigLoader.close();
1189
1206
  await (await typegenWatcherPromise)?.close();
1190
1207
  typegenWatcherPromise = void 0;
@@ -1226,6 +1243,12 @@ const reactRouterVitePlugin = () => {
1226
1243
  buildManifest
1227
1244
  };
1228
1245
  };
1246
+ let updateServerBundlePaths = () => {
1247
+ if (!ctx.buildManifest?.serverBundles) return;
1248
+ invariant(viteConfig);
1249
+ let { normalizePath } = getVite();
1250
+ for (let bundle of Object.values(ctx.buildManifest.serverBundles)) bundle.file = normalizePath(path$2.join(path$2.relative(ctx.rootDirectory, getServerBuildDirectory(viteConfig, `${SSR_BUNDLE_PREFIX}${bundle.id}`)), ctx.reactRouterConfig.serverBuildFile));
1251
+ };
1229
1252
  let getServerEntry = async ({ routeIds }) => {
1230
1253
  invariant(viteConfig, "viteconfig required to generate the server entry");
1231
1254
  let routes = routeIds ? pick(ctx.reactRouterConfig.routes, routeIds) : ctx.reactRouterConfig.routes;
@@ -1237,14 +1260,16 @@ const reactRouterVitePlugin = () => {
1237
1260
  import * as entryServer from ${JSON.stringify(resolveFileUrl(ctx, ctx.entryServerFilePath, { publicPath: ctx.publicPath }))};
1238
1261
  ${Object.keys(routes).map((key, index) => {
1239
1262
  let route = routes[key];
1240
- if (isSpaMode && key !== "root") return `const route${index} = { default: () => null };`;
1263
+ if (isSpaMode && !isApiOnlyMode(ctx.reactRouterConfig) && key !== "root") return `const route${index} = { default: () => null };`;
1241
1264
  else return `import * as route${index} from ${JSON.stringify(resolveFileUrl(ctx, resolveRelativeRouteFilePath(route, ctx.reactRouterConfig), { publicPath: ctx.publicPath }))};`;
1242
1265
  }).join("\n")}
1243
1266
  export { default as assets } from ${JSON.stringify(virtual$1.serverManifest.id)};
1244
- export const assetsBuildDirectory = ${JSON.stringify(path$2.relative(ctx.rootDirectory, getClientBuildDirectory$1(ctx.reactRouterConfig)))};
1267
+ export const assetsBuildDirectory = ${JSON.stringify(path$2.relative(ctx.rootDirectory, getClientBuildDirectory$1(viteConfig)))};
1245
1268
  export const basename = ${JSON.stringify(ctx.reactRouterConfig.basename)};
1246
1269
  export const future = ${JSON.stringify(ctx.reactRouterConfig.future)};
1247
- export const ssr = ${ctx.reactRouterConfig.ssr};
1270
+ export const ssr = ${Boolean(ctx.reactRouterConfig.ssr)};
1271
+ export const unstable_apiOnly = ${isApiOnlyMode(ctx.reactRouterConfig)};
1272
+ export const unstable_apiServerOrigin = ${isApiOnlyMode(ctx.reactRouterConfig) ? JSON.stringify(ctx.reactRouterConfig.unstable_apiServerOrigin) : "undefined"};
1248
1273
  export const isSpaMode = ${isSpaMode};
1249
1274
  export const prerender = ${JSON.stringify(prerenderPaths)};
1250
1275
  export const routeDiscovery = ${JSON.stringify(ctx.reactRouterConfig.routeDiscovery)};
@@ -1283,8 +1308,8 @@ const reactRouterVitePlugin = () => {
1283
1308
  let chunkAssetPaths = Object.values(viteManifest).flatMap((chunk) => chunk.assets ?? []);
1284
1309
  return new Set([...cssUrlPaths, ...chunkAssetPaths]);
1285
1310
  };
1286
- let generateSriManifest = async (ctx) => {
1287
- let clientBuildDirectory = getClientBuildDirectory$1(ctx.reactRouterConfig);
1311
+ let generateSriManifest = async (ctx, viteConfig) => {
1312
+ let clientBuildDirectory = getClientBuildDirectory$1(viteConfig);
1288
1313
  let entries = readdirSync(clientBuildDirectory, {
1289
1314
  withFileTypes: true,
1290
1315
  recursive: true
@@ -1307,7 +1332,7 @@ const reactRouterVitePlugin = () => {
1307
1332
  };
1308
1333
  let generateReactRouterManifestsForBuild = async ({ viteConfig, routeIds }) => {
1309
1334
  invariant(viteConfig);
1310
- let viteManifest = await loadViteManifest(getClientBuildDirectory$1(ctx.reactRouterConfig));
1335
+ let viteManifest = await loadViteManifest(getClientBuildDirectory$1(viteConfig));
1311
1336
  let allDynamicCssFiles = getAllDynamicCssFiles(ctx, viteManifest);
1312
1337
  let entry = getReactRouterManifestBuildAssets(ctx, viteConfig, viteManifest, allDynamicCssFiles, ctx.entryClientFilePath, null);
1313
1338
  let browserRoutes = {};
@@ -1372,9 +1397,9 @@ const reactRouterVitePlugin = () => {
1372
1397
  ...nonFingerprintedValues,
1373
1398
  sri: void 0
1374
1399
  };
1375
- await writeFileSafe(path$2.join(getClientBuildDirectory$1(ctx.reactRouterConfig), manifestPath), `window.__reactRouterManifest=${JSON.stringify(reactRouterBrowserManifest)};`);
1400
+ await writeFileSafe(path$2.join(getClientBuildDirectory$1(viteConfig), manifestPath), `window.__reactRouterManifest=${JSON.stringify(reactRouterBrowserManifest)};`);
1376
1401
  let sri = void 0;
1377
- if (ctx.reactRouterConfig.subResourceIntegrity) sri = await generateSriManifest(ctx);
1402
+ if (ctx.reactRouterConfig.subResourceIntegrity) sri = await generateSriManifest(ctx, viteConfig);
1378
1403
  return {
1379
1404
  reactRouterBrowserManifest,
1380
1405
  reactRouterServerManifest: {
@@ -1485,12 +1510,17 @@ const reactRouterVitePlugin = () => {
1485
1510
  watch: viteCommand === "serve"
1486
1511
  });
1487
1512
  await updatePluginContext();
1513
+ if (_viteConfigEnv.isPreview && ctx.reactRouterConfig.serverBundles) ctx.buildManifest = await getBuildManifest({
1514
+ reactRouterConfig: ctx.reactRouterConfig,
1515
+ rootDirectory: ctx.rootDirectory
1516
+ });
1488
1517
  let environments = await getEnvironmentsOptions(ctx, viteCommand, { viteUserConfig });
1489
1518
  let serverEnvironment = getServerEnvironmentValues(ctx, environments)[0];
1490
1519
  invariant(serverEnvironment);
1491
1520
  return {
1492
1521
  __reactRouterPluginContext: ctx,
1493
- appType: viteCommand === "serve" && _viteConfigEnv.mode === "production" && ctx.reactRouterConfig.ssr === false ? "spa" : "custom",
1522
+ __reactRouterBuildContext: { close: closePluginResources },
1523
+ appType: viteCommand === "serve" && _viteConfigEnv.mode === "production" && (!ctx.reactRouterConfig.ssr || ctx.reactRouterConfig.ssr === "unstable_api-only") ? isApiOnlyMode(ctx.reactRouterConfig) ? "mpa" : "spa" : "custom",
1494
1524
  ssr: {
1495
1525
  external: serverEnvironment.resolve?.external,
1496
1526
  resolve: serverEnvironment.resolve
@@ -1543,7 +1573,7 @@ const reactRouterVitePlugin = () => {
1543
1573
  await builder.build(builder.environments.client);
1544
1574
  let serverEnvironments = getServerEnvironmentValues(ctx, builder.environments);
1545
1575
  await Promise.all(serverEnvironments.map(builder.build));
1546
- await cleanViteManifests(environments, ctx);
1576
+ await cleanViteManifests([builder.environments.client, ...serverEnvironments], ctx);
1547
1577
  }
1548
1578
  }
1549
1579
  };
@@ -1676,11 +1706,10 @@ const reactRouterVitePlugin = () => {
1676
1706
  let cachedHandler = null;
1677
1707
  async function getHandler() {
1678
1708
  if (cachedHandler) return cachedHandler;
1709
+ invariant(viteConfig);
1710
+ updateServerBundlePaths();
1679
1711
  let bundledHandlers = [];
1680
- let buildManifest = ctx.buildManifest ?? (ctx.reactRouterConfig.serverBundles ? await getBuildManifest({
1681
- reactRouterConfig: ctx.reactRouterConfig,
1682
- rootDirectory: ctx.rootDirectory
1683
- }) : null);
1712
+ let { buildManifest } = ctx;
1684
1713
  if (buildManifest?.serverBundles) {
1685
1714
  let routesByServerBundleId = getRoutesByServerBundleId(buildManifest);
1686
1715
  for (let bundle of Object.values(buildManifest.serverBundles)) {
@@ -1691,7 +1720,7 @@ const reactRouterVitePlugin = () => {
1691
1720
  });
1692
1721
  }
1693
1722
  } else {
1694
- let serverEntryPath = path$2.resolve(getServerBuildDirectory(ctx.reactRouterConfig), "index.js");
1723
+ let serverEntryPath = path$2.resolve(getServerBuildDirectory(viteConfig), "index.js");
1695
1724
  let build = await import(url.pathToFileURL(serverEntryPath).href);
1696
1725
  bundledHandlers.push({
1697
1726
  handler: createRequestHandler(build, "production"),
@@ -1723,6 +1752,21 @@ const reactRouterVitePlugin = () => {
1723
1752
  previewServer.middlewares.use(async (req, res, next) => {
1724
1753
  if (!ctx.reactRouterConfig.ssr && (!process.env.hasOwnProperty("IS_RR_BUILD_REQUEST") ? true : process.env.IS_RR_BUILD_REQUEST !== "yes")) return next();
1725
1754
  try {
1755
+ if (isApiOnlyMode(ctx.reactRouterConfig) && process.env.IS_RR_BUILD_REQUEST !== "yes") {
1756
+ let { basename, routeDiscovery } = ctx.reactRouterConfig;
1757
+ let manifestPath = routeDiscovery?.mode === "lazy" ? path$2.posix.join(basename, routeDiscovery.manifestPath ?? "/__manifest") : void 0;
1758
+ let pathname = new URL(req.originalUrl ?? req.url ?? "/", "http://localhost").pathname;
1759
+ if (!pathname.endsWith(".data") && pathname !== manifestPath) {
1760
+ let clientBuildDirectory = getClientBuildDirectory$1(previewServer.config);
1761
+ let previewPathname = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
1762
+ if (previewPathname.endsWith(".html") && existsSync(path$2.join(clientBuildDirectory, previewPathname))) return next();
1763
+ let fallbackFile = path$2.join(clientBuildDirectory, "__spa-fallback.html");
1764
+ if (!existsSync(fallbackFile)) fallbackFile = path$2.join(clientBuildDirectory, "index.html");
1765
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1766
+ res.end(await readFile(fallbackFile));
1767
+ return;
1768
+ }
1769
+ }
1726
1770
  let handler = await getHandler();
1727
1771
  let request = await fromNodeRequest(req, res);
1728
1772
  await sendResponse(res, await handler(request, await reactRouterDevLoadContext(request)));
@@ -1735,9 +1779,8 @@ const reactRouterVitePlugin = () => {
1735
1779
  writeBundle: { async handler() {
1736
1780
  if (!isReactRouterServerEnvironment(ctx, this.environment.name)) return;
1737
1781
  invariant(viteConfig);
1738
- let clientBuildDirectory = getClientBuildDirectory$1(ctx.reactRouterConfig);
1739
- let serverBuildDirectory = this.environment.config?.build?.outDir;
1740
- invariant(serverBuildDirectory);
1782
+ let clientBuildDirectory = getClientBuildDirectory$1(viteConfig);
1783
+ let serverBuildDirectory = path$2.resolve(this.environment.config.root, this.environment.config.build.outDir);
1741
1784
  let ssrViteManifest = await loadViteManifest(serverBuildDirectory);
1742
1785
  let ssrAssetPaths = getViteManifestAssetPaths(ssrViteManifest);
1743
1786
  let userSsrEmitAssets = viteUserConfig.environments?.ssr?.build?.ssrEmitAssets ?? viteUserConfig.environments?.ssr?.build?.emitAssets ?? viteUserConfig.build?.ssrEmitAssets ?? false;
@@ -1795,8 +1838,8 @@ const reactRouterVitePlugin = () => {
1795
1838
  logHandledAssets(copiedAssetPaths, `${copiedAssetPaths.length} asset${copiedAssetPaths.length > 1 ? "s" : ""} copied from React Router server build to client assets.`);
1796
1839
  if (handledAssetPaths.length) viteConfig.logger.info("");
1797
1840
  } },
1798
- async buildEnd() {
1799
- if (viteConfig?.command !== "build") await closePluginResources();
1841
+ async buildEnd(error) {
1842
+ if (viteConfig?.command !== "build" || error) await closePluginResources();
1800
1843
  }
1801
1844
  },
1802
1845
  {
@@ -1955,7 +1998,8 @@ const reactRouterVitePlugin = () => {
1955
1998
  if (isRouteChunkModuleId(id)) id = id.split("?")[0];
1956
1999
  let route = getRoute(ctx.reactRouterConfig, id);
1957
2000
  if (!route) return;
1958
- if (!options?.ssr && isSpaModeEnabled(ctx.reactRouterConfig)) {
2001
+ let isServerEnvironment = isReactRouterServerEnvironment(ctx, this.environment.name);
2002
+ if (!options?.ssr && isSpaModeEnabled(ctx.reactRouterConfig) && !isApiOnlyMode(ctx.reactRouterConfig)) {
1959
2003
  let exportNames = getExportNames(code);
1960
2004
  let serverOnlyExports = exportNames.filter((exp) => {
1961
2005
  if (route.id === "root" && exp === "loader") return false;
@@ -1976,6 +2020,7 @@ const reactRouterVitePlugin = () => {
1976
2020
  let [filepath] = id.split("?");
1977
2021
  let ast = parse$1(code, { sourceType: "module" });
1978
2022
  if (!options?.ssr) removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS);
2023
+ else if (isApiOnlyMode(ctx.reactRouterConfig) && isServerEnvironment && !isPrerenderingEnabled(ctx.reactRouterConfig) && viteCommand === "build" && route.id !== "root") removeExports(ast, CLIENT_ROUTE_EXPORTS$1);
1979
2024
  decorateComponentExportsWithProps(ast);
1980
2025
  return generate(ast, {
1981
2026
  sourceMaps: true,
@@ -2092,10 +2137,7 @@ const reactRouterVitePlugin = () => {
2092
2137
  prerender({
2093
2138
  config() {
2094
2139
  process.env.IS_RR_BUILD_REQUEST = "yes";
2095
- return {
2096
- buildDirectory: getClientBuildDirectory$1(ctx.reactRouterConfig),
2097
- concurrency: getPrerenderConcurrencyConfig$1(ctx.reactRouterConfig)
2098
- };
2140
+ return { concurrency: getPrerenderConcurrencyConfig$1(ctx.reactRouterConfig) };
2099
2141
  },
2100
2142
  async requests() {
2101
2143
  invariant(viteConfig);
@@ -2117,7 +2159,7 @@ const reactRouterVitePlugin = () => {
2117
2159
  else requests.push(createRouteRequest(prerenderPath, reactRouterConfig));
2118
2160
  }
2119
2161
  }
2120
- if (!ctx.reactRouterConfig.ssr) requests.push(createSpaModeRequest(ctx.reactRouterConfig));
2162
+ if (!ctx.reactRouterConfig.ssr || ctx.reactRouterConfig.ssr === "unstable_api-only") requests.push(createSpaModeRequest(ctx.reactRouterConfig));
2121
2163
  return requests;
2122
2164
  },
2123
2165
  async postProcess(request, response, metadata) {
@@ -2181,10 +2223,13 @@ const reactRouterVitePlugin = () => {
2181
2223
  if (metadata.type === "spa") return;
2182
2224
  viteConfig.logger.info(`Prerender (${metadata.type}): ${metadata.path} -> ${colors.bold(outputPath)}`);
2183
2225
  },
2226
+ cleanup() {
2227
+ return closePluginResources();
2228
+ },
2184
2229
  async finalize(buildDirectory) {
2185
2230
  invariant(viteConfig);
2186
2231
  let { ssr } = ctx.reactRouterConfig;
2187
- if (!ssr) {
2232
+ if (!ssr || ssr === "unstable_api-only") {
2188
2233
  let spaFallback = path$2.join(buildDirectory, "__spa-fallback.html");
2189
2234
  let index = path$2.join(buildDirectory, "index.html");
2190
2235
  let finalSpaPath;
@@ -2202,6 +2247,7 @@ const reactRouterVitePlugin = () => {
2202
2247
  }),
2203
2248
  {
2204
2249
  name: "react-router-build-end",
2250
+ enforce: "post",
2205
2251
  sharedDuringBuild: true,
2206
2252
  config: {
2207
2253
  order: "post",
@@ -2209,19 +2255,31 @@ const reactRouterVitePlugin = () => {
2209
2255
  return { builder: { async buildApp(builder) {
2210
2256
  try {
2211
2257
  await buildApp?.(builder);
2212
- invariant(viteConfig);
2213
- let { buildManifest, reactRouterConfig } = ctx;
2214
- invariant(buildManifest, "Expected build manifest");
2215
- await reactRouterConfig.buildEnd?.({
2216
- buildManifest,
2217
- reactRouterConfig,
2218
- viteConfig
2219
- });
2220
- } finally {
2258
+ } catch (error) {
2221
2259
  await closePluginResources();
2260
+ throw error;
2222
2261
  }
2262
+ await closeChildCompiler();
2223
2263
  } } };
2224
2264
  }
2265
+ },
2266
+ buildApp: {
2267
+ order: "post",
2268
+ async handler() {
2269
+ try {
2270
+ invariant(viteConfig);
2271
+ let { buildManifest, reactRouterConfig } = ctx;
2272
+ invariant(buildManifest, "Expected build manifest");
2273
+ updateServerBundlePaths();
2274
+ await reactRouterConfig.buildEnd?.({
2275
+ buildManifest,
2276
+ reactRouterConfig,
2277
+ viteConfig
2278
+ });
2279
+ } finally {
2280
+ await closePluginResources();
2281
+ }
2282
+ }
2225
2283
  }
2226
2284
  },
2227
2285
  validatePluginOrder(),
@@ -2319,7 +2377,10 @@ function isPrerenderingEnabled(reactRouterConfig) {
2319
2377
  return reactRouterConfig.prerender != null && reactRouterConfig.prerender !== false;
2320
2378
  }
2321
2379
  function isSpaModeEnabled(reactRouterConfig) {
2322
- return reactRouterConfig.ssr === false && !isPrerenderingEnabled(reactRouterConfig);
2380
+ return (!reactRouterConfig.ssr || reactRouterConfig.ssr === "unstable_api-only") && !isPrerenderingEnabled(reactRouterConfig);
2381
+ }
2382
+ function isApiOnlyMode(reactRouterConfig) {
2383
+ return reactRouterConfig.ssr === "unstable_api-only";
2323
2384
  }
2324
2385
  function getStaticPrerenderPaths(routes) {
2325
2386
  let paths = ["/"];
@@ -2512,12 +2573,8 @@ async function cleanBuildDirectory(viteConfig, ctx) {
2512
2573
  recursive: true
2513
2574
  });
2514
2575
  }
2515
- async function cleanViteManifests(environmentsOptions, ctx) {
2516
- let viteManifestPaths = Object.entries(environmentsOptions).map(([environmentName, options]) => {
2517
- let outDir = options.build?.outDir;
2518
- invariant(outDir, `Expected build.outDir for ${environmentName}`);
2519
- return path$2.join(outDir, ".vite/manifest.json");
2520
- });
2576
+ async function cleanViteManifests(environments, ctx) {
2577
+ let viteManifestPaths = environments.map(({ config }) => path$2.resolve(config.root, config.build.outDir, ".vite/manifest.json"));
2521
2578
  await Promise.all(viteManifestPaths.map(async (viteManifestPath) => {
2522
2579
  if (!existsSync(viteManifestPath)) return;
2523
2580
  if (!ctx.viteManifestEnabled) await rm(viteManifestPath, {
@@ -2535,7 +2592,7 @@ async function getBuildManifest({ reactRouterConfig, rootDirectory }) {
2535
2592
  let { routes, serverBundles, appDirectory } = reactRouterConfig;
2536
2593
  if (!serverBundles) return { routes };
2537
2594
  let { normalizePath } = await import("vite");
2538
- let serverBuildDirectory = getServerBuildDirectory(reactRouterConfig);
2595
+ let serverBuildDirectory = getDefaultServerBuildDirectory(reactRouterConfig);
2539
2596
  let resolvedAppDirectory = path$2.resolve(rootDirectory, appDirectory);
2540
2597
  let buildManifest = {
2541
2598
  serverBundles: {},
@@ -2634,14 +2691,14 @@ async function getEnvironmentOptionsResolvers(ctx, viteCommand) {
2634
2691
  return path$2.posix.join(assetsDir, `[name]${routeChunkSuffix}-[hash].js`);
2635
2692
  } }
2636
2693
  },
2637
- outDir: getClientBuildDirectory$1(ctx.reactRouterConfig)
2694
+ outDir: getDefaultClientBuildDirectory(ctx.reactRouterConfig)
2638
2695
  } }) };
2639
2696
  let serverBundleIds = getServerBundleIds(ctx);
2640
2697
  if (serverBundleIds) for (let serverBundleId of serverBundleIds) {
2641
2698
  const environmentName = `${SSR_BUNDLE_PREFIX}${serverBundleId}`;
2642
- environmentOptionsResolvers[environmentName] = ({ viteUserConfig }) => mergeEnvironmentOptions(getBaseServerOptions({ viteUserConfig }), { build: { outDir: getServerBuildDirectory(ctx.reactRouterConfig, { serverBundleId }) } }, viteUserConfig.environments?.ssr ?? {});
2699
+ environmentOptionsResolvers[environmentName] = ({ viteUserConfig }) => mergeEnvironmentOptions(getBaseServerOptions({ viteUserConfig }), { build: { outDir: getDefaultServerBuildDirectory(ctx.reactRouterConfig, { serverBundleId }) } }, viteUserConfig.environments?.ssr ?? {});
2643
2700
  }
2644
- else environmentOptionsResolvers.ssr = ({ viteUserConfig }) => mergeEnvironmentOptions(getBaseServerOptions({ viteUserConfig }), { build: { outDir: getServerBuildDirectory(ctx.reactRouterConfig) } });
2701
+ else environmentOptionsResolvers.ssr = ({ viteUserConfig }) => mergeEnvironmentOptions(getBaseServerOptions({ viteUserConfig }), { build: { outDir: getDefaultServerBuildDirectory(ctx.reactRouterConfig) } });
2645
2702
  return environmentOptionsResolvers;
2646
2703
  }
2647
2704
  function resolveEnvironmentsOptions(environmentResolvers, resolverOptions) {
@@ -2677,7 +2734,7 @@ function createDataRequest(prerenderPath, reactRouterConfig, onlyRoutes, isResou
2677
2734
  let url = new URL(`http://localhost${normalizedPath}`);
2678
2735
  if (onlyRoutes?.length) url.searchParams.set("_routes", onlyRoutes.join(","));
2679
2736
  return {
2680
- request: new Request(url),
2737
+ request: new Request(url, { headers: { "X-React-Router-Prerender": "yes" } }),
2681
2738
  metadata: {
2682
2739
  type: "data",
2683
2740
  path: prerenderPath,
@@ -2687,7 +2744,7 @@ function createDataRequest(prerenderPath, reactRouterConfig, onlyRoutes, isResou
2687
2744
  }
2688
2745
  function createRouteRequest(prerenderPath, reactRouterConfig, data) {
2689
2746
  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(/\/\/+/g, "/");
2690
- let headers = new Headers();
2747
+ let headers = new Headers({ "X-React-Router-Prerender": "yes" });
2691
2748
  if (data) {
2692
2749
  let encodedData = encodeURI(data);
2693
2750
  if (encodedData.length < 8 * 1024) headers.set("X-React-Router-Prerender-Data", encodedData);
@@ -2702,8 +2759,13 @@ function createRouteRequest(prerenderPath, reactRouterConfig, data) {
2702
2759
  }
2703
2760
  function createResourceRouteRequest(prerenderPath, reactRouterConfig, requestInit) {
2704
2761
  let normalizedPath = `${reactRouterConfig.basename}${prerenderPath}/`.replace(/\/\/+/g, "/").replace(/\/$/g, "");
2762
+ let headers = new Headers(requestInit?.headers);
2763
+ headers.set("X-React-Router-Prerender", "yes");
2705
2764
  return {
2706
- request: new Request(`http://localhost${normalizedPath}`, requestInit),
2765
+ request: new Request(`http://localhost${normalizedPath}`, {
2766
+ ...requestInit,
2767
+ headers
2768
+ }),
2707
2769
  metadata: {
2708
2770
  type: "resource",
2709
2771
  path: prerenderPath
@@ -2863,6 +2925,38 @@ const ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR = `
2863
2925
  import * as ___EnsureClientRouteModuleForHMR_REACT___ from "react";
2864
2926
  export function EnsureClientRouteModuleForHMR___() { return ___EnsureClientRouteModuleForHMR_REACT___.createElement(___EnsureClientRouteModuleForHMR_REACT___.Fragment, null) }
2865
2927
  `;
2928
+ function createEmptySourceMap() {
2929
+ return {
2930
+ version: 3,
2931
+ names: [],
2932
+ sources: [],
2933
+ mappings: ""
2934
+ };
2935
+ }
2936
+ function generateRouteModule(code, map, filename) {
2937
+ const ast = parse$1(code, { sourceType: "module" });
2938
+ const generatorOptions = {
2939
+ sourceMaps: true,
2940
+ sourceFileName: filename,
2941
+ inputSourceMap: map
2942
+ };
2943
+ return {
2944
+ ast,
2945
+ generate: () => generate(ast, generatorOptions)
2946
+ };
2947
+ }
2948
+ function prependUnmappedCode(prefix, generated) {
2949
+ if (generated.map) generated.map.mappings = ";".repeat(prefix.match(/\n/g)?.length ?? 0) + generated.map.mappings;
2950
+ return {
2951
+ code: prefix + generated.code,
2952
+ map: generated.map
2953
+ };
2954
+ }
2955
+ function appendUnmappedCode(generated, code) {
2956
+ if (!generated.code.endsWith("\n") && !code.startsWith("\n")) code = "\n" + code;
2957
+ if (generated.map) generated.map.mappings += ";A".repeat(code.match(/\n/g)?.length ?? 0);
2958
+ generated.code += code;
2959
+ }
2866
2960
  function virtualRouteModulesPlugin({ enforceSplitRouteModules, environments: { client = ["client", "ssr"], server = ["rsc"] } = {}, getRouteIdForFile, isRootRouteModule, transformToJs, shouldTransform }) {
2867
2961
  let clientEnvironments = new Set(client);
2868
2962
  let serverEnvironments = new Set(server);
@@ -2898,7 +2992,10 @@ function virtualRouteModulesPlugin({ enforceSplitRouteModules, environments: { c
2898
2992
  }
2899
2993
  });
2900
2994
  }
2901
- return { code: "\"use client\";\n" + result };
2995
+ return {
2996
+ code: "\"use client\";\n" + result,
2997
+ map: createEmptySourceMap()
2998
+ };
2902
2999
  }
2903
3000
  async function createServerRouteEntry(id, code, isRootRouteModule, routeId) {
2904
3001
  let result = "";
@@ -2938,16 +3035,19 @@ ${result}`;
2938
3035
  }
2939
3036
  });
2940
3037
  }
2941
- return { code: result };
3038
+ return {
3039
+ code: result,
3040
+ map: createEmptySourceMap()
3041
+ };
2942
3042
  }
2943
- function createServerRouteModule(code) {
2944
- const ast = parse$1(code, { sourceType: "module" });
3043
+ function createServerRouteModule(code, map, filename) {
3044
+ const { ast, generate } = generateRouteModule(code, map, filename);
2945
3045
  removeExports(ast, CLIENT_ROUTE_EXPORTS);
2946
- return generate(ast);
3046
+ return generate();
2947
3047
  }
2948
- async function createClientRouteModuleChunk(id, code, chunk, routeId, isRootRouteModule, isDevMode) {
3048
+ async function createClientRouteModuleChunk(id, code, chunk, routeId, isRootRouteModule, isDevMode, map, filename) {
2949
3049
  let routeChunks = detectRouteChunks(cache, id, code, isRootRouteModule);
2950
- const ast = parse$1(code, { sourceType: "module" });
3050
+ const { ast, generate } = generateRouteModule(code, map, filename);
2951
3051
  const { staticExports } = await parseRouteExports(code);
2952
3052
  if (chunk === "shared") removeExports(ast, [...SERVER_ROUTE_EXPORTS, ...routeChunks.chunkedExports]);
2953
3053
  else {
@@ -2955,33 +3055,26 @@ ${result}`;
2955
3055
  toRemove.delete(chunk);
2956
3056
  removeExports(ast, Array.from(toRemove));
2957
3057
  }
2958
- let result = "\"use client\";\n" + generate(ast).code;
3058
+ let result = prependUnmappedCode("\"use client\";\n", generate());
2959
3059
  if (chunk === "shared") {
2960
- if (isRootRouteModule && !staticExports.includes("ErrorBoundary") && !staticExports.includes("ServerErrorBoundary")) {
2961
- const hasRootLayout = staticExports.includes("Layout") || staticExports.includes("ServerLayout");
2962
- result += `\nimport { createElement as __rr_createElement } from "react";\n`;
2963
- result += `import { UNSAFE_RSCDefaultRootErrorBoundary } from "react-router";\n`;
2964
- result += `export function ErrorBoundary() {\n`;
2965
- result += ` return __rr_createElement(UNSAFE_RSCDefaultRootErrorBoundary, { hasRootLayout: ${hasRootLayout} });\n`;
2966
- result += `}\n`;
2967
- }
2968
- result += ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR;
3060
+ if (isRootRouteModule && !staticExports.includes("ErrorBoundary") && !staticExports.includes("ServerErrorBoundary")) appendUnmappedCode(result, `
3061
+ import { createElement as __rr_createElement } from "react";
3062
+ import { UNSAFE_RSCDefaultRootErrorBoundary } from "react-router";
3063
+ export function ErrorBoundary() {
3064
+ return __rr_createElement(UNSAFE_RSCDefaultRootErrorBoundary, { hasRootLayout: ${staticExports.includes("Layout") || staticExports.includes("ServerLayout")} });\n}\n`);
3065
+ appendUnmappedCode(result, ENSURE_CLIENT_ROUTE_MODULE_CHUNK_FOR_HMR);
2969
3066
  }
2970
3067
  let hasAction = staticExports.includes("action");
2971
3068
  let hasLoader = staticExports.includes("loader");
2972
3069
  let hasComponent = staticExports.includes("default") || staticExports.includes("ServerComponent");
2973
3070
  let hasErrorBoundary = staticExports.includes("ErrorBoundary") || staticExports.includes("ServerErrorBoundary");
2974
- if (isDevMode) {
2975
- result += `export function ReactRouterHMRMeta___() {return null;};\n`;
2976
- result += `Object.assign(ReactRouterHMRMeta___, {
3071
+ if (isDevMode) appendUnmappedCode(result, `export function ReactRouterHMRMeta___() {return null;};\nObject.assign(ReactRouterHMRMeta___, {
2977
3072
  hasAction: ${JSON.stringify(hasAction)},
2978
3073
  hasComponent: ${JSON.stringify(hasComponent)},
2979
3074
  hasErrorBoundary: ${JSON.stringify(hasErrorBoundary)},
2980
3075
  hasLoader: ${JSON.stringify(hasLoader)},
2981
3076
  hasClientLoader: ${JSON.stringify(staticExports.includes("clientLoader"))},
2982
- });\n`;
2983
- result += `\nif (import.meta.hot) {\n`;
2984
- result += ` import.meta.hot.accept((mod) => {
3077
+ });\n\nif (import.meta.hot) {\n import.meta.hot.accept((mod) => {
2985
3078
  if (typeof __reactRouterDataRouter === "object") {
2986
3079
  __reactRouterDataRouter._updateRoutesForHMR(new Map([[${JSON.stringify(routeId)}, {
2987
3080
  routeModule: mod,
@@ -2995,10 +3088,8 @@ ${result}`;
2995
3088
  }
2996
3089
  }
2997
3090
  });
2998
- `;
2999
- result += `}\n`;
3000
- }
3001
- return { code: result };
3091
+ }\n`);
3092
+ return result;
3002
3093
  }
3003
3094
  return {
3004
3095
  name: "react-router-rsc-virtual-route-modules",
@@ -3010,12 +3101,13 @@ ${result}`;
3010
3101
  let isClientEnvironment = clientEnvironments.has(this.environment.name);
3011
3102
  let isServerEnvironment = serverEnvironments.has(this.environment.name);
3012
3103
  if (!isClientEnvironment && !isServerEnvironment) return;
3013
- let code = await transformToJs(_code, filename);
3104
+ let transformed = await transformToJs(_code, filename);
3105
+ let code = transformed.code;
3014
3106
  let searchParams = rest.length > 0 ? new URLSearchParams(rest.join("?")) : null;
3015
3107
  let clientRouteModuleType = searchParams?.get("client-route-module");
3016
3108
  let isServerRouteModule = searchParams?.has("server-route-module");
3017
- if (clientRouteModuleType) return await createClientRouteModuleChunk(id, code, clientRouteModuleType, routeId, isRootRouteModule(filename), this.environment.mode === "dev");
3018
- if (isServerRouteModule) return createServerRouteModule(code);
3109
+ if (clientRouteModuleType) return await createClientRouteModuleChunk(id, code, clientRouteModuleType, routeId, isRootRouteModule(filename), this.environment.mode === "dev", transformed.map, filename);
3110
+ if (isServerRouteModule) return createServerRouteModule(code, transformed.map, filename);
3019
3111
  if (isClientEnvironment) return await createClientRouteEntry(id, code, isRootRouteModule(filename), routeId);
3020
3112
  return await createServerRouteEntry(id, code, isRootRouteModule(filename), routeId);
3021
3113
  }
@@ -3181,7 +3273,7 @@ function reactRouterRSCVitePlugin() {
3181
3273
  await preloadVite();
3182
3274
  let vite = getVite();
3183
3275
  let lang = getTransformLanguage(filename);
3184
- return ("transformWithOxc" in vite && typeof vite.transformWithOxc === "function" ? await vite.transformWithOxc(code, filename, {
3276
+ return "transformWithOxc" in vite && typeof vite.transformWithOxc === "function" ? await vite.transformWithOxc(code, filename, {
3185
3277
  lang,
3186
3278
  jsx: {
3187
3279
  runtime: "automatic",
@@ -3194,7 +3286,7 @@ function reactRouterRSCVitePlugin() {
3194
3286
  format: "esm",
3195
3287
  jsx: "automatic",
3196
3288
  jsxDev: viteCommand !== "build"
3197
- })).code;
3289
+ });
3198
3290
  }
3199
3291
  async function getClientVersion(source) {
3200
3292
  const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
@@ -3223,6 +3315,7 @@ function reactRouterRSCVitePlugin() {
3223
3315
  if (userConfig.buildEnd) errors.push("buildEnd");
3224
3316
  if (userConfig.presets?.length) errors.push("presets");
3225
3317
  if (userConfig.serverBundles) errors.push("serverBundles");
3318
+ if (userConfig.ssr === "unstable_api-only") errors.push("ssr: \"unstable_api-only\"");
3226
3319
  if (errors.length) return `RSC Framework Mode does not currently support the following React Router config:\n${errors.map((x) => ` - ${x}`).join("\n")}\n`;
3227
3320
  }
3228
3321
  });
@@ -3240,6 +3333,7 @@ function reactRouterRSCVitePlugin() {
3240
3333
  });
3241
3334
  let routeFiles = new Set(Object.values(config.routes).map((route) => resolveRelativeRouteFilePath(route, config)));
3242
3335
  return {
3336
+ __reactRouterBuildContext: { close: () => configLoader.close() },
3243
3337
  resolve: { dedupe: [
3244
3338
  "react",
3245
3339
  "react/jsx-runtime",
@@ -3355,7 +3449,7 @@ format: config.serverModuleFormat }
3355
3449
  });
3356
3450
  },
3357
3451
  configurePreviewServer(previewServer) {
3358
- const clientBuildDirectory = getClientBuildDirectory(config);
3452
+ const clientBuildDirectory = getClientBuildDirectory(resolvedViteConfig);
3359
3453
  if ((config.prerender || config.ssr === false) && process.env.IS_RR_BUILD_REQUEST !== "yes") {
3360
3454
  previewServer.middlewares.use(async (req, res, next) => {
3361
3455
  try {
@@ -3390,11 +3484,8 @@ format: config.serverModuleFormat }
3390
3484
  };
3391
3485
  }
3392
3486
  },
3393
- buildApp: {
3394
- order: "post",
3395
- async handler() {
3396
- await configLoader.close();
3397
- }
3487
+ async buildEnd(error) {
3488
+ if (error) await configLoader.close();
3398
3489
  }
3399
3490
  },
3400
3491
  (() => {
@@ -3561,12 +3652,10 @@ export default assetsManifest.clientVersion;
3561
3652
  warnOnClientSourceMaps(),
3562
3653
  prerender({
3563
3654
  config() {
3564
- return {
3565
- buildDirectory: getClientBuildDirectory(config),
3566
- concurrency: getPrerenderConcurrencyConfig(config)
3567
- };
3655
+ return { concurrency: getPrerenderConcurrencyConfig(config) };
3568
3656
  },
3569
3657
  logFile: (path) => logger.info(`Prerendered ${colors.bold(path)}`),
3658
+ cleanup: () => configLoader.close(),
3570
3659
  async requests() {
3571
3660
  const prerenderPaths = new Set(await getPrerenderPaths(config.prerender, config.ssr, config.routes, true));
3572
3661
  let basename = !config.basename || config.basename === "/" ? "/" : config.basename.endsWith("/") ? config.basename : config.basename + "/";
@@ -3622,7 +3711,18 @@ export default assetsManifest.clientVersion;
3622
3711
  }
3623
3712
  return files;
3624
3713
  }
3625
- })
3714
+ }),
3715
+ {
3716
+ name: "react-router/rsc/build-end",
3717
+ enforce: "post",
3718
+ sharedDuringBuild: true,
3719
+ buildApp: {
3720
+ order: "post",
3721
+ async handler() {
3722
+ await configLoader.close();
3723
+ }
3724
+ }
3725
+ }
3626
3726
  ];
3627
3727
  }
3628
3728
  const virtual = {
@@ -3654,7 +3754,7 @@ function createRSCOptimizeDepsRouteModulesPlugin({ routeFiles, transformToJs })
3654
3754
  let normalizedFilename = getVite().normalizePath(filename);
3655
3755
  if (!routeFiles.has(normalizedFilename)) return;
3656
3756
  return {
3657
- code: createClientRouteModuleForOptimizeDepsScan(await transformToJs(code, filename)).code,
3757
+ code: createClientRouteModuleForOptimizeDepsScan((await transformToJs(code, filename)).code).code,
3658
3758
  map: null,
3659
3759
  moduleType: "js"
3660
3760
  };
@@ -3662,7 +3762,7 @@ function createRSCOptimizeDepsRouteModulesPlugin({ routeFiles, transformToJs })
3662
3762
  }
3663
3763
  };
3664
3764
  }
3665
- const getClientBuildDirectory = (reactRouterConfig) => path$1.join(reactRouterConfig.buildDirectory, "client");
3765
+ const getClientBuildDirectory = (viteConfig) => path$1.resolve(viteConfig.root, viteConfig.environments.client.build.outDir);
3666
3766
  function getPrerenderConcurrencyConfig(reactRouterConfig) {
3667
3767
  let concurrency = 1;
3668
3768
  let { prerender } = reactRouterConfig;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-router/dev",
3
3
  "type": "module",
4
- "version": "8.4.0",
4
+ "version": "8.5.1",
5
5
  "description": "Dev tools and CLI for React Router",
6
6
  "keywords": [
7
7
  "react",
@@ -107,7 +107,7 @@
107
107
  "semver": "^7.8.1",
108
108
  "tinyglobby": "^0.2.16",
109
109
  "valibot": "^1.4.1",
110
- "@react-router/node": "8.4.0"
110
+ "@react-router/node": "8.5.1"
111
111
  },
112
112
  "devDependencies": {
113
113
  "@babel/plugin-syntax-jsx": "^7.29.7",
@@ -132,8 +132,8 @@
132
132
  "vite": "^7.0.0",
133
133
  "wireit": "0.14.12",
134
134
  "wrangler": "^4.95.0",
135
- "react-router": "^8.4.0",
136
- "@react-router/serve": "8.4.0"
135
+ "@react-router/serve": "8.5.1",
136
+ "react-router": "^8.5.1"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@vitejs/plugin-rsc": "~0.5.26",
@@ -141,8 +141,8 @@
141
141
  "typescript": "^5.1.0 || ^6.0.0 || ^7.0.0",
142
142
  "vite": "^7.0.0 || ^8.0.0",
143
143
  "wrangler": "^4.0.0",
144
- "@react-router/serve": "^8.4.0",
145
- "react-router": "^8.4.0"
144
+ "@react-router/serve": "^8.5.1",
145
+ "react-router": "^8.5.1"
146
146
  },
147
147
  "peerDependenciesMeta": {
148
148
  "@vitejs/plugin-rsc": {