Skip to content

Commit 9f6a507

Browse files
authored
Support flexible ordering of Vite plugins that override SSR environment (#13183)
1 parent f3a07bb commit 9f6a507

File tree

5 files changed

+191
-67
lines changed

5 files changed

+191
-67
lines changed

.changeset/dull-hotels-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
When `future.unstable_viteEnvironmentApi` is enabled, allow plugins that override the default SSR environment (such as `@cloudflare/vite-plugin`) to be placed before or after the React Router plugin.

integration/helpers/vite-plugin-cloudflare-template/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"serialize-javascript": "^6.0.1"
1919
},
2020
"devDependencies": {
21-
"@cloudflare/vite-plugin": "^0.1.1",
21+
"@cloudflare/vite-plugin": "^0.1.9",
2222
"@cloudflare/workers-types": "^4.20250214.0",
2323
"@react-router/dev": "workspace:*",
2424
"@react-router/fs-routes": "workspace:*",

integration/vite-plugin-cloudflare-test.ts

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,70 @@ import { type Files, test, viteConfig } from "./helpers/vite.js";
66
const tsx = dedent;
77
const css = dedent;
88

9-
test.describe("vite-plugin-cloudflare", () => {
9+
function defineFiles({
10+
reversePlugins = false,
11+
}: { reversePlugins?: boolean } = {}): Files {
1012
const files: Files = async ({ port }) => ({
1113
"vite.config.ts": tsx`
12-
import { defineConfig } from "vite";
13-
import { cloudflare } from "@cloudflare/vite-plugin";
14-
import { reactRouter } from "@react-router/dev/vite";
15-
16-
export default defineConfig({
17-
${await viteConfig.server({ port })}
18-
plugins: [
19-
cloudflare({ viteEnvironment: { name: "ssr" } }),
20-
reactRouter(),
21-
],
22-
});
23-
`,
14+
import { defineConfig } from "vite";
15+
import { cloudflare } from "@cloudflare/vite-plugin";
16+
import { reactRouter } from "@react-router/dev/vite";
17+
18+
export default defineConfig({
19+
${await viteConfig.server({ port })}
20+
plugins: [
21+
cloudflare({ viteEnvironment: { name: "ssr" } }),
22+
reactRouter(),
23+
]${reversePlugins ? ".reverse()" : ""},
24+
});
25+
`,
2426
"app/routes/env.tsx": tsx`
25-
import type { Route } from "./+types/env";
26-
export function loader({ context }: Route.LoaderArgs) {
27-
return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };
28-
}
29-
export default function EnvRoute({ loaderData }: Route.RouteComponentProps) {
30-
return <div data-loader-message>{loaderData.message}</div>;
31-
}
32-
`,
27+
import type { Route } from "./+types/env";
28+
export function loader({ context }: Route.LoaderArgs) {
29+
return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE };
30+
}
31+
export default function EnvRoute({ loaderData }: Route.RouteComponentProps) {
32+
return <div data-loader-message>{loaderData.message}</div>;
33+
}
34+
`,
3335
"app/routes/css-side-effect/route.tsx": tsx`
34-
import "./styles.css";
35-
36-
export default function CssSideEffectRoute() {
37-
return <div className="css-side-effect" data-css-side-effect>CSS Side Effect</div>;
38-
}
39-
`,
36+
import "./styles.css";
37+
38+
export default function CssSideEffectRoute() {
39+
return <div className="css-side-effect" data-css-side-effect>CSS Side Effect</div>;
40+
}
41+
`,
4042
"app/routes/css-side-effect/styles.css": css`
4143
.css-side-effect {
4244
padding: 20px;
4345
}
4446
`,
4547
});
48+
return files;
49+
}
4650

51+
test.describe("vite-plugin-cloudflare", () => {
4752
test("handles Cloudflare env", async ({ dev, page }) => {
53+
const files = defineFiles();
54+
const { port } = await dev(files, "vite-plugin-cloudflare-template");
55+
56+
await page.goto(`http://localhost:${port}/env`, {
57+
waitUntil: "networkidle",
58+
});
59+
60+
// Ensure no errors on page load
61+
expect(page.errors).toEqual([]);
62+
63+
await expect(page.locator("[data-loader-message]")).toHaveText(
64+
"Hello from Cloudflare"
65+
);
66+
});
67+
68+
test("handles Cloudflare env with plugin order reversed", async ({
69+
dev,
70+
page,
71+
}) => {
72+
const files = defineFiles({ reversePlugins: true });
4873
const { port } = await dev(files, "vite-plugin-cloudflare-template");
4974

5075
await page.goto(`http://localhost:${port}/env`, {
@@ -66,6 +91,7 @@ test.describe("vite-plugin-cloudflare", () => {
6691
dev,
6792
page,
6893
}) => {
94+
const files = defineFiles();
6995
const { port } = await dev(files, "vite-plugin-cloudflare-template");
7096

7197
await page.goto(`http://localhost:${port}/css-side-effect`, {
@@ -78,4 +104,21 @@ test.describe("vite-plugin-cloudflare", () => {
78104
);
79105
});
80106
});
107+
108+
test("handles CSS side effects during SSR in dev with plugin order reversed", async ({
109+
dev,
110+
page,
111+
}) => {
112+
const files = defineFiles({ reversePlugins: true });
113+
const { port } = await dev(files, "vite-plugin-cloudflare-template");
114+
115+
await page.goto(`http://localhost:${port}/css-side-effect`, {
116+
waitUntil: "networkidle",
117+
});
118+
119+
await expect(page.locator("[data-css-side-effect]")).toHaveCSS(
120+
"padding",
121+
"20px"
122+
);
123+
});
81124
});

packages/react-router-dev/vite/plugin.ts

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,7 @@ const CSS_DEV_HELPER_ENVIRONMENT_NAME =
160160
"__react_router_css_dev_helper__" as const;
161161
type CssDevHelperEnvironmentName = typeof CSS_DEV_HELPER_ENVIRONMENT_NAME;
162162

163-
type EnvironmentOptions = Pick<
164-
Vite.EnvironmentOptions,
165-
"build" | "resolve" | "optimizeDeps"
166-
>;
163+
type EnvironmentOptions = Pick<Vite.EnvironmentOptions, "build" | "resolve">;
167164

168165
type EnvironmentOptionsResolver = (options: {
169166
viteUserConfig: Vite.UserConfig;
@@ -178,19 +175,13 @@ export type EnvironmentBuildContext = {
178175
resolveOptions: EnvironmentOptionsResolver;
179176
};
180177

181-
function isSeverBundleEnvironmentName(
182-
name: string
183-
): name is SsrEnvironmentName {
184-
return name.startsWith(SSR_BUNDLE_PREFIX);
185-
}
186-
187178
function getServerEnvironmentEntries<T>(
188179
ctx: ReactRouterPluginContext,
189180
record: Record<string, T>
190181
): [SsrEnvironmentName, T][] {
191182
return Object.entries(record).filter(([name]) =>
192183
ctx.buildManifest?.serverBundles
193-
? isSeverBundleEnvironmentName(name)
184+
? isSsrBundleEnvironmentName(name)
194185
: name === "ssr"
195186
) as [SsrEnvironmentName, T][];
196187
}
@@ -1295,6 +1286,50 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
12951286
}),
12961287
};
12971288
},
1289+
configEnvironment(name, options) {
1290+
if (
1291+
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi &&
1292+
(ctx.buildManifest?.serverBundles
1293+
? isSsrBundleEnvironmentName(name)
1294+
: name === "ssr")
1295+
) {
1296+
const vite = getVite();
1297+
1298+
return {
1299+
resolve: {
1300+
external:
1301+
// This check is required to honor the "noExternal: true" config
1302+
// provided by vite-plugin-cloudflare within this repo. When compiling
1303+
// for Cloudflare, all server dependencies are pre-bundled, but our
1304+
// `ssrExternals` config inadvertently overrides this. This doesn't
1305+
// impact consumers because for them `ssrExternals` is undefined and
1306+
// Cloudflare's "noExternal: true" config remains intact.
1307+
options.resolve?.noExternal === true ? undefined : ssrExternals,
1308+
},
1309+
optimizeDeps:
1310+
options.optimizeDeps?.noDiscovery === false
1311+
? {
1312+
entries: [
1313+
vite.normalizePath(ctx.entryServerFilePath),
1314+
...Object.values(ctx.reactRouterConfig.routes).map(
1315+
(route) =>
1316+
resolveRelativeRouteFilePath(
1317+
route,
1318+
ctx.reactRouterConfig
1319+
)
1320+
),
1321+
],
1322+
include: [
1323+
"react",
1324+
"react/jsx-dev-runtime",
1325+
"react-dom/server",
1326+
"react-router",
1327+
],
1328+
}
1329+
: undefined,
1330+
};
1331+
}
1332+
},
12981333
async configResolved(resolvedViteConfig) {
12991334
await initEsModuleLexer;
13001335

@@ -1531,6 +1566,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
15311566
let vite = getVite();
15321567
let ssrEnvironment = viteDevServer.environments.ssr;
15331568
if (!vite.isRunnableDevEnvironment(ssrEnvironment)) {
1569+
next();
15341570
return;
15351571
}
15361572
build = (await ssrEnvironment.runner.import(
@@ -3315,14 +3351,8 @@ export async function getEnvironmentOptionsResolvers(
33153351
return mergeEnvironmentOptions(getBaseOptions({ viteUserConfig }), {
33163352
resolve: {
33173353
external:
3318-
// This check is required to honor the "noExternal: true" config
3319-
// provided by vite-plugin-cloudflare within this repo. When compiling
3320-
// for Cloudflare, all server dependencies are externalized, but our
3321-
// `ssrExternals` config inadvertently overrides this. This doesn't
3322-
// impact consumers because for them `ssrExternals` is undefined and
3323-
// Cloudflare's "noExternal: true" config remains intact.
3324-
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi &&
3325-
viteUserConfig.environments?.ssr?.resolve?.noExternal === true
3354+
// If `unstable_viteEnvironmentApi` is `true`, `resolve.external` is set in the `configEnvironment` hook
3355+
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi
33263356
? undefined
33273357
: ssrExternals,
33283358
conditions,
@@ -3435,24 +3465,6 @@ export async function getEnvironmentOptionsResolvers(
34353465
build: {
34363466
outDir: getServerBuildDirectory(ctx.reactRouterConfig),
34373467
},
3438-
optimizeDeps:
3439-
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi &&
3440-
viteUserConfig.environments?.ssr?.optimizeDeps?.noDiscovery === false
3441-
? {
3442-
entries: [
3443-
vite.normalizePath(ctx.entryServerFilePath),
3444-
...Object.values(ctx.reactRouterConfig.routes).map((route) =>
3445-
resolveRelativeRouteFilePath(route, ctx.reactRouterConfig)
3446-
),
3447-
],
3448-
include: [
3449-
"react",
3450-
"react/jsx-dev-runtime",
3451-
"react-dom/server",
3452-
"react-router",
3453-
],
3454-
}
3455-
: undefined,
34563468
});
34573469
}
34583470

0 commit comments

Comments
 (0)