Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-css-double-bundle.md
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand All @@ -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;
}
});
Expand Down
60 changes: 60 additions & 0 deletions packages/astro/test/css-double-bundle.test.js
Original file line number Diff line number Diff line change
@@ -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)',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from 'astro/config';

export default defineConfig({});
8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/css-double-bundle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/css-double-bundle",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<button>Button</button>

<script>
import "../styles/base.css";
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import "../styles/base.css";
import Button from "../components/Button.astro";
---

<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<Button />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
button {
background: purple;
}
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading