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
5 changes: 5 additions & 0 deletions .changeset/cyan-bags-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Fix missing styles when Vite's `build.cssCodeSplit` option is disabled
10 changes: 9 additions & 1 deletion integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type ViteConfigServerArgs = {
type ViteConfigBuildArgs = {
assetsInlineLimit?: number;
assetsDir?: string;
cssCodeSplit?: boolean;
};

type ViteConfigBaseArgs = {
Expand Down Expand Up @@ -99,7 +100,11 @@ export const viteConfig = {
`;
return text;
},
build: ({ assetsInlineLimit, assetsDir }: ViteConfigBuildArgs = {}) => {
build: ({
assetsInlineLimit,
assetsDir,
cssCodeSplit,
}: ViteConfigBuildArgs = {}) => {
return dedent`
build: {
// Detect rolldown-vite. This should ideally use "rolldownVersion"
Expand All @@ -116,6 +121,9 @@ export const viteConfig = {
: undefined,
assetsInlineLimit: ${assetsInlineLimit ?? "undefined"},
assetsDir: ${assetsDir ? `"${assetsDir}"` : "undefined"},
cssCodeSplit: ${
cssCodeSplit !== undefined ? cssCodeSplit : "undefined"
},
},
`;
},
Expand Down
64 changes: 60 additions & 4 deletions integration/vite-css-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,16 +155,18 @@ const files = {
const VITE_CONFIG = async ({
port,
base,
cssCodeSplit,
}: {
port: number;
base?: string;
cssCodeSplit?: boolean;
}) => dedent`
import { reactRouter } from "@react-router/dev/vite";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";

export default async () => ({
${await viteConfig.server({ port })}
${viteConfig.build()}
${viteConfig.build({ cssCodeSplit })}
${base ? `base: "${base}",` : ""}
plugins: [
reactRouter(),
Expand Down Expand Up @@ -289,7 +291,7 @@ test.describe("Vite CSS", () => {
});
});

test.describe(async () => {
test.describe("vite build", async () => {
let port: number;
let cwd: string;
let stop: () => void;
Expand Down Expand Up @@ -327,14 +329,68 @@ test.describe("Vite CSS", () => {

test.describe(() => {
test.use({ javaScriptEnabled: false });
test("vite build / without JS", async ({ page }) => {
test("without JS", async ({ page }) => {
await pageLoadWorkflow({ page, port });
});
});

test.describe(() => {
test.use({ javaScriptEnabled: true });
test("with JS", async ({ page }) => {
await pageLoadWorkflow({ page, port });
});
});
});

test.describe("vite build with CSS code splitting disabled", async () => {
let port: number;
let cwd: string;
let stop: () => void;

test.beforeAll(async () => {
port = await getPort();
cwd = await createProject(
{
"vite.config.ts": await VITE_CONFIG({
port,
cssCodeSplit: false,
}),
...files,
},
templateName
);

let edit = createEditor(cwd);
await edit("package.json", (contents) =>
contents.replace(
'"sideEffects": false',
'"sideEffects": ["*.css.ts"]'
)
);

let { stderr, status } = build({
cwd,
env: {
// Vanilla Extract uses Vite's CJS build which emits a warning to stderr
VITE_CJS_IGNORE_WARNING: "true",
},
});
expect(stderr.toString()).toBeFalsy();
expect(status).toBe(0);
stop = await reactRouterServe({ cwd, port });
});
test.afterAll(() => stop());

test.describe(() => {
test.use({ javaScriptEnabled: false });
test("without JS", async ({ page }) => {
await pageLoadWorkflow({ page, port });
});
});

test.describe(() => {
test.use({ javaScriptEnabled: true });
test("vite build / with JS", async ({ page }) => {
test("with JS", async ({ page }) => {
await pageLoadWorkflow({ page, port });
});
});
Expand Down
90 changes: 68 additions & 22 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,21 +324,45 @@ const getPublicModulePathForEntry = (
return entryChunk ? `${ctx.publicPath}${entryChunk.file}` : undefined;
};

const getCssCodeSplitDisabledFile = (
ctx: ReactRouterPluginContext,
viteConfig: Vite.ResolvedConfig,
viteManifest: Vite.Manifest
) => {
if (viteConfig.build.cssCodeSplit) {
return null;
}

let cssFile = viteManifest["style.css"]?.file;
invariant(
cssFile,
"Expected `style.css` to be present in Vite manifest when `build.cssCodeSplit` is disabled"
);

return `${ctx.publicPath}${cssFile}`;
};

const getClientEntryChunk = (
ctx: ReactRouterPluginContext,
viteManifest: Vite.Manifest
) => {
let filePath = ctx.entryClientFilePath;
let chunk = resolveChunk(ctx, viteManifest, filePath);
invariant(chunk, `Chunk not found: ${filePath}`);
return chunk;
};

const getReactRouterManifestBuildAssets = (
ctx: ReactRouterPluginContext,
viteConfig: Vite.ResolvedConfig,
viteManifest: Vite.Manifest,
entryFilePath: string,
prependedAssetFilePaths: string[] = []
route: RouteManifestEntry | null
): ReactRouterManifest["entry"] & { css: string[] } => {
let entryChunk = resolveChunk(ctx, viteManifest, entryFilePath);
invariant(entryChunk, `Chunk not found: ${entryFilePath}`);

// This is here to support prepending client entry assets to the root route
let prependedAssetChunks = prependedAssetFilePaths.map((filePath) => {
let chunk = resolveChunk(ctx, viteManifest, filePath);
invariant(chunk, `Chunk not found: ${filePath}`);
return chunk;
});
let isRootRoute = Boolean(route && route.parentId === undefined);

let routeModuleChunks = routeChunkNames
.map((routeChunkName) =>
Expand All @@ -350,22 +374,41 @@ const getReactRouterManifestBuildAssets = (
)
.filter(isNonNullable);

let chunks = resolveDependantChunks(viteManifest, [
...prependedAssetChunks,
entryChunk,
...routeModuleChunks,
]);
let chunks = resolveDependantChunks(
viteManifest,
[
// If this is the root route, we also need to include assets from the
// client entry file as this is a common way for consumers to import
// global reset styles, etc.
isRootRoute ? getClientEntryChunk(ctx, viteManifest) : null,
entryChunk,
routeModuleChunks,
]
.flat(1)
.filter(isNonNullable)
);

return {
module: `${ctx.publicPath}${entryChunk.file}`,
imports:
dedupe(chunks.flatMap((e) => e.imports ?? [])).map((imported) => {
return `${ctx.publicPath}${viteManifest[imported].file}`;
}) ?? [],
css:
dedupe(chunks.flatMap((e) => e.css ?? [])).map((href) => {
return `${ctx.publicPath}${href}`;
}) ?? [],
css: dedupe(
[
// If CSS code splitting is disabled, Vite includes a singular 'style.css' asset
// in the manifest that isn't tied to any route file. If we want to render these
// styles correctly, we need to include them in the root route.
isRootRoute
? getCssCodeSplitDisabledFile(ctx, viteConfig, viteManifest)
: null,
chunks
.flatMap((e) => e.css ?? [])
.map((href) => `${ctx.publicPath}${href}`),
]
.flat(1)
.filter(isNonNullable)
),
};
};

Expand Down Expand Up @@ -851,8 +894,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
};

let generateReactRouterManifestsForBuild = async ({
viteConfig,
routeIds,
}: {
viteConfig: Vite.ResolvedConfig;
routeIds?: Array<string>;
}): Promise<{
reactRouterBrowserManifest: ReactRouterManifest;
Expand All @@ -866,8 +911,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {

let entry = getReactRouterManifestBuildAssets(
ctx,
viteConfig,
viteManifest,
ctx.entryClientFilePath
ctx.entryClientFilePath,
null
);

let browserRoutes: ReactRouterManifest["routes"] = {};
Expand All @@ -883,7 +930,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
for (let route of Object.values(ctx.reactRouterConfig.routes)) {
let routeFile = path.join(ctx.reactRouterConfig.appDirectory, route.file);
let sourceExports = routeManifestExports[route.id];
let isRootRoute = route.parentId === undefined;
let hasClientAction = sourceExports.includes("clientAction");
let hasClientLoader = sourceExports.includes("clientLoader");
let hasClientMiddleware = sourceExports.includes(
Expand Down Expand Up @@ -930,12 +976,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
...getReactRouterManifestBuildAssets(
ctx,
viteConfig,
viteManifest,
`${routeFile}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
// If this is the root route, we also need to include assets from the
// client entry file as this is a common way for consumers to import
// global reset styles, etc.
isRootRoute ? [ctx.entryClientFilePath] : []
route
),
clientActionModule: hasRouteChunkByExportName.clientAction
? getPublicModulePathForEntry(
Expand Down Expand Up @@ -2035,10 +2079,12 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
}
case virtual.serverManifest.resolvedId: {
let routeIds = getServerBundleRouteIds(this, ctx);
invariant(viteConfig);
let reactRouterManifest =
viteCommand === "build"
? (
await generateReactRouterManifestsForBuild({
viteConfig,
routeIds,
})
).reactRouterServerManifest
Expand Down