summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Arsh <69170106+lilnasy@users.noreply.github.com> 2023-05-04 00:19:06 +0530
committerGravatar GitHub <noreply@github.com> 2023-05-03 14:49:06 -0400
commit80e3d4d3d0f7719d8eae5435bba3805503057511 (patch)
tree71509a245806e658ebdad4b5805ea6aa0bcfd568
parent8d75340b7a9699b17a1d1ec87674d8d7d750d570 (diff)
downloadastro-80e3d4d3d0f7719d8eae5435bba3805503057511.tar.gz
astro-80e3d4d3d0f7719d8eae5435bba3805503057511.tar.zst
astro-80e3d4d3d0f7719d8eae5435bba3805503057511.zip
feature: configuration for css inlining behavior (#6659)
* feature(inline stylesheets): implement as experimental * test: rename css-inline -> css-import-as-inline * test(content collections): add de-duplication of css * test: add new suite for inlineStylesheets configuration * fix(inline stylesheets): did not act on propagated styles * hack(inline stylesheets testing): duplicate fixtures Content collections reuses build data across multiple fixture.builds, even though a configuration change may have changed it. Duplicating fixtures avoids usage of the stale cache. https://cdn.discordapp.com/attachments/1039830843440504872/1097795182340092024/Screenshot_87_colored.png * refactor(css plugin): reduce nesting * optimization(css rendering): merge <style> tags Chrome, but not Safari or Firefox, is slower to match rules when they are split across multiple files or style tags. https://nolanlawson.com/2022/06/22/style-scoping-versus-shadow-dom-which-is-fastest/ Having the abiility to inline stylesheets opens us up to this optimization. Ideally, it would extend to propagated styles, but that ended up being a rabbit hole. * typedocs(inlineStylesheets config): ensure consistency Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * chore(build internals): update comment * correct minor mistake in test * test(inline stylesheets): unique package names for duplicate fixtures * refactor(css build plugin): maps -> records * refactor(css build plugin): remove use of spread operator --------- Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
-rw-r--r--.changeset/friendly-fishes-sing.md5
-rw-r--r--packages/astro/src/@types/astro.ts20
-rw-r--r--packages/astro/src/content/runtime.ts13
-rw-r--r--packages/astro/src/content/vite-plugin-content-assets.ts16
-rw-r--r--packages/astro/src/core/app/index.ts7
-rw-r--r--packages/astro/src/core/app/types.ts5
-rw-r--r--packages/astro/src/core/build/generate.ts47
-rw-r--r--packages/astro/src/core/build/internal.ts77
-rw-r--r--packages/astro/src/core/build/page-data.ts4
-rw-r--r--packages/astro/src/core/build/plugins/plugin-css.ts463
-rw-r--r--packages/astro/src/core/build/plugins/plugin-ssr.ts17
-rw-r--r--packages/astro/src/core/build/types.ts8
-rw-r--r--packages/astro/src/core/config/schema.ts5
-rw-r--r--packages/astro/src/core/render/ssr-element.ts38
-rw-r--r--packages/astro/src/runtime/server/index.ts1
-rw-r--r--packages/astro/src/runtime/server/render/head.ts6
-rw-r--r--packages/astro/src/runtime/server/render/index.ts2
-rw-r--r--packages/astro/src/runtime/server/render/tags.ts38
-rw-r--r--packages/astro/test/content-collections-render.test.js41
-rw-r--r--packages/astro/test/css-import-as-inline.test.js (renamed from packages/astro/test/css-inline.test.js)2
-rw-r--r--packages/astro/test/css-inline-stylesheets.js285
-rw-r--r--packages/astro/test/fixtures/content/src/components/H3.astro2
-rw-r--r--packages/astro/test/fixtures/content/src/components/LayoutProp.astro3
-rw-r--r--packages/astro/test/fixtures/content/src/pages/with-layout-prop.astro4
-rw-r--r--packages/astro/test/fixtures/css-import-as-inline/package.json (renamed from packages/astro/test/fixtures/css-inline/package.json)2
-rw-r--r--packages/astro/test/fixtures/css-import-as-inline/src/inline.css (renamed from packages/astro/test/fixtures/css-inline/src/inline.css)0
-rw-r--r--packages/astro/test/fixtures/css-import-as-inline/src/layouts/Layout.astro (renamed from packages/astro/test/fixtures/css-inline/src/layouts/Layout.astro)0
-rw-r--r--packages/astro/test/fixtures/css-import-as-inline/src/pages/index.astro (renamed from packages/astro/test/fixtures/css-inline/src/pages/index.astro)0
-rw-r--r--packages/astro/test/fixtures/css-import-as-inline/src/raw.css (renamed from packages/astro/test/fixtures/css-inline/src/raw.css)0
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/package.json8
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro86
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro35
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro17
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json8
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro86
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro35
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro17
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/package.json8
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro86
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css15
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro35
-rw-r--r--packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro17
-rw-r--r--packages/astro/test/units/dev/head-injection.test.js6
-rw-r--r--pnpm-lock.yaml20
49 files changed, 1355 insertions, 310 deletions
diff --git a/.changeset/friendly-fishes-sing.md b/.changeset/friendly-fishes-sing.md
new file mode 100644
index 000000000..9da10e6bf
--- /dev/null
+++ b/.changeset/friendly-fishes-sing.md
@@ -0,0 +1,5 @@
+---
+'astro': minor
+---
+
+Implement Inline Stylesheets RFC as experimental
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index b8d7338f6..f45893821 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -1038,6 +1038,26 @@ export interface AstroUserConfig {
/**
* @docs
+ * @name experimental.inlineStylesheets
+ * @type {('always' | 'auto' | 'never')}
+ * @default `never`
+ * @description
+ * Control whether styles are sent to the browser in a separate css file or inlined into <style> tags. Choose from the following options:
+ * - `'always'` - all styles are inlined into <style> tags
+ * - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets.
+ * - `'never'` - all styles are sent in external stylesheets
+ *
+ * ```js
+ * {
+ * experimental: {
+ * inlineStylesheets: `auto`,
+ * },
+ * }
+ */
+ inlineStylesheets?: 'always' | 'auto' | 'never';
+
+ /**
+ * @docs
* @name experimental.middleware
* @type {boolean}
* @default `false`
diff --git a/packages/astro/src/content/runtime.ts b/packages/astro/src/content/runtime.ts
index b97133dde..c0fddde09 100644
--- a/packages/astro/src/content/runtime.ts
+++ b/packages/astro/src/content/runtime.ts
@@ -6,7 +6,6 @@ import {
createHeadAndContent,
renderComponent,
renderScriptElement,
- renderStyleElement,
renderTemplate,
renderUniqueStylesheet,
unescapeHTML,
@@ -152,13 +151,21 @@ async function render({
links = '',
scripts = '';
if (Array.isArray(collectedStyles)) {
- styles = collectedStyles.map((style: any) => renderStyleElement(style)).join('');
+ styles = collectedStyles
+ .map((style: any) => {
+ return renderUniqueStylesheet(result, {
+ type: 'inline',
+ content: style,
+ });
+ })
+ .join('');
}
if (Array.isArray(collectedLinks)) {
links = collectedLinks
.map((link: any) => {
return renderUniqueStylesheet(result, {
- href: prependForwardSlash(link),
+ type: 'external',
+ src: prependForwardSlash(link),
});
})
.join('');
diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts
index 09fb6dbee..efce94e9c 100644
--- a/packages/astro/src/content/vite-plugin-content-assets.ts
+++ b/packages/astro/src/content/vite-plugin-content-assets.ts
@@ -123,7 +123,8 @@ export function astroConfigBuildPlugin(
chunk.type === 'chunk' &&
(chunk.code.includes(LINKS_PLACEHOLDER) || chunk.code.includes(SCRIPTS_PLACEHOLDER))
) {
- let entryCSS = new Set<string>();
+ let entryStyles = new Set<string>();
+ let entryLinks = new Set<string>();
let entryScripts = new Set<string>();
for (const id of Object.keys(chunk.modules)) {
@@ -137,7 +138,8 @@ export function astroConfigBuildPlugin(
const _entryScripts = pageData.propagatedScripts?.get(id);
if (_entryCss) {
for (const value of _entryCss) {
- entryCSS.add(value);
+ if (value.type === 'inline') entryStyles.add(value.content);
+ if (value.type === 'external') entryLinks.add(value.src);
}
}
if (_entryScripts) {
@@ -150,10 +152,16 @@ export function astroConfigBuildPlugin(
}
let newCode = chunk.code;
- if (entryCSS.size) {
+ if (entryStyles.size) {
+ newCode = newCode.replace(
+ JSON.stringify(STYLES_PLACEHOLDER),
+ JSON.stringify(Array.from(entryStyles))
+ );
+ }
+ if (entryLinks.size) {
newCode = newCode.replace(
JSON.stringify(LINKS_PLACEHOLDER),
- JSON.stringify(Array.from(entryCSS).map(prependBase))
+ JSON.stringify(Array.from(entryLinks).map(prependBase))
);
}
if (entryScripts.size) {
diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts
index b499a875b..13fcc8d45 100644
--- a/packages/astro/src/core/app/index.ts
+++ b/packages/astro/src/core/app/index.ts
@@ -24,7 +24,7 @@ import {
import { RouteCache } from '../render/route-cache.js';
import {
createAssetLink,
- createLinkStylesheetElementSet,
+ createStylesheetElementSet,
createModuleScriptElement,
} from '../render/ssr-element.js';
import { matchRoute } from '../routing/match.js';
@@ -180,7 +180,9 @@ export class App {
const url = new URL(request.url);
const pathname = '/' + this.removeBase(url.pathname);
const info = this.#routeDataToRouteInfo.get(routeData!)!;
- const links = createLinkStylesheetElementSet(info.links);
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const links = new Set<never>();
+ const styles = createStylesheetElementSet(info.styles);
let scripts = new Set<SSRElement>();
for (const script of info.scripts) {
@@ -203,6 +205,7 @@ export class App {
pathname,
componentMetadata: this.#manifest.componentMetadata,
scripts,
+ styles,
links,
route: routeData,
status,
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index 79503161d..ab6a50b9c 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -11,6 +11,10 @@ import type {
export type ComponentPath = string;
+export type StylesheetAsset =
+ | { type: 'inline'; content: string }
+ | { type: 'external'; src: string };
+
export interface RouteInfo {
routeData: RouteData;
file: string;
@@ -21,6 +25,7 @@ export interface RouteInfo {
// Hoisted
| { type: 'inline' | 'external'; value: string }
)[];
+ styles: StylesheetAsset[];
}
export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index d89575bd4..d00cef268 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -40,15 +40,25 @@ import { createEnvironment, createRenderContext, renderPage } from '../render/in
import { callGetStaticPaths } from '../render/route-cache.js';
import {
createAssetLink,
- createLinkStylesheetElementSet,
+ createStylesheetElementSet,
createModuleScriptsSet,
} from '../render/ssr-element.js';
import { createRequest } from '../request.js';
import { matchRoute } from '../routing/match.js';
import { getOutputFilename } from '../util.js';
import { getOutDirWithinCwd, getOutFile, getOutFolder } from './common.js';
-import { eachPageData, getPageDataByComponent, sortedCSS } from './internal.js';
-import type { PageBuildData, SingleFileBuiltModule, StaticBuildOptions } from './types';
+import {
+ eachPageData,
+ getPageDataByComponent,
+ cssOrder,
+ mergeInlineCss,
+} from './internal.js';
+import type {
+ PageBuildData,
+ SingleFileBuiltModule,
+ StaticBuildOptions,
+ StylesheetAsset,
+} from './types';
import { getTimeStat } from './util.js';
function shouldSkipDraft(pageModule: ComponentInstance, settings: AstroSettings): boolean {
@@ -161,8 +171,14 @@ async function generatePage(
const renderers = ssrEntry.renderers;
const pageInfo = getPageDataByComponent(internals, pageData.route.component);
- const linkIds: string[] = sortedCSS(pageData);
+
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const linkIds: [] = [];
const scripts = pageInfo?.hoistedScript ?? null;
+ const styles = pageData.styles
+ .sort(cssOrder)
+ .map(({ sheet }) => sheet)
+ .reduce(mergeInlineCss, []);
const pageModule = ssrEntry.pageMap?.get(pageData.component);
const middleware = ssrEntry.middleware;
@@ -183,6 +199,7 @@ async function generatePage(
internals,
linkIds,
scripts,
+ styles,
mod: pageModule,
renderers,
};
@@ -273,6 +290,7 @@ interface GeneratePathOptions {
internals: BuildInternals;
linkIds: string[];
scripts: { type: 'inline' | 'external'; value: string } | null;
+ styles: StylesheetAsset[];
mod: ComponentInstance;
renderers: SSRLoadedRenderer[];
}
@@ -341,7 +359,15 @@ async function generatePath(
middleware?: AstroMiddlewareInstance<unknown>
) {
const { settings, logging, origin, routeCache } = opts;
- const { mod, internals, linkIds, scripts: hoistedScripts, pageData, renderers } = gopts;
+ const {
+ mod,
+ internals,
+ linkIds,
+ scripts: hoistedScripts,
+ styles: _styles,
+ pageData,
+ renderers,
+ } = gopts;
// This adds the page name to the array so it can be shown as part of stats.
if (pageData.route.type === 'page') {
@@ -350,13 +376,15 @@ async function generatePath(
debug('build', `Generating: ${pathname}`);
- const links = createLinkStylesheetElementSet(
- linkIds,
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const links = new Set<never>();
+ const scripts = createModuleScriptsSet(
+ hoistedScripts ? [hoistedScripts] : [],
settings.config.base,
settings.config.build.assetsPrefix
);
- const scripts = createModuleScriptsSet(
- hoistedScripts ? [hoistedScripts] : [],
+ const styles = createStylesheetElementSet(
+ _styles,
settings.config.base,
settings.config.build.assetsPrefix
);
@@ -431,6 +459,7 @@ async function generatePath(
request: createRequest({ url, headers: new Headers(), logging, ssr }),
componentMetadata: internals.componentMetadata,
scripts,
+ styles,
links,
route: pageData.route,
env,
diff --git a/packages/astro/src/core/build/internal.ts b/packages/astro/src/core/build/internal.ts
index 8cd052ffa..eff3f5bec 100644
--- a/packages/astro/src/core/build/internal.ts
+++ b/packages/astro/src/core/build/internal.ts
@@ -1,5 +1,5 @@
import type { Rollup } from 'vite';
-import type { PageBuildData, ViteID } from './types';
+import type { PageBuildData, StylesheetAsset, ViteID } from './types';
import type { SSRResult } from '../../@types/astro';
import type { PageOptions } from '../../vite-plugin-astro/types';
@@ -224,39 +224,56 @@ 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.
- * The return of this function is an array of CSS paths, with shared CSS on top
- * and page-level CSS on bottom.
+ * Can be used to sort stylesheets so that shared rules come first
+ * and page-specific rules come after.
*/
-export function sortedCSS(pageData: PageBuildData) {
- return Array.from(pageData.css)
- .sort((a, b) => {
- let depthA = a[1].depth,
- depthB = b[1].depth,
- orderA = a[1].order,
- orderB = b[1].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;
- }
- }
- })
- .map(([id]) => id);
+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;
}
export function isHoistedScript(internals: BuildInternals, id: string): boolean {
diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts
index 71de5da4b..0a53745c5 100644
--- a/packages/astro/src/core/build/page-data.ts
+++ b/packages/astro/src/core/build/page-data.ts
@@ -53,7 +53,7 @@ export async function collectPagesData(
component: route.component,
route,
moduleSpecifier: '',
- css: new Map(),
+ styles: [],
propagatedStyles: new Map(),
propagatedScripts: new Map(),
hoistedScript: undefined,
@@ -76,7 +76,7 @@ export async function collectPagesData(
component: route.component,
route,
moduleSpecifier: '',
- css: new Map(),
+ styles: [],
propagatedStyles: new Map(),
propagatedScripts: new Map(),
hoistedScript: undefined,
diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts
index 2df111487..c6a48c091 100644
--- a/packages/astro/src/core/build/plugins/plugin-css.ts
+++ b/packages/astro/src/core/build/plugins/plugin-css.ts
@@ -5,7 +5,7 @@ import { type Plugin as VitePlugin, type ResolvedConfig } from 'vite';
import { isBuildableCSSRequest } from '../../render/dev/util.js';
import type { BuildInternals } from '../internal';
import type { AstroBuildPlugin } from '../plugin';
-import type { PageBuildData, StaticBuildOptions } from '../types';
+import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types';
import { PROPAGATED_ASSET_FLAG } from '../../../content/consts.js';
import * as assetName from '../css-asset-name.js';
@@ -25,236 +25,295 @@ interface PluginOptions {
target: 'client' | 'server';
}
-export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
+/***** ASTRO PLUGIN *****/
+
+export function pluginCSS(
+ options: StaticBuildOptions,
+ internals: BuildInternals
+): AstroBuildPlugin {
+ return {
+ build: 'both',
+ hooks: {
+ 'build:before': ({ build }) => {
+ let plugins = rollupPluginAstroBuildCSS({
+ buildOptions: options,
+ internals,
+ target: build === 'ssr' ? 'server' : 'client',
+ });
+
+ return {
+ vitePlugin: plugins,
+ };
+ },
+ },
+ };
+}
+
+/***** ROLLUP SUB-PLUGINS *****/
+
+function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
const { internals, buildOptions } = options;
const { settings } = buildOptions;
let resolvedConfig: ResolvedConfig;
- function createNameHash(baseId: string, hashIds: string[]): string {
- const baseName = baseId ? npath.parse(baseId).name : 'index';
- const hash = crypto.createHash('sha256');
- for (const id of hashIds) {
- hash.update(id, 'utf-8');
- }
- const h = hash.digest('hex').slice(0, 8);
- const proposedName = baseName + '.' + h;
- return proposedName;
- }
+ // stylesheet filenames are kept in here until "post", when they are rendered and ready to be inlined
+ const pagesToCss: Record<string, Record<string, { order: number; depth: number }>> = {}
+ const pagesToPropagatedCss: Record<string, Record<string, Set<string>>> = {}
- function* getParentClientOnlys(
- id: string,
- ctx: { getModuleInfo: GetModuleInfo }
- ): Generator<PageBuildData, void, unknown> {
- for (const [info] of walkParentInfos(id, ctx)) {
- yield* getPageDatasByClientOnlyID(internals, info.id);
- }
- }
+ const cssBuildPlugin: VitePlugin = {
+ name: 'astro:rollup-plugin-build-css',
- return [
- {
- name: 'astro:rollup-plugin-build-css',
+ transform(_, id) {
+ // In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
+ // In the client build, if we're also bundling the same style, return an empty string to
+ // deduplicate the final CSS output.
+ if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
+ return '';
+ }
+ },
- transform(_, id) {
- // In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
- // In the client build, if we're also bundling the same style, return an empty string to
- // deduplicate the final CSS output.
- if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
- return '';
- }
- },
+ outputOptions(outputOptions) {
+ // Skip in client builds as its module graph doesn't have reference to Astro pages
+ // to be able to chunk based on it's related top-level pages.
+ if (options.target === 'client') return;
- outputOptions(outputOptions) {
- // Skip in client builds as its module graph doesn't have reference to Astro pages
- // to be able to chunk based on it's related top-level pages.
- if (options.target === 'client') return;
-
- const assetFileNames = outputOptions.assetFileNames;
- const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
- const createNameForParentPages = namingIncludesHash
- ? assetName.shortHashedName
- : assetName.createSlugger(settings);
-
- extendManualChunks(outputOptions, {
- after(id, meta) {
- // For CSS, create a hash of all of the pages that use it.
- // This causes CSS to be built into shared chunks when used by multiple pages.
- if (isBuildableCSSRequest(id)) {
- for (const [pageInfo] of walkParentInfos(id, {
- getModuleInfo: meta.getModuleInfo,
- })) {
- if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
- // Split delayed assets to separate modules
- // so they can be injected where needed
- return createNameHash(id, [id]);
- }
+ const assetFileNames = outputOptions.assetFileNames;
+ const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
+ const createNameForParentPages = namingIncludesHash
+ ? assetName.shortHashedName
+ : assetName.createSlugger(settings);
+
+ extendManualChunks(outputOptions, {
+ after(id, meta) {
+ // For CSS, create a hash of all of the pages that use it.
+ // This causes CSS to be built into shared chunks when used by multiple pages.
+ if (isBuildableCSSRequest(id)) {
+ for (const [pageInfo] of walkParentInfos(id, {
+ getModuleInfo: meta.getModuleInfo,
+ })) {
+ if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
+ // Split delayed assets to separate modules
+ // so they can be injected where needed
+ return createNameHash(id, [id]);
}
- return createNameForParentPages(id, meta);
}
- },
- });
- },
+ return createNameForParentPages(id, meta);
+ }
+ },
+ });
+ },
- async generateBundle(_outputOptions, bundle) {
- type ViteMetadata = {
- importedAssets: Set<string>;
- importedCss: Set<string>;
- };
+ async generateBundle(_outputOptions, bundle) {
+ for (const [_, chunk] of Object.entries(bundle)) {
+ if (chunk.type !== 'chunk') continue;
+ if ('viteMetadata' in chunk === false) continue;
+ const meta = chunk.viteMetadata as ViteMetadata;
- const appendCSSToPage = (
- pageData: PageBuildData,
- meta: ViteMetadata,
- depth: number,
- order: number
- ) => {
- for (const importedCssImport of meta.importedCss) {
- // CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
- // Depth info is used when sorting the links on the page.
- if (pageData?.css.has(importedCssImport)) {
- // eslint-disable-next-line
- const cssInfo = pageData?.css.get(importedCssImport)!;
- if (depth < cssInfo.depth) {
- cssInfo.depth = depth;
- }
+ // Skip if the chunk has no CSS, we want to handle CSS chunks only
+ if (meta.importedCss.size < 1) continue;
- // Update the order, preferring the lowest order we have.
- if (cssInfo.order === -1) {
- cssInfo.order = order;
- } else if (order < cssInfo.order && order > -1) {
- cssInfo.order = order;
+ // In the SSR build, keep track of all CSS chunks' modules as the client build may
+ // duplicate them, e.g. for `client:load` components that render in SSR and client
+ // for hydation.
+ if (options.target === 'server') {
+ for (const id of Object.keys(chunk.modules)) {
+ internals.cssChunkModuleIds.add(id);
+ }
+ }
+
+ // For the client build, client:only styles need to be mapped
+ // over to their page. For this chunk, determine if it's a child of a
+ // client:only component and if so, add its CSS to the page it belongs to.
+ if (options.target === 'client') {
+ for (const id of Object.keys(chunk.modules)) {
+ for (const pageData of getParentClientOnlys(id, this, internals)) {
+ for (const importedCssImport of meta.importedCss) {
+ const cssToInfoRecord = pagesToCss[pageData.moduleSpecifier] ??= {}
+ cssToInfoRecord[importedCssImport] = { depth: -1, order: -1 };
}
- } else {
- pageData?.css.set(importedCssImport, { depth, order });
}
}
- };
+ }
- for (const [_, chunk] of Object.entries(bundle)) {
- if (chunk.type === 'chunk') {
- const c = chunk;
-
- if ('viteMetadata' in chunk) {
- const meta = chunk['viteMetadata'] as ViteMetadata;
-
- // Chunks that have the viteMetadata.importedCss are CSS chunks
- if (meta.importedCss.size) {
- // In the SSR build, keep track of all CSS chunks' modules as the client build may
- // duplicate them, e.g. for `client:load` components that render in SSR and client
- // for hydation.
- if (options.target === 'server') {
- for (const id of Object.keys(c.modules)) {
- internals.cssChunkModuleIds.add(id);
- }
- }
+ // 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)) {
+ for (const [pageInfo, depth, order] of walkParentInfos(
+ id,
+ this,
+ function until(importer) {
+ return new URL(importer, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG);
+ }
+ )) {
+ if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
+ for (const parent of walkParentInfos(id, this)) {
+ const parentInfo = parent[0];
+ if (moduleIsTopLevelPage(parentInfo) === false) continue;
- // For the client build, client:only styles need to be mapped
- // over to their page. For this chunk, determine if it's a child of a
- // client:only component and if so, add its CSS to the page it belongs to.
- if (options.target === 'client') {
- for (const id of Object.keys(c.modules)) {
- for (const pageData of getParentClientOnlys(id, this)) {
- for (const importedCssImport of meta.importedCss) {
- pageData.css.set(importedCssImport, { depth: -1, order: -1 });
- }
- }
- }
- }
+ const pageViteID = parentInfo.id;
+ const pageData = getPageDataByViteID(internals, pageViteID);
+ if (pageData === undefined) continue;
+
+ for (const css of meta.importedCss) {
+ const propagatedStyles = pagesToPropagatedCss[pageData.moduleSpecifier] ??= {}
+ const existingCss = propagatedStyles[pageInfo.id] ??= new Set();
- // For this CSS chunk, walk parents until you find a page. Add the CSS to that page.
- for (const id of Object.keys(c.modules)) {
- for (const [pageInfo, depth, order] of walkParentInfos(
- id,
- this,
- function until(importer) {
- return new URL(importer, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG);
- }
- )) {
- if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
- for (const parent of walkParentInfos(id, this)) {
- const parentInfo = parent[0];
- if (moduleIsTopLevelPage(parentInfo)) {
- const pageViteID = parentInfo.id;
- const pageData = getPageDataByViteID(internals, pageViteID);
- if (pageData) {
- for (const css of meta.importedCss) {
- const existingCss =
- pageData.propagatedStyles.get(pageInfo.id) ?? new Set();
- pageData.propagatedStyles.set(
- pageInfo.id,
- new Set([...existingCss, css])
- );
- }
- }
- }
- }
- } else if (moduleIsTopLevelPage(pageInfo)) {
- const pageViteID = pageInfo.id;
- const pageData = getPageDataByViteID(internals, pageViteID);
- if (pageData) {
- appendCSSToPage(pageData, meta, depth, order);
- }
- } else if (
- options.target === 'client' &&
- isHoistedScript(internals, pageInfo.id)
- ) {
- for (const pageData of getPageDatasByHoistedScriptId(
- internals,
- pageInfo.id
- )) {
- appendCSSToPage(pageData, meta, -1, order);
- }
- }
- }
+ existingCss.add(css);
}
}
+ } else if (moduleIsTopLevelPage(pageInfo)) {
+ const pageViteID = pageInfo.id;
+ const pageData = getPageDataByViteID(internals, pageViteID);
+ if (pageData) {
+ appendCSSToPage(pageData, meta, pagesToCss, depth, order);
+ }
+ } else if (options.target === 'client' && isHoistedScript(internals, pageInfo.id)) {
+ for (const pageData of getPageDatasByHoistedScriptId(internals, pageInfo.id)) {
+ appendCSSToPage(pageData, meta, pagesToCss, -1, order);
+ }
}
}
}
- },
+ }
},
- {
- name: 'astro:rollup-plugin-single-css',
- enforce: 'post',
- configResolved(config) {
- resolvedConfig = config;
- },
- generateBundle(_, bundle) {
- // If user disable css code-splitting, search for Vite's hardcoded
- // `style.css` and add it as css for each page.
- // Ref: https://github.com/vitejs/vite/blob/b2c0ee04d4db4a0ef5a084c50f49782c5f88587c/packages/vite/src/node/plugins/html.ts#L690-L705
- if (!resolvedConfig.build.cssCodeSplit) {
- const cssChunk = Object.values(bundle).find(
- (chunk) => chunk.type === 'asset' && chunk.name === 'style.css'
- );
- if (cssChunk) {
- for (const pageData of eachPageData(internals)) {
- pageData.css.set(cssChunk.fileName, { depth: -1, order: -1 });
- }
- }
- }
- },
+ };
+
+ const singleCssPlugin: VitePlugin = {
+ name: 'astro:rollup-plugin-single-css',
+ enforce: 'post',
+ configResolved(config) {
+ resolvedConfig = config;
},
- ];
-}
+ generateBundle(_, bundle) {
+ // If user disable css code-splitting, search for Vite's hardcoded
+ // `style.css` and add it as css for each page.
+ // Ref: https://github.com/vitejs/vite/blob/b2c0ee04d4db4a0ef5a084c50f49782c5f88587c/packages/vite/src/node/plugins/html.ts#L690-L705
+ if (resolvedConfig.build.cssCodeSplit) return;
+ const cssChunk = Object.values(bundle).find(
+ (chunk) => chunk.type === 'asset' && chunk.name === 'style.css'
+ );
+ if (cssChunk === undefined) return;
+ for (const pageData of eachPageData(internals)) {
+ const cssToInfoMap = pagesToCss[pageData.moduleSpecifier] ??= {};
+ cssToInfoMap[cssChunk.fileName] = { depth: -1, order: -1 };
+ }
+ },
+ };
-export function pluginCSS(
- options: StaticBuildOptions,
- internals: BuildInternals
-): AstroBuildPlugin {
- return {
- build: 'both',
- hooks: {
- 'build:before': ({ build }) => {
- let plugins = rollupPluginAstroBuildCSS({
- buildOptions: options,
- internals,
- target: build === 'ssr' ? 'server' : 'client',
- });
+ const inlineStylesheetsPlugin: VitePlugin = {
+ name: 'astro:rollup-plugin-inline-stylesheets',
+ enforce: 'post',
+ async generateBundle(_outputOptions, bundle) {
+ const inlineConfig = settings.config.experimental.inlineStylesheets;
+ const { assetsInlineLimit = 4096 } = settings.config.vite?.build ?? {};
- return {
- vitePlugin: plugins,
- };
- },
+ Object.entries(bundle).forEach(([id, stylesheet]) => {
+ if (
+ stylesheet.type !== 'asset' ||
+ stylesheet.name?.endsWith('.css') !== true ||
+ typeof stylesheet.source !== 'string'
+ )
+ return;
+
+ const assetSize = new TextEncoder().encode(stylesheet.source).byteLength;
+
+ const toBeInlined =
+ inlineConfig === 'always'
+ ? true
+ : inlineConfig === 'never'
+ ? false
+ : assetSize <= assetsInlineLimit;
+
+ if (toBeInlined) delete bundle[id];
+
+ // there should be a single js object for each stylesheet,
+ // allowing the single reference to be shared and checked for duplicates
+ const sheet: StylesheetAsset = toBeInlined
+ ? { type: 'inline', content: stylesheet.source }
+ : { type: 'external', src: stylesheet.fileName };
+
+ const pages = Array.from(eachPageData(internals));
+
+ pages.forEach((pageData) => {
+ const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName];
+ if (orderingInfo !== undefined) return pageData.styles.push({ ...orderingInfo, sheet });
+
+ const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier];
+ if (propagatedPaths === undefined) return;
+ Object.entries(propagatedPaths).forEach(([pageInfoId, css]) => {
+ // return early if sheet does not need to be propagated
+ if (css.has(stylesheet.fileName) !== true) return;
+
+ // return early if the stylesheet needing propagation has already been included
+ if (pageData.styles.some((s) => s.sheet === sheet)) return;
+
+ const propagatedStyles =
+ pageData.propagatedStyles.get(pageInfoId) ??
+ pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!;
+
+ propagatedStyles.add(sheet);
+ });
+ });
+ });
},
};
+
+ return [cssBuildPlugin, singleCssPlugin, inlineStylesheetsPlugin];
+}
+
+/***** UTILITY FUNCTIONS *****/
+
+function createNameHash(baseId: string, hashIds: string[]): string {
+ const baseName = baseId ? npath.parse(baseId).name : 'index';
+ const hash = crypto.createHash('sha256');
+ for (const id of hashIds) {
+ hash.update(id, 'utf-8');
+ }
+ const h = hash.digest('hex').slice(0, 8);
+ const proposedName = baseName + '.' + h;
+ return proposedName;
+}
+
+function* getParentClientOnlys(
+ id: string,
+ ctx: { getModuleInfo: GetModuleInfo },
+ internals: BuildInternals
+): Generator<PageBuildData, void, unknown> {
+ for (const [info] of walkParentInfos(id, ctx)) {
+ yield* getPageDatasByClientOnlyID(internals, info.id);
+ }
+}
+
+type ViteMetadata = {
+ importedAssets: Set<string>;
+ importedCss: Set<string>;
+};
+
+function appendCSSToPage(
+ pageData: PageBuildData,
+ meta: ViteMetadata,
+ pagesToCss: Record<string, Record<string, { order: number; depth: number }>>,
+ depth: number,
+ order: number
+) {
+ for (const importedCssImport of meta.importedCss) {
+ // CSS is prioritized based on depth. Shared CSS has a higher depth due to being imported by multiple pages.
+ // Depth info is used when sorting the links on the page.
+ const cssInfo = pagesToCss[pageData.moduleSpecifier]?.[importedCssImport];
+ if (cssInfo !== undefined) {
+ if (depth < cssInfo.depth) {
+ cssInfo.depth = depth;
+ }
+
+ // Update the order, preferring the lowest order we have.
+ if (cssInfo.order === -1) {
+ cssInfo.order = order;
+ } else if (order < cssInfo.order && order > -1) {
+ cssInfo.order = order;
+ }
+ } else {
+ const cssToInfoRecord = pagesToCss[pageData.moduleSpecifier] ??= {};
+ cssToInfoRecord[importedCssImport] = { depth, order };
+ }
+ }
}
diff --git a/packages/astro/src/core/build/plugins/plugin-ssr.ts b/packages/astro/src/core/build/plugins/plugin-ssr.ts
index 65e104425..9b0a7e848 100644
--- a/packages/astro/src/core/build/plugins/plugin-ssr.ts
+++ b/packages/astro/src/core/build/plugins/plugin-ssr.ts
@@ -13,7 +13,11 @@ import { joinPaths, prependForwardSlash } from '../../path.js';
import { serializeRouteData } from '../../routing/index.js';
import { addRollupInput } from '../add-rollup-input.js';
import { getOutFile, getOutFolder } from '../common.js';
-import { eachPageData, sortedCSS } from '../internal.js';
+import {
+ eachPageData,
+ cssOrder,
+ mergeInlineCss,
+} from '../internal.js';
import type { AstroBuildPlugin } from '../plugin';
export const virtualModuleId = '@astrojs-ssr-virtual-entry';
@@ -171,6 +175,7 @@ function buildManifest(
file,
links: [],
scripts: [],
+ styles: [],
routeData: serializeRouteData(pageData.route, settings.config.trailingSlash),
});
staticFiles.push(file);
@@ -197,7 +202,14 @@ function buildManifest(
});
}
- const links = sortedCSS(pageData).map((pth) => prefixAssetPath(pth));
+ // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc.
+ const links: [] = [];
+
+ const styles = pageData.styles
+ .sort(cssOrder)
+ .map(({ sheet }) => sheet)
+ .map((s) => (s.type === 'external' ? { ...s, src: prefixAssetPath(s.src) } : s))
+ .reduce(mergeInlineCss, []);
routes.push({
file: '',
@@ -208,6 +220,7 @@ function buildManifest(
.filter((script) => script.stage === 'head-inline')
.map(({ stage, content }) => ({ stage, children: content })),
],
+ styles,
routeData: serializeRouteData(pageData.route, settings.config.trailingSlash),
});
}
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index fc7839390..d04f7476c 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -17,14 +17,18 @@ export type ComponentPath = string;
export type ViteID = string;
export type PageOutput = AstroConfig['output'];
+export type StylesheetAsset =
+ | { type: 'inline'; content: string }
+ | { type: 'external'; src: string };
+
export interface PageBuildData {
component: ComponentPath;
route: RouteData;
moduleSpecifier: string;
- css: Map<string, { depth: number; order: number }>;
- propagatedStyles: Map<string, Set<string>>;
+ propagatedStyles: Map<string, Set<StylesheetAsset>>;
propagatedScripts: Map<string, Set<string>>;
hoistedScript: { type: 'inline' | 'external'; value: string } | undefined;
+ styles: Array<{ depth: number; order: number; sheet: StylesheetAsset }>;
}
export type AllPagesData = Record<ComponentPath, PageBuildData>;
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 6d081d126..033e55222 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -37,6 +37,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
legacy: {},
experimental: {
assets: false,
+ inlineStylesheets: 'never',
middleware: false,
},
};
@@ -188,6 +189,10 @@ export const AstroConfigSchema = z.object({
experimental: z
.object({
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
+ inlineStylesheets: z
+ .enum(['always', 'auto', 'never'])
+ .optional()
+ .default(ASTRO_CONFIG_DEFAULTS.experimental.inlineStylesheets),
middleware: z.oboolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.middleware),
})
.optional()
diff --git a/packages/astro/src/core/render/ssr-element.ts b/packages/astro/src/core/render/ssr-element.ts
index 2ebcf7bb8..5b8b3e21d 100644
--- a/packages/astro/src/core/render/ssr-element.ts
+++ b/packages/astro/src/core/render/ssr-element.ts
@@ -1,5 +1,6 @@
import slashify from 'slash';
import type { SSRElement } from '../../@types/astro';
+import type { StylesheetAsset } from '../app/types';
import { joinPaths, prependForwardSlash } from '../../core/path.js';
export function createAssetLink(href: string, base?: string, assetsPrefix?: string): string {
@@ -12,28 +13,35 @@ export function createAssetLink(href: string, base?: string, assetsPrefix?: stri
}
}
-export function createLinkStylesheetElement(
- href: string,
+export function createStylesheetElement(
+ stylesheet: StylesheetAsset,
base?: string,
assetsPrefix?: string
): SSRElement {
- return {
- props: {
- rel: 'stylesheet',
- href: createAssetLink(href, base, assetsPrefix),
- },
- children: '',
- };
+ if (stylesheet.type === 'inline') {
+ return {
+ props: {
+ type: 'text/css',
+ },
+ children: stylesheet.content,
+ };
+ } else {
+ return {
+ props: {
+ rel: 'stylesheet',
+ href: createAssetLink(stylesheet.src, base, assetsPrefix),
+ },
+ children: '',
+ };
+ }
}
-export function createLinkStylesheetElementSet(
- hrefs: string[],
+export function createStylesheetElementSet(
+ stylesheets: StylesheetAsset[],
base?: string,
assetsPrefix?: string
-) {
- return new Set<SSRElement>(
- hrefs.map((href) => createLinkStylesheetElement(href, base, assetsPrefix))
- );
+): Set<SSRElement> {
+ return new Set(stylesheets.map((s) => createStylesheetElement(s, base, assetsPrefix)));
}
export function createModuleScriptElement(
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 3c388fafa..1f1e1e97b 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -19,7 +19,6 @@ export {
renderScriptElement,
renderSlot,
renderSlotToString,
- renderStyleElement,
renderTemplate as render,
renderTemplate,
renderToString,
diff --git a/packages/astro/src/runtime/server/render/head.ts b/packages/astro/src/runtime/server/render/head.ts
index 16a97e9ab..52923c790 100644
--- a/packages/astro/src/runtime/server/render/head.ts
+++ b/packages/astro/src/runtime/server/render/head.ts
@@ -16,7 +16,11 @@ export function renderAllHeadContent(result: SSRResult) {
result._metadata.hasRenderedHead = true;
const styles = Array.from(result.styles)
.filter(uniqueElements)
- .map((style) => renderElement('style', style));
+ .map((style) =>
+ style.props.rel === 'stylesheet'
+ ? renderElement('link', style)
+ : renderElement('style', style)
+ );
// Clear result.styles so that any new styles added will be inlined.
result.styles.clear();
const scripts = Array.from(result.scripts)
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index efa0715d2..d34bdd6c7 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -11,6 +11,6 @@ export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
export { renderSlot, renderSlotToString, type ComponentSlots } from './slot.js';
-export { renderScriptElement, renderStyleElement, renderUniqueStylesheet } from './tags.js';
+export { renderScriptElement, renderUniqueStylesheet } from './tags.js';
export type { RenderInstruction } from './types';
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
diff --git a/packages/astro/src/runtime/server/render/tags.ts b/packages/astro/src/runtime/server/render/tags.ts
index fe9829d0c..f15da6571 100644
--- a/packages/astro/src/runtime/server/render/tags.ts
+++ b/packages/astro/src/runtime/server/render/tags.ts
@@ -1,15 +1,7 @@
import type { SSRElement, SSRResult } from '../../../@types/astro';
+import type { StylesheetAsset } from '../../../core/app/types';
import { renderElement } from './util.js';
-const stylesheetRel = 'stylesheet';
-
-export function renderStyleElement(children: string) {
- return renderElement('style', {
- props: {},
- children,
- });
-}
-
export function renderScriptElement({ props, children }: SSRElement) {
return renderElement('script', {
props,
@@ -17,26 +9,14 @@ export function renderScriptElement({ props, children }: SSRElement) {
});
}
-export function renderStylesheet({ href }: { href: string }) {
- return renderElement(
- 'link',
- {
- props: {
- rel: stylesheetRel,
- href,
- },
- children: '',
- },
- false
- );
-}
-
-export function renderUniqueStylesheet(result: SSRResult, link: { href: string }) {
- for (const existingLink of result.links) {
- if (existingLink.props.rel === stylesheetRel && existingLink.props.href === link.href) {
- return '';
- }
+export function renderUniqueStylesheet(result: SSRResult, sheet: StylesheetAsset) {
+ if (sheet.type === 'external') {
+ if (Array.from(result.styles).some((s) => s.props.href === sheet.src)) return '';
+ return renderElement('link', { props: { rel: 'stylesheet', href: sheet.src }, children: '' });
}
- return renderStylesheet(link);
+ if (sheet.type === 'inline') {
+ if (Array.from(result.styles).some((s) => s.children.includes(sheet.content))) return '';
+ return renderElement('style', { props: { type: 'text/css' }, children: sheet.content });
+ }
}
diff --git a/packages/astro/test/content-collections-render.test.js b/packages/astro/test/content-collections-render.test.js
index 2aea21a74..e1107b10f 100644
--- a/packages/astro/test/content-collections-render.test.js
+++ b/packages/astro/test/content-collections-render.test.js
@@ -36,6 +36,25 @@ describe('Content Collections - render()', () => {
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
});
+ it('De-duplicates CSS used both in layout and directly in target page', async () => {
+ const html = await fixture.readFile('/with-layout-prop/index.html');
+ const $ = cheerio.load(html);
+
+ const set = new Set();
+
+ $('link[rel=stylesheet]').each((_, linkEl) => {
+ const href = linkEl.attribs.href;
+ expect(set).to.not.contain(href);
+ set.add(href);
+ });
+
+ $('style').each((_, styleEl) => {
+ const textContent = styleEl.children[0].data;
+ expect(set).to.not.contain(textContent);
+ set.add(textContent);
+ });
+ });
+
it('Includes component scripts for rendered entry', async () => {
const html = await fixture.readFile('/launch-week-component-scripts/index.html');
const $ = cheerio.load(html);
@@ -116,6 +135,28 @@ describe('Content Collections - render()', () => {
expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
});
+ it('De-duplicates CSS used both in layout and directly in target page', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/with-layout-prop/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ const set = new Set();
+
+ $('link[rel=stylesheet]').each((_, linkEl) => {
+ const href = linkEl.attribs.href;
+ expect(set).to.not.contain(href);
+ set.add(href);
+ });
+
+ $('style').each((_, styleEl) => {
+ const textContent = styleEl.children[0].data;
+ expect(set).to.not.contain(textContent);
+ set.add(textContent);
+ });
+ });
+
it('Applies MDX components export', async () => {
const app = await fixture.loadTestAdapterApp();
const request = new Request('http://example.com/launch-week-components-export');
diff --git a/packages/astro/test/css-inline.test.js b/packages/astro/test/css-import-as-inline.test.js
index 03bee955b..7dacab377 100644
--- a/packages/astro/test/css-inline.test.js
+++ b/packages/astro/test/css-import-as-inline.test.js
@@ -6,7 +6,7 @@ describe('Importing raw/inlined CSS', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
- root: './fixtures/css-inline/',
+ root: './fixtures/css-import-as-inline/',
});
});
describe('Build', () => {
diff --git a/packages/astro/test/css-inline-stylesheets.js b/packages/astro/test/css-inline-stylesheets.js
new file mode 100644
index 000000000..d4009ed85
--- /dev/null
+++ b/packages/astro/test/css-inline-stylesheets.js
@@ -0,0 +1,285 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import { loadFixture } from './test-utils.js';
+import testAdapter from './test-adapter.js';
+
+describe('Setting inlineStylesheets to never in static output', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/never/',
+ output: 'static',
+ experimental: {
+ inlineStylesheets: 'never',
+ },
+ });
+ await fixture.build();
+ });
+
+ it('Does not render any <style> tags', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('style').toArray()).to.be.empty;
+ });
+
+ describe('Inspect linked stylesheets', () => {
+ // object, so it can be passed by reference
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromStaticOutput(fixture);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+describe('Setting inlineStylesheets to never in server output', () => {
+ let app;
+
+ before(async () => {
+ const fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/never/',
+ output: 'server',
+ adapter: testAdapter(),
+ experimental: {
+ inlineStylesheets: 'never',
+ },
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('Does not render any <style> tags', async () => {
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ expect($('style').toArray()).to.be.empty;
+ });
+
+ describe('Inspect linked stylesheets', () => {
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromServer(app);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+describe('Setting inlineStylesheets to auto in static output', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/auto/',
+ output: 'static',
+ experimental: {
+ inlineStylesheets: 'auto',
+ },
+ vite: {
+ build: {
+ assetsInlineLimit: 512,
+ },
+ },
+ });
+ await fixture.build();
+ });
+
+ it('Renders some <style> and some <link> tags', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ // the count of style/link tags depends on our css chunking logic
+ // this test should be updated if it changes
+ expect($('style')).to.have.lengthOf(3);
+ expect($('link[rel=stylesheet]')).to.have.lengthOf(1);
+ });
+
+ describe('Inspect linked and inlined stylesheets', () => {
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromStaticOutput(fixture);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+describe('Setting inlineStylesheets to auto in server output', () => {
+ let app;
+
+ before(async () => {
+ const fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/auto/',
+ output: 'server',
+ adapter: testAdapter(),
+ experimental: {
+ inlineStylesheets: 'auto',
+ },
+ vite: {
+ build: {
+ assetsInlineLimit: 512,
+ },
+ },
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('Renders some <style> and some <link> tags', async () => {
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ // the count of style/link tags depends on our css chunking logic
+ // this test should be updated if it changes
+ expect($('style')).to.have.lengthOf(3);
+ expect($('link[rel=stylesheet]')).to.have.lengthOf(1);
+ });
+
+ describe('Inspect linked and inlined stylesheets', () => {
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromServer(app);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+describe('Setting inlineStylesheets to always in static output', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/always/',
+ output: 'static',
+ experimental: {
+ inlineStylesheets: 'always',
+ },
+ });
+ await fixture.build();
+ });
+
+ it('Does not render any <link> tags', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('link[rel=stylesheet]').toArray()).to.be.empty;
+ });
+
+ describe('Inspect inlined stylesheets', () => {
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromStaticOutput(fixture);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+describe('Setting inlineStylesheets to always in server output', () => {
+ let app;
+
+ before(async () => {
+ const fixture = await loadFixture({
+ root: './fixtures/css-inline-stylesheets/always/',
+ output: 'server',
+ adapter: testAdapter(),
+ experimental: {
+ inlineStylesheets: 'always',
+ },
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('Does not render any <link> tags', async () => {
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ expect($('link[rel=stylesheet]').toArray()).to.be.empty;
+ });
+
+ describe('Inspect inlined stylesheets', () => {
+ const allStyles = {};
+
+ before(async () => {
+ allStyles.value = await stylesFromServer(app);
+ });
+
+ commonExpectations(allStyles);
+ });
+});
+
+async function stylesFromStaticOutput(fixture) {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ const links = $('link[rel=stylesheet]');
+ const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
+ const allLinkedStylesheets = await Promise.all(hrefs.map((href) => fixture.readFile(href)));
+ const allLinkedStyles = allLinkedStylesheets.join('');
+
+ const styles = $('style');
+ const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
+ const allInlinedStyles = allInlinedStylesheets.join('');
+
+ return allLinkedStyles + allInlinedStyles;
+}
+
+async function stylesFromServer(app) {
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ const links = $('link[rel=stylesheet]');
+ const hrefs = links.map((_, linkEl) => linkEl.attribs.href).toArray();
+ const allLinkedStylesheets = await Promise.all(
+ hrefs.map(async (href) => {
+ const cssRequest = new Request(`http://example.com${href}`);
+ const cssResponse = await app.render(cssRequest);
+ return await cssResponse.text();
+ })
+ );
+ const allLinkedStyles = allLinkedStylesheets.join('');
+
+ const styles = $('style');
+ const allInlinedStylesheets = styles.map((_, styleEl) => styleEl.children[0].data).toArray();
+ const allInlinedStyles = allInlinedStylesheets.join('');
+ return allLinkedStyles + allInlinedStyles;
+}
+
+function commonExpectations(allStyles) {
+ it('Includes all authored css', () => {
+ // authored in imported.css
+ expect(allStyles.value).to.include('.bg-lightcoral');
+
+ // authored in index.astro
+ expect(allStyles.value).to.include('#welcome');
+
+ // authored in components/Button.astro
+ expect(allStyles.value).to.include('.variant-outline');
+
+ // authored in layouts/Layout.astro
+ expect(allStyles.value).to.include('Menlo');
+ });
+
+ it('Styles used both in content layout and directly in page are included only once', () => {
+ // authored in components/Button.astro
+ expect(allStyles.value.match(/cubic-bezier/g)).to.have.lengthOf(1);
+ });
+}
diff --git a/packages/astro/test/fixtures/content/src/components/H3.astro b/packages/astro/test/fixtures/content/src/components/H3.astro
new file mode 100644
index 000000000..fa476e929
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/components/H3.astro
@@ -0,0 +1,2 @@
+<style>h3 { margin: 1rem }</style>
+<h3><slot /></h3> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/content/src/components/LayoutProp.astro b/packages/astro/test/fixtures/content/src/components/LayoutProp.astro
index df7493c3e..a2954162a 100644
--- a/packages/astro/test/fixtures/content/src/components/LayoutProp.astro
+++ b/packages/astro/test/fixtures/content/src/components/LayoutProp.astro
@@ -1,6 +1,6 @@
---
import { CollectionEntry, getCollection } from 'astro:content';
-
+import H3 from './H3.astro'
// Test for recursive `getCollection()` calls
const blog = await getCollection('blog');
@@ -23,6 +23,7 @@ const {
</head>
<body data-layout-prop="true">
<h1>{title}</h1>
+ <H3>H3 inserted in the layout</H3>
<nav>
<ul>
{blog.map((post) => (
diff --git a/packages/astro/test/fixtures/content/src/pages/with-layout-prop.astro b/packages/astro/test/fixtures/content/src/pages/with-layout-prop.astro
index 672430ab5..4cbb87624 100644
--- a/packages/astro/test/fixtures/content/src/pages/with-layout-prop.astro
+++ b/packages/astro/test/fixtures/content/src/pages/with-layout-prop.astro
@@ -1,7 +1,9 @@
---
import { getEntryBySlug } from 'astro:content';
+import H3 from '../components/H3.astro';
const entry = await getEntryBySlug('blog', 'with-layout-prop');
const { Content } = await entry.render();
---
-<Content />
+<H3>H3 directly inserted to the page</H3>
+<Content /> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline/package.json b/packages/astro/test/fixtures/css-import-as-inline/package.json
index 5fce13947..3efc3a6ec 100644
--- a/packages/astro/test/fixtures/css-inline/package.json
+++ b/packages/astro/test/fixtures/css-import-as-inline/package.json
@@ -1,5 +1,5 @@
{
- "name": "@test/css-inline",
+ "name": "@test/css-import-as-inline",
"version": "0.0.0",
"private": true,
"dependencies": {
diff --git a/packages/astro/test/fixtures/css-inline/src/inline.css b/packages/astro/test/fixtures/css-import-as-inline/src/inline.css
index ea8200bd9..ea8200bd9 100644
--- a/packages/astro/test/fixtures/css-inline/src/inline.css
+++ b/packages/astro/test/fixtures/css-import-as-inline/src/inline.css
diff --git a/packages/astro/test/fixtures/css-inline/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-import-as-inline/src/layouts/Layout.astro
index f1a62a537..f1a62a537 100644
--- a/packages/astro/test/fixtures/css-inline/src/layouts/Layout.astro
+++ b/packages/astro/test/fixtures/css-import-as-inline/src/layouts/Layout.astro
diff --git a/packages/astro/test/fixtures/css-inline/src/pages/index.astro b/packages/astro/test/fixtures/css-import-as-inline/src/pages/index.astro
index 89a7288ae..89a7288ae 100644
--- a/packages/astro/test/fixtures/css-inline/src/pages/index.astro
+++ b/packages/astro/test/fixtures/css-import-as-inline/src/pages/index.astro
diff --git a/packages/astro/test/fixtures/css-inline/src/raw.css b/packages/astro/test/fixtures/css-import-as-inline/src/raw.css
index 916081876..916081876 100644
--- a/packages/astro/test/fixtures/css-inline/src/raw.css
+++ b/packages/astro/test/fixtures/css-import-as-inline/src/raw.css
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/always/package.json
new file mode 100644
index 000000000..0d4a8617d
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/css-inline-stylesheets-always",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro
new file mode 100644
index 000000000..3f25cbd3e
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro
@@ -0,0 +1,86 @@
+---
+const { class: className = '', style, href } = Astro.props;
+const { variant = 'primary' } = Astro.props;
+---
+
+<span class:list={[`link pixel variant-${variant}`, className]} >
+ <a {href}>
+ <span><slot /></span>
+ </a>
+</span>
+
+<style>
+ .link {
+ --border-radius: 8;
+ --duration: 200ms;
+ --delay: 30ms;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ display: flex;
+ color: white;
+ font-size: 1.25rem;
+ width: max-content;
+ }
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.67rem 1.25rem;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ color: inherit !important;
+ /* Indicates the button boundaries for forced colors users in older browsers */
+ outline: 1px solid transparent;
+ }
+
+ @media (forced-colors: active) {
+ a {
+ border: 1px solid LinkText;
+ }
+ }
+
+ a > :global(* + *) {
+ margin-inline-start: 0.25rem;
+ }
+
+ .variant-primary {
+ --variant: primary;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ }
+ .variant-primary:hover,
+ .variant-primary:focus-within {
+ --link-color-stop-a: #6d39ff;
+ --link-color-stop-b: #af43ff;
+ }
+ .variant-primary:active {
+ --link-color-stop-a: #5f31e1;
+ --link-color-stop-b: #a740f3;
+ }
+
+ .variant-outline {
+ --variant: outline;
+ --background: none;
+ color: var(--background);
+ }
+ .variant-outline > a::before {
+ position: absolute;
+ top: 0;
+ right: calc(var(--pixel-size) * 1px);
+ bottom: calc(var(--pixel-size) * 1px);
+ left: calc(var(--pixel-size) * 1px);
+ content: '';
+ display: block;
+ transform-origin: bottom center;
+ background: linear-gradient(to top, var(--background), rgba(255, 255, 255, 0));
+ opacity: 0.3;
+ transform: scaleY(0);
+ transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+ .variant-outline:hover > a::before,
+ .variant-outline:focus-within > a::before {
+ transform: scaleY(1);
+ }
+ .variant-outline:active > a::before {
+ transform: scaleY(1);
+ }
+</style> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md
new file mode 100644
index 000000000..240eeeae3
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md
@@ -0,0 +1,15 @@
+---
+title: Endeavour
+description: 'Learn about the Endeavour NASA space shuttle.'
+publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
+layout: '../../layouts/Layout.astro'
+tags: [space, 90s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
+
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
+
+The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
+
+NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css
new file mode 100644
index 000000000..3959523ff
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css
@@ -0,0 +1,15 @@
+.bg-skyblue {
+ background: skyblue;
+}
+
+.bg-lightcoral {
+ background: lightcoral;
+}
+
+.red {
+ color: darkred;
+}
+
+.blue {
+ color: royalblue;
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro
new file mode 100644
index 000000000..b78de296c
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro
@@ -0,0 +1,35 @@
+---
+import '../imported.css';
+import Button from '../components/Button.astro';
+
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <Button>Button used in layout</Button>
+ <slot />
+ </body>
+</html>
+<style is:global>
+ html {
+ font-family: system-ui, sans-serif;
+ background-color: #F6F6F6;
+ }
+ code {
+ font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
+ Bitstream Vera Sans Mono, Courier New, monospace;
+ }
+</style>
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro
new file mode 100644
index 000000000..bfdbeb5f8
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro
@@ -0,0 +1,17 @@
+---
+import Button from '../components/Button.astro';
+import { getEntryBySlug } from 'astro:content';
+
+const entry = await getEntryBySlug('en', 'endeavour');
+const { Content } = await entry.render();
+---
+<style>
+ #welcome::after {
+ content: '🚀'
+ }
+</style>
+<main>
+ <h1 id="welcome">Welcome to Astro</h1>
+ <Content/>
+ <Button>Button used directly in page</Button>
+</main>
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json
new file mode 100644
index 000000000..3eb8e9d51
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/css-inline-stylesheets-auto",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro
new file mode 100644
index 000000000..3f25cbd3e
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro
@@ -0,0 +1,86 @@
+---
+const { class: className = '', style, href } = Astro.props;
+const { variant = 'primary' } = Astro.props;
+---
+
+<span class:list={[`link pixel variant-${variant}`, className]} >
+ <a {href}>
+ <span><slot /></span>
+ </a>
+</span>
+
+<style>
+ .link {
+ --border-radius: 8;
+ --duration: 200ms;
+ --delay: 30ms;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ display: flex;
+ color: white;
+ font-size: 1.25rem;
+ width: max-content;
+ }
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.67rem 1.25rem;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ color: inherit !important;
+ /* Indicates the button boundaries for forced colors users in older browsers */
+ outline: 1px solid transparent;
+ }
+
+ @media (forced-colors: active) {
+ a {
+ border: 1px solid LinkText;
+ }
+ }
+
+ a > :global(* + *) {
+ margin-inline-start: 0.25rem;
+ }
+
+ .variant-primary {
+ --variant: primary;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ }
+ .variant-primary:hover,
+ .variant-primary:focus-within {
+ --link-color-stop-a: #6d39ff;
+ --link-color-stop-b: #af43ff;
+ }
+ .variant-primary:active {
+ --link-color-stop-a: #5f31e1;
+ --link-color-stop-b: #a740f3;
+ }
+
+ .variant-outline {
+ --variant: outline;
+ --background: none;
+ color: var(--background);
+ }
+ .variant-outline > a::before {
+ position: absolute;
+ top: 0;
+ right: calc(var(--pixel-size) * 1px);
+ bottom: calc(var(--pixel-size) * 1px);
+ left: calc(var(--pixel-size) * 1px);
+ content: '';
+ display: block;
+ transform-origin: bottom center;
+ background: linear-gradient(to top, var(--background), rgba(255, 255, 255, 0));
+ opacity: 0.3;
+ transform: scaleY(0);
+ transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+ .variant-outline:hover > a::before,
+ .variant-outline:focus-within > a::before {
+ transform: scaleY(1);
+ }
+ .variant-outline:active > a::before {
+ transform: scaleY(1);
+ }
+</style> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md
new file mode 100644
index 000000000..240eeeae3
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md
@@ -0,0 +1,15 @@
+---
+title: Endeavour
+description: 'Learn about the Endeavour NASA space shuttle.'
+publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
+layout: '../../layouts/Layout.astro'
+tags: [space, 90s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
+
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
+
+The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
+
+NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css
new file mode 100644
index 000000000..3959523ff
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css
@@ -0,0 +1,15 @@
+.bg-skyblue {
+ background: skyblue;
+}
+
+.bg-lightcoral {
+ background: lightcoral;
+}
+
+.red {
+ color: darkred;
+}
+
+.blue {
+ color: royalblue;
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro
new file mode 100644
index 000000000..b78de296c
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro
@@ -0,0 +1,35 @@
+---
+import '../imported.css';
+import Button from '../components/Button.astro';
+
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <Button>Button used in layout</Button>
+ <slot />
+ </body>
+</html>
+<style is:global>
+ html {
+ font-family: system-ui, sans-serif;
+ background-color: #F6F6F6;
+ }
+ code {
+ font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
+ Bitstream Vera Sans Mono, Courier New, monospace;
+ }
+</style>
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro
new file mode 100644
index 000000000..bfdbeb5f8
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro
@@ -0,0 +1,17 @@
+---
+import Button from '../components/Button.astro';
+import { getEntryBySlug } from 'astro:content';
+
+const entry = await getEntryBySlug('en', 'endeavour');
+const { Content } = await entry.render();
+---
+<style>
+ #welcome::after {
+ content: '🚀'
+ }
+</style>
+<main>
+ <h1 id="welcome">Welcome to Astro</h1>
+ <Content/>
+ <Button>Button used directly in page</Button>
+</main>
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json b/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json
new file mode 100644
index 000000000..382288fbc
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@test/css-inline-stylesheets-never",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro
new file mode 100644
index 000000000..3f25cbd3e
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro
@@ -0,0 +1,86 @@
+---
+const { class: className = '', style, href } = Astro.props;
+const { variant = 'primary' } = Astro.props;
+---
+
+<span class:list={[`link pixel variant-${variant}`, className]} >
+ <a {href}>
+ <span><slot /></span>
+ </a>
+</span>
+
+<style>
+ .link {
+ --border-radius: 8;
+ --duration: 200ms;
+ --delay: 30ms;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ display: flex;
+ color: white;
+ font-size: 1.25rem;
+ width: max-content;
+ }
+ a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.67rem 1.25rem;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ color: inherit !important;
+ /* Indicates the button boundaries for forced colors users in older browsers */
+ outline: 1px solid transparent;
+ }
+
+ @media (forced-colors: active) {
+ a {
+ border: 1px solid LinkText;
+ }
+ }
+
+ a > :global(* + *) {
+ margin-inline-start: 0.25rem;
+ }
+
+ .variant-primary {
+ --variant: primary;
+ --background: linear-gradient(180deg, var(--link-color-stop-a), var(--link-color-stop-b));
+ }
+ .variant-primary:hover,
+ .variant-primary:focus-within {
+ --link-color-stop-a: #6d39ff;
+ --link-color-stop-b: #af43ff;
+ }
+ .variant-primary:active {
+ --link-color-stop-a: #5f31e1;
+ --link-color-stop-b: #a740f3;
+ }
+
+ .variant-outline {
+ --variant: outline;
+ --background: none;
+ color: var(--background);
+ }
+ .variant-outline > a::before {
+ position: absolute;
+ top: 0;
+ right: calc(var(--pixel-size) * 1px);
+ bottom: calc(var(--pixel-size) * 1px);
+ left: calc(var(--pixel-size) * 1px);
+ content: '';
+ display: block;
+ transform-origin: bottom center;
+ background: linear-gradient(to top, var(--background), rgba(255, 255, 255, 0));
+ opacity: 0.3;
+ transform: scaleY(0);
+ transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+ .variant-outline:hover > a::before,
+ .variant-outline:focus-within > a::before {
+ transform: scaleY(1);
+ }
+ .variant-outline:active > a::before {
+ transform: scaleY(1);
+ }
+</style> \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md
new file mode 100644
index 000000000..240eeeae3
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md
@@ -0,0 +1,15 @@
+---
+title: Endeavour
+description: 'Learn about the Endeavour NASA space shuttle.'
+publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
+layout: '../../layouts/Layout.astro'
+tags: [space, 90s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
+
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
+
+The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
+
+NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly. \ No newline at end of file
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css
new file mode 100644
index 000000000..3959523ff
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css
@@ -0,0 +1,15 @@
+.bg-skyblue {
+ background: skyblue;
+}
+
+.bg-lightcoral {
+ background: lightcoral;
+}
+
+.red {
+ color: darkred;
+}
+
+.blue {
+ color: royalblue;
+}
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro
new file mode 100644
index 000000000..b78de296c
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro
@@ -0,0 +1,35 @@
+---
+import '../imported.css';
+import Button from '../components/Button.astro';
+
+export interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <Button>Button used in layout</Button>
+ <slot />
+ </body>
+</html>
+<style is:global>
+ html {
+ font-family: system-ui, sans-serif;
+ background-color: #F6F6F6;
+ }
+ code {
+ font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
+ Bitstream Vera Sans Mono, Courier New, monospace;
+ }
+</style>
diff --git a/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro
new file mode 100644
index 000000000..bfdbeb5f8
--- /dev/null
+++ b/packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro
@@ -0,0 +1,17 @@
+---
+import Button from '../components/Button.astro';
+import { getEntryBySlug } from 'astro:content';
+
+const entry = await getEntryBySlug('en', 'endeavour');
+const { Content } = await entry.render();
+---
+<style>
+ #welcome::after {
+ content: '🚀'
+ }
+</style>
+<main>
+ <h1 id="welcome">Welcome to Astro</h1>
+ <Content/>
+ <Button>Button used directly in page</Button>
+</main>
diff --git a/packages/astro/test/units/dev/head-injection.test.js b/packages/astro/test/units/dev/head-injection.test.js
index 9d76e0a91..ed3e085d3 100644
--- a/packages/astro/test/units/dev/head-injection.test.js
+++ b/packages/astro/test/units/dev/head-injection.test.js
@@ -35,7 +35,8 @@ describe('head injection', () => {
factory(result, props, slots) {
return createHeadAndContent(
unescapeHTML(renderUniqueStylesheet(result, {
- href: '/some/fake/styles.css'
+ type: 'external',
+ src: '/some/fake/styles.css'
})),
renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
);
@@ -113,7 +114,8 @@ describe('head injection', () => {
factory(result, props, slots) {
return createHeadAndContent(
unescapeHTML(renderUniqueStylesheet(result, {
- href: '/some/fake/styles.css'
+ type: 'external',
+ src: '/some/fake/styles.css'
})),
renderTemplate\`$\{renderComponent(result, 'Other', Other, props, slots)}\`
);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 972bf9384..83629c310 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2408,12 +2408,30 @@ importers:
packages/astro/test/fixtures/css-assets/packages/font-awesome: {}
- packages/astro/test/fixtures/css-inline:
+ packages/astro/test/fixtures/css-import-as-inline:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/css-inline-stylesheets/always:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../../..
+
+ packages/astro/test/fixtures/css-inline-stylesheets/auto:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../../..
+
+ packages/astro/test/fixtures/css-inline-stylesheets/never:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../../..
+
packages/astro/test/fixtures/css-no-code-split:
dependencies:
astro: