Skip to content

Commit 9383e9c

Browse files
fix(dev/vite): Fix missing styles when Vite's build.cssCodeSplit is disabled (#13943)
1 parent 83587a3 commit 9383e9c

File tree

4 files changed

+142
-27
lines changed

4 files changed

+142
-27
lines changed

.changeset/cyan-bags-thank.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+
Fix missing styles when Vite's `build.cssCodeSplit` option is disabled

integration/helpers/vite.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type ViteConfigServerArgs = {
7272
type ViteConfigBuildArgs = {
7373
assetsInlineLimit?: number;
7474
assetsDir?: string;
75+
cssCodeSplit?: boolean;
7576
};
7677

7778
type ViteConfigBaseArgs = {
@@ -99,7 +100,11 @@ export const viteConfig = {
99100
`;
100101
return text;
101102
},
102-
build: ({ assetsInlineLimit, assetsDir }: ViteConfigBuildArgs = {}) => {
103+
build: ({
104+
assetsInlineLimit,
105+
assetsDir,
106+
cssCodeSplit,
107+
}: ViteConfigBuildArgs = {}) => {
103108
return dedent`
104109
build: {
105110
// Detect rolldown-vite. This should ideally use "rolldownVersion"
@@ -116,6 +121,9 @@ export const viteConfig = {
116121
: undefined,
117122
assetsInlineLimit: ${assetsInlineLimit ?? "undefined"},
118123
assetsDir: ${assetsDir ? `"${assetsDir}"` : "undefined"},
124+
cssCodeSplit: ${
125+
cssCodeSplit !== undefined ? cssCodeSplit : "undefined"
126+
},
119127
},
120128
`;
121129
},

integration/vite-css-test.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,18 @@ const files = {
155155
const VITE_CONFIG = async ({
156156
port,
157157
base,
158+
cssCodeSplit,
158159
}: {
159160
port: number;
160161
base?: string;
162+
cssCodeSplit?: boolean;
161163
}) => dedent`
162164
import { reactRouter } from "@react-router/dev/vite";
163165
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
164166
165167
export default async () => ({
166168
${await viteConfig.server({ port })}
167-
${viteConfig.build()}
169+
${viteConfig.build({ cssCodeSplit })}
168170
${base ? `base: "${base}",` : ""}
169171
plugins: [
170172
reactRouter(),
@@ -289,7 +291,7 @@ test.describe("Vite CSS", () => {
289291
});
290292
});
291293

292-
test.describe(async () => {
294+
test.describe("vite build", async () => {
293295
let port: number;
294296
let cwd: string;
295297
let stop: () => void;
@@ -327,14 +329,68 @@ test.describe("Vite CSS", () => {
327329

328330
test.describe(() => {
329331
test.use({ javaScriptEnabled: false });
330-
test("vite build / without JS", async ({ page }) => {
332+
test("without JS", async ({ page }) => {
333+
await pageLoadWorkflow({ page, port });
334+
});
335+
});
336+
337+
test.describe(() => {
338+
test.use({ javaScriptEnabled: true });
339+
test("with JS", async ({ page }) => {
340+
await pageLoadWorkflow({ page, port });
341+
});
342+
});
343+
});
344+
345+
test.describe("vite build with CSS code splitting disabled", async () => {
346+
let port: number;
347+
let cwd: string;
348+
let stop: () => void;
349+
350+
test.beforeAll(async () => {
351+
port = await getPort();
352+
cwd = await createProject(
353+
{
354+
"vite.config.ts": await VITE_CONFIG({
355+
port,
356+
cssCodeSplit: false,
357+
}),
358+
...files,
359+
},
360+
templateName
361+
);
362+
363+
let edit = createEditor(cwd);
364+
await edit("package.json", (contents) =>
365+
contents.replace(
366+
'"sideEffects": false',
367+
'"sideEffects": ["*.css.ts"]'
368+
)
369+
);
370+
371+
let { stderr, status } = build({
372+
cwd,
373+
env: {
374+
// Vanilla Extract uses Vite's CJS build which emits a warning to stderr
375+
VITE_CJS_IGNORE_WARNING: "true",
376+
},
377+
});
378+
expect(stderr.toString()).toBeFalsy();
379+
expect(status).toBe(0);
380+
stop = await reactRouterServe({ cwd, port });
381+
});
382+
test.afterAll(() => stop());
383+
384+
test.describe(() => {
385+
test.use({ javaScriptEnabled: false });
386+
test("without JS", async ({ page }) => {
331387
await pageLoadWorkflow({ page, port });
332388
});
333389
});
334390

335391
test.describe(() => {
336392
test.use({ javaScriptEnabled: true });
337-
test("vite build / with JS", async ({ page }) => {
393+
test("with JS", async ({ page }) => {
338394
await pageLoadWorkflow({ page, port });
339395
});
340396
});

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

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,45 @@ const getPublicModulePathForEntry = (
324324
return entryChunk ? `${ctx.publicPath}${entryChunk.file}` : undefined;
325325
};
326326

327+
const getCssCodeSplitDisabledFile = (
328+
ctx: ReactRouterPluginContext,
329+
viteConfig: Vite.ResolvedConfig,
330+
viteManifest: Vite.Manifest
331+
) => {
332+
if (viteConfig.build.cssCodeSplit) {
333+
return null;
334+
}
335+
336+
let cssFile = viteManifest["style.css"]?.file;
337+
invariant(
338+
cssFile,
339+
"Expected `style.css` to be present in Vite manifest when `build.cssCodeSplit` is disabled"
340+
);
341+
342+
return `${ctx.publicPath}${cssFile}`;
343+
};
344+
345+
const getClientEntryChunk = (
346+
ctx: ReactRouterPluginContext,
347+
viteManifest: Vite.Manifest
348+
) => {
349+
let filePath = ctx.entryClientFilePath;
350+
let chunk = resolveChunk(ctx, viteManifest, filePath);
351+
invariant(chunk, `Chunk not found: ${filePath}`);
352+
return chunk;
353+
};
354+
327355
const getReactRouterManifestBuildAssets = (
328356
ctx: ReactRouterPluginContext,
357+
viteConfig: Vite.ResolvedConfig,
329358
viteManifest: Vite.Manifest,
330359
entryFilePath: string,
331-
prependedAssetFilePaths: string[] = []
360+
route: RouteManifestEntry | null
332361
): ReactRouterManifest["entry"] & { css: string[] } => {
333362
let entryChunk = resolveChunk(ctx, viteManifest, entryFilePath);
334363
invariant(entryChunk, `Chunk not found: ${entryFilePath}`);
335364

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

343367
let routeModuleChunks = routeChunkNames
344368
.map((routeChunkName) =>
@@ -350,22 +374,41 @@ const getReactRouterManifestBuildAssets = (
350374
)
351375
.filter(isNonNullable);
352376

353-
let chunks = resolveDependantChunks(viteManifest, [
354-
...prependedAssetChunks,
355-
entryChunk,
356-
...routeModuleChunks,
357-
]);
377+
let chunks = resolveDependantChunks(
378+
viteManifest,
379+
[
380+
// If this is the root route, we also need to include assets from the
381+
// client entry file as this is a common way for consumers to import
382+
// global reset styles, etc.
383+
isRootRoute ? getClientEntryChunk(ctx, viteManifest) : null,
384+
entryChunk,
385+
routeModuleChunks,
386+
]
387+
.flat(1)
388+
.filter(isNonNullable)
389+
);
358390

359391
return {
360392
module: `${ctx.publicPath}${entryChunk.file}`,
361393
imports:
362394
dedupe(chunks.flatMap((e) => e.imports ?? [])).map((imported) => {
363395
return `${ctx.publicPath}${viteManifest[imported].file}`;
364396
}) ?? [],
365-
css:
366-
dedupe(chunks.flatMap((e) => e.css ?? [])).map((href) => {
367-
return `${ctx.publicPath}${href}`;
368-
}) ?? [],
397+
css: dedupe(
398+
[
399+
// If CSS code splitting is disabled, Vite includes a singular 'style.css' asset
400+
// in the manifest that isn't tied to any route file. If we want to render these
401+
// styles correctly, we need to include them in the root route.
402+
isRootRoute
403+
? getCssCodeSplitDisabledFile(ctx, viteConfig, viteManifest)
404+
: null,
405+
chunks
406+
.flatMap((e) => e.css ?? [])
407+
.map((href) => `${ctx.publicPath}${href}`),
408+
]
409+
.flat(1)
410+
.filter(isNonNullable)
411+
),
369412
};
370413
};
371414

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

853896
let generateReactRouterManifestsForBuild = async ({
897+
viteConfig,
854898
routeIds,
855899
}: {
900+
viteConfig: Vite.ResolvedConfig;
856901
routeIds?: Array<string>;
857902
}): Promise<{
858903
reactRouterBrowserManifest: ReactRouterManifest;
@@ -866,8 +911,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
866911

867912
let entry = getReactRouterManifestBuildAssets(
868913
ctx,
914+
viteConfig,
869915
viteManifest,
870-
ctx.entryClientFilePath
916+
ctx.entryClientFilePath,
917+
null
871918
);
872919

873920
let browserRoutes: ReactRouterManifest["routes"] = {};
@@ -883,7 +930,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
883930
for (let route of Object.values(ctx.reactRouterConfig.routes)) {
884931
let routeFile = path.join(ctx.reactRouterConfig.appDirectory, route.file);
885932
let sourceExports = routeManifestExports[route.id];
886-
let isRootRoute = route.parentId === undefined;
887933
let hasClientAction = sourceExports.includes("clientAction");
888934
let hasClientLoader = sourceExports.includes("clientLoader");
889935
let hasClientMiddleware = sourceExports.includes(
@@ -930,12 +976,10 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
930976
hasErrorBoundary: sourceExports.includes("ErrorBoundary"),
931977
...getReactRouterManifestBuildAssets(
932978
ctx,
979+
viteConfig,
933980
viteManifest,
934981
`${routeFile}${BUILD_CLIENT_ROUTE_QUERY_STRING}`,
935-
// If this is the root route, we also need to include assets from the
936-
// client entry file as this is a common way for consumers to import
937-
// global reset styles, etc.
938-
isRootRoute ? [ctx.entryClientFilePath] : []
982+
route
939983
),
940984
clientActionModule: hasRouteChunkByExportName.clientAction
941985
? getPublicModulePathForEntry(
@@ -2035,10 +2079,12 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
20352079
}
20362080
case virtual.serverManifest.resolvedId: {
20372081
let routeIds = getServerBundleRouteIds(this, ctx);
2082+
invariant(viteConfig);
20382083
let reactRouterManifest =
20392084
viteCommand === "build"
20402085
? (
20412086
await generateReactRouterManifestsForBuild({
2087+
viteConfig,
20422088
routeIds,
20432089
})
20442090
).reactRouterServerManifest

0 commit comments

Comments
 (0)