diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index 412dd4e16893..c242b7c4d4b6 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -5,6 +5,7 @@ import type { RouteData, SSRElement, } from '../../../types/public/index.js'; +import { getDevCSSModuleName } from '../../../vite-plugin-css/util.js'; import { type HeadElements, Pipeline, type TryRewriteResult } from '../../base-pipeline.js'; import { ASTRO_VERSION } from '../../constants.js'; import { createModuleScriptElement, createStylesheetElementSet } from '../../render/ssr-element.js'; @@ -92,7 +93,7 @@ export class DevPipeline extends Pipeline { scripts.add({ props: {}, children }); } - const { css } = await import('virtual:astro:dev-css'); + const { css } = await import(getDevCSSModuleName(routeData.component)); // Pass framework CSS in as style tags to be appended to the page. for (const { id, url: src, content } of css) { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 92e17959a5ac..b95bfa260db8 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -228,7 +228,7 @@ async function buildManifest( scripts.push({ type: 'external', - value: prefixAssetPath(src), + value: src, }); } @@ -238,7 +238,6 @@ async function buildManifest( const styles = pageData.styles .sort(cssOrder) .map(({ sheet }) => sheet) - .map((s) => (s.type === 'external' ? { ...s, src: prefixAssetPath(s.src) } : s)) .reduce(mergeInlineCss, []); routes.push({ diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index f0ff5625702b..f99995ead339 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -235,6 +235,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter entryFileNames: `${settings.config.build.assets}/[name].[hash].js`, chunkFileNames: `${settings.config.build.assets}/[name].[hash].js`, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, + ...viteConfig.environments?.client?.build?.rollupOptions?.output, }, }, }, diff --git a/packages/astro/src/vite-plugin-app/pipeline.ts b/packages/astro/src/vite-plugin-app/pipeline.ts index b67e7eb7206a..3970811960fb 100644 --- a/packages/astro/src/vite-plugin-app/pipeline.ts +++ b/packages/astro/src/vite-plugin-app/pipeline.ts @@ -24,6 +24,7 @@ import type { } from '../types/public/index.js'; import { getComponentMetadata } from '../vite-plugin-astro-server/metadata.js'; import { createResolve } from '../vite-plugin-astro-server/resolve.js'; +import { getDevCSSModuleName } from '../vite-plugin-css/util.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; export class AstroServerPipeline extends Pipeline { @@ -120,7 +121,7 @@ export class AstroServerPipeline extends Pipeline { } } - const { css } = await import('virtual:astro:dev-css'); + const { css } = await loader.import(getDevCSSModuleName(routeData.component)); // Pass framework CSS in as style tags to be appended to the page. const links = new Set(); diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index b2c4d2d01326..6ee1e0bf1f4f 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -1,7 +1,6 @@ import type { Plugin, RunnableDevEnvironment } from 'vite'; import { wrapId } from '../core/util.js'; import type { ImportedDevStyle, RoutesList } from '../types/astro.js'; -import type { RouteData } from '../types/public/index.js'; import { isBuildableCSSRequest } from '../vite-plugin-astro-server/util.js'; interface AstroVitePluginOptions { @@ -11,36 +10,43 @@ interface AstroVitePluginOptions { const MODULE_DEV_CSS = 'virtual:astro:dev-css'; const RESOLVED_MODULE_DEV_CSS = '\0' + MODULE_DEV_CSS; +const MODULE_DEV_CSS_PREFIX = 'virtual:astro:dev-css:'; +const RESOLVED_MODULE_DEV_CSS_PREFIX = '\0' + MODULE_DEV_CSS_PREFIX; +const ASTRO_CSS_EXTENSION_POST_PATTERN = '@_@'; + +/** + * Extract the original component path from a masked virtual module name. + * Inverse function of getVirtualModulePageName(). + */ +function getComponentFromVirtualModuleCssName(virtualModulePrefix: string, id: string): string { + return id.slice(virtualModulePrefix.length).replace(new RegExp(ASTRO_CSS_EXTENSION_POST_PATTERN, 'g'), '.'); +} /** * This plugin tracks the CSS that should be applied by route. * - * The virtual module should be used only during development + * The virtual module should be used only during development. + * Per-route virtual modules are created to avoid invalidation loops. * * @param routesList */ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOptions): Plugin { let environment: undefined | RunnableDevEnvironment = undefined; - let cssMap = new Set(); - let currentRoute: RouteData | undefined = undefined; + let routeCssMap = new Map>(); return { name: MODULE_DEV_CSS, async configureServer(server) { environment = server.environments.ssr as RunnableDevEnvironment; - - server.middlewares.use(async (req, _res, next) => { - if (!req.url) return next(); - - currentRoute = routesList.routes.find((r) => req.url && r.pattern.test(req.url)); - return next(); - }); }, - + resolveId(id) { if (id === MODULE_DEV_CSS) { return RESOLVED_MODULE_DEV_CSS; } + if (id.startsWith(MODULE_DEV_CSS_PREFIX)) { + return RESOLVED_MODULE_DEV_CSS_PREFIX + id.slice(MODULE_DEV_CSS_PREFIX.length); + } }, async load(id) { @@ -49,6 +55,13 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption code: `export const css = new Set()`, }; } + if (id.startsWith(RESOLVED_MODULE_DEV_CSS_PREFIX)) { + const componentPath = getComponentFromVirtualModuleCssName(RESOLVED_MODULE_DEV_CSS_PREFIX, id); + const cssSet = routeCssMap.get(componentPath) || new Set(); + return { + code: `export const css = new Set(${JSON.stringify(Array.from(cssSet.values()))})`, + }; + } }, async transform(code, id) { @@ -58,34 +71,24 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption const info = this.getModuleInfo(id); if (!info) return; - if (id.startsWith('/') && currentRoute) { + if (id.startsWith('/')) { const mod = environment?.moduleGraph.getModuleById(id); - if (mod) { - if (isBuildableCSSRequest(id)) { - cssMap.add({ + if (mod && isBuildableCSSRequest(id)) { + // Find which routes use CSS that imports this file + for (const route of routesList.routes) { + if (!routeCssMap.has(route.component)) { + routeCssMap.set(route.component, new Set()); + } + const cssSet = routeCssMap.get(route.component)!; + cssSet.add({ content: code, id: wrapId(mod.id ?? mod.url), url: wrapId(mod.url), }); - environment?.moduleGraph.invalidateModule(mod); - return; } + return; } } - - const hasAddedCss = cssMap.size > 0; - const mod = environment?.moduleGraph.getModuleById(RESOLVED_MODULE_DEV_CSS); - if (mod) { - environment?.moduleGraph.invalidateModule(mod); - } - - if (id === RESOLVED_MODULE_DEV_CSS && hasAddedCss) { - const moduleCode = `export const css = new Set(${JSON.stringify(Array.from(cssMap.values()))})`; - // We need to clear the map, so the next time we render a new page - // we return the new CSS - cssMap.clear(); - return moduleCode; - } }, }; } diff --git a/packages/astro/src/vite-plugin-css/util.ts b/packages/astro/src/vite-plugin-css/util.ts new file mode 100644 index 000000000000..4ec240624cd7 --- /dev/null +++ b/packages/astro/src/vite-plugin-css/util.ts @@ -0,0 +1,11 @@ +import { getVirtualModulePageName } from '../vite-plugin-pages/util.js'; + +const MODULE_DEV_CSS_PREFIX = 'virtual:astro:dev-css:'; + +/** + * Get the virtual module name for a dev CSS import. + * Usage: `await loader.import(getDevCSSModuleName(routeData.component))` + */ +export function getDevCSSModuleName(componentPath: string): string { + return getVirtualModulePageName(MODULE_DEV_CSS_PREFIX, componentPath); +} diff --git a/packages/astro/test/ssr-script.test.js b/packages/astro/test/ssr-script.test.js index 2ccf86492a0b..5104c6434ee6 100644 --- a/packages/astro/test/ssr-script.test.js +++ b/packages/astro/test/ssr-script.test.js @@ -138,14 +138,20 @@ describe('External scripts in SSR', () => { vite: { build: { assetsInlineLimit: 0, - rollupOptions: { - output: { - entryFileNames: 'assets/entry.[hash].mjs', - chunkFileNames: 'assets/chunks/chunk.[hash].mjs', - assetFileNames: 'assets/asset.[hash][extname]', - }, - }, }, + environments: { + client: { + build: { + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + } + } + } + } + } }, }); await fixture.build(); @@ -166,14 +172,20 @@ describe('External scripts in SSR', () => { vite: { build: { assetsInlineLimit: 0, - rollupOptions: { - output: { - entryFileNames: 'assets/entry.[hash].mjs', - chunkFileNames: 'assets/chunks/chunk.[hash].mjs', - assetFileNames: 'assets/asset.[hash][extname]', - }, - }, }, + environments: { + client: { + build: { + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + } + } + } + } + } }, base: '/hello', }); @@ -198,14 +210,20 @@ describe('External scripts in SSR', () => { vite: { build: { assetsInlineLimit: 0, - rollupOptions: { - output: { - entryFileNames: 'assets/entry.[hash].mjs', - chunkFileNames: 'assets/chunks/chunk.[hash].mjs', - assetFileNames: 'assets/asset.[hash][extname]', - }, - }, }, + environments: { + client: { + build: { + rollupOptions: { + output: { + entryFileNames: 'assets/entry.[hash].mjs', + chunkFileNames: 'assets/chunks/chunk.[hash].mjs', + assetFileNames: 'assets/asset.[hash][extname]', + } + } + } + } + } }, }); await fixture.build();