From eed25301791deacb0fad9dd0660045b7f3bbf17f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 14:56:42 -0500 Subject: [PATCH 01/25] Fixes CSS import order and cloudflare --- packages/astro/dev-only.d.ts | 5 + packages/astro/src/core/app/dev/pipeline.ts | 15 ++- packages/astro/src/manifest/serialized.ts | 4 +- packages/astro/src/vite-plugin-css/index.ts | 132 +++++++++++++++---- packages/astro/src/vite-plugin-pages/util.ts | 9 ++ 5 files changed, 139 insertions(+), 26 deletions(-) diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index d328c03e5344..493e680685e5 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -79,3 +79,8 @@ declare module 'virtual:astro:dev-css' { import type { ImportedDevStyles } from './src/types/astro.js'; export const css: Set; } + +declare module 'virtual:astro:dev-css-all' { + import type { ImportedDevStyles } from './src/types/astro.js'; + export const devCSSMap: Map Promise<{ css: Set }>>; +} diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index be03bf00d47d..c04a71954228 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -1,4 +1,4 @@ -import type { ComponentInstance } from '../../../types/astro.js'; +import type { ComponentInstance, ImportedDevStyle } from '../../../types/astro.js'; import type { DevToolbarMetadata, RewritePayload, @@ -93,7 +93,18 @@ export class DevPipeline extends Pipeline { scripts.add({ props: {}, children }); } - const { css } = await import(getDevCSSModuleName(routeData.component)); + const { devCSSMap } = await import('virtual:astro:dev-css-all'); + + let css: Set = new Set(); + try { + const importer = devCSSMap.get(routeData.component); + if(importer) { + const cssModule = await importer(); + css = cssModule.css; + } + } catch { + // An unknown route, ignore + } // 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/manifest/serialized.ts b/packages/astro/src/manifest/serialized.ts index 12e914df337f..2a32b587620c 100644 --- a/packages/astro/src/manifest/serialized.ts +++ b/packages/astro/src/manifest/serialized.ts @@ -68,6 +68,8 @@ export function serializedManifestPlugin({ import { pageMap } from '${VIRTUAL_PAGES_MODULE_ID}'; const _manifest = _deserializeManifest((${manifestData})); + const manifestRoutes = import.meta.env.DEV ? routes : _manifest.routes; + const manifest = Object.assign(_manifest, { renderers, actions: () => import('${ACTIONS_ENTRYPOINT_VIRTUAL_MODULE_ID}'), @@ -76,7 +78,7 @@ export function serializedManifestPlugin({ serverIslandMappings: () => import('${SERVER_ISLAND_MANIFEST}'), // _manifest.routes contains enriched route info with scripts and styles, // while routes only has raw route data. Fallback to routes if _manifest.routes is not available. - routes: _manifest.routes ?? routes, + routes: manifestRoutes, pageMap, }); export { manifest }; diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index 5c22a3c0b162..5646d2be8aec 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -1,7 +1,10 @@ import type { Plugin, RunnableDevEnvironment } from 'vite'; import { wrapId } from '../core/util.js'; import type { ImportedDevStyle, RoutesList } from '../types/astro.js'; +import type * as vite from 'vite'; import { isBuildableCSSRequest } from '../vite-plugin-astro-server/util.js'; +import { getVirtualModulePageNameForComponent } from '../vite-plugin-pages/util.js'; +import { getDevCSSModuleName } from './util.js'; interface AstroVitePluginOptions { routesList: RoutesList; @@ -12,6 +15,8 @@ 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 MODULE_DEV_CSS_ALL = 'virtual:astro:dev-css-all'; +const RESOLVED_MODULE_DEV_CSS_ALL = '\0' + MODULE_DEV_CSS_ALL; const ASTRO_CSS_EXTENSION_POST_PATTERN = '@_@'; /** @@ -24,6 +29,40 @@ function getComponentFromVirtualModuleCssName(virtualModulePrefix: string, id: s .replace(new RegExp(ASTRO_CSS_EXTENSION_POST_PATTERN, 'g'), '.'); } +/** + * Walk down the dependency tree to collect CSS with depth/order. + * Performs depth-first traversal to ensure correct CSS ordering based on import order. + */ +function* collectCSSWithOrder( + id: string, + mod: vite.EnvironmentModuleNode, + seen = new Set(), +): Generator { + seen.add(id); + + // 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); + + // Check if this module is CSS and should be collected + if (isBuildableCSSRequest(id)) { + yield { + id, + idKey: id, + content: '', + url: wrapId(id), + }; + return; + } + + // Recursively walk imported modules (depth-first) + for (let idx = 0; idx < imported.length; idx++) { + const imp = imported[idx]; + if (imp.id && !seen.has(imp?.id)) { + yield* collectCSSWithOrder(imp.id, imp, seen); + } + } +} + /** * This plugin tracks the CSS that should be applied by route. * @@ -32,10 +71,12 @@ function getComponentFromVirtualModuleCssName(virtualModulePrefix: string, id: s * * @param routesList */ -export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOptions): Plugin { +export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOptions): Plugin[] { let environment: undefined | RunnableDevEnvironment = undefined; - let routeCssMap = new Map>(); - return { + // Cache CSS content by module ID to avoid re-reading + const cssContentCache = new Map(); + + return [{ name: MODULE_DEV_CSS, async configureServer(server) { @@ -62,9 +103,47 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption RESOLVED_MODULE_DEV_CSS_PREFIX, id, ); - const cssSet = routeCssMap.get(componentPath) || new Set(); + + // Collect CSS by walking the dependency tree from the component + const cssWithOrder: Map = new Map(); + + // The virtual module name for this page, like virtual:astro:dev-css:index@_@astro + const componentPageId = getVirtualModulePageNameForComponent(componentPath); + + // Ensure the page module is loaded. This will populate the graph and allow us to walk through. + await environment?.runner?.import(componentPageId); + const resolved = await environment?.pluginContainer.resolveId(componentPageId); + + if(!resolved?.id) { + return { + code: 'export const css = new Set()' + }; + } + + // the vite.EnvironmentModuleNode has all of the info we need + const mod = environment?.moduleGraph.getModuleById(resolved.id); + + if(!mod) { + return { + code: 'export const css = new Set()' + }; + } + + // Walk through the graph depth-first + for (const collected of collectCSSWithOrder(componentPageId, mod!)) { + // Use the CSS file ID as the key to deduplicate while keeping best ordering + if (!cssWithOrder.has(collected.idKey)) { + // Look up actual content from cache if available + const content = cssContentCache.get(collected.id) || collected.content; + cssWithOrder.set(collected.idKey, { ...collected, content }); + } + } + + const cssArray = Array.from(cssWithOrder.values()); + // Remove the temporary fields added during collection + const cleanedCss = cssArray.map(({ content, id, url }) => ({ content, id, url })); return { - code: `export const css = new Set(${JSON.stringify(Array.from(cssSet.values()))})`, + code: `export const css = new Set(${JSON.stringify(cleanedCss)})`, }; } }, @@ -73,27 +152,34 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption if (command === 'build') { return; } - const info = this.getModuleInfo(id); - if (!info) return; - if (id.startsWith('/')) { + // Cache CSS content as we see it + if (isBuildableCSSRequest(id)) { const mod = environment?.moduleGraph.getModuleById(id); - 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), - }); - } - return; + if (mod) { + cssContentCache.set(id, code); } } }, - }; + }, { + name: MODULE_DEV_CSS_ALL, + resolveId(id) { + if(id === MODULE_DEV_CSS_ALL) { + return RESOLVED_MODULE_DEV_CSS_ALL + } + }, + load(id) { + if(id === RESOLVED_MODULE_DEV_CSS_ALL) { + // Creates a map of the component name to a function to import it + let code = `export const devCSSMap = new Map([`; + for(const route of routesList.routes) { + code += `\n\t[${JSON.stringify(route.component)}, () => import(${JSON.stringify(getDevCSSModuleName(route.component))})],` + } + code += ']);' + return { + code + }; + } + } + }]; } diff --git a/packages/astro/src/vite-plugin-pages/util.ts b/packages/astro/src/vite-plugin-pages/util.ts index 8767cfb7d9c4..6c2bd066ffc5 100644 --- a/packages/astro/src/vite-plugin-pages/util.ts +++ b/packages/astro/src/vite-plugin-pages/util.ts @@ -1,4 +1,5 @@ import { fileExtension } from '@astrojs/internal-helpers/path'; +import { VIRTUAL_PAGE_MODULE_ID } from './const.js'; // This is an arbitrary string that we use to replace the dot of the extension. const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; @@ -18,3 +19,11 @@ export function getVirtualModulePageName(virtualModulePrefix: string, path: stri : path) ); } + +export function getVirtualModulePageNameForComponent(component: string) { + const virtualModuleName = getVirtualModulePageName( + VIRTUAL_PAGE_MODULE_ID, + component, + ); + return virtualModuleName; +} From 91138fcd1696461817f15b23605a79c79c0a3d52 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 15:06:39 -0500 Subject: [PATCH 02/25] fix build --- packages/astro/src/core/app/dev/pipeline.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index c04a71954228..9ab2520cfe4f 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -5,7 +5,6 @@ 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'; From f6e4463eab575c971a333357e9c6892efee54ece Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 15:29:58 -0500 Subject: [PATCH 03/25] linting obo --- packages/astro/dev-only.d.ts | 2 +- packages/astro/src/core/app/dev/pipeline.ts | 2 +- packages/astro/src/vite-plugin-css/index.ts | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/astro/dev-only.d.ts b/packages/astro/dev-only.d.ts index 493e680685e5..0a39fe82960a 100644 --- a/packages/astro/dev-only.d.ts +++ b/packages/astro/dev-only.d.ts @@ -82,5 +82,5 @@ declare module 'virtual:astro:dev-css' { declare module 'virtual:astro:dev-css-all' { import type { ImportedDevStyles } from './src/types/astro.js'; - export const devCSSMap: Map Promise<{ css: Set }>>; + export const devCSSMap: Map Promise<{ css: Set }>>; } diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index 9ab2520cfe4f..958e1173929f 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -94,7 +94,7 @@ export class DevPipeline extends Pipeline { const { devCSSMap } = await import('virtual:astro:dev-css-all'); - let css: Set = new Set(); + let css = new Set(); try { const importer = devCSSMap.get(routeData.component); if(importer) { diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index 5646d2be8aec..42539314a6d3 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -55,8 +55,7 @@ function* collectCSSWithOrder( } // Recursively walk imported modules (depth-first) - for (let idx = 0; idx < imported.length; idx++) { - const imp = imported[idx]; + for (const imp of imported) { if (imp.id && !seen.has(imp?.id)) { yield* collectCSSWithOrder(imp.id, imp, seen); } @@ -105,7 +104,7 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption ); // Collect CSS by walking the dependency tree from the component - const cssWithOrder: Map = new Map(); + const cssWithOrder = new Map(); // The virtual module name for this page, like virtual:astro:dev-css:index@_@astro const componentPageId = getVirtualModulePageNameForComponent(componentPath); @@ -141,7 +140,7 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption const cssArray = Array.from(cssWithOrder.values()); // Remove the temporary fields added during collection - const cleanedCss = cssArray.map(({ content, id, url }) => ({ content, id, url })); + const cleanedCss = cssArray.map(({ content, id: cssId, url }) => ({ content, id: cssId, url })); return { code: `export const css = new Set(${JSON.stringify(cleanedCss)})`, }; From 4578d2ddfe06717095b41db1259fec03d966da20 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 16:09:29 -0500 Subject: [PATCH 04/25] Remove the unused ssr-manifest module --- .changeset/wet-lines-wear.md | 7 +++ packages/astro/src/core/create-vite.ts | 2 - .../src/vite-plugin-ssr-manifest/index.ts | 26 ---------- .../fixtures/ssr-manifest/astro.config.mjs | 22 --------- .../fixtures/ssr-manifest/entrypoint-test.js | 9 ---- .../test/fixtures/ssr-manifest/package.json | 8 ---- .../ssr-manifest/src/pages/manifest.json.js | 5 -- packages/astro/test/ssr-manifest.test.js | 47 ------------------- 8 files changed, 7 insertions(+), 119 deletions(-) create mode 100644 .changeset/wet-lines-wear.md delete mode 100644 packages/astro/src/vite-plugin-ssr-manifest/index.ts delete mode 100644 packages/astro/test/fixtures/ssr-manifest/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/ssr-manifest/entrypoint-test.js delete mode 100644 packages/astro/test/fixtures/ssr-manifest/package.json delete mode 100644 packages/astro/test/fixtures/ssr-manifest/src/pages/manifest.json.js delete mode 100644 packages/astro/test/ssr-manifest.test.js diff --git a/.changeset/wet-lines-wear.md b/.changeset/wet-lines-wear.md new file mode 100644 index 000000000000..a962ea1264b6 --- /dev/null +++ b/.changeset/wet-lines-wear.md @@ -0,0 +1,7 @@ +--- +'astro': major +--- + +Remove astro:ssr-manifest module + +The `astro:ssr-manifest` module was created to allow integrations to access the manifest. It's no longer used by any integrations and is not used by Astro internally, so has been removed. diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index c9789d302186..5a8b7901f5f1 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -43,7 +43,6 @@ import vitePluginRenderers from '../vite-plugin-renderers/index.js'; import astroPluginRoutes from '../vite-plugin-routes/index.js'; import astroScriptsPlugin from '../vite-plugin-scripts/index.js'; import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js'; -import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js'; import type { Logger } from './logger/core.js'; import { createViteLogger } from './logger/vite.js'; import { vitePluginMiddleware } from './middleware/vite-plugin.js'; @@ -178,7 +177,6 @@ export async function createVite( astroContentImportPlugin({ fs, settings, logger }), astroContentAssetPropagationPlugin({ settings }), vitePluginMiddleware({ settings }), - vitePluginSSRManifest(), astroAssetsPlugin({ fs, settings, sync, logger }), astroPrefetch({ settings }), astroTransitions({ settings }), diff --git a/packages/astro/src/vite-plugin-ssr-manifest/index.ts b/packages/astro/src/vite-plugin-ssr-manifest/index.ts deleted file mode 100644 index 1828a95946f7..000000000000 --- a/packages/astro/src/vite-plugin-ssr-manifest/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Plugin as VitePlugin } from 'vite'; - -const manifestVirtualModuleId = 'astro:ssr-manifest'; -const resolvedManifestVirtualModuleId = '\0' + manifestVirtualModuleId; - -export function vitePluginSSRManifest(): VitePlugin { - return { - name: '@astrojs/vite-plugin-astro-ssr-manifest', - enforce: 'post', - resolveId(id) { - if (id === manifestVirtualModuleId) { - return resolvedManifestVirtualModuleId; - } - }, - load(id) { - if (id === resolvedManifestVirtualModuleId) { - return { - code: `export let manifest = {}; -export function _privateSetManifestDontUseThis(ssrManifest) { - manifest = ssrManifest; -}`, - }; - } - }, - }; -} diff --git a/packages/astro/test/fixtures/ssr-manifest/astro.config.mjs b/packages/astro/test/fixtures/ssr-manifest/astro.config.mjs deleted file mode 100644 index 6a605bd77b87..000000000000 --- a/packages/astro/test/fixtures/ssr-manifest/astro.config.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from 'astro/config'; -import testAdapter from '../../test-adapter.js'; -import { fileURLToPath } from 'url'; - -export default defineConfig({ - output: 'server', - adapter: testAdapter(), - integrations: [ - { - name: 'test', - hooks: { - 'astro:config:setup'({ injectRoute }) { - injectRoute({ - entrypoint: fileURLToPath(new URL('./entrypoint-test.js', import.meta.url)), - pattern: '[...slug]', - prerender: true, - }); - }, - }, - }, - ], -}); diff --git a/packages/astro/test/fixtures/ssr-manifest/entrypoint-test.js b/packages/astro/test/fixtures/ssr-manifest/entrypoint-test.js deleted file mode 100644 index 457e89bf7df2..000000000000 --- a/packages/astro/test/fixtures/ssr-manifest/entrypoint-test.js +++ /dev/null @@ -1,9 +0,0 @@ -export const prerender = true; - -export function getStaticPaths() { - return [{ params: { slug: 'test' } }]; -} - -export function GET() { - return new Response('OK — test'); -} diff --git a/packages/astro/test/fixtures/ssr-manifest/package.json b/packages/astro/test/fixtures/ssr-manifest/package.json deleted file mode 100644 index 95e82614f10c..000000000000 --- a/packages/astro/test/fixtures/ssr-manifest/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/ssr-manifest", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/ssr-manifest/src/pages/manifest.json.js b/packages/astro/test/fixtures/ssr-manifest/src/pages/manifest.json.js deleted file mode 100644 index 41ae39f405da..000000000000 --- a/packages/astro/test/fixtures/ssr-manifest/src/pages/manifest.json.js +++ /dev/null @@ -1,5 +0,0 @@ -import { manifest } from 'astro:ssr-manifest'; - -export function GET() { - return Response.json(manifest); -} diff --git a/packages/astro/test/ssr-manifest.test.js b/packages/astro/test/ssr-manifest.test.js deleted file mode 100644 index 254ea304cd12..000000000000 --- a/packages/astro/test/ssr-manifest.test.js +++ /dev/null @@ -1,47 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; - -describe('astro:ssr-manifest', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-manifest/', - }); - await fixture.build(); - }); - - it('works', async () => { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com/manifest.json'); - const response = await app.render(request); - const manifest = await response.json(); - assert.equal(typeof manifest, 'object'); - assert.equal(manifest.adapterName, 'my-ssr-adapter'); - }); - - it('includes compressHTML', async () => { - const app = await fixture.loadTestAdapterApp(); - // NOTE: `app.manifest` is actually a private property - assert.equal(app.manifest.compressHTML, true); - }); - - it('includes correct routes', async () => { - const app = await fixture.loadTestAdapterApp(); - // NOTE: `app.manifest` is actually a private property - - const manifestJsonEndpoint = app.manifest.routes.find( - (route) => route.routeData.route === '/manifest.json', - ); - assert.ok(manifestJsonEndpoint); - assert.equal(manifestJsonEndpoint.routeData.prerender, false); - - // There should be no route for prerendered injected routes - const injectedEndpoint = app.manifest.routes.find( - (route) => route.routeData.route === '/[...slug]', - ); - assert.equal(injectedEndpoint, undefined); - }); -}); From 9c3aa380d6ec42fe7b76a867f8c5003f4eef6b1b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 16:15:49 -0500 Subject: [PATCH 05/25] fix config-vite tests --- packages/astro/test/config-vite.test.js | 2 +- .../test/fixtures/config-vite/astro.config.mjs | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/astro/test/config-vite.test.js b/packages/astro/test/config-vite.test.js index f14712b2d724..ba0ff295c05a 100644 --- a/packages/astro/test/config-vite.test.js +++ b/packages/astro/test/config-vite.test.js @@ -21,7 +21,7 @@ describe('Vite Config', async () => { it('Allows overriding bundle naming options in the build', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - assert.match($('link').attr('href'), /\/assets\/testing-[a-z\d]+\.css/); + assert.match($('link').attr('href'), /\/assets\/testing-(.)+\.css/); }); }); diff --git a/packages/astro/test/fixtures/config-vite/astro.config.mjs b/packages/astro/test/fixtures/config-vite/astro.config.mjs index 47a05dd8db63..b254bb1f195e 100644 --- a/packages/astro/test/fixtures/config-vite/astro.config.mjs +++ b/packages/astro/test/fixtures/config-vite/astro.config.mjs @@ -2,13 +2,17 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ vite: { - build: { - rollupOptions: { - output: { - chunkFileNames: 'assets/testing-[name].mjs', - assetFileNames: 'assets/testing-[name].[ext]' + environments: { + prerender: { + build: { + rollupOptions: { + output: { + chunkFileNames: 'assets/testing-[name].mjs', + assetFileNames: 'assets/testing-[name].[ext]' + } } } + } } } }) From 5fd8edf9cf9853d21054c52329341ab5046cd0cc Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 17:40:26 -0500 Subject: [PATCH 06/25] fix content-collections.test.js --- packages/astro/src/core/build/internal.ts | 71 -------------- packages/astro/src/core/build/pipeline.ts | 96 +------------------ .../src/core/build/plugins/plugin-manifest.ts | 3 +- packages/astro/src/core/build/runtime.ts | 73 ++++++++++++++ 4 files changed, 80 insertions(+), 163 deletions(-) create mode 100644 packages/astro/src/core/build/runtime.ts diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts index c973b6b31599..413224aded18 100644 --- a/packages/astro/src/core/build/internal.ts +++ b/packages/astro/src/core/build/internal.ts @@ -1,7 +1,6 @@ import type { SSRResult } from '../../types/public/internal.js'; import { prependForwardSlash, removeFileExtension } from '../path.js'; import { viteID } from '../util.js'; -import { makePageDataKey } from './plugins/util.js'; import type { PageBuildData, StylesheetAsset, ViteID } from './types.js'; export interface BuildInternals { @@ -203,24 +202,6 @@ export function* getPageDatasByClientOnlyID( } } -/** - * From its route and component, get the page data from the build internals. - * @param internals Build Internals with all the pages - * @param route The route of the page, used to identify the page - * @param component The component of the page, used to identify the page - */ -export function getPageData( - internals: BuildInternals, - route: string, - component: string, -): PageBuildData | undefined { - let pageData = internals.pagesByKeys.get(makePageDataKey(route, component)); - if (pageData) { - return pageData; - } - return undefined; -} - export function getPageDataByViteID( internals: BuildInternals, viteid: ViteID, @@ -239,55 +220,3 @@ export function hasPrerenderedPages(internals: BuildInternals) { } return false; } - -interface OrderInfo { - depth: number; - order: number; -} - -/** - * Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents. - * A lower depth means it comes directly from the top-level page. - * Can be used to sort stylesheets so that shared rules come first - * and page-specific rules come after. - */ -export function cssOrder(a: OrderInfo, b: OrderInfo) { - let depthA = a.depth, - depthB = b.depth, - orderA = a.order, - orderB = b.order; - - if (orderA === -1 && orderB >= 0) { - return 1; - } else if (orderB === -1 && orderA >= 0) { - return -1; - } else if (orderA > orderB) { - return 1; - } else if (orderA < orderB) { - return -1; - } else { - if (depthA === -1) { - return -1; - } else if (depthB === -1) { - return 1; - } else { - return depthA > depthB ? -1 : 1; - } - } -} - -export function mergeInlineCss( - acc: Array, - current: StylesheetAsset, -): Array { - const lastAdded = acc.at(acc.length - 1); - const lastWasInline = lastAdded?.type === 'inline'; - const currentIsInline = current?.type === 'inline'; - if (lastWasInline && currentIsInline) { - const merged = { type: 'inline' as const, content: lastAdded.content + current.content }; - acc[acc.length - 1] = merged; - return acc; - } - acc.push(current); - return acc; -} diff --git a/packages/astro/src/core/build/pipeline.ts b/packages/astro/src/core/build/pipeline.ts index a87bfece868e..e6e12e41e126 100644 --- a/packages/astro/src/core/build/pipeline.ts +++ b/packages/astro/src/core/build/pipeline.ts @@ -2,7 +2,6 @@ import type { ComponentInstance } from '../../types/astro.js'; import type { RewritePayload } from '../../types/public/common.js'; import type { RouteData, SSRElement, SSRResult } from '../../types/public/internal.js'; import { - VIRTUAL_PAGE_MODULE_ID, VIRTUAL_PAGE_RESOLVED_MODULE_ID, } from '../../vite-plugin-pages/const.js'; import { getVirtualModulePageName } from '../../vite-plugin-pages/util.js'; @@ -10,14 +9,14 @@ import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-sc import { createConsoleLogger } from '../app/index.js'; import type { SSRManifest } from '../app/types.js'; import type { TryRewriteResult } from '../base-pipeline.js'; -import { RedirectSinglePageBuiltModule } from '../redirects/index.js'; -import { Pipeline } from '../render/index.js'; +import { RedirectSinglePageBuiltModule } from '../redirects/component.js'; +import { Pipeline } from '../base-pipeline.js'; import { createAssetLink, createStylesheetElementSet } from '../render/ssr-element.js'; import { createDefaultRoutes } from '../routing/default.js'; import { getFallbackRoute, routeIsFallback, routeIsRedirect } from '../routing/helpers.js'; import { findRouteToRewrite } from '../routing/rewrite.js'; -import { getOutDirWithinCwd } from './common.js'; -import { type BuildInternals, cssOrder, getPageData, mergeInlineCss } from './internal.js'; +import type { BuildInternals } from './internal.js'; +import { cssOrder, mergeInlineCss, getPageData } from './runtime.js'; import type { SinglePageBuiltModule, StaticBuildOptions } from './types.js'; /** @@ -31,10 +30,6 @@ export class BuildPipeline extends Pipeline { return 'BuildPipeline'; } - #componentsInterner: WeakMap = new WeakMap< - RouteData, - SinglePageBuiltModule - >(); /** * This cache is needed to map a single `RouteData` to its file path. * @private @@ -62,13 +57,6 @@ export class BuildPipeline extends Pipeline { return this.internals; } - get outFolder() { - const settings = this.getSettings(); - return settings.buildOutput === 'server' - ? settings.config.build.server - : getOutDirWithinCwd(settings.config.outDir); - } - private constructor( readonly manifest: SSRManifest, readonly defaultRoutes = createDefaultRoutes(manifest), @@ -151,6 +139,7 @@ export class BuildPipeline extends Pipeline { }); } } + return { scripts, styles, links }; } @@ -265,81 +254,6 @@ export class BuildPipeline extends Pipeline { const componentInstance = await this.getComponentByRoute(routeData); return { routeData, componentInstance, newUrl, pathname }; } - - async retrieveSsrEntry(route: RouteData, filePath: string): Promise { - if (this.#componentsInterner.has(route)) { - // SAFETY: it is checked inside the if - return this.#componentsInterner.get(route)!; - } - - let entry; - if (routeIsRedirect(route)) { - entry = await this.#getEntryForRedirectRoute(route, this.outFolder); - } else if (routeIsFallback(route)) { - entry = await this.#getEntryForFallbackRoute(route, this.outFolder); - } else { - const ssrEntryURLPage = createEntryURL(filePath, this.outFolder); - entry = await import(ssrEntryURLPage.toString()); - } - this.#componentsInterner.set(route, entry); - return entry; - } - - async #getEntryForFallbackRoute( - route: RouteData, - outFolder: URL, - ): Promise { - const internals = this.getInternals(); - if (route.type !== 'fallback') { - throw new Error(`Expected a redirect route.`); - } - - // Retrieve the route where we should fall back - let fallbackRoute = getFallbackRoute(route, this.manifest.routes); - - if (fallbackRoute) { - const filePath = getEntryFilePath(internals, fallbackRoute); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; - } - - async #getEntryForRedirectRoute( - route: RouteData, - outFolder: URL, - ): Promise { - const internals = this.getInternals(); - if (route.type !== 'redirect') { - throw new Error(`Expected a redirect route.`); - } - if (route.redirectRoute) { - const filePath = getEntryFilePath(internals, route.redirectRoute); - if (filePath) { - const url = createEntryURL(filePath, outFolder); - const ssrEntryPage: SinglePageBuiltModule = await import(url.toString()); - return ssrEntryPage; - } - } - - return RedirectSinglePageBuiltModule; - } -} - -function createEntryURL(filePath: string, outFolder: URL) { - return new URL('./' + filePath + `?time=${Date.now()}`, outFolder); -} - -/** - * For a given pageData, returns the entry file path—aka a resolved virtual module in our internals' specifiers. - */ -function getEntryFilePath(internals: BuildInternals, pageData: RouteData) { - const id = '\x00' + getVirtualModulePageName(VIRTUAL_PAGE_MODULE_ID, pageData.component); - return internals.entrySpecifierToBundleMap.get(id); } function i18nHasFallback(manifest: SSRManifest): boolean { diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index c443370ea948..b7137eb0060a 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -31,7 +31,8 @@ import { encodeKey } from '../../encryption.js'; import { fileExtension, joinPaths, prependForwardSlash } from '../../path.js'; import { DEFAULT_COMPONENTS } from '../../routing/default.js'; import { getOutFile, getOutFolder } from '../common.js'; -import { type BuildInternals, cssOrder, mergeInlineCss } from '../internal.js'; +import type { BuildInternals } from '../internal.js'; +import { cssOrder, mergeInlineCss } from '../runtime.js'; import type { StaticBuildOptions } from '../types.js'; import { makePageDataKey } from './util.js'; diff --git a/packages/astro/src/core/build/runtime.ts b/packages/astro/src/core/build/runtime.ts new file mode 100644 index 000000000000..9cf5243d2809 --- /dev/null +++ b/packages/astro/src/core/build/runtime.ts @@ -0,0 +1,73 @@ +import type { BuildInternals } from './internal.js'; +import type { PageBuildData, StylesheetAsset } from './types.js'; +import { makePageDataKey } from './plugins/util.js'; + +/** + * From its route and component, get the page data from the build internals. + * @param internals Build Internals with all the pages + * @param route The route of the page, used to identify the page + * @param component The component of the page, used to identify the page + */ +export function getPageData( + internals: BuildInternals, + route: string, + component: string, +): PageBuildData | undefined { + let pageData = internals.pagesByKeys.get(makePageDataKey(route, component)); + if (pageData) { + return pageData; + } + return undefined; +} + +interface OrderInfo { + depth: number; + order: number; +} + +/** + * Sort a page's CSS by depth. A higher depth means that the CSS comes from shared subcomponents. + * A lower depth means it comes directly from the top-level page. + * Can be used to sort stylesheets so that shared rules come first + * and page-specific rules come after. + */ +export function cssOrder(a: OrderInfo, b: OrderInfo) { + let depthA = a.depth, + depthB = b.depth, + orderA = a.order, + orderB = b.order; + + if (orderA === -1 && orderB >= 0) { + return 1; + } else if (orderB === -1 && orderA >= 0) { + return -1; + } else if (orderA > orderB) { + return 1; + } else if (orderA < orderB) { + return -1; + } else { + if (depthA === -1) { + return -1; + } else if (depthB === -1) { + return 1; + } else { + return depthA > depthB ? -1 : 1; + } + } +} + +export function mergeInlineCss( + acc: Array, + current: StylesheetAsset, +): Array { + const lastAdded = acc.at(acc.length - 1); + const lastWasInline = lastAdded?.type === 'inline'; + const currentIsInline = current?.type === 'inline'; + if (lastWasInline && currentIsInline) { + const merged = { type: 'inline' as const, content: lastAdded.content + current.content }; + acc[acc.length - 1] = merged; + return acc; + } + acc.push(current); + return acc; +} From 4509536f3967f402c908a13f49d1f3dc74fe2ab0 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Fri, 21 Nov 2025 17:41:51 -0500 Subject: [PATCH 07/25] fix entry-file-names.test.js --- .../fixtures/entry-file-names/astro.config.mjs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/astro/test/fixtures/entry-file-names/astro.config.mjs b/packages/astro/test/fixtures/entry-file-names/astro.config.mjs index 6855c058940b..a5e09a09519c 100644 --- a/packages/astro/test/fixtures/entry-file-names/astro.config.mjs +++ b/packages/astro/test/fixtures/entry-file-names/astro.config.mjs @@ -5,12 +5,16 @@ import { defineConfig } from 'astro/config'; export default defineConfig({ integrations: [preact()], vite: { - build: { - rollupOptions: { - output: { - entryFileNames: `assets/js/[name].js`, - }, - }, - }, + environments: { + client: { + build: { + rollupOptions: { + output: { + entryFileNames: `assets/js/[name].js`, + }, + }, + }, + }, + }, }, }); From 11ab96eba352fea72caaaf7c00507ee09bf41953 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 08:26:29 -0500 Subject: [PATCH 08/25] Fix redirects-i18n test --- packages/astro/test/fixtures/redirects-i18n/astro.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/test/fixtures/redirects-i18n/astro.config.mjs b/packages/astro/test/fixtures/redirects-i18n/astro.config.mjs index 8686edd9d8a0..d8c4697d70f7 100644 --- a/packages/astro/test/fixtures/redirects-i18n/astro.config.mjs +++ b/packages/astro/test/fixtures/redirects-i18n/astro.config.mjs @@ -6,6 +6,7 @@ export default defineConfig({ locales: ['de', 'en'], routing: { prefixDefaultLocale: true, + redirectToDefaultLocale: true, }, fallback: { en: 'de', From 15fc25ee5d0b530372969cb5fb0bd04437e4bb16 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 09:16:17 -0500 Subject: [PATCH 09/25] Fix server islands tests If the module with the server island mapping is in a chunk, need to use a parent-relative import statement to reach the server island module itself. --- packages/astro/src/core/server-islands/endpoint.ts | 2 +- .../core/server-islands/vite-plugin-server-islands.ts | 10 ++++++++-- packages/astro/test/server-islands.test.js | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index ed0788868b2d..b66ceee94ea5 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -127,7 +127,7 @@ export function createEndpoint(manifest: SSRManifest) { const key = await manifest.key; const encryptedProps = data.encryptedProps; -let props = {}; + let props = {}; if (encryptedProps !== '') { try { diff --git a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts index 03a6557124ac..519fb0453e8d 100644 --- a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts +++ b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts @@ -117,7 +117,7 @@ export function vitePluginServerIslands({ settings }: AstroPluginOptions): ViteP } }, - renderChunk(code) { + renderChunk(code, chunk) { if (code.includes(serverIslandPlaceholderMap)) { if (referenceIdMap.size === 0) { // If there's no reference, we can fast-path to an empty map replacement @@ -129,12 +129,18 @@ export function vitePluginServerIslands({ settings }: AstroPluginOptions): ViteP map: null, }; } + // The server island modules are in chunks/ + // This checks if this module is also in chunks/ and if so + // make the import like import('../chunks/name.mjs') + // TODO we could possibly refactor this to not need to emit separate chunks. + const isRelativeChunk = !chunk.isEntry; + const dots = isRelativeChunk ? '..' : '.'; let mapSource = 'new Map(['; let nameMapSource = 'new Map('; for (let [resolvedPath, referenceId] of referenceIdMap) { const fileName = this.getFileName(referenceId); const islandName = serverIslandNameMap.get(resolvedPath)!; - mapSource += `\n\t['${islandName}', () => import('./${fileName}')],`; + mapSource += `\n\t['${islandName}', () => import('${dots}/${fileName}')],`; } nameMapSource += `${JSON.stringify(Array.from(serverIslandNameMap.entries()), null, 2)}`; mapSource += '\n])'; diff --git a/packages/astro/test/server-islands.test.js b/packages/astro/test/server-islands.test.js index d26890ba3fda..3978c10259ee 100644 --- a/packages/astro/test/server-islands.test.js +++ b/packages/astro/test/server-islands.test.js @@ -59,6 +59,8 @@ describe('SSR dev', () => { }); it('island is not indexed', async () => { + // Page has to be fetched in order for island modules to be loaded + await fixture.fetch('/'); const res = await fixture.fetch('/_server-islands/Island', { method: 'POST', body: JSON.stringify({ From 8897c3edb008b6deda5a8e5c909962ff25cad3bb Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 10:43:43 -0500 Subject: [PATCH 10/25] Prevent CSS from being duplicated between SSR and prerender --- .../astro/src/core/build/plugins/plugin-css.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index b92b0944c90f..56b274d79cf2 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -41,6 +41,8 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const pagesToCss: Record> = {}; // Map of module Ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) to its imported CSS const moduleIdToPropagatedCss: Record> = {}; + // Keep track of CSS that has been bundled to avoid duplication between ssr and prerender. + const cssModulesInBundles = new Set(); const cssBuildPlugin: VitePlugin = { name: 'astro:rollup-plugin-build-css', @@ -49,6 +51,21 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { return environment.name === 'client' || environment.name === 'ssr' || environment.name === 'prerender'; }, + transform(_code, id) { + if(isCSSRequest(id)) { + // In prerender, don't rebundle CSS that was already bundled in SSR. + // Return an empty string here to prevent it. + if(this.environment.name === 'prerender') { + if(cssModulesInBundles.has(id)) { + return { + code: '' + } + } + } + cssModulesInBundles.add(id); + } + }, + async generateBundle(_outputOptions, bundle) { // Collect CSS modules that were bundled during SSR build for deduplication in client build if (this.environment?.name === 'ssr' || this.environment?.name === 'prerender') { From de91192670ca27a41dc3bea3d322e9ce03b7f742 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 10:49:29 -0500 Subject: [PATCH 11/25] Prevent ?raw CSS imports from being added as styles in dev --- packages/astro/src/vite-plugin-css/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index 42539314a6d3..5b3bfdf38a70 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -53,6 +53,10 @@ function* collectCSSWithOrder( }; return; } + // ?raw imports the underlying css but is handled as a string in the JS. + else if(id.endsWith('?raw')) { + return; + } // Recursively walk imported modules (depth-first) for (const imp of imported) { From d5b6a76ce8e3e300b56b175bf69ad5cb53783d4b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 10:57:04 -0500 Subject: [PATCH 12/25] Fixes vitest.test.js Properly handle isDev in the virtual module --- packages/astro/src/manifest/serialized.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/manifest/serialized.ts b/packages/astro/src/manifest/serialized.ts index 2a32b587620c..2eede8e3077e 100644 --- a/packages/astro/src/manifest/serialized.ts +++ b/packages/astro/src/manifest/serialized.ts @@ -68,7 +68,12 @@ export function serializedManifestPlugin({ import { pageMap } from '${VIRTUAL_PAGES_MODULE_ID}'; const _manifest = _deserializeManifest((${manifestData})); - const manifestRoutes = import.meta.env.DEV ? routes : _manifest.routes; + + // _manifest.routes contains enriched route info with scripts and styles, + // TODO port this info over to virtual:astro:routes to prevent the need to + // have this duplication + const isDev = ${JSON.stringify(command === 'dev')}; + const manifestRoutes = isDev ? routes : _manifest.routes; const manifest = Object.assign(_manifest, { renderers, @@ -76,8 +81,6 @@ export function serializedManifestPlugin({ middleware: () => import('${MIDDLEWARE_MODULE_ID}'), sessionDriver: () => import('${VIRTUAL_SESSION_DRIVER_ID}'), serverIslandMappings: () => import('${SERVER_ISLAND_MANIFEST}'), - // _manifest.routes contains enriched route info with scripts and styles, - // while routes only has raw route data. Fallback to routes if _manifest.routes is not available. routes: manifestRoutes, pageMap, }); From 779445222b7023dbdf280d05ee15713a6d9645ab Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 13:55:21 -0500 Subject: [PATCH 13/25] When prerendering, properly handle trailing slash with base --- packages/astro/src/core/build/generate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index f2f2054b3d64..bc0662f44900 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -9,6 +9,7 @@ import { prepareAssetsGenerationEnv, } from '../../assets/build/generate.js'; import { + collapseDuplicateTrailingSlashes, isRelativePath, joinPaths, removeLeadingForwardSlash, @@ -470,7 +471,7 @@ function getUrlForPath( } let buildPathname: string; if (pathname === '/' || pathname === '') { - buildPathname = base; + buildPathname = collapseDuplicateTrailingSlashes(base + ending, trailingSlash !== 'never'); } else if (routeType === 'endpoint') { const buildPathRelative = removeLeadingForwardSlash(pathname); buildPathname = joinPaths(base, buildPathRelative); From 59cd5181321fd8b5bc9b12d0781803f43c42da68 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 14:18:16 -0500 Subject: [PATCH 14/25] Prevent empty CSS chunks from being written to disk --- packages/astro/src/core/build/plugins/plugin-css.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index 56b274d79cf2..b7c925d1885a 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -204,6 +204,13 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { ) return; + // Delete empty CSS chunks. In prerender these are likely duplicates + // from SSR. + if(stylesheet.source.length === 0) { + delete bundle[id]; + return; + } + const toBeInlined = inlineConfig === 'always' ? true From ce5ed6bffcfc1f2ac7e019cab6d50f0fb7a7fefc Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 14:31:50 -0500 Subject: [PATCH 15/25] Update core-image-unconventional-settings tests to use environments API --- ...core-image-unconventional-settings.test.js | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/astro/test/core-image-unconventional-settings.test.js b/packages/astro/test/core-image-unconventional-settings.test.js index b5d5fc6640de..f6fdda530de9 100644 --- a/packages/astro/test/core-image-unconventional-settings.test.js +++ b/packages/astro/test/core-image-unconventional-settings.test.js @@ -99,11 +99,18 @@ describe('astro:assets - Support unconventional build settings properly', () => it('supports custom vite.build.rollupOptions.output.assetFileNames', async () => { fixture = await loadFixture({ ...defaultSettings, + build: { + assets: 'images' + }, vite: { - build: { - rollupOptions: { - output: { - assetFileNames: 'images/hello_[name].[ext]', + environments: { + prerender: { + build: { + rollupOptions: { + output: { + assetFileNames: 'images/hello_[name].[ext]', + }, + }, }, }, }, @@ -125,11 +132,18 @@ describe('astro:assets - Support unconventional build settings properly', () => it('supports complex vite.build.rollupOptions.output.assetFileNames', async () => { fixture = await loadFixture({ ...defaultSettings, + build: { + assets: 'assets' + }, vite: { - build: { - rollupOptions: { - output: { - assetFileNames: 'assets/[hash]/[name][extname]', + environments: { + prerender: { + build: { + rollupOptions: { + output: { + assetFileNames: 'assets/[hash]/[name][extname]', + }, + }, }, }, }, @@ -153,15 +167,20 @@ describe('astro:assets - Support unconventional build settings properly', () => fixture = await loadFixture({ ...defaultSettings, vite: { - build: { - rollupOptions: { - output: { - assetFileNames: 'images/hello_[name].[ext]', + environments: { + prerender: { + build: { + rollupOptions: { + output: { + assetFileNames: 'images/hello_[name].[ext]', + }, + }, }, }, }, }, build: { + assets: 'images', assetsPrefix: 'https://cdn.example.com/', }, }); From ad982c6610e8ee6e53b7116d081d2f82c60de166 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 15:37:25 -0500 Subject: [PATCH 16/25] Prevent adding CSS to pages not belonged to --- packages/astro/src/core/build/plugins/plugin-css.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index b7c925d1885a..ad844ec98b45 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -131,6 +131,9 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { // For this CSS chunk, walk parents until you find a page. Add the CSS to that page. for (const id of Object.keys(chunk.modules)) { + // Only walk up for dependencies that are CSS + if(!isCSSRequest(id)) continue; + const parentModuleInfos = getParentExtendedModuleInfos(id, this, hasAssetPropagationFlag); for (const { info: pageInfo, depth, order } of parentModuleInfos) { if (hasAssetPropagationFlag(pageInfo.id)) { From 732cfaf7afe1f337daea057fbc432fef2aef6b0e Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 15:41:19 -0500 Subject: [PATCH 17/25] Update astro-global.test.js Prod now matches dev, we default to adding a trailing slash to the Astro.request.url. Improves an annoying dev/build different in Astro 5. --- packages/astro/test/astro-global.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js index c8f8e2292537..9ee3ab9ee9fb 100644 --- a/packages/astro/test/astro-global.test.js +++ b/packages/astro/test/astro-global.test.js @@ -78,10 +78,10 @@ describe('Astro Global', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - assert.equal($('#pathname').text(), '/blog'); + assert.equal($('#pathname').text(), '/blog/'); assert.equal($('#searchparams').text(), '{}'); - assert.equal($('#child-pathname').text(), '/blog'); - assert.equal($('#nested-child-pathname').text(), '/blog'); + assert.equal($('#child-pathname').text(), '/blog/'); + assert.equal($('#nested-child-pathname').text(), '/blog/'); }); it('Astro.site', async () => { From 1bedc3651d604266831ea1681d6a62157ca875f2 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 16:11:23 -0500 Subject: [PATCH 18/25] Prepend base to propagated styles Propagated styles get embedded in the JS and rendered at runtime if they are used on a page. These need to include the base, which this change makes happen. --- packages/astro/src/content/vite-plugin-content-assets.ts | 6 +++++- packages/astro/src/core/build/static-build.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 98ab04b98677..a969ec5d69f1 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -18,6 +18,9 @@ import { STYLES_PLACEHOLDER, } from './consts.js'; import { hasContentFlag } from './utils.js'; +import type { BuildOptions } from 'esbuild'; +import type { StaticBuildOptions } from '../core/build/types.js'; +import { joinPaths, prependForwardSlash, slash } from '@astrojs/internal-helpers/path'; export function astroContentAssetPropagationPlugin({ settings, @@ -194,6 +197,7 @@ async function getStylesForURL( * with actual styles from propagatedStylesMap. */ export async function contentAssetsBuildPostHook( + opts: StaticBuildOptions, internals: BuildInternals, { ssrOutputs, @@ -233,7 +237,7 @@ export async function contentAssetsBuildPostHook( // links. Refactor this away in the future. for (const value of entryCss) { if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(value.src); + if (value.type === 'external') entryLinks.add(prependForwardSlash(joinPaths(opts.settings.config.base, slash(value.src)))); } } } diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index 58368b038304..cd4b0401c9f6 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -364,7 +364,7 @@ async function runManifestInjection( mutate, }); - await contentAssetsBuildPostHook(internals, { + await contentAssetsBuildPostHook(opts, internals, { ssrOutputs, prerenderOutputs, mutate, From f2989dda7208ecd67b7f66d649d36b39a3e7c981 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sat, 22 Nov 2025 16:15:00 -0500 Subject: [PATCH 19/25] fix the build --- packages/astro/src/content/vite-plugin-content-assets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index a969ec5d69f1..0303d0510d67 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -18,7 +18,6 @@ import { STYLES_PLACEHOLDER, } from './consts.js'; import { hasContentFlag } from './utils.js'; -import type { BuildOptions } from 'esbuild'; import type { StaticBuildOptions } from '../core/build/types.js'; import { joinPaths, prependForwardSlash, slash } from '@astrojs/internal-helpers/path'; From c370419d83254ad90e523c26cdc015686b179a11 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sun, 23 Nov 2025 08:07:55 -0500 Subject: [PATCH 20/25] increase the test timeout --- scripts/cmd/test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/cmd/test.js b/scripts/cmd/test.js index 1dd3f124aa29..1e63c7482615 100644 --- a/scripts/cmd/test.js +++ b/scripts/cmd/test.js @@ -7,7 +7,8 @@ import { parseArgs } from 'node:util'; import { glob } from 'tinyglobby'; const isCI = !!process.env.CI; -const defaultTimeout = isCI ? 1400000 : 600000; +// 30 minutes in CI, 10 locally +const defaultTimeout = isCI ? 1860000 : 600000; export default async function test() { const args = parseArgs({ From 6beed57b4e04b4f78c05f5540ab48f60a0c72928 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sun, 23 Nov 2025 09:10:01 -0500 Subject: [PATCH 21/25] Use the href for saving unix-style paths for image metadata --- packages/astro/src/assets/utils/node.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/assets/utils/node.ts b/packages/astro/src/assets/utils/node.ts index fe44e37bbdd9..b56dc483296b 100644 --- a/packages/astro/src/assets/utils/node.ts +++ b/packages/astro/src/assets/utils/node.ts @@ -96,7 +96,8 @@ export async function emitImageMetadata( Object.defineProperty(emittedImage, 'fsPath', { enumerable: false, writable: false, - value: id, + // Use url.href here because referencedImages relies on unix-style paths + value: url.href, }); // Build From fdf0670c1e78f590b7539f5257f77433b98cc17f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Sun, 23 Nov 2025 10:02:03 -0500 Subject: [PATCH 22/25] use fileURLToNormalizedPath --- packages/astro/src/assets/utils/node.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/astro/src/assets/utils/node.ts b/packages/astro/src/assets/utils/node.ts index b56dc483296b..a95bf20daa2b 100644 --- a/packages/astro/src/assets/utils/node.ts +++ b/packages/astro/src/assets/utils/node.ts @@ -96,8 +96,7 @@ export async function emitImageMetadata( Object.defineProperty(emittedImage, 'fsPath', { enumerable: false, writable: false, - // Use url.href here because referencedImages relies on unix-style paths - value: url.href, + value: fileURLToNormalizedPath(url), }); // Build From eeb4ea9e7e71afa87fc88efb6565de07d4cef488 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 24 Nov 2025 08:38:38 -0500 Subject: [PATCH 23/25] PR comments --- packages/astro/src/core/app/dev/pipeline.ts | 14 ++++++-------- packages/astro/src/core/build/static-build.ts | 2 +- packages/astro/src/vite-plugin-css/index.ts | 2 ++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index 958e1173929f..c4ac7c7d51d2 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -94,15 +94,13 @@ export class DevPipeline extends Pipeline { const { devCSSMap } = await import('virtual:astro:dev-css-all'); + const importer = devCSSMap.get(routeData.component); let css = new Set(); - try { - const importer = devCSSMap.get(routeData.component); - if(importer) { - const cssModule = await importer(); - css = cssModule.css; - } - } catch { - // An unknown route, ignore + if(importer) { + const cssModule = await importer(); + css = cssModule.css; + } else { + this.logger.warn('assets', `Unable to find CSS for ${routeData.component}. This is likely a bug in Astro.`); } // Pass framework CSS in as style tags to be appended to the page. diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index cd4b0401c9f6..86488d440209 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -364,7 +364,7 @@ async function runManifestInjection( mutate, }); - await contentAssetsBuildPostHook(opts, internals, { + await contentAssetsBuildPostHook(opts.settings.config.base, internals, { ssrOutputs, prerenderOutputs, mutate, diff --git a/packages/astro/src/vite-plugin-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index 5b3bfdf38a70..21023207a135 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -72,6 +72,8 @@ function* collectCSSWithOrder( * The virtual module should be used only during development. * Per-route virtual modules are created to avoid invalidation loops. * + * The second plugin imports all of the individual CSS modules in a map. + * * @param routesList */ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOptions): Plugin[] { From d7b534cb1f01511947d9cd391b42f049322bab3f Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 24 Nov 2025 08:42:35 -0500 Subject: [PATCH 24/25] fix build --- packages/astro/src/content/vite-plugin-content-assets.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index 0303d0510d67..c9b8b6bc947c 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -18,7 +18,6 @@ import { STYLES_PLACEHOLDER, } from './consts.js'; import { hasContentFlag } from './utils.js'; -import type { StaticBuildOptions } from '../core/build/types.js'; import { joinPaths, prependForwardSlash, slash } from '@astrojs/internal-helpers/path'; export function astroContentAssetPropagationPlugin({ @@ -196,7 +195,7 @@ async function getStylesForURL( * with actual styles from propagatedStylesMap. */ export async function contentAssetsBuildPostHook( - opts: StaticBuildOptions, + base: string, internals: BuildInternals, { ssrOutputs, @@ -236,7 +235,7 @@ export async function contentAssetsBuildPostHook( // links. Refactor this away in the future. for (const value of entryCss) { if (value.type === 'inline') entryStyles.add(value.content); - if (value.type === 'external') entryLinks.add(prependForwardSlash(joinPaths(opts.settings.config.base, slash(value.src)))); + if (value.type === 'external') entryLinks.add(prependForwardSlash(joinPaths(base, slash(value.src)))); } } } From b7eeccba9a3c5f05f3d44a6fc578b5aca654087b Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Mon, 24 Nov 2025 08:58:12 -0500 Subject: [PATCH 25/25] add comment on what mergeInlineCss does --- packages/astro/src/core/build/runtime.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/astro/src/core/build/runtime.ts b/packages/astro/src/core/build/runtime.ts index 9cf5243d2809..5210d1600841 100644 --- a/packages/astro/src/core/build/runtime.ts +++ b/packages/astro/src/core/build/runtime.ts @@ -56,6 +56,10 @@ export function cssOrder(a: OrderInfo, b: OrderInfo) { } } +/** + * Merges inline CSS into as few stylesheets as possible, + * preserving ordering when there are non-inlined in between. + */ export function mergeInlineCss( acc: Array, current: StylesheetAsset,