Fix CSS deduping and missing chunks (#7218)
This commit is contained in:
parent
6b12f93b57
commit
6c7df28ab3
7 changed files with 69 additions and 28 deletions
5
.changeset/eighty-gifts-cheer.md
Normal file
5
.changeset/eighty-gifts-cheer.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix CSS deduping and missing chunks
|
|
@ -8,10 +8,13 @@ import type { PageBuildData, StylesheetAsset, ViteID } from './types';
|
||||||
|
|
||||||
export interface BuildInternals {
|
export interface BuildInternals {
|
||||||
/**
|
/**
|
||||||
* The module ids of all CSS chunks, used to deduplicate CSS assets between
|
* Each CSS module is named with a chunk id derived from the Astro pages they
|
||||||
* SSR build and client build in vite-plugin-css.
|
* are used in by default. It's easy to crawl this relation in the SSR build as
|
||||||
|
* the Astro pages are the entrypoint, but not for the client build as hydratable
|
||||||
|
* components are the entrypoint instead. This map is used as a cache from the SSR
|
||||||
|
* build so the client can pick up the same information and use the same chunk ids.
|
||||||
*/
|
*/
|
||||||
cssChunkModuleIds: Set<string>;
|
cssModuleToChunkIdMap: Map<string, string>;
|
||||||
|
|
||||||
// A mapping of hoisted script ids back to the exact hoisted scripts it references
|
// A mapping of hoisted script ids back to the exact hoisted scripts it references
|
||||||
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
|
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
|
||||||
|
@ -92,7 +95,7 @@ export function createBuildInternals(): BuildInternals {
|
||||||
const hoistedScriptIdToPagesMap = new Map<string, Set<string>>();
|
const hoistedScriptIdToPagesMap = new Map<string, Set<string>>();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cssChunkModuleIds: new Set(),
|
cssModuleToChunkIdMap: new Map(),
|
||||||
hoistedScriptIdToHoistedMap,
|
hoistedScriptIdToHoistedMap,
|
||||||
hoistedScriptIdToPagesMap,
|
hoistedScriptIdToPagesMap,
|
||||||
entrySpecifierToBundleMap: new Map<string, string>(),
|
entrySpecifierToBundleMap: new Map<string, string>(),
|
||||||
|
|
|
@ -64,20 +64,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||||
const cssBuildPlugin: VitePlugin = {
|
const cssBuildPlugin: VitePlugin = {
|
||||||
name: 'astro:rollup-plugin-build-css',
|
name: 'astro:rollup-plugin-build-css',
|
||||||
|
|
||||||
transform(_, id) {
|
|
||||||
// In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
|
|
||||||
// In the client build, if we're also bundling the same style, return an empty string to
|
|
||||||
// deduplicate the final CSS output.
|
|
||||||
if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
outputOptions(outputOptions) {
|
outputOptions(outputOptions) {
|
||||||
// Skip in client builds as its module graph doesn't have reference to Astro pages
|
|
||||||
// to be able to chunk based on it's related top-level pages.
|
|
||||||
if (options.target === 'client') return;
|
|
||||||
|
|
||||||
const assetFileNames = outputOptions.assetFileNames;
|
const assetFileNames = outputOptions.assetFileNames;
|
||||||
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
|
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
|
||||||
const createNameForParentPages = namingIncludesHash
|
const createNameForParentPages = namingIncludesHash
|
||||||
|
@ -89,16 +76,31 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||||
// For CSS, create a hash of all of the pages that use it.
|
// For CSS, create a hash of all of the pages that use it.
|
||||||
// This causes CSS to be built into shared chunks when used by multiple pages.
|
// This causes CSS to be built into shared chunks when used by multiple pages.
|
||||||
if (isBuildableCSSRequest(id)) {
|
if (isBuildableCSSRequest(id)) {
|
||||||
|
// For client builds that has hydrated components as entrypoints, there's no way
|
||||||
|
// to crawl up and find the pages that use it. So we lookup the cache during SSR
|
||||||
|
// build (that has the pages information) to derive the same chunk id so they
|
||||||
|
// match up on build, making sure both builds has the CSS deduped.
|
||||||
|
// NOTE: Components that are only used with `client:only` may not exist in the cache
|
||||||
|
// and that's okay. We can use Rollup's default chunk strategy instead as these CSS
|
||||||
|
// are outside of the SSR build scope, which no dedupe is needed.
|
||||||
|
if (options.target === 'client') {
|
||||||
|
return internals.cssModuleToChunkIdMap.get(id)!;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [pageInfo] of walkParentInfos(id, {
|
for (const [pageInfo] of walkParentInfos(id, {
|
||||||
getModuleInfo: meta.getModuleInfo,
|
getModuleInfo: meta.getModuleInfo,
|
||||||
})) {
|
})) {
|
||||||
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
|
||||||
// Split delayed assets to separate modules
|
// Split delayed assets to separate modules
|
||||||
// so they can be injected where needed
|
// so they can be injected where needed
|
||||||
return createNameHash(id, [id]);
|
const chunkId = createNameHash(id, [id]);
|
||||||
|
internals.cssModuleToChunkIdMap.set(id, chunkId);
|
||||||
|
return chunkId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createNameForParentPages(id, meta);
|
const chunkId = createNameForParentPages(id, meta);
|
||||||
|
internals.cssModuleToChunkIdMap.set(id, chunkId);
|
||||||
|
return chunkId;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -113,15 +115,6 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||||
// Skip if the chunk has no CSS, we want to handle CSS chunks only
|
// Skip if the chunk has no CSS, we want to handle CSS chunks only
|
||||||
if (meta.importedCss.size < 1) continue;
|
if (meta.importedCss.size < 1) continue;
|
||||||
|
|
||||||
// In the SSR build, keep track of all CSS chunks' modules as the client build may
|
|
||||||
// duplicate them, e.g. for `client:load` components that render in SSR and client
|
|
||||||
// for hydation.
|
|
||||||
if (options.target === 'server') {
|
|
||||||
for (const id of Object.keys(chunk.modules)) {
|
|
||||||
internals.cssChunkModuleIds.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the client build, client:only styles need to be mapped
|
// For the client build, client:only styles need to be mapped
|
||||||
// over to their page. For this chunk, determine if it's a child of a
|
// over to their page. For this chunk, determine if it's a child of a
|
||||||
// client:only component and if so, add its CSS to the page it belongs to.
|
// client:only component and if so, add its CSS to the page it belongs to.
|
||||||
|
|
|
@ -265,6 +265,21 @@ describe('CSS', function () {
|
||||||
new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:ComicSansMS`)
|
new RegExp(`.svelte-scss.${scopedClass}[^{]*{font-family:ComicSansMS`)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('client:only and SSR in two pages, both should have styles', async () => {
|
||||||
|
const onlyHtml = await fixture.readFile('/client-only-and-ssr/only/index.html');
|
||||||
|
const $onlyHtml = cheerio.load(onlyHtml);
|
||||||
|
const onlyHtmlCssHref = $onlyHtml('link[rel=stylesheet][href^=/_astro/]').attr('href');
|
||||||
|
const onlyHtmlCss = await fixture.readFile(onlyHtmlCssHref.replace(/^\/?/, '/'));
|
||||||
|
|
||||||
|
const ssrHtml = await fixture.readFile('/client-only-and-ssr/ssr/index.html');
|
||||||
|
const $ssrHtml = cheerio.load(ssrHtml);
|
||||||
|
const ssrHtmlCssHref = $ssrHtml('link[rel=stylesheet][href^=/_astro/]').attr('href');
|
||||||
|
const ssrHtmlCss = await fixture.readFile(ssrHtmlCssHref.replace(/^\/?/, '/'));
|
||||||
|
|
||||||
|
expect(onlyHtmlCss).to.include('.svelte-only-and-ssr');
|
||||||
|
expect(ssrHtmlCss).to.include('.svelte-only-and-ssr');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Vite features', () => {
|
describe('Vite features', () => {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!-- This file will be used as client:only and SSR on two different pages -->
|
||||||
|
|
||||||
|
<div class="svelte-only-and-ssr">
|
||||||
|
Svelte only and SSR
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.svelte-only-and-ssr {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
</style>
|
7
packages/astro/test/fixtures/0-css/src/pages/client-only-and-ssr/only.astro
vendored
Normal file
7
packages/astro/test/fixtures/0-css/src/pages/client-only-and-ssr/only.astro
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import SvelteOnlyAndSsr from './_components/SvelteOnlyAndSsr.svelte'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SvelteOnlyAndSsr client:only />
|
||||||
|
</div>
|
7
packages/astro/test/fixtures/0-css/src/pages/client-only-and-ssr/ssr.astro
vendored
Normal file
7
packages/astro/test/fixtures/0-css/src/pages/client-only-and-ssr/ssr.astro
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
import SvelteOnlyAndSsr from './_components/SvelteOnlyAndSsr.svelte'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SvelteOnlyAndSsr />
|
||||||
|
</div>
|
Loading…
Reference in a new issue