Skip to content

Scope helper environment to only handle CSS #13045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 19, 2025
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
112 changes: 77 additions & 35 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ import {
} from "../config/config";
import * as WithProps from "./with-props";

export type LoadModule = (
export type LoadCssContents = (
viteDevServer: Vite.ViteDevServer,
url: string
) => Promise<any>;
mod: Vite.ModuleNode
) => Promise<string>;

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

export type EnvironmentName = "client" | SsrEnvironmentName;
export type EnvironmentName =
| "client"
| SsrEnvironmentName
| CssDevHelperEnvironmentName;

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

// We use a separate environment for loading the critical CSS during
// development. This is because "ssrLoadModule" isn't available if the "ssr"
// environment has been defined by another plugin (e.g.
// vite-plugin-cloudflare) as a custom Vite.DevEnvironment rather than a
// Vite.RunnableDevEnvironment:
// https://vite.dev/guide/api-environment-frameworks.html#runtime-agnostic-ssr
const CSS_DEV_HELPER_ENVIRONMENT_NAME =
"__react_router_css_dev_helper__" as const;
type CssDevHelperEnvironmentName = typeof CSS_DEV_HELPER_ENVIRONMENT_NAME;

type EnvironmentOptions = Pick<
Vite.EnvironmentOptions,
"build" | "resolve" | "optimizeDeps"
Expand Down Expand Up @@ -477,6 +490,9 @@ let getServerBuildDirectory = (
let getClientBuildDirectory = (reactRouterConfig: ResolvedReactRouterConfig) =>
path.join(reactRouterConfig.buildDirectory, "client");

const injectQuery = (url: string, query: string) =>
url.includes("?") ? url.replace("?", `?${query}&`) : `${url}?${query}`;

let defaultEntriesDir = path.resolve(
path.dirname(require.resolve("@react-router/dev/package.json")),
"dist",
Expand Down Expand Up @@ -834,6 +850,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
};

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

Expand Down Expand Up @@ -901,7 +918,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
};
}

return {
let reactRouterManifestForDev = {
version: String(Math.random()),
url: combineURLs(ctx.publicPath, virtual.browserManifest.url),
hmr: {
Expand All @@ -916,31 +933,56 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
},
routes,
};
};

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

