Skip to content

Get critical CSS in dev from client env #13503

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 1 commit into from
Apr 30, 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
5 changes: 5 additions & 0 deletions .changeset/proud-needles-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

When extracting critical CSS during development, ensure it's loaded from the client environment to avoid issues with plugins that handle the SSR environment differently
68 changes: 16 additions & 52 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ import type { Cache } from "./cache";
import { generate, parse } from "./babel";
import type { NodeRequestHandler } from "./node-adapter";
import { fromNodeRequest, toNodeRequest } from "./node-adapter";
import { getStylesForPathname, isCssModulesFile } from "./styles";
import {
getCssStringFromViteDevModuleCode,
getStylesForPathname,
isCssModulesFile,
} from "./styles";
import * as VirtualModule from "./virtual-module";
import { resolveFileUrl } from "./resolve-file-url";
import { combineURLs } from "./combine-urls";
Expand Down Expand Up @@ -136,10 +140,7 @@ 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
| CssDevHelperEnvironmentName;
export type EnvironmentName = "client" | SsrEnvironmentName;

const SSR_BUNDLE_PREFIX = "ssrBundle_";
type SsrBundleEnvironmentName = `${typeof SSR_BUNDLE_PREFIX}${string}`;
Expand All @@ -151,16 +152,6 @@ function isSsrBundleEnvironmentName(
return name.startsWith(SSR_BUNDLE_PREFIX);
}

// 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;
Comment on lines -154 to -162
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're loading the CSS from the client environment, we don't need to introduce a custom Vite.RunnableDevEnvironment anymore. This was only introduced because we were originally importing the CSS from the SSR environment, but when using vite-plugin-cloudflare, the SSR environment was a custom Vite.DevEnvironment that couldn't be imported from. With our new approach in this PR, this distinction doesn't matter.


type EnvironmentOptions = Pick<Vite.EnvironmentOptions, "build" | "resolve">;

type EnvironmentOptionsResolver = (options: {
Expand Down Expand Up @@ -1121,40 +1112,20 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
return cssModulesManifest[dep.file];
}

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);
}

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

return cssMod.default;
let cssString = getCssStringFromViteDevModuleCode(transformedCssCode);
invariant(
typeof cssString === "string",
`Failed to extract CSS for ${dep.file ?? dep.url}`
);

return cssString;
};

return [
Expand Down Expand Up @@ -3591,13 +3562,6 @@ export async function getEnvironmentOptionsResolvers(
});
}

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

return environmentOptionsResolvers;
}

Expand Down
24 changes: 24 additions & 0 deletions packages/react-router-dev/vite/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ResolvedReactRouterConfig } from "../config/config";
import type { RouteManifest, RouteManifestEntry } from "../config/routes";
import type { LoadCssContents } from "./plugin";
import { resolveFileUrl } from "./resolve-file-url";
import * as babel from "./babel";

// Style collection logic adapted from solid-start: https://github.com/solidjs/solid-start

Expand Down Expand Up @@ -248,3 +249,26 @@ export const getStylesForPathname = async ({

return styles;
};

export const getCssStringFromViteDevModuleCode = (
code: string
): string | undefined => {
let cssContent = undefined;

const ast = babel.parse(code, { sourceType: "module" });
babel.traverse(ast, {
VariableDeclaration(path) {
const declaration = path.node.declarations[0];
if (
declaration?.id?.type === "Identifier" &&
declaration.id.name === "__vite__css" &&
declaration.init?.type === "StringLiteral"
) {
cssContent = declaration.init.value;
path.stop();
}
},
});

return cssContent;
};