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..7710912526 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", + 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;