Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: configuration for css inlining behavior #6659

Merged
merged 14 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/friendly-fishes-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Implement Inline Stylesheets RFC as experimental
20 changes: 20 additions & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions packages/astro/src/content/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
createHeadAndContent,
renderComponent,
renderScriptElement,
renderStyleElement,
renderTemplate,
renderUniqueStylesheet,
unescapeHTML,
Expand Down Expand Up @@ -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('');
Expand Down
16 changes: 12 additions & 4 deletions packages/astro/src/content/vite-plugin-content-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions packages/astro/src/core/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -203,6 +205,7 @@ export class App {
pathname,
componentMetadata: this.#manifest.componentMetadata,
scripts,
styles,
links,
route: routeData,
status,
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +25,7 @@ export interface RouteInfo {
// Hoisted
| { type: 'inline' | 'external'; value: string }
)[];
styles: StylesheetAsset[];
}

export type SerializedRouteInfo = Omit<RouteInfo, 'routeData'> & {
Expand Down
47 changes: 38 additions & 9 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -183,6 +199,7 @@ async function generatePage(
internals,
linkIds,
scripts,
styles,
mod: pageModule,
renderers,
};
Expand Down Expand Up @@ -273,6 +290,7 @@ interface GeneratePathOptions {
internals: BuildInternals;
linkIds: string[];
scripts: { type: 'inline' | 'external'; value: string } | null;
styles: StylesheetAsset[];
mod: ComponentInstance;
renderers: SSRLoadedRenderer[];
}
Expand Down Expand Up @@ -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') {
Expand All @@ -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
);
Expand Down Expand Up @@ -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,
Expand Down
77 changes: 47 additions & 30 deletions packages/astro/src/core/build/internal.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
lilnasy marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/build/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading