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>
This commit is contained in:
parent
8d75340b7a
commit
80e3d4d3d0
49 changed files with 1364 additions and 319 deletions
5
.changeset/friendly-fishes-sing.md
Normal file
5
.changeset/friendly-fishes-sing.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': minor
|
||||
---
|
||||
|
||||
Implement Inline Stylesheets RFC as experimental
|
|
@ -1036,6 +1036,26 @@ export interface AstroUserConfig {
|
|||
*/
|
||||
assets?: boolean;
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'> & {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(([id]) => id);
|
||||
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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,217 +25,7 @@ interface PluginOptions {
|
|||
target: 'client' | 'server';
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
function* getParentClientOnlys(
|
||||
id: string,
|
||||
ctx: { getModuleInfo: GetModuleInfo }
|
||||
): Generator<PageBuildData, void, unknown> {
|
||||
for (const [info] of walkParentInfos(id, ctx)) {
|
||||
yield* getPageDatasByClientOnlyID(internals, info.id);
|
||||
}
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
},
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
return createNameForParentPages(id, meta);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async generateBundle(_outputOptions, bundle) {
|
||||
type ViteMetadata = {
|
||||
importedAssets: Set<string>;
|
||||
importedCss: Set<string>;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
/***** ASTRO PLUGIN *****/
|
||||
|
||||
export function pluginCSS(
|
||||
options: StaticBuildOptions,
|
||||
|
@ -258,3 +48,272 @@ export function pluginCSS(
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
/***** ROLLUP SUB-PLUGINS *****/
|
||||
|
||||
function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
|
||||
const { internals, buildOptions } = options;
|
||||
const { settings } = buildOptions;
|
||||
|
||||
let resolvedConfig: ResolvedConfig;
|
||||
|
||||
// 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>>> = {}
|
||||
|
||||
const cssBuildPlugin: VitePlugin = {
|
||||
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 '';
|
||||
}
|
||||
},
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
return createNameForParentPages(id, meta);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
// Skip if the chunk has no CSS, we want to handle CSS chunks only
|
||||
if (meta.importedCss.size < 1) continue;
|
||||
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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 ?? {};
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -19,7 +19,6 @@ export {
|
|||
renderScriptElement,
|
||||
renderSlot,
|
||||
renderSlotToString,
|
||||
renderStyleElement,
|
||||
renderTemplate as render,
|
||||
renderTemplate,
|
||||
renderToString,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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', () => {
|
285
packages/astro/test/css-inline-stylesheets.js
Normal file
285
packages/astro/test/css-inline-stylesheets.js
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
2
packages/astro/test/fixtures/content/src/components/H3.astro
vendored
Normal file
2
packages/astro/test/fixtures/content/src/components/H3.astro
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
<style>h3 { margin: 1rem }</style>
|
||||
<h3><slot /></h3>
|
|
@ -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) => (
|
||||
|
|
|
@ -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 />
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@test/css-inline",
|
||||
"name": "@test/css-import-as-inline",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
8
packages/astro/test/fixtures/css-inline-stylesheets/always/package.json
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets/always/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/css-inline-stylesheets-always",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
86
packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro
vendored
Normal file
86
packages/astro/test/fixtures/css-inline-stylesheets/always/src/components/Button.astro
vendored
Normal file
|
@ -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>
|
15
packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/always/src/content/en/endeavour.md
vendored
Normal file
|
@ -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.
|
15
packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/always/src/imported.css
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
.bg-skyblue {
|
||||
background: skyblue;
|
||||
}
|
||||
|
||||
.bg-lightcoral {
|
||||
background: lightcoral;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: royalblue;
|
||||
}
|
35
packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro
vendored
Normal file
35
packages/astro/test/fixtures/css-inline-stylesheets/always/src/layouts/Layout.astro
vendored
Normal file
|
@ -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>
|
17
packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro
vendored
Normal file
17
packages/astro/test/fixtures/css-inline-stylesheets/always/src/pages/index.astro
vendored
Normal file
|
@ -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>
|
8
packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets/auto/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/css-inline-stylesheets-auto",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
86
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro
vendored
Normal file
86
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/components/Button.astro
vendored
Normal file
|
@ -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>
|
15
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/content/en/endeavour.md
vendored
Normal file
|
@ -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.
|
15
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/imported.css
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
.bg-skyblue {
|
||||
background: skyblue;
|
||||
}
|
||||
|
||||
.bg-lightcoral {
|
||||
background: lightcoral;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: royalblue;
|
||||
}
|
35
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro
vendored
Normal file
35
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/layouts/Layout.astro
vendored
Normal file
|
@ -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>
|
17
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro
vendored
Normal file
17
packages/astro/test/fixtures/css-inline-stylesheets/auto/src/pages/index.astro
vendored
Normal file
|
@ -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>
|
8
packages/astro/test/fixtures/css-inline-stylesheets/never/package.json
vendored
Normal file
8
packages/astro/test/fixtures/css-inline-stylesheets/never/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/css-inline-stylesheets-never",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
86
packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro
vendored
Normal file
86
packages/astro/test/fixtures/css-inline-stylesheets/never/src/components/Button.astro
vendored
Normal file
|
@ -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>
|
15
packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/never/src/content/en/endeavour.md
vendored
Normal file
|
@ -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.
|
15
packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css
vendored
Normal file
15
packages/astro/test/fixtures/css-inline-stylesheets/never/src/imported.css
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
.bg-skyblue {
|
||||
background: skyblue;
|
||||
}
|
||||
|
||||
.bg-lightcoral {
|
||||
background: lightcoral;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: royalblue;
|
||||
}
|
35
packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro
vendored
Normal file
35
packages/astro/test/fixtures/css-inline-stylesheets/never/src/layouts/Layout.astro
vendored
Normal file
|
@ -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>
|
17
packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro
vendored
Normal file
17
packages/astro/test/fixtures/css-inline-stylesheets/never/src/pages/index.astro
vendored
Normal file
|
@ -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>
|
|
@ -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)}\`
|
||||
);
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue