Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@
- omar-moquete
- OnurGvnc
- otabekshoyimov
- p-dubovitsky
- p13i
- parched
- parveen232
Expand Down
115 changes: 115 additions & 0 deletions integration/vite-extra-server-environment-test.ts
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
export default function Index() {
return <h1>Hello</h1>;
}
`,
"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<typeof loader>();
return <a href={loaderData.txtUrl}>txtUrl</a>;
}
`,
"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);
});
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 12 additions & 7 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
ctx: ReactRouterPluginContext,
record: Record<string, T>,
): [SsrEnvironmentName, T][] {
return Object.entries(record).filter(([name]) =>
ctx.buildManifest?.serverBundles
? isSsrBundleEnvironmentName(name)
: name === "ssr",
isReactRouterServerEnvironment(ctx, name),
) as [SsrEnvironmentName, T][];
}

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -1860,7 +1865,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {

if (
future.v8_viteEnvironmentApi
? this.environment.name === "client"
? !isReactRouterServerEnvironment(ctx, this.environment.name)
: !viteConfigEnv.isSsrBuild
) {
return;
Expand Down