diff --git a/.changeset/lucky-kiwis-swim.md b/.changeset/lucky-kiwis-swim.md new file mode 100644 index 000000000000..01c615bd6fc8 --- /dev/null +++ b/.changeset/lucky-kiwis-swim.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a bug where page-level CSS could leak between unrelated pages when traversing style parents across top-level route boundaries diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index c2c40c9b4d86..d412403ff0df 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -30,6 +30,12 @@ interface PluginOptions { buildOptions: StaticBuildOptions; } +function isBuildCssBoundary(id: string, ctx: { getModuleInfo: GetModuleInfo }): boolean { + if (isPropagatedAssetBoundary(id)) return true; + const info = ctx.getModuleInfo(id); + return info ? moduleIsTopLevelPage(info) : false; +} + function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const { internals, buildOptions } = options; const { settings } = buildOptions; @@ -158,7 +164,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const parentModuleInfos = getParentExtendedModuleInfos( scopedToModule, this, - isPropagatedAssetBoundary, + (moduleId) => isBuildCssBoundary(moduleId, this), ); for (const { info: pageInfo, depth, order } of parentModuleInfos) { if (moduleIsTopLevelPage(pageInfo)) { @@ -230,7 +236,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const parentModuleInfos = getParentExtendedModuleInfos( id, this, - isPropagatedAssetBoundary, + (importer) => isBuildCssBoundary(importer, this), ); for (const { info: pageInfo, depth, order } of parentModuleInfos) { if (isPropagatedAssetBoundary(pageInfo.id)) { diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/astro.config.mjs b/packages/astro/test/fixtures/i18n-css-leak-basic/astro.config.mjs new file mode 100644 index 000000000000..9a4d452f1628 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/astro.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + build: { + inlineStylesheets: 'never', + }, + i18n: { + locales: ['en'], + defaultLocale: 'en', + routing: { + redirectToDefaultLocale: false, + }, + }, +}); diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/package.json b/packages/astro/test/fixtures/i18n-css-leak-basic/package.json new file mode 100644 index 000000000000..1efbbaff7eba --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/i18n-css-leak-basic", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/components/Header.astro b/packages/astro/test/fixtures/i18n-css-leak-basic/src/components/Header.astro new file mode 100644 index 000000000000..202a09e3f04c --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/components/Header.astro @@ -0,0 +1,9 @@ +--- +import { getRelativeLocaleUrl } from 'astro:i18n'; + +const docsHref = getRelativeLocaleUrl('en', 'docs'); +--- + +
+ Docs +
diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/DocsLayout.astro b/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/DocsLayout.astro new file mode 100644 index 000000000000..13a014e3f214 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/DocsLayout.astro @@ -0,0 +1,12 @@ +--- +import '../styles/docs.css'; +--- + + + + Docs + + + + + diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/SiteLayout.astro b/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/SiteLayout.astro new file mode 100644 index 000000000000..c264ec08e0c9 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/SiteLayout.astro @@ -0,0 +1,14 @@ +--- +import Header from '../components/Header.astro'; +import '../styles/site.css'; +--- + + + + Site + + +
+ + + diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/docs/index.astro b/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/docs/index.astro new file mode 100644 index 000000000000..997686a93b13 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/docs/index.astro @@ -0,0 +1,7 @@ +--- +import DocsLayout from '../../layouts/DocsLayout.astro'; +--- + + +

Docs

+
diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/index.astro b/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/index.astro new file mode 100644 index 000000000000..d8703482bd48 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/index.astro @@ -0,0 +1,7 @@ +--- +import SiteLayout from '../layouts/SiteLayout.astro'; +--- + + +

Home

+
diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/docs.css b/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/docs.css new file mode 100644 index 000000000000..d6295bd006d6 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/docs.css @@ -0,0 +1,7 @@ +body { + background: black; +} + +h1 { + color: red; +} diff --git a/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/site.css b/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/site.css new file mode 100644 index 000000000000..5b6976fbff55 --- /dev/null +++ b/packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/site.css @@ -0,0 +1,3 @@ +body { + background: white; +} diff --git a/packages/astro/test/i18n-css-leak.test.js b/packages/astro/test/i18n-css-leak.test.js new file mode 100644 index 000000000000..839d9b946140 --- /dev/null +++ b/packages/astro/test/i18n-css-leak.test.js @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('CSS graph boundaries with astro:i18n', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-css-leak-basic/', + build: { inlineStylesheets: 'never' }, + }); + await fixture.build(); + }); + + async function getPageCss(pathname) { + const html = await fixture.readFile(pathname); + const $ = cheerioLoad(html); + const hrefs = $('link[rel=stylesheet]') + .map((_index, el) => $(el).attr('href')) + .get(); + const stylesheets = await Promise.all(hrefs.map((href) => fixture.readFile(href))); + return stylesheets.join('\n'); + } + + it('does not attach docs-only CSS to unrelated pages', async () => { + const css = await getPageCss('/index.html'); + assert.match(css, /background:#fff/); + assert.doesNotMatch(css, /background:#000/); + assert.doesNotMatch(css, /color:red/); + }); + + it('keeps docs-only CSS on the docs page', async () => { + const css = await getPageCss('/docs/index.html'); + assert.match(css, /background:#000/); + assert.match(css, /color:red/); + assert.doesNotMatch(css, /background:#fff/); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 127f98557456..498e9f6e178b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3424,6 +3424,12 @@ importers: specifier: ^10.29.0 version: 10.29.0 + packages/astro/test/fixtures/i18n-css-leak-basic: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/i18n-routing-base: dependencies: astro: