Skip to content

Commit 8677247

Browse files
Scope helper environment to only handle CSS (#13045)
1 parent 7d49de2 commit 8677247

File tree

2 files changed

+85
-74
lines changed

2 files changed

+85
-74
lines changed

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

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ import {
6464
} from "../config/config";
6565
import * as WithProps from "./with-props";
6666

67-
export type LoadModule = (
67+
export type LoadCssContents = (
6868
viteDevServer: Vite.ViteDevServer,
69-
url: string
70-
) => Promise<any>;
69+
mod: Vite.ModuleNode
70+
) => Promise<string>;
7171

7272
export async function resolveViteConfig({
7373
configFile,
@@ -122,11 +122,24 @@ exports are only ever used on the server. Without this optimization we can't
122122
tree-shake any unused custom exports because routes are entry points. */
123123
const BUILD_CLIENT_ROUTE_QUERY_STRING = "?__react-router-build-client-route";
124124

125-
export type EnvironmentName = "client" | SsrEnvironmentName;
125+
export type EnvironmentName =
126+
| "client"
127+
| SsrEnvironmentName
128+
| CssDevHelperEnvironmentName;
126129

127130
const SSR_BUNDLE_PREFIX = "ssrBundle_";
128131
type SsrEnvironmentName = "ssr" | `${typeof SSR_BUNDLE_PREFIX}${string}`;
129132

133+
// We use a separate environment for loading the critical CSS during
134+
// development. This is because "ssrLoadModule" isn't available if the "ssr"
135+
// environment has been defined by another plugin (e.g.
136+
// vite-plugin-cloudflare) as a custom Vite.DevEnvironment rather than a
137+
// Vite.RunnableDevEnvironment:
138+
// https://vite.dev/guide/api-environment-frameworks.html#runtime-agnostic-ssr
139+
const CSS_DEV_HELPER_ENVIRONMENT_NAME =
140+
"__react_router_css_dev_helper__" as const;
141+
type CssDevHelperEnvironmentName = typeof CSS_DEV_HELPER_ENVIRONMENT_NAME;
142+
130143
type EnvironmentOptions = Pick<
131144
Vite.EnvironmentOptions,
132145
"build" | "resolve" | "optimizeDeps"
@@ -477,6 +490,9 @@ let getServerBuildDirectory = (
477490
let getClientBuildDirectory = (reactRouterConfig: ResolvedReactRouterConfig) =>
478491
path.join(reactRouterConfig.buildDirectory, "client");
479492

493+
const injectQuery = (url: string, query: string) =>
494+
url.includes("?") ? url.replace("?", `?${query}&`) : `${url}?${query}`;
495+
480496
let defaultEntriesDir = path.resolve(
481497
path.dirname(require.resolve("@react-router/dev/package.json")),
482498
"dist",
@@ -834,6 +850,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
834850
};
835851

836852
// In dev, the server and browser manifests are the same
853+
let currentReactRouterManifestForDev: ReactRouterManifest | null = null;
837854
let getReactRouterManifestForDev = async (): Promise<ReactRouterManifest> => {
838855
let routes: ReactRouterManifest["routes"] = {};
839856

@@ -901,7 +918,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
901918
};
902919
}
903920

904-
return {
921+
let reactRouterManifestForDev = {
905922
version: String(Math.random()),
906923
url: combineURLs(ctx.publicPath, virtual.browserManifest.url),
907924
hmr: {
@@ -916,31 +933,56 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
916933
},
917934
routes,
918935
};
919-
};
920936

921-
// We use a separate environment for loading the server manifest and inlined
922-
// CSS during development. This is because "ssrLoadModule" isn't available if
923-
// the "ssr" environment has been defined by another plugin (e.g.
924-
// vite-plugin-cloudflare) as a custom Vite.DevEnvironment rather than a
925-
// Vite.RunnableDevEnvironment:
926-
// https://vite.dev/guide/api-environment-frameworks.html#runtime-agnostic-ssr
927-
const HELPER_ENVIRONMENT_NAME = "__react_router_helper__";
937+
currentReactRouterManifestForDev = reactRouterManifestForDev;
928938

929-
const loadModule: LoadModule = (viteDevServer, url) => {
930-
if (ctx.reactRouterConfig.future.unstable_viteEnvironmentApi) {
931-
const vite = getVite();
932-
const helperEnvironment =
933-
viteDevServer.environments[HELPER_ENVIRONMENT_NAME];
939+
return reactRouterManifestForDev;
940+
};
934941

935-
invariant(
936-
helperEnvironment && vite.isRunnableDevEnvironment(helperEnvironment),
937-
"Missing helper environment"
938-
);
942+
const loadCssContents: LoadCssContents = async (viteDevServer, dep) => {
943+
invariant(
944+
viteCommand === "serve",
945+
"loadCssContents is only available in dev mode"
946+
);
939947

940-
return helperEnvironment.runner.import(url);
948+
if (dep.file && isCssModulesFile(dep.file)) {
949+
return cssModulesManifest[dep.file];
941950
}
942951

943-
return viteDevServer.ssrLoadModule(url);
952+
const vite = getVite();
953+
const viteMajor = parseInt(vite.version.split(".")[0], 10);
954+
955+
const url =
956+
viteMajor >= 6
957+
? // We need the ?inline query in Vite v6 when loading CSS in SSR
958+
// since it does not expose the default export for CSS in a
959+
// server environment. This is to align with non-SSR
960+
// environments. For backwards compatibility with v5 we keep
961+
// using the URL without ?inline query because the HMR code was
962+
// relying on the implicit SSR-client module graph relationship.
963+
injectQuery(dep.url, "inline")
964+
: dep.url;
965+
966+
let cssMod: unknown;
967+
if (ctx.reactRouterConfig.future.unstable_viteEnvironmentApi) {
968+
const cssDevHelperEnvironment =
969+
viteDevServer.environments[CSS_DEV_HELPER_ENVIRONMENT_NAME];
970+
invariant(cssDevHelperEnvironment, "Missing CSS dev helper environment");
971+
invariant(vite.isRunnableDevEnvironment(cssDevHelperEnvironment));
972+
cssMod = await cssDevHelperEnvironment.runner.import(url);
973+
} else {
974+
cssMod = await viteDevServer.ssrLoadModule(url);
975+
}
976+
977+
invariant(
978+
typeof cssMod === "object" &&
979+
cssMod !== null &&
980+
"default" in cssMod &&
981+
typeof cssMod.default === "string",
982+
`Failed to load CSS for ${dep.file ?? dep.url}`
983+
);
984+
985+
return cssMod.default;
944986
};
945987

946988
return [
@@ -1094,10 +1136,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
10941136

10951137
...(ctx.reactRouterConfig.future.unstable_viteEnvironmentApi
10961138
? {
1097-
environments: {
1098-
...environments,
1099-
[HELPER_ENVIRONMENT_NAME]: {},
1100-
},
1139+
environments,
11011140
build: {
11021141
// This isn't honored by the SSR environment config (which seems
11031142
// to be a Vite bug?) so we set it here too.
@@ -1298,10 +1337,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
12981337
entryClientFilePath: ctx.entryClientFilePath,
12991338
reactRouterConfig: ctx.reactRouterConfig,
13001339
viteDevServer,
1301-
cssModulesManifest,
1340+
loadCssContents,
13021341
build,
13031342
url,
1304-
loadModule,
13051343
});
13061344
},
13071345
// If an error is caught within the request handler, let Vite fix the
@@ -1977,11 +2015,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
19772015

19782016
if (route) {
19792017
// invalidate manifest on route exports change
1980-
let serverManifest = (
1981-
await loadModule(server, virtual.serverManifest.id)
1982-
).default as ReactRouterManifest;
1983-
1984-
let oldRouteMetadata = serverManifest.routes[route.id];
2018+
let oldRouteMetadata =
2019+
currentReactRouterManifestForDev?.routes[route.id];
19852020
let newRouteMetadata = await getRouteMetadata(
19862021
cache,
19872022
ctx,
@@ -3274,6 +3309,13 @@ export async function getEnvironmentOptionsResolvers(
32743309
});
32753310
}
32763311

3312+
if (
3313+
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi &&
3314+
viteCommand === "serve"
3315+
) {
3316+
environmentOptionsResolvers[CSS_DEV_HELPER_ENVIRONMENT_NAME] = () => ({});
3317+
}
3318+
32773319
return environmentOptionsResolvers;
32783320
}
32793321

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

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import { matchRoutes } from "react-router";
44
import type { ModuleNode, ViteDevServer } from "vite";
55

66
import type { ResolvedReactRouterConfig } from "../config/config";
7+
import type { LoadCssContents } from "./plugin";
78
import { resolveFileUrl } from "./resolve-file-url";
8-
import { getVite } from "./vite";
9-
import type { LoadModule } from "./plugin";
109

1110
type ServerRouteManifest = ServerBuild["routes"];
1211
type ServerRoute = ServerRouteManifest[string];
@@ -50,25 +49,17 @@ export const isCssUrlWithoutSideEffects = (url: string) => {
5049
return false;
5150
};
5251

53-
const injectQuery = (url: string, query: string) =>
54-
url.includes("?") ? url.replace("?", `?${query}&`) : `${url}?${query}`;
55-
5652
const getStylesForFiles = async ({
5753
viteDevServer,
5854
rootDirectory,
59-
cssModulesManifest,
55+
loadCssContents,
6056
files,
61-
loadModule,
6257
}: {
6358
viteDevServer: ViteDevServer;
6459
rootDirectory: string;
65-
cssModulesManifest: Record<string, string>;
60+
loadCssContents: LoadCssContents;
6661
files: string[];
67-
loadModule: LoadModule;
6862
}): Promise<string | undefined> => {
69-
let vite = getVite();
70-
let viteMajor = parseInt(vite.version.split(".")[0], 10);
71-
7263
let styles: Record<string, string> = {};
7364
let deps = new Set<ModuleNode>();
7465

@@ -111,28 +102,9 @@ const getStylesForFiles = async ({
111102
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
112103
) {
113104
try {
114-
let css = isCssModulesFile(dep.file)
115-
? cssModulesManifest[dep.file]
116-
: (
117-
await loadModule(
118-
viteDevServer,
119-
// We need the ?inline query in Vite v6 when loading CSS in SSR
120-
// since it does not expose the default export for CSS in a
121-
// server environment. This is to align with non-SSR
122-
// environments. For backwards compatibility with v5 we keep
123-
// using the URL without ?inline query because the HMR code was
124-
// relying on the implicit SSR-client module graph relationship.
125-
viteMajor >= 6 ? injectQuery(dep.url, "inline") : dep.url
126-
)
127-
).default;
128-
129-
if (css === undefined) {
130-
throw new Error();
131-
}
132-
133-
styles[dep.url] = css;
105+
styles[dep.url] = await loadCssContents(viteDevServer, dep);
134106
} catch {
135-
console.warn(`Could not load ${dep.file}`);
107+
console.warn(`Failed to load CSS for ${dep.file}`);
136108
// this can happen with dynamically imported modules, I think
137109
// because the Vite module graph doesn't distinguish between
138110
// static and dynamic imports? TODO investigate, submit fix
@@ -225,19 +197,17 @@ export const getStylesForUrl = async ({
225197
rootDirectory,
226198
reactRouterConfig,
227199
entryClientFilePath,
228-
cssModulesManifest,
200+
loadCssContents,
229201
build,
230202
url,
231-
loadModule,
232203
}: {
233204
viteDevServer: ViteDevServer;
234205
rootDirectory: string;
235206
reactRouterConfig: Pick<ResolvedReactRouterConfig, "appDirectory" | "routes">;
236207
entryClientFilePath: string;
237-
cssModulesManifest: Record<string, string>;
208+
loadCssContents: LoadCssContents;
238209
build: ServerBuild;
239210
url: string | undefined;
240-
loadModule: LoadModule;
241211
}): Promise<string | undefined> => {
242212
if (url === undefined || url.includes("?_data=")) {
243213
return undefined;
@@ -253,14 +223,13 @@ export const getStylesForUrl = async ({
253223
let styles = await getStylesForFiles({
254224
viteDevServer,
255225
rootDirectory,
256-
cssModulesManifest,
226+
loadCssContents,
257227
files: [
258228
// Always include the client entry file when crawling the module graph for CSS
259229
path.relative(rootDirectory, entryClientFilePath),
260230
// Then include any styles from the matched routes
261231
...documentRouteFiles,
262232
],
263-
loadModule,
264233
});
265234

266235
return styles;

0 commit comments

Comments
 (0)