From d12c47a4a2bff49884baa0c92d1ce97eb431afcc Mon Sep 17 00:00:00 2001 From: amankumarpandeyin Date: Sun, 14 Dec 2025 11:53:45 +0530 Subject: [PATCH 1/2] fix(css): prevent double-bundling when CSS imported from multiple locations Fixes #14991 When CSS was imported in both a page's frontmatter and a component's script tag, it was bundled twice in production builds. This happened because the CSS plugins run for both SSR and client builds, adding CSS to the same pageData.styles array. The fix adds content-based deduplication that checks existing styles before adding new ones - comparing by content for inline CSS and by src for external CSS. --- .changeset/fix-css-double-bundle.md | 5 ++ .../src/core/build/plugins/plugin-css.ts | 19 +++++- packages/astro/test/css-double-bundle.test.js | 60 +++++++++++++++++++ .../css-double-bundle/astro.config.mjs | 3 + .../fixtures/css-double-bundle/package.json | 8 +++ .../src/components/Button.astro | 5 ++ .../css-double-bundle/src/pages/index.astro | 13 ++++ .../css-double-bundle/src/styles/base.css | 3 + 8 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-css-double-bundle.md create mode 100644 packages/astro/test/css-double-bundle.test.js create mode 100644 packages/astro/test/fixtures/css-double-bundle/astro.config.mjs create mode 100644 packages/astro/test/fixtures/css-double-bundle/package.json create mode 100644 packages/astro/test/fixtures/css-double-bundle/src/components/Button.astro create mode 100644 packages/astro/test/fixtures/css-double-bundle/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/css-double-bundle/src/styles/base.css diff --git a/.changeset/fix-css-double-bundle.md b/.changeset/fix-css-double-bundle.md new file mode 100644 index 000000000000..4aff18a65e61 --- /dev/null +++ b/.changeset/fix-css-double-bundle.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes CSS double-bundling when the same CSS file is imported in both a page's frontmatter and a component's script tag diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index c8854e408f18..b1bb8873c30c 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -185,6 +185,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { }, async generateBundle(_outputOptions, bundle) { const inlineConfig = settings.config.build.inlineStylesheets; + Object.entries(bundle).forEach(([id, stylesheet]) => { if ( stylesheet.type !== 'asset' || @@ -211,7 +212,23 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { internals.pagesByKeys.forEach((pageData) => { const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName]; if (orderingInfo !== undefined) { - pageData.styles.push({ ...orderingInfo, sheet }); + // Check if this stylesheet was already added to this page. + // We check both inline (by content) and external (by src) styles to prevent + // duplicates that can occur when CSS is imported from both a page's frontmatter + // and a component's script tag, or when the same plugin runs in both SSR and client builds. + const alreadyAdded = pageData.styles.some((s) => { + if (s.sheet.type === 'external' && sheet.type === 'external') { + return s.sheet.src === sheet.src; + } + if (s.sheet.type === 'inline' && sheet.type === 'inline') { + return s.sheet.content === sheet.content; + } + return false; + }); + + if (!alreadyAdded) { + pageData.styles.push({ ...orderingInfo, sheet }); + } sheetAddedToPage = true; } }); diff --git a/packages/astro/test/css-double-bundle.test.js b/packages/astro/test/css-double-bundle.test.js new file mode 100644 index 000000000000..f03ba17fe51c --- /dev/null +++ b/packages/astro/test/css-double-bundle.test.js @@ -0,0 +1,60 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('CSS Double Bundling Prevention', function () { + /** @type {import('./test-utils').Fixture} */ + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/css-double-bundle/', + }); + await fixture.build(); + }); + + it('CSS imported in both page frontmatter and component script should only be bundled once', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + + // Get all CSS content (both inline and linked) + let allCss = ''; + + // Check inline styles + $('style').each((_, el) => { + allCss += $(el).html(); + }); + + // Check linked stylesheets + const cssLinks = $('link[rel="stylesheet"][href^="/_astro/"]'); + for (let i = 0; i < cssLinks.length; i++) { + const href = cssLinks.eq(i).attr('href'); + const cssContent = await fixture.readFile(href.replace(/^\//, '/')); + allCss += cssContent; + } + + // Count occurrences of the CSS rule - should appear exactly once + const matches = allCss.match(/button\s*\{[^}]*background:\s*purple/g) || []; + + assert.equal( + matches.length, + 1, + `Expected CSS rule "button{background:purple}" to appear exactly once, but found ${matches.length} occurrences. CSS should not be double-bundled when imported from both page frontmatter and component script.`, + ); + }); + + it('CSS should still be present in the build output', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + + // Should have either inline styles or linked stylesheet + const hasInlineStyle = $('style').length > 0; + const hasLinkedStylesheet = $('link[rel="stylesheet"]').length > 0; + + assert.ok( + hasInlineStyle || hasLinkedStylesheet, + 'Expected CSS to be present in the build output (either inline or as a linked stylesheet)', + ); + }); +}); diff --git a/packages/astro/test/fixtures/css-double-bundle/astro.config.mjs b/packages/astro/test/fixtures/css-double-bundle/astro.config.mjs new file mode 100644 index 000000000000..86dbfb924824 --- /dev/null +++ b/packages/astro/test/fixtures/css-double-bundle/astro.config.mjs @@ -0,0 +1,3 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({}); diff --git a/packages/astro/test/fixtures/css-double-bundle/package.json b/packages/astro/test/fixtures/css-double-bundle/package.json new file mode 100644 index 000000000000..35f81d86aaf4 --- /dev/null +++ b/packages/astro/test/fixtures/css-double-bundle/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/css-double-bundle", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/css-double-bundle/src/components/Button.astro b/packages/astro/test/fixtures/css-double-bundle/src/components/Button.astro new file mode 100644 index 000000000000..ff563a1fba0a --- /dev/null +++ b/packages/astro/test/fixtures/css-double-bundle/src/components/Button.astro @@ -0,0 +1,5 @@ + + + diff --git a/packages/astro/test/fixtures/css-double-bundle/src/pages/index.astro b/packages/astro/test/fixtures/css-double-bundle/src/pages/index.astro new file mode 100644 index 000000000000..9bdf33a027dc --- /dev/null +++ b/packages/astro/test/fixtures/css-double-bundle/src/pages/index.astro @@ -0,0 +1,13 @@ +--- +import "../styles/base.css"; +import Button from "../components/Button.astro"; +--- + + + + + + +