diff --git a/.changeset/clever-suits-hide.md b/.changeset/clever-suits-hide.md new file mode 100644 index 000000000..31fdb518b --- /dev/null +++ b/.changeset/clever-suits-hide.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Bugfix: CSS import ordering, empty CSS output on build diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts index d46f56d13..2989faee2 100644 --- a/packages/astro/src/vite-plugin-build-css/index.ts +++ b/packages/astro/src/vite-plugin-build-css/index.ts @@ -124,6 +124,8 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { } } + if (!chunkCSS) return null; // don’t output empty .css files + if (isPureCSS) { const { code: minifiedCSS } = await esbuild.transform(chunkCSS, { loader: 'css', diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts index 7650c0224..cdc5c1877 100644 --- a/packages/astro/src/vite-plugin-build-html/index.ts +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -55,6 +55,7 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { const astroAssetMap = new Map>(); const cssChunkMap = new Map(); + const pageStyleImportOrder: string[] = []; return { name: PLUGIN_NAME, @@ -176,6 +177,11 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { const jsSource = assetImports.map((sid) => `import '${sid}';`).join('\n'); astroPageStyleMap.set(pageStyleId, jsSource); assetInput.add(pageStyleId); + + // preserve asset order in the order we encounter them + for (const assetHref of assetImports) { + if (!pageStyleImportOrder.includes(assetHref)) pageStyleImportOrder.push(assetHref); + } } } } @@ -260,17 +266,43 @@ export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin { assetIdMap.set(assetPath, referenceId); } + // Sort CSS in order of appearance in HTML (pageStyleImportOrder) + // This is the “global ordering” used below + const sortedCSSChunks = [...pureCSSChunks]; + sortedCSSChunks.sort((a, b) => { + let aIndex = Math.min( + ...Object.keys(a.modules).map((id) => { + const i = pageStyleImportOrder.findIndex((url) => id.endsWith(url)); + return i >= 0 ? i : Infinity; // if -1 is encountered (unknown order), move to the end (Infinity) + }) + ); + let bIndex = Math.min( + ...Object.keys(b.modules).map((id) => { + const i = pageStyleImportOrder.findIndex((url) => id.endsWith(url)); + return i >= 0 ? i : Infinity; + }) + ); + return aIndex - bIndex; + }); + const sortedChunkNames = sortedCSSChunks.map(({ fileName }) => fileName); + // Create a mapping of chunks to dependent chunks, used to add the proper // link tags for CSS. - for (const chunk of pureCSSChunks) { - const chunkReferenceIds: string[] = []; - for (const [specifier, chunkRefID] of chunkToReferenceIdMap.entries()) { - if (chunk.imports.includes(specifier) || specifier === chunk.fileName) { - chunkReferenceIds.push(chunkRefID); - } + for (const chunk of sortedCSSChunks) { + const chunkModules = [chunk.fileName, ...chunk.imports]; + // For each HTML output, sort CSS in HTML order Note: here we actually + // want -1 to be first. Since the last CSS “wins”, we want to load + // “unknown” (-1) CSS ordering first, followed by “known” ordering at + // the end so it takes priority + chunkModules.sort((a, b) => sortedChunkNames.indexOf(a) - sortedChunkNames.indexOf(b)); + + const referenceIDs: string[] = []; + for (const chunkID of chunkModules) { + const referenceID = chunkToReferenceIdMap.get(chunkID); + if (referenceID) referenceIDs.push(referenceID); } - for (const [id] of Object.entries(chunk.modules)) { - cssChunkMap.set(id, chunkReferenceIds); + for (const id of Object.keys(chunk.modules)) { + cssChunkMap.set(id, referenceIDs); } } diff --git a/packages/astro/test/astro-css-bundling-import.test.js b/packages/astro/test/astro-css-bundling-import.test.js index ea605143e..ae9b085d6 100644 --- a/packages/astro/test/astro-css-bundling-import.test.js +++ b/packages/astro/test/astro-css-bundling-import.test.js @@ -10,7 +10,7 @@ describe('CSS Bundling (ESM import)', () => { await fixture.build(); }); - it.skip('CSS output in import order', async () => { + it('CSS output in import order', async () => { // note: this test is a little confusing, but the main idea is that // page-2.astro contains all of page-1.astro, plus some unique styles. // we only test page-2 to ensure the proper order is observed. @@ -29,11 +29,10 @@ describe('CSS Bundling (ESM import)', () => { expect(css.indexOf('p{color:green}')).to.be.greaterThan(css.indexOf('p{color:red}')); // test 2: insure green comes after blue (page-1.css) - expect(css.indexOf('p{color:green}')).to.be.greaterThan(css.indexOf('p{color:red}')); + expect(css.indexOf('p{color:green}')).to.be.greaterThan(css.indexOf('p{color:#00f}')); }); - // TODO: need more investigation to fix this - it.skip('no empty CSS files', async () => { + it('no empty CSS files', async () => { for (const page of ['/page-1/index.html', '/page-2/index.html']) { const html = await fixture.readFile(page); const $ = cheerio.load(html);