const loadModule: LoadModule = (viteDevServer, url) => {
if (ctx.reactRouterConfig.future.unstable_viteEnvironmentApi) {
const vite = getVite();
const helperEnvironment =
viteDevServer.environments[HELPER_ENVIRONMENT_NAME];
return reactRouterManifestForDev;
};

invariant(
helperEnvironment && vite.isRunnableDevEnvironment(helperEnvironment),
"Missing helper environment"
);
const loadCssContents: LoadCssContents = async (viteDevServer, dep) => {
invariant(
viteCommand === "serve",
"loadCssContents is only available in dev mode"
);

return helperEnvironment.runner.import(url);
if (dep.file && isCssModulesFile(dep.file)) {
return cssModulesManifest[dep.file];
}

return viteDevServer.ssrLoadModule(url);
const vite = getVite();
const viteMajor = parseInt(vite.version.split(".")[0], 10);

const url =
viteMajor >= 6
? // We need the ?inline query in Vite v6 when loading CSS in SSR
// since it does not expose the default export for CSS in a
// server environment. This is to align with non-SSR
// environments. For backwards compatibility with v5 we keep
// using the URL without ?inline query because the HMR code was
// relying on the implicit SSR-client module graph relationship.
injectQuery(dep.url, "inline")
: dep.url;

let cssMod: unknown;
if (ctx.reactRouterConfig.future.unstable_viteEnvironmentApi) {
const cssDevHelperEnvironment =
viteDevServer.environments[CSS_DEV_HELPER_ENVIRONMENT_NAME];
invariant(cssDevHelperEnvironment, "Missing CSS dev helper environment");
invariant(vite.isRunnableDevEnvironment(cssDevHelperEnvironment));
cssMod = await cssDevHelperEnvironment.runner.import(url);
} else {
cssMod = await viteDevServer.ssrLoadModule(url);
}

invariant(
typeof cssMod === "object" &&
cssMod !== null &&
"default" in cssMod &&
typeof cssMod.default === "string",
`Failed to load CSS for ${dep.file ?? dep.url}`
);

return cssMod.default;
};

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

...(ctx.reactRouterConfig.future.unstable_viteEnvironmentApi
? {
environments: {
...environments,
[HELPER_ENVIRONMENT_NAME]: {},
},
environments,
build: {
// This isn't honored by the SSR environment config (which seems
// to be a Vite bug?) so we set it here too.
Expand Down Expand Up @@ -1298,10 +1337,9 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
entryClientFilePath: ctx.entryClientFilePath,
reactRouterConfig: ctx.reactRouterConfig,
viteDevServer,
cssModulesManifest,
loadCssContents,
build,
url,
loadModule,
});
},
// If an error is caught within the request handler, let Vite fix the
Expand Down Expand Up @@ -1977,11 +2015,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {

if (route) {
// invalidate manifest on route exports change
let serverManifest = (
await loadModule(server, virtual.serverManifest.id)
).default as ReactRouterManifest;

let oldRouteMetadata = serverManifest.routes[route.id];
let oldRouteMetadata =
currentReactRouterManifestForDev?.routes[route.id];
let newRouteMetadata = await getRouteMetadata(
cache,
ctx,
Expand Down Expand Up @@ -3257,6 +3292,13 @@ export async function getEnvironmentOptionsResolvers(
});
}

if (
ctx.reactRouterConfig.future.unstable_viteEnvironmentApi &&
viteCommand === "serve"
) {
environmentOptionsResolvers[CSS_DEV_HELPER_ENVIRONMENT_NAME] = () => ({});
}

return environmentOptionsResolvers;
}

Expand Down
47 changes: 8 additions & 39 deletions packages/react-router-dev/vite/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { matchRoutes } from "react-router";
import type { ModuleNode, ViteDevServer } from "vite";

import type { ResolvedReactRouterConfig } from "../config/config";
import type { LoadCssContents } from "./plugin";
import { resolveFileUrl } from "./resolve-file-url";
import { getVite } from "./vite";
import type { LoadModule } from "./plugin";

type ServerRouteManifest = ServerBuild["routes"];
type ServerRoute = ServerRouteManifest[string];
Expand Down Expand Up @@ -50,25 +49,17 @@ export const isCssUrlWithoutSideEffects = (url: string) => {
return false;
};

const injectQuery = (url: string, query: string) =>
url.includes("?") ? url.replace("?", `?${query}&`) : `${url}?${query}`;

const getStylesForFiles = async ({
viteDevServer,
rootDirectory,
cssModulesManifest,
loadCssContents,
files,
loadModule,
}: {
viteDevServer: ViteDevServer;
rootDirectory: string;
cssModulesManifest: Record<string, string>;
loadCssContents: LoadCssContents;
files: string[];
loadModule: LoadModule;
}): Promise<string | undefined> => {
let vite = getVite();
let viteMajor = parseInt(vite.version.split(".")[0], 10);

let styles: Record<string, string> = {};
let deps = new Set<ModuleNode>();

Expand Down Expand Up @@ -111,28 +102,9 @@ const getStylesForFiles = async ({
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
) {
try {
let css = isCssModulesFile(dep.file)
? cssModulesManifest[dep.file]
: (
await loadModule(
viteDevServer,
// We need the ?inline query in Vite v6 when loading CSS in SSR
// since it does not expose the default export for CSS in a
// server environment. This is to align with non-SSR
// environments. For backwards compatibility with v5 we keep
// using the URL without ?inline query because the HMR code was
// relying on the implicit SSR-client module graph relationship.
viteMajor >= 6 ? injectQuery(dep.url, "inline") : dep.url
)
).default;

if (css === undefined) {
throw new Error();
}

styles[dep.url] = css;
styles[dep.url] = await loadCssContents(viteDevServer, dep);
} catch {
console.warn(`Could not load ${dep.file}`);
console.warn(`Failed to load CSS for ${dep.file}`);
// this can happen with dynamically imported modules, I think
// because the Vite module graph doesn't distinguish between
// static and dynamic imports? TODO investigate, submit fix
Expand Down Expand Up @@ -225,19 +197,17 @@ export const getStylesForUrl = async ({
rootDirectory,
reactRouterConfig,
entryClientFilePath,
cssModulesManifest,
loadCssContents,
build,
url,
loadModule,
}: {
viteDevServer: ViteDevServer;
rootDirectory: string;
reactRouterConfig: Pick<ResolvedReactRouterConfig, "appDirectory" | "routes">;
entryClientFilePath: string;
cssModulesManifest: Record<string, string>;
loadCssContents: LoadCssContents;
build: ServerBuild;
url: string | undefined;
loadModule: LoadModule;
}): Promise<string | undefined> => {
if (url === undefined || url.includes("?_data=")) {
return undefined;
Expand All @@ -253,14 +223,13 @@ export const getStylesForUrl = async ({
let styles = await getStylesForFiles({
viteDevServer,
rootDirectory,
cssModulesManifest,
loadCssContents,
files: [
// Always include the client entry file when crawling the module graph for CSS
path.relative(rootDirectory, entryClientFilePath),
// Then include any styles from the matched routes
...documentRouteFiles,
],
loadModule,
});

return styles;
Expand Down