diff --git a/.changeset/big-forks-lead.md b/.changeset/big-forks-lead.md new file mode 100644 index 000000000000..15c24ed26da1 --- /dev/null +++ b/.changeset/big-forks-lead.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes an issue where CSS from unused components, when using content collections, could be incorrectly included between page navigations in development mode. diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts index c2902cb70b13..b3aebada997c 100644 --- a/packages/astro/src/content/consts.ts +++ b/packages/astro/src/content/consts.ts @@ -1,4 +1,5 @@ export const PROPAGATED_ASSET_FLAG = 'astroPropagatedAssets'; +export const PROPAGATED_ASSET_QUERY_PARAM = `?${PROPAGATED_ASSET_FLAG}`; export const CONTENT_RENDER_FLAG = 'astroRenderContent'; export const CONTENT_FLAG = 'astroContentCollectionEntry'; export const DATA_FLAG = 'astroDataCollectionEntry'; diff --git a/packages/astro/src/vite-plugin-astro-server/vite.ts b/packages/astro/src/vite-plugin-astro-server/vite.ts index dfff85927def..ad997a19b6cb 100644 --- a/packages/astro/src/vite-plugin-astro-server/vite.ts +++ b/packages/astro/src/vite-plugin-astro-server/vite.ts @@ -3,6 +3,7 @@ import { type EnvironmentModuleNode, isCSSRequest, type RunnableDevEnvironment } import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../core/constants.js'; import { unwrapId } from '../core/util.js'; import { hasSpecialQueries } from '../vite-plugin-utils/index.js'; +import { PROPAGATED_ASSET_QUERY_PARAM } from '../content/consts.js'; /** * List of file extensions signalling we can (and should) SSR ahead-of-time @@ -76,7 +77,7 @@ export async function* crawlGraph( const isFileTypeNeedingSSR = fileExtensionsToSSR.has(npath.extname(importedModulePathname)); // A propagation stopping point is a module with the ?astroPropagatedAssets flag. // When we encounter one of these modules we don't want to continue traversing. - const isPropagationStoppingPoint = importedModule.id.includes('?astroPropagatedAssets'); + const isPropagationStoppingPoint = importedModule.id.includes(PROPAGATED_ASSET_QUERY_PARAM); if ( isFileTypeNeedingSSR && // Should not SSR a module with ?astroPropagatedAssets diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index 54925c522d26..fd689040eb34 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -8,6 +8,7 @@ import { inlineRE, isBuildableCSSRequest, rawRE } from '../vite-plugin-astro-ser import { getVirtualModulePageNameForComponent } from '../vite-plugin-pages/util.js'; import { getDevCSSModuleName } from './util.js'; import { CSS_LANGS_RE } from '../core/viteUtils.js'; +import { PROPAGATED_ASSET_QUERY_PARAM } from '../content/consts.js'; interface AstroVitePluginOptions { routesList: RoutesList; @@ -44,6 +45,16 @@ function* collectCSSWithOrder( ): Generator { seen.add(id); + // Stop traversing if we reach an asset propagation stopping point to ensure we only collect CSS + // relevant to a content collection entry, if any. Not doing so could cause CSS from other + // entries to potentially be collected and bleed into the CSS included on the page, causing + // unexpected styles, for example when a module shared between 2 pages would import + // `astro:content` and thus potentially adding multiple content collection entry assets to the + // module graph. + if (id.includes(PROPAGATED_ASSET_QUERY_PARAM)) { + return; + } + // Keep all of the imported modules into an array so we can go through them one at a time const imported = Array.from(mod.importedModules); diff --git a/packages/astro/test/content-collections-render.test.js b/packages/astro/test/content-collections-render.test.js index 2338b04e0dc3..75f47bc4eade 100644 --- a/packages/astro/test/content-collections-render.test.js +++ b/packages/astro/test/content-collections-render.test.js @@ -218,5 +218,25 @@ describe('Content Collections - render()', () => { assert.equal(h2.length, 1); assert.equal(h2.attr('data-components-export-applied'), 'true'); }); + + it('Stops collecting CSS when reaching a propagation stopping point', async () => { + let response = await fixture.fetch('/blog/5-big-news', { method: 'GET' }); + assert.equal(response.status, 200); + + let html = await response.text(); + let $ = cheerio.load(html); + + // Includes the red button styles used in the MDX blog post + assert.ok($('head > style').text().includes('background-color:red;')); + + response = await fixture.fetch('/blog/about', { method: 'GET' }); + assert.equal(response.status, 200); + + html = await response.text(); + $ = cheerio.load(html); + + // Does not include the red button styles not used in this page + assert.equal($('head > style').text().includes('background-color:red;'), false); + }); }); }); diff --git a/packages/astro/test/fixtures/content/src/components/AboutLayout.astro b/packages/astro/test/fixtures/content/src/components/AboutLayout.astro new file mode 100644 index 000000000000..244ce2026e35 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/components/AboutLayout.astro @@ -0,0 +1,9 @@ +--- +import BlogLayout from './BlogLayout.astro'; +import { doSomething } from '../libs/routes'; + +// Calling a random function from a file importing `astro:content`. +doSomething() +--- + + diff --git a/packages/astro/test/fixtures/content/src/components/BlogLayout.astro b/packages/astro/test/fixtures/content/src/components/BlogLayout.astro new file mode 100644 index 000000000000..7dd76accf03a --- /dev/null +++ b/packages/astro/test/fixtures/content/src/components/BlogLayout.astro @@ -0,0 +1,23 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + + + + + + + + Astro + + +
+

{title}

+ +
+ + diff --git a/packages/astro/test/fixtures/content/src/components/RedButton.astro b/packages/astro/test/fixtures/content/src/components/RedButton.astro new file mode 100644 index 000000000000..1592fc872d6e --- /dev/null +++ b/packages/astro/test/fixtures/content/src/components/RedButton.astro @@ -0,0 +1,8 @@ + + + diff --git a/packages/astro/test/fixtures/content/src/content/blog/5-big-news.mdx b/packages/astro/test/fixtures/content/src/content/blog/5-big-news.mdx new file mode 100644 index 000000000000..a6a349b91327 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/content/blog/5-big-news.mdx @@ -0,0 +1,10 @@ +--- +title: Big News +description: Just some big news. +--- + +This is the content of a blog post with big news. + +import RedButton from "../../components/RedButton.astro"; + + diff --git a/packages/astro/test/fixtures/content/src/libs/routes.ts b/packages/astro/test/fixtures/content/src/libs/routes.ts new file mode 100644 index 000000000000..27cfa433ef85 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/libs/routes.ts @@ -0,0 +1,7 @@ +import { getCollection } from "astro:content"; + +export const routes = await getCollection("blog"); + +export function doSomething() { + // … +} diff --git a/packages/astro/test/fixtures/content/src/pages/blog/[...slug].astro b/packages/astro/test/fixtures/content/src/pages/blog/[...slug].astro new file mode 100644 index 000000000000..4889bd7f7d20 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/pages/blog/[...slug].astro @@ -0,0 +1,23 @@ +--- +import { getEntry, render } from "astro:content"; +import BlogLayout from "../../components/BlogLayout.astro"; +import { routes } from "../../libs/routes"; + +export function getStaticPaths() { + return [routes.map((route) => ({ params: { slug: route.id } }))].flat(); +} + +const { slug } = Astro.params; + +const entry = await getEntry("blog", slug); + +if (!entry) { + throw new Error(`Entry not found for slug: ${slug}`); +} + +const { Content } = await render(entry) +--- + + + + diff --git a/packages/astro/test/fixtures/content/src/pages/blog/about.astro b/packages/astro/test/fixtures/content/src/pages/blog/about.astro new file mode 100644 index 000000000000..e4ef1d255e60 --- /dev/null +++ b/packages/astro/test/fixtures/content/src/pages/blog/about.astro @@ -0,0 +1,7 @@ +--- +import AboutLayout from "../../components/AboutLayout.astro"; +--- + + +

This is the content of the about page.

+