diff --git a/.changeset/forty-goats-warn.md b/.changeset/forty-goats-warn.md new file mode 100644 index 000000000..6f0d863e6 --- /dev/null +++ b/.changeset/forty-goats-warn.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes client:only CSS diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index d74722b1a..d9a21a7de 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,7 +1,8 @@ -import type { RouteData } from '../../@types/astro'; +import type { AstroConfig, RouteData } from '../../@types/astro'; import type { RenderedChunk } from 'rollup'; import type { PageBuildData, ViteID } from './types'; +import { fileURLToPath } from 'url'; import { viteID } from '../util.js'; export interface BuildInternals { @@ -25,6 +26,11 @@ export interface BuildInternals { */ pagesByViteID: Map; + /** + * A map for page-specific information by a client:only component + */ + pagesByClientOnly: Map>; + /** * chunkToReferenceIdMap maps them to a hash id used to find the final file. * @deprecated This Map is only used for the legacy build. @@ -73,6 +79,7 @@ export function createBuildInternals(): BuildInternals { pagesByComponent: new Map(), pagesByViteID: new Map(), + pagesByClientOnly: new Map(), }; } @@ -88,6 +95,30 @@ export function trackPageData( internals.pagesByViteID.set(viteID(componentURL), pageData); } +/** + * Tracks client-only components to the pages they are associated with. + */ +export function trackClientOnlyPageDatas( + internals: BuildInternals, + pageData: PageBuildData, + clientOnlys: string[], + astroConfig: AstroConfig, +) { + for(const clientOnlyComponent of clientOnlys) { + const coPath = viteID(new URL('.' + clientOnlyComponent, astroConfig.root)); + let pageDataSet: Set; + if(internals.pagesByClientOnly.has(coPath)) { + pageDataSet = internals.pagesByClientOnly.get(coPath)!; + } else { + pageDataSet = new Set(); + internals.pagesByClientOnly.set(coPath, pageDataSet); + } + pageDataSet.add(pageData); + } +} + + + export function* getPageDatasByChunk( internals: BuildInternals, chunk: RenderedChunk @@ -100,6 +131,22 @@ export function* getPageDatasByChunk( } } +export function* getPageDatasByClientOnlyChunk( + internals: BuildInternals, + chunk: RenderedChunk +): Generator { + const pagesByClientOnly = internals.pagesByClientOnly; + if(pagesByClientOnly.size) { + for (const [modulePath] of Object.entries(chunk.modules)) { + if (pagesByClientOnly.has(modulePath)) { + for(const pageData of pagesByClientOnly.get(modulePath)!) { + yield pageData; + } + } + } + } +} + export function getPageDataByComponent( internals: BuildInternals, component: string diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 40107844d..6be7ee4e2 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -1,5 +1,5 @@ import type { RollupOutput } from 'rollup'; -import type { BuildInternals } from '../../core/build/internal.js'; +import { BuildInternals, trackClientOnlyPageDatas } from '../../core/build/internal.js'; import type { ViteConfigWithSSR } from '../create-vite'; import type { PageBuildData, StaticBuildOptions } from './types'; import glob from 'fast-glob'; @@ -54,13 +54,17 @@ export async function staticBuild(opts: StaticBuildOptions) { const [renderers, mod] = pageData.preload; const metadata = mod.$$metadata; + // Track client:only usage so we can map their CSS back to the Page they are used in. + const clientOnlys = Array.from(metadata.clientOnlyComponentPaths()); + trackClientOnlyPageDatas(internals, pageData, clientOnlys, astroConfig); + const topLevelImports = new Set([ // Any component that gets hydrated // 'components/Counter.jsx' // { 'components/Counter.jsx': 'counter.hash.js' } ...metadata.hydratedComponentPaths(), // Client-only components - ...metadata.clientOnlyComponentPaths(), + ...clientOnlys, // Any hydration directive like astro/client/idle.js ...metadata.hydrationDirectiveSpecifiers(), // The client path for each renderer @@ -148,6 +152,7 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp rollupPluginAstroBuildCSS({ internals, legacy: false, + target: 'server' }), ...(viteConfig.plugins || []), // SSR needs to be last @@ -218,6 +223,7 @@ async function clientBuild( rollupPluginAstroBuildCSS({ internals, legacy: false, + target: 'client' }), ...(viteConfig.plugins || []), ], diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts index 150cd7451..de62a9933 100644 --- a/packages/astro/src/vite-plugin-build-css/index.ts +++ b/packages/astro/src/vite-plugin-build-css/index.ts @@ -9,6 +9,7 @@ import { getPageDatasByChunk, getPageDataByViteID, hasPageDataByViteID, + getPageDatasByClientOnlyChunk, } from '../core/build/internal.js'; const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css'; @@ -54,6 +55,7 @@ function isRawOrUrlModule(id: string) { interface PluginOptions { internals: BuildInternals; legacy: boolean; + target: 'client' | 'server'; } export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { @@ -172,7 +174,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { }, async renderChunk(_code, chunk) { - if (!legacy) return null; + if (options.target === 'server') return null; let chunkCSS = ''; let isPureCSS = true; @@ -207,6 +209,10 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin { for (const pageData of getPageDatasByChunk(internals, chunk)) { pageData.css.add(fileName); } + // Adds this CSS for client:only components to the appropriate page + for (const pageData of getPageDatasByClientOnlyChunk(internals, chunk)) { + pageData.css.add(fileName); + } } return null; diff --git a/packages/astro/test/astro-client-only.test.js b/packages/astro/test/astro-client-only.test.js index 812b7229b..dcd7faf17 100644 --- a/packages/astro/test/astro-client-only.test.js +++ b/packages/astro/test/astro-client-only.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import cheerio from 'cheerio'; +import { load as cheerioLoad } from 'cheerio'; import { loadFixture } from './test-utils.js'; describe('Client only components', () => { @@ -14,7 +14,7 @@ describe('Client only components', () => { it('Loads pages using client:only hydrator', async () => { const html = await fixture.readFile('/index.html'); - const $ = cheerio.load(html); + const $ = cheerioLoad(html); // test 1: is empty expect($('astro-root').html()).to.equal(''); @@ -24,4 +24,10 @@ describe('Client only components', () => { // test 2: svelte renderer is on the page expect(/import\(".\/entry.*/g.test(script)).to.be.ok; }); + + it('Adds the CSS to the page', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerioLoad(html); + expect($('link[rel=stylesheet]')).to.have.lengthOf(2); + }) }); diff --git a/packages/astro/test/fixtures/astro-client-only/astro.config.mjs b/packages/astro/test/fixtures/astro-client-only/astro.config.mjs index 77fdcd1b9..1c8866ea7 100644 --- a/packages/astro/test/fixtures/astro-client-only/astro.config.mjs +++ b/packages/astro/test/fixtures/astro-client-only/astro.config.mjs @@ -1,7 +1,8 @@ import { defineConfig } from 'astro/config'; import svelte from '@astrojs/svelte'; +import react from '@astrojs/react'; // https://astro.build/config export default defineConfig({ - integrations: [svelte()], + integrations: [svelte(), react()], }); diff --git a/packages/astro/test/fixtures/astro-client-only/package.json b/packages/astro/test/fixtures/astro-client-only/package.json index 038e6f99d..96f83eac2 100644 --- a/packages/astro/test/fixtures/astro-client-only/package.json +++ b/packages/astro/test/fixtures/astro-client-only/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@astrojs/svelte": "workspace:*", + "@astrojs/react": "workspace:*", "astro": "workspace:*" } } diff --git a/packages/astro/test/fixtures/astro-client-only/src/components/JSXComponent.jsx b/packages/astro/test/fixtures/astro-client-only/src/components/JSXComponent.jsx new file mode 100644 index 000000000..e9d2411d1 --- /dev/null +++ b/packages/astro/test/fixtures/astro-client-only/src/components/JSXComponent.jsx @@ -0,0 +1,6 @@ +import React from 'react'; +import './global.css'; + +export default function() { + return
i am react
+} diff --git a/packages/astro/test/fixtures/astro-client-only/src/components/PersistentCounter.svelte b/packages/astro/test/fixtures/astro-client-only/src/components/PersistentCounter.svelte index 92d005415..855be29ea 100644 --- a/packages/astro/test/fixtures/astro-client-only/src/components/PersistentCounter.svelte +++ b/packages/astro/test/fixtures/astro-client-only/src/components/PersistentCounter.svelte @@ -12,7 +12,11 @@ count -= 1; } - +
{ count }
diff --git a/packages/astro/test/fixtures/astro-client-only/src/components/global.css b/packages/astro/test/fixtures/astro-client-only/src/components/global.css new file mode 100644 index 000000000..e495b6de9 --- /dev/null +++ b/packages/astro/test/fixtures/astro-client-only/src/components/global.css @@ -0,0 +1,3 @@ +body { + font-family: 'Courier New', Courier, monospace; +} diff --git a/packages/astro/test/fixtures/astro-client-only/src/pages/index.astro b/packages/astro/test/fixtures/astro-client-only/src/pages/index.astro index cb277d194..bf239197f 100644 --- a/packages/astro/test/fixtures/astro-client-only/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-client-only/src/pages/index.astro @@ -1,9 +1,11 @@ --- import PersistentCounter from '../components/PersistentCounter.svelte'; +import ReactComponent from '../components/JSXComponent.jsx'; --- Client only pages + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 445165259..2c8331563 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -684,9 +684,11 @@ importers: packages/astro/test/fixtures/astro-client-only: specifiers: + '@astrojs/react': workspace:* '@astrojs/svelte': workspace:* astro: workspace:* dependencies: + '@astrojs/react': link:../../../../integrations/react '@astrojs/svelte': link:../../../../integrations/svelte astro: link:../../..