Skip to content

Commit 6c7df28

Browse files
authored
Fix CSS deduping and missing chunks (#7218)
1 parent 6b12f93 commit 6c7df28

File tree

7 files changed

+69
-28
lines changed

7 files changed

+69
-28
lines changed

.changeset/eighty-gifts-cheer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Fix CSS deduping and missing chunks

packages/astro/src/core/build/internal.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import type { PageBuildData, StylesheetAsset, ViteID } from './types';
88

99
export interface BuildInternals {
1010
/**
11-
* The module ids of all CSS chunks, used to deduplicate CSS assets between
12-
* SSR build and client build in vite-plugin-css.
11+
* Each CSS module is named with a chunk id derived from the Astro pages they
12+
* are used in by default. It's easy to crawl this relation in the SSR build as
13+
* the Astro pages are the entrypoint, but not for the client build as hydratable
14+
* components are the entrypoint instead. This map is used as a cache from the SSR
15+
* build so the client can pick up the same information and use the same chunk ids.
1316
*/
14-
cssChunkModuleIds: Set<string>;
17+
cssModuleToChunkIdMap: Map<string, string>;
1518

1619
// A mapping of hoisted script ids back to the exact hoisted scripts it references
1720
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
@@ -92,7 +95,7 @@ export function createBuildInternals(): BuildInternals {
9295
const hoistedScriptIdToPagesMap = new Map<string, Set<string>>();
9396

9497
return {
95-
cssChunkModuleIds: new Set(),
98+
cssModuleToChunkIdMap: new Map(),
9699
hoistedScriptIdToHoistedMap,
97100
hoistedScriptIdToPagesMap,
98101
entrySpecifierToBundleMap: new Map<string, string>(),

packages/astro/src/core/build/plugins/plugin-css.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
6464
const cssBuildPlugin: VitePlugin = {
6565
name: 'astro:rollup-plugin-build-css',
6666

67-
transform(_, id) {
68-
// In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
69-
// In the client build, if we're also bundling the same style, return an empty string to
70-
// deduplicate the final CSS output.
71-
if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
72-
return '';
73-
}
74-
},
75-
7667
outputOptions(outputOptions) {
77-
// Skip in client builds as its module graph doesn't have reference to Astro pages
78-
// to be able to chunk based on it's related top-level pages.
79-
if (options.target === 'client') return;
80-
8168
const assetFileNames = outputOptions.assetFileNames;
8269
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
8370
const createNameForParentPages = namingIncludesHash
@@ -89,16 +76,31 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
8976
// For CSS, create a hash of all of the pages that use it.
9077
// This causes CSS to be built into shared chunks when used by multiple pages.
9178
if (isBuildableCSSRequest(id)) {
79+
// For client builds that has hydrated components as entrypoints, there's no way
80+
// to crawl up and find the pages that use it. So we lookup the cache during SSR
81+
// build (that has the pages information) to derive the same chunk id so they
82+
// match up on build, making sure both builds has the CSS deduped.
83+
// NOTE: Components that are only used with `client:only` may not exist in the cache
84+
// and that's okay. We can use Rollup's default chunk strategy instead as these CSS
85+
// are outside of the SSR build scope, which no dedupe is needed.
86+
if (options.target === 'client') {
87+
return internals.cssModuleToChunkIdMap.get(id)!;
88+
}
89+
9290
for (const [pageInfo] of walkParentInfos(id, {
9391
getModuleInfo: meta.getModuleInfo,
9492
})) {
9593
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
9694
// Split delayed assets to separate modules
9795
// so they can be injected where needed
98-
return createNameHash(id, [id]);
96+
const chunkId = createNameHash(id, [id]);
97+
internals.cssModuleToChunkIdMap.set(id, chunkId);
98+
return chunkId;
9999
}
100100
}
101-
return createNameForParentPages(id, meta);
101+
const chunkId = createNameForParentPages(id, meta);
102+
internals.cssModuleToChunkIdMap.set(id, chunkId);
103+
return chunkId;
102104
}
103105
},
104106
});
@@ -113,15 +115,6 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
113115
// Skip if the chunk has no CSS, we want to handle CSS chunks only
114116
if (meta.importedCss.size < 1) continue;
115117

116-
// In the SSR build, keep track of all CSS chunks' modules as the client build may
117-
// duplicate them, e.g. for `client:load` components that render in SSR and client
118-
// for hydation.
119-
if (options.target === 'server') {
120-
for (const id of Object.keys(chunk.modules)) {
121-
internals.cssChunkModuleIds.add(id);
122-
}
123-
}
124-
125118
// For the client build, client:only styles need to be mapped
126119
// over to their page. For this chunk, determine if it's a child of a
127120
// client:only component and if so, add its CSS to the page it belongs to.

packages/astro/test/0-css.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,21 @@ describe('CSS', function () {
265265
new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:ComicSansMS`)
266266
);
267267
});
268+
269+
it('client:only and SSR in two pages, both should have styles', async () => {
270+
const onlyHtml = await fixture.readFile('/client-only-and-ssr/only/index.html');
271+
const $onlyHtml = cheerio.load(onlyHtml);
272+
const onlyHtmlCssHref = $onlyHtml('link[rel=stylesheet][href^=/_astro/]').attr('href');
273+
const onlyHtmlCss = await fixture.readFile(onlyHtmlCssHref.replace(/^\/?/, '/'));
274+
275+
const ssrHtml = await fixture.readFile('/client-only-and-ssr/ssr/index.html');
276+
const $ssrHtml = cheerio.load(ssrHtml);
277+
const ssrHtmlCssHref = $ssrHtml('link[rel=stylesheet][href^=/_astro/]').attr('href');
278+
const ssrHtmlCss = await fixture.readFile(ssrHtmlCssHref.replace(/^\/?/, '/'));
279+
280+
expect(onlyHtmlCss).to.include('.svelte-only-and-ssr');
281+
expect(ssrHtmlCss).to.include('.svelte-only-and-ssr');
282+
});
268283
});
269284

270285
describe('Vite features', () => {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!-- This file will be used as client:only and SSR on two different pages -->
2+
3+
<div class="svelte-only-and-ssr">
4+
Svelte only and SSR
5+
</div>
6+
7+
<style>
8+
.svelte-only-and-ssr {
9+
background-color: green;
10+
}
11+
</style>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
import SvelteOnlyAndSsr from './_components/SvelteOnlyAndSsr.svelte'
3+
---
4+
5+
<div>
6+
<SvelteOnlyAndSsr client:only />
7+
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
import SvelteOnlyAndSsr from './_components/SvelteOnlyAndSsr.svelte'
3+
---
4+
5+
<div>
6+
<SvelteOnlyAndSsr />
7+
</div>

0 commit comments

Comments
 (0)