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][]; }