Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eed2530
Fixes CSS import order and cloudflare
matthewp Nov 21, 2025
91138fc
fix build
matthewp Nov 21, 2025
f6e4463
linting obo
matthewp Nov 21, 2025
4578d2d
Remove the unused ssr-manifest module
matthewp Nov 21, 2025
9c3aa38
fix config-vite tests
matthewp Nov 21, 2025
5fd8edf
fix content-collections.test.js
matthewp Nov 21, 2025
4509536
fix entry-file-names.test.js
matthewp Nov 21, 2025
11ab96e
Fix redirects-i18n test
matthewp Nov 22, 2025
15fc25e
Fix server islands tests
matthewp Nov 22, 2025
8897c3e
Prevent CSS from being duplicated between SSR and prerender
matthewp Nov 22, 2025
de91192
Prevent ?raw CSS imports from being added as styles in dev
matthewp Nov 22, 2025
d5b6a76
Fixes vitest.test.js
matthewp Nov 22, 2025
7794452
When prerendering, properly handle trailing slash with base
matthewp Nov 22, 2025
59cd518
Prevent empty CSS chunks from being written to disk
matthewp Nov 22, 2025
ce5ed6b
Update core-image-unconventional-settings tests to use environments API
matthewp Nov 22, 2025
ad982c6
Prevent adding CSS to pages not belonged to
matthewp Nov 22, 2025
732cfaf
Update astro-global.test.js
matthewp Nov 22, 2025
1bedc36
Prepend base to propagated styles
matthewp Nov 22, 2025
f2989dd
fix the build
matthewp Nov 22, 2025
c370419
increase the test timeout
matthewp Nov 23, 2025
6beed57
Use the href for saving unix-style paths for image metadata
matthewp Nov 23, 2025
fdf0670
use fileURLToNormalizedPath
matthewp Nov 23, 2025
eeb4ea9
PR comments
matthewp Nov 24, 2025
d7b534c
fix build
matthewp Nov 24, 2025
b7eeccb
add comment on what mergeInlineCss does
matthewp Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/wet-lines-wear.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/astro/dev-only.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ declare module 'virtual:astro:dev-css' {
import type { ImportedDevStyles } from './src/types/astro.js';
export const css: Set<ImportedDevStyles>;
}

declare module 'virtual:astro:dev-css-all' {
import type { ImportedDevStyles } from './src/types/astro.js';
export const devCSSMap: Map<string, () => Promise<{ css: Set<ImportedDevStyles> }>>;
}
2 changes: 1 addition & 1 deletion packages/astro/src/assets/utils/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function emitImageMetadata(
Object.defineProperty(emittedImage, 'fsPath', {
enumerable: false,
writable: false,
value: id,
value: fileURLToNormalizedPath(url),
});

// Build
Expand Down
4 changes: 3 additions & 1 deletion packages/astro/src/content/vite-plugin-content-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
STYLES_PLACEHOLDER,
} from './consts.js';
import { hasContentFlag } from './utils.js';
import { joinPaths, prependForwardSlash, slash } from '@astrojs/internal-helpers/path';

export function astroContentAssetPropagationPlugin({
settings,
Expand Down Expand Up @@ -194,6 +195,7 @@ async function getStylesForURL(
* with actual styles from propagatedStylesMap.
*/
export async function contentAssetsBuildPostHook(
base: string,
internals: BuildInternals,
{
ssrOutputs,
Expand Down Expand Up @@ -233,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(value.src);
if (value.type === 'external') entryLinks.add(prependForwardSlash(joinPaths(base, slash(value.src))));
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions packages/astro/src/core/app/dev/pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { ComponentInstance } from '../../../types/astro.js';
import type { ComponentInstance, ImportedDevStyle } from '../../../types/astro.js';
import type {
DevToolbarMetadata,
RewritePayload,
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';
Expand Down Expand Up @@ -93,7 +92,16 @@ 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');

const importer = devCSSMap.get(routeData.component);
let css = new Set<ImportedDevStyle>();
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.
for (const { id, url: src, content } of css) {
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
prepareAssetsGenerationEnv,
} from '../../assets/build/generate.js';
import {
collapseDuplicateTrailingSlashes,
isRelativePath,
joinPaths,
removeLeadingForwardSlash,
Expand Down Expand Up @@ -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);
Expand Down
71 changes: 0 additions & 71 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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<StylesheetAsset>,
current: StylesheetAsset,
): Array<StylesheetAsset> {
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;
}
96 changes: 5 additions & 91 deletions packages/astro/src/core/build/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@ 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';
import { BEFORE_HYDRATION_SCRIPT_ID, PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
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';

/**
Expand All @@ -31,10 +30,6 @@ export class BuildPipeline extends Pipeline {
return 'BuildPipeline';
}

#componentsInterner: WeakMap<RouteData, SinglePageBuiltModule> = new WeakMap<
RouteData,
SinglePageBuiltModule
>();
/**
* This cache is needed to map a single `RouteData` to its file path.
* @private
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -151,6 +139,7 @@ export class BuildPipeline extends Pipeline {
});
}
}

return { scripts, styles, links };
}

Expand Down Expand Up @@ -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<SinglePageBuiltModule> {
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<SinglePageBuiltModule> {
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<SinglePageBuiltModule> {
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 {
Expand Down
27 changes: 27 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
const pagesToCss: Record<string, Record<string, { order: number; depth: number }>> = {};
// Map of module Ids (usually something like `/Users/...blog.mdx?astroPropagatedAssets`) to its imported CSS
const moduleIdToPropagatedCss: Record<string, Set<string>> = {};
// 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',
Expand All @@ -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') {
Expand Down Expand Up @@ -114,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)) {
Expand Down Expand Up @@ -187,6 +207,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;
}
Comment on lines +210 to +215
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check that we're inside the prerender pipeline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be ok. Should we leave in empty CSS chunks in the other environments?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The minor issue I see is that the comment doesn't reflect the actual code, because I can't see anything regarding "SSR" and "prerender" , so I'm not sure how to interpret the code.


const toBeInlined =
inlineConfig === 'always'
? true
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading
Loading