From 9b677aac45c5c54cd2c981b0d5f2b69e16643fd6 Mon Sep 17 00:00:00 2001
From: Pavel Dubovitsky <38439220+p-dubovitsky@users.noreply.github.com>
Date: Thu, 4 Jun 2026 20:15:55 +0200
Subject: [PATCH 1/2] fix: ignore external Vite server environments in React
Router build hooks (#14883)
---
contributors.yml | 1 +
.../vite-extra-server-environment-test.ts | 115 ++++++++++++++++++
...ironments-in-framework-mode-build-hooks.md | 5 +
packages/react-router-dev/vite/plugin.ts | 21 ++--
4 files changed, 134 insertions(+), 8 deletions(-)
create mode 100644 integration/vite-extra-server-environment-test.ts
create mode 100644 packages/react-router-dev/.changes/patch.ignore-external-server-environments-in-framework-mode-build-hooks.md
diff --git a/contributors.yml b/contributors.yml
index 0bdabdb44c..06bbf577e4 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -340,6 +340,7 @@
- omar-moquete
- OnurGvnc
- otabekshoyimov
+- p-dubovitsky
- p13i
- parched
- parveen232
diff --git a/integration/vite-extra-server-environment-test.ts b/integration/vite-extra-server-environment-test.ts
new file mode 100644
index 0000000000..295fdf75b1
--- /dev/null
+++ b/integration/vite-extra-server-environment-test.ts
@@ -0,0 +1,115 @@
+import { test, expect } from "@playwright/test";
+import glob from "glob";
+
+import { build, createProject, reactRouterConfig } from "./helpers/vite.js";
+
+const js = String.raw;
+
+test("ignores external server environments without skipping React Router build hooks", async () => {
+ let cwd = await createProject(
+ {
+ "react-router.config.ts": reactRouterConfig({
+ future: { v8_viteEnvironmentApi: true },
+ }),
+ "vite.config.ts": js`
+ import { defineConfig } from "vite";
+ import { reactRouter } from "@react-router/dev/vite";
+
+ function extraServerEnvironment() {
+ return {
+ name: "extra-server-environment",
+ config() {
+ return {
+ environments: {
+ externalServerEnv: {
+ consumer: "server",
+ build: {
+ rollupOptions: { input: "./external-server-env.ts" },
+ },
+ },
+ },
+ builder: {
+ sharedConfigBuild: true,
+ sharedPlugins: true,
+ async buildApp(builder) {
+ // External build orchestrators can introduce additional
+ // server environments that React Router should ignore.
+ await builder.build(builder.environments.client);
+ await builder.build(builder.environments.ssr);
+ await builder.build(builder.environments.externalServerEnv);
+ },
+ },
+ };
+ },
+ };
+ }
+
+ export default defineConfig({
+ build: {
+ assetsInlineLimit: 0,
+ },
+ plugins: [
+ reactRouter(),
+ extraServerEnvironment(),
+ ],
+ });
+ `,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Hello
;
+ }
+ `,
+ "app/assets/test.txt": "test",
+ "app/ssr-only-asset.server.ts": js`
+ import txtUrl from "./assets/test.txt?url";
+
+ export { txtUrl };
+ `,
+ "app/routes/ssr-only-assets.tsx": js`
+ import { useLoaderData } from "react-router";
+
+ export const loader = async () => {
+ let { txtUrl } = await import("../ssr-only-asset.server");
+ return { txtUrl };
+ };
+
+ export default function SsrOnlyAssetsRoute() {
+ const loaderData = useLoaderData();
+ return txtUrl;
+ }
+ `,
+ "external-server-env.ts": js`
+ export default {
+ async fetch() {
+ return new Response("ok");
+ },
+ };
+ `,
+ },
+ "vite-6-template",
+ );
+
+ let { status, stderr } = build({ cwd });
+
+ expect(stderr.toString().trim()).toBeFalsy();
+ expect(status).toBe(0);
+ expect(glob.sync("build/client/assets/test-*.txt", { cwd }).length).toBe(1);
+});
diff --git a/packages/react-router-dev/.changes/patch.ignore-external-server-environments-in-framework-mode-build-hooks.md b/packages/react-router-dev/.changes/patch.ignore-external-server-environments-in-framework-mode-build-hooks.md
new file mode 100644
index 0000000000..448a6528d2
--- /dev/null
+++ b/packages/react-router-dev/.changes/patch.ignore-external-server-environments-in-framework-mode-build-hooks.md
@@ -0,0 +1,5 @@
+Ignore external Vite server environments in Framework Mode build hooks
+
+When `future.v8_viteEnvironmentApi` is enabled, React Router previously treated any non-client Vite environment as its own server build. This caused issues with integrations like Nitro, where plugins can register additional environments.
+
+Framework Mode build hooks now ignore external server environments and only process the app's own server build.
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 0d785a8a67..2f5f64b445 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -179,14 +179,21 @@ export type EnvironmentBuildContext = {
resolveOptions: EnvironmentOptionsResolver;
};
+function isReactRouterServerEnvironment(
+ ctx: ReactRouterPluginContext,
+ environmentName: string,
+): environmentName is SsrEnvironmentName {
+ return ctx.buildManifest?.serverBundles
+ ? isSsrBundleEnvironmentName(environmentName)
+ : environmentName === "ssr";
+}
+
function getServerEnvironmentEntries(
ctx: ReactRouterPluginContext,
record: Record,
): [SsrEnvironmentName, T][] {
- return Object.entries(record).filter(([name]) =>
- ctx.buildManifest?.serverBundles
- ? isSsrBundleEnvironmentName(name)
- : name === "ssr",
+ return Object.entries(record).filter(
+ ([name]) => isReactRouterServerEnvironment(ctx, name),
) as [SsrEnvironmentName, T][];
}
@@ -1460,9 +1467,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
configEnvironment(name, options) {
if (
ctx.reactRouterConfig.future.v8_viteEnvironmentApi &&
- (ctx.buildManifest?.serverBundles
- ? isSsrBundleEnvironmentName(name)
- : name === "ssr")
+ isReactRouterServerEnvironment(ctx, name)
) {
const vite = getVite();
@@ -1860,7 +1865,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
if (
future.v8_viteEnvironmentApi
- ? this.environment.name === "client"
+ ? !isReactRouterServerEnvironment(ctx, this.environment.name)
: !viteConfigEnv.isSsrBuild
) {
return;
From 3927a948a6f0ad77358338eede8690ea91612715 Mon Sep 17 00:00:00 2001
From: Remix Run Bot
Date: Thu, 4 Jun 2026 18:16:34 +0000
Subject: [PATCH 2/2] chore: format
---
packages/react-router-dev/vite/plugin.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 2f5f64b445..7710912526 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -192,8 +192,8 @@ function getServerEnvironmentEntries(
ctx: ReactRouterPluginContext,
record: Record,
): [SsrEnvironmentName, T][] {
- return Object.entries(record).filter(
- ([name]) => isReactRouterServerEnvironment(ctx, name),
+ return Object.entries(record).filter(([name]) =>
+ isReactRouterServerEnvironment(ctx, name),
) as [SsrEnvironmentName, T][];
}