Build/bundle assets and CSS (#1786)

* Bundling CSS

* Current progress of building assets

* New build progress

* Its finally working

* Force css to go through the build

* Prettier filenames

* Split into separate CSS and HTML plugins

* Always have at least one input

* Bring back in sitemaps + output

* Bring back srcset support

* Bundle CSS

* Bring back minify

* Update dynamic tests

* Update remaining tests

* Linting

* Fix remaining broken test

* Use fs directly

* Adding a changeset

* Use path.posix

* Debugging windows

* More debugging

* Pass URLs into readFile

* Remove some debugging stuff

* Remove force flag from transformWithVite

* Update packages/astro/src/vite-plugin-build-css/index.ts

Co-authored-by: Drew Powers <1369770+drwpow@users.noreply.github.com>

Co-authored-by: Drew Powers <1369770+drwpow@users.noreply.github.com>
This commit is contained in:
Matthew Phillips 2021-11-11 08:44:11 -05:00 committed by GitHub
parent 437203b74f
commit fd52bceea4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1211 additions and 403 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Update the build to build/bundle assets

View file

@ -66,7 +66,7 @@
"@babel/traverse": "^7.15.4",
"@proload/core": "^0.2.1",
"@proload/plugin-tsm": "^0.1.0",
"@web/rollup-plugin-html": "^1.10.1",
"@web/parse5-utils": "^1.3.0",
"astring": "^1.7.5",
"ci-info": "^3.2.0",
"connect": "^3.7.0",
@ -80,6 +80,7 @@
"mime": "^2.5.2",
"morphdom": "^2.6.1",
"node-fetch": "^2.6.5",
"parse5": "^6.0.1",
"path-to-regexp": "^6.2.0",
"prismjs": "^1.25.0",
"remark-slug": "^7.0.0",
@ -91,6 +92,7 @@
"shorthash": "^0.0.2",
"slash": "^4.0.0",
"sourcemap-codec": "^1.4.8",
"srcset-parse": "^1.1.0",
"string-width": "^5.0.0",
"strip-ansi": "^7.0.1",
"strip-indent": "^4.0.0",

View file

@ -1,7 +1,7 @@
import type babel from '@babel/core';
import type { z } from 'zod';
import type { AstroConfigSchema } from '../core/config';
import type { AstroComponentFactory } from '../runtime/server';
import type { AstroComponentFactory, Metadata } from '../runtime/server';
import type vite from '../../vendor/vite';
export interface AstroComponentMetadata {
@ -139,11 +139,9 @@ export interface CollectionRSS {
/** Generic interface for a component (Astro, Svelte, React, etc.) */
export interface ComponentInstance {
$$metadata: {
modules: { module: Record<string, unknown>; specifier: string }[];
fileURL: URL;
};
$$metadata: Metadata;
default: AstroComponentFactory;
css?: string[];
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}

View file

@ -48,6 +48,7 @@ export interface TopLevelAstro {
export interface SSRMetadata {
renderers: Renderer[];
pathname: string;
}
export interface SSRResult {

View file

@ -1,17 +1,19 @@
import type { InputHTMLOptions } from '@web/rollup-plugin-html';
import type { AstroConfig, ComponentInstance, GetStaticPathsResult, ManifestData, RouteCache, RouteData, RSSResult } from '../../@types/astro-core';
import type { LogOptions } from '../logger';
import type { AllPagesData } from './types';
import type { RenderedChunk } from 'rollup';
import { rollupPluginHTML } from '@web/rollup-plugin-html';
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
import fs from 'fs';
import { bold, cyan, green, dim } from 'kleur/colors';
import { bold, cyan, green } from 'kleur/colors';
import { performance } from 'perf_hooks';
import vite, { ViteDevServer } from '../vite.js';
import { fileURLToPath } from 'url';
import { createVite } from '../create-vite.js';
import { pad } from '../dev/util.js';
import { debug, defaultLogOptions, levels, timerMessage, warn } from '../logger.js';
import { ssr } from '../ssr/index.js';
import { preload as ssrPreload } from '../ssr/index.js';
import { generatePaginateFunction } from '../ssr/paginate.js';
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
import { generateRssFunction } from '../ssr/rss.js';
@ -71,9 +73,9 @@ class AstroBuilder {
this.viteServer = viteServer;
debug(logging, 'build', timerMessage('Vite started', timer.viteStart));
timer.renderStart = performance.now();
timer.loadStart = performance.now();
const assets: Record<string, string> = {};
const allPages: Record<string, RouteData & { paths: string[] }> = {};
const allPages: AllPagesData = {};
// Collect all routes ahead-of-time, before we start the build.
// NOTE: This enforces that `getStaticPaths()` is only called once per route,
// and is then cached across all future SSR builds. In the past, we've had trouble
@ -82,7 +84,21 @@ class AstroBuilder {
this.manifest.routes.map(async (route) => {
// static route:
if (route.pathname) {
allPages[route.component] = { ...route, paths: [route.pathname] };
allPages[route.component] = {
route,
paths: [route.pathname],
preload: await ssrPreload({
astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot),
logging,
mode: 'production',
origin,
pathname: route.pathname,
route,
routeCache: this.routeCache,
viteServer,
})
};
return;
}
// dynamic route:
@ -94,38 +110,37 @@ class AstroBuilder {
}
assets[fileURLToPath(rssFile)] = result.rss.xml;
}
allPages[route.component] = { ...route, paths: result.paths };
allPages[route.component] = {
route,
paths: result.paths,
preload: await ssrPreload({
astroConfig: this.config,
filePath: new URL(`./${route.component}`, this.config.projectRoot),
logging,
mode: 'production',
origin,
pathname: result.paths[0],
route,
routeCache: this.routeCache,
viteServer,
})
};
})
);
debug(logging, 'build', timerMessage('All pages loaded', timer.loadStart));
// After all routes have been collected, start building them.
// TODO: test parallel vs. serial performance. Promise.all() may be
// making debugging harder without any perf gain. If parallel is best,
// then we should set a max number of parallel builds.
const input: InputHTMLOptions[] = [];
await Promise.all(
Object.entries(allPages).map(([component, route]) =>
Promise.all(
route.paths.map(async (pathname) => {
input.push({
html: await ssr({
astroConfig: this.config,
filePath: new URL(`./${component}`, this.config.projectRoot),
logging,
mode: 'production',
origin,
pathname,
route,
routeCache: this.routeCache,
viteServer,
}),
name: pathname.replace(/\/?$/, '/index.html').replace(/^\//, ''),
});
})
)
)
);
debug(logging, 'build', timerMessage('All pages rendered', timer.renderStart));
// Pure CSS chunks are chunks that only contain CSS.
// This is all of them, and chunkToReferenceIdMap maps them to a hash id used to find the final file.
const pureCSSChunks = new Set<RenderedChunk>();
const chunkToReferenceIdMap = new Map<string, string>();
// This is a mapping of pathname to the string source of all collected
// inline <style> for a page.
const astroStyleMap = new Map<string, string>();
// This is a virtual JS module that imports all dependent styles for a page.
const astroPageStyleMap = new Map<string, string>();
const pageNames: string[] = [];
// Bundle the assets in your final build: This currently takes the HTML output
// of every page (stored in memory) and bundles the assets pointed to on those pages.
@ -138,17 +153,32 @@ class AstroBuilder {
minify: 'esbuild', // significantly faster than "terser" but may produce slightly-bigger bundles
outDir: fileURLToPath(this.config.dist),
rollupOptions: {
// The `input` will be populated in the build rollup plugin.
input: [],
output: { format: 'esm' },
},
target: 'es2020', // must match an esbuild target
},
plugins: [
rollupPluginHTML({
rootDir: viteConfig.root,
input,
extractAssets: false,
}) as any, // "any" needed for CI; also we dont need typedefs for this anyway
rollupPluginAstroBuildHTML({
astroConfig: this.config,
astroPageStyleMap,
astroStyleMap,
chunkToReferenceIdMap,
pureCSSChunks,
logging,
origin,
allPages,
pageNames,
routeCache: this.routeCache,
viteServer
}),
rollupPluginAstroBuildCSS({
astroPageStyleMap,
astroStyleMap,
chunkToReferenceIdMap,
pureCSSChunks
}),
...(viteConfig.plugins || []),
],
publicDir: viteConfig.publicDir,
@ -172,7 +202,7 @@ class AstroBuilder {
timer.sitemapStart = performance.now();
if (this.config.buildOptions.sitemap && this.config.buildOptions.site) {
const sitemapStart = performance.now();
const sitemap = generateSitemap(input.map(({ name }) => new URL(`/${name}`, this.config.buildOptions.site).href));
const sitemap = generateSitemap(pageNames.map(pageName => new URL(`/${pageName}`, this.config.buildOptions.site).href));
const sitemapPath = new URL('./sitemap.xml', this.config.dist);
await fs.promises.mkdir(new URL('./', sitemapPath), { recursive: true });
await fs.promises.writeFile(sitemapPath, sitemap, 'utf8');
@ -182,7 +212,7 @@ class AstroBuilder {
// You're done! Time to clean up.
await viteServer.close();
if (logging.level && levels[logging.level] <= levels['info']) {
await this.printStats({ cwd: this.config.dist, pageCount: input.length });
await this.printStats({ cwd: this.config.dist, pageCount: pageNames.length });
}
}

View file

@ -0,0 +1,10 @@
import type { ComponentPreload } from '../ssr/index';
import type { RouteData } from '../../@types/astro-core';
export interface PageBuildData {
paths: string[];
preload: ComponentPreload;
route: RouteData;
}
export type AllPagesData = Record<string, PageBuildData>;

View file

@ -7,7 +7,7 @@ import type { LogOptions } from '../logger';
import fs from 'fs';
import path from 'path';
import { renderPage, renderSlot } from '../../runtime/server/index.js';
import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency } from '../util.js';
import { canonicalURL as getCanonicalURL, codeFrame, resolveDependency, viteifyPath } from '../util.js';
import { getStylesForID } from './css.js';
import { injectTags } from './html.js';
import { generatePaginateFunction } from './paginate.js';
@ -72,145 +72,174 @@ async function resolveRenderers(viteServer: vite.ViteDevServer, astroConfig: Ast
return renderers;
}
/** use Vite to SSR */
export async function ssr({ astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer }: SSROptions): Promise<string> {
try {
// Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await resolveRenderers(viteServer, astroConfig);
// Load the module from the Vite SSR Runtime.
const viteFriendlyURL = `/@fs${filePath.pathname}`;
const mod = (await viteServer.ssrLoadModule(viteFriendlyURL)) as ComponentInstance;
// Handle dynamic routes
let params: Params = {};
let pageProps: Props = {};
if (route && !route.pathname) {
if (route.params.length) {
const paramsMatch = route.pattern.exec(pathname);
if (paramsMatch) {
params = getParams(route.params)(paramsMatch);
}
}
validateGetStaticPathsModule(mod);
if (!routeCache[route.component]) {
routeCache[route.component] = await (
await mod.getStaticPaths!({
paginate: generatePaginateFunction(route),
rss: () => {
/* noop */
},
})
).flat();
}
validateGetStaticPathsResult(routeCache[route.component], logging);
const routePathParams: GetStaticPathsResult = routeCache[route.component];
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
if (!matchedStaticPath) {
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
}
pageProps = { ...matchedStaticPath.props } || {};
}
// Validate the page component before rendering the page
const Component = await mod.default;
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params,
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
// This is used for <Markdown> but shouldn't be used publicly
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null);
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
let render = astroConfig.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(render)) {
renderOpts = render[1];
render = render[0];
}
if (typeof render === 'string') {
({ default: render } = await import(render));
}
const { code } = await render(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
_metadata: { renderers },
};
let html = await renderPage(result, Component, pageProps, null);
// inject tags
const tags: vite.HtmlTagDescriptor[] = [];
// dev only: inject Astro HMR client
if (mode === 'development') {
tags.push({
tag: 'script',
attrs: { type: 'module' },
children: `import 'astro/runtime/client/hmr.js';`,
injectTo: 'head',
});
}
// inject CSS
[...getStylesForID(filePath.pathname, viteServer)].forEach((href) => {
tags.push({
tag: 'link',
attrs: { type: 'text/css', rel: 'stylesheet', href },
injectTo: 'head',
});
});
// add injected tags
html = injectTags(html, tags);
// run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development') {
html = await viteServer.transformIndexHtml(filePath.pathname, html, pathname);
}
return html;
} catch (e: any) {
async function errorHandler(e: unknown, viteServer: vite.ViteDevServer, filePath: URL) {
if(e instanceof Error) {
viteServer.ssrFixStacktrace(e);
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
if (e.errors) {
const { location, pluginName, text } = (e as BuildResult).errors[0];
const err = new Error(text) as SSRError;
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
const frame = codeFrame(await fs.promises.readFile(filePath, 'utf8'), err.loc);
err.frame = frame;
err.id = location?.file;
err.message = `${location?.file}: ${text}
}
// Astro error (thrown by esbuild so it needs to be formatted for Vite)
const anyError = e as any;
if (anyError.errors) {
const { location, pluginName, text } = (e as BuildResult).errors[0];
const err = new Error(text) as SSRError;
if (location) err.loc = { file: location.file, line: location.line, column: location.column };
const frame = codeFrame(await fs.promises.readFile(filePath, 'utf8'), err.loc);
err.frame = frame;
err.id = location?.file;
err.message = `${location?.file}: ${text}
${frame}
`;
err.stack = e.stack;
if (pluginName) err.plugin = pluginName;
throw err;
}
err.stack = anyError.stack;
if (pluginName) err.plugin = pluginName;
throw err;
}
// Generic error (probably from Vite, and already formatted)
// Generic error (probably from Vite, and already formatted)
throw e;
}
export type ComponentPreload = [Renderer[], ComponentInstance];
export async function preload({ astroConfig, filePath, viteServer }: SSROptions): Promise<ComponentPreload> {
// Important: This needs to happen first, in case a renderer provides polyfills.
const renderers = await resolveRenderers(viteServer, astroConfig);
// Load the module from the Vite SSR Runtime.
const viteFriendlyURL = viteifyPath(filePath.pathname);
const mod = (await viteServer.ssrLoadModule(viteFriendlyURL)) as ComponentInstance;
return [renderers, mod];
}
/** use Vite to SSR */
export async function render(renderers: Renderer[], mod: ComponentInstance, ssrOpts: SSROptions): Promise<string> {
const { astroConfig, filePath, logging, mode, origin, pathname, route, routeCache, viteServer } = ssrOpts;
// Handle dynamic routes
let params: Params = {};
let pageProps: Props = {};
if (route && !route.pathname) {
if (route.params.length) {
const paramsMatch = route.pattern.exec(pathname);
if (paramsMatch) {
params = getParams(route.params)(paramsMatch);
}
}
validateGetStaticPathsModule(mod);
if (!routeCache[route.component]) {
routeCache[route.component] = await (
await mod.getStaticPaths!({
paginate: generatePaginateFunction(route),
rss: () => {
/* noop */
},
})
).flat();
}
validateGetStaticPathsResult(routeCache[route.component], logging);
const routePathParams: GetStaticPathsResult = routeCache[route.component];
const matchedStaticPath = routePathParams.find(({ params: _params }) => JSON.stringify(_params) === JSON.stringify(params));
if (!matchedStaticPath) {
throw new Error(`[getStaticPaths] route pattern matched, but no matching static path found. (${pathname})`);
}
pageProps = { ...matchedStaticPath.props } || {};
}
// Validate the page component before rendering the page
const Component = await mod.default;
if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`);
if (!Component.isAstroComponentFactory) throw new Error(`Unable to SSR non-Astro component (${route?.component})`);
// Create the result object that will be passed into the render function.
// This object starts here as an empty shell (not yet the result) but then
// calling the render() function will populate the object with scripts, styles, etc.
const result: SSRResult = {
styles: new Set<SSRElement>(),
scripts: new Set<SSRElement>(),
/** This function returns the `Astro` faux-global */
createAstro(astroGlobal: TopLevelAstro, props: Record<string, any>, slots: Record<string, any> | null) {
const site = new URL(origin);
const url = new URL('.' + pathname, site);
const canonicalURL = getCanonicalURL('.' + pathname, astroConfig.buildOptions.site || origin);
return {
__proto__: astroGlobal,
props,
request: {
canonicalURL,
params,
url,
},
slots: Object.fromEntries(Object.entries(slots || {}).map(([slotName]) => [slotName, true])),
// This is used for <Markdown> but shouldn't be used publicly
privateRenderSlotDoNotUse(slotName: string) {
return renderSlot(result, slots ? slots[slotName] : null);
},
// <Markdown> also needs the same `astroConfig.markdownOptions.render` as `.md` pages
async privateRenderMarkdownDoNotUse(content: string, opts: any) {
let mdRender = astroConfig.markdownOptions.render;
let renderOpts = {};
if (Array.isArray(mdRender)) {
renderOpts = mdRender[1];
mdRender = mdRender[0];
}
if (typeof mdRender === 'string') {
({ default: mdRender } = await import(mdRender));
}
const { code } = await mdRender(content, { ...renderOpts, ...(opts ?? {}) });
return code;
},
} as unknown as AstroGlobal;
},
_metadata: {
renderers,
pathname
},
};
let html = await renderPage(result, Component, pageProps, null);
// inject tags
const tags: vite.HtmlTagDescriptor[] = [];
// dev only: inject Astro HMR client
if (mode === 'development') {
tags.push({
tag: 'script',
attrs: { type: 'module' },
children: `import 'astro/runtime/client/hmr.js';`,
injectTo: 'head',
});
}
// inject CSS
[...getStylesForID(filePath.pathname, viteServer)].forEach((href) => {
tags.push({
tag: 'link',
attrs: {
rel: 'stylesheet',
href,
'data-astro-injected': true
},
injectTo: 'head',
});
});
// add injected tags
html = injectTags(html, tags);
// run transformIndexHtml() in dev to run Vite dev transformations
if (mode === 'development') {
html = await viteServer.transformIndexHtml(filePath.pathname, html, pathname);
}
return html;
}
export async function ssr(ssrOpts: SSROptions): Promise<string> {
try {
const [renderers, mod] = await preload(ssrOpts);
return render(renderers, mod, ssrOpts);
} catch (e: unknown) {
await errorHandler(e, ssrOpts.viteServer, ssrOpts.filePath);
throw e;
}
}
}

View file

@ -71,3 +71,7 @@ export function resolveDependency(dep: string, astroConfig: AstroConfig) {
// For Windows compat, we need a fully resolved `file://` URL string
return pathToFileURL(resolved).toString();
}
export function viteifyPath(pathname: string): string {
return `/@fs${pathname}`;
}

View file

@ -6,6 +6,7 @@ import shorthash from 'shorthash';
import { extractDirectives, generateHydrateScript } from './hydration.js';
import { serializeListValue } from './util.js';
export { createMetadata } from './metadata.js';
export type { Metadata } from './metadata';
// INVESTIGATE:
// 2. Less anys when possible and make it well known when they are needed.
@ -271,10 +272,16 @@ export async function renderPage(result: SSRResult, Component: AstroComponentFac
const template = await renderToString(result, Component, props, children);
const styles = Array.from(result.styles)
.filter(uniqueElements)
.map((style) => renderElement('style', style));
.map((style) => renderElement('style', {
...style,
props: { ...style.props, 'astro-style': true }
}));
const scripts = Array.from(result.scripts)
.filter(uniqueElements)
.map((script) => renderElement('script', script));
.map((script, i) => renderElement('script', {
...script,
props: { ...script.props, 'astro-script': result._metadata.pathname + '/script-' + i }
}));
return template.replace('</head>', styles.join('\n') + scripts.join('\n') + '</head>');
}

View file

@ -8,10 +8,10 @@ interface ComponentMetadata {
componentUrl: string;
}
class Metadata {
export class Metadata {
public fileURL: URL;
private metadataCache: Map<any, ComponentMetadata | null>;
constructor(fileURL: string, public modules: ModuleInfo[], components: any[]) {
constructor(fileURL: string, public modules: ModuleInfo[], public hydratedComponents: any[], public hoisted: any[]) {
this.fileURL = new URL(fileURL);
this.metadataCache = new Map<any, ComponentMetadata | null>();
}
@ -26,6 +26,26 @@ class Metadata {
return metadata?.componentExport || null;
}
// Recursively collect all of the hydrated components' paths.
getAllHydratedComponentPaths(): Set<string> {
const paths = new Set<string>();
for(const component of this.hydratedComponents) {
const path = this.getPath(component);
if(path) {
paths.add(path);
}
}
for(const {module: mod} of this.modules) {
if(typeof mod.$$metadata !== 'undefined') {
for(const path of mod.$$metadata.getAllHydratedComponentPaths()) {
paths.add(path);
}
}
}
return paths;
}
private getComponentMetadata(Component: any): ComponentMetadata | null {
if (this.metadataCache.has(Component)) {
return this.metadataCache.get(Component)!;
@ -66,5 +86,5 @@ interface CreateMetadataOptions {
}
export function createMetadata(fileURL: string, options: CreateMetadataOptions) {
return new Metadata(fileURL, options.modules, options.hydratedComponents);
return new Metadata(fileURL, options.modules, options.hydratedComponents, options.hoisted);
}

View file

@ -0,0 +1,146 @@
import type { ResolveIdHook, LoadHook, RenderedChunk } from 'rollup';
import type { Plugin as VitePlugin } from 'vite';
import { STYLE_EXTENSIONS } from '../core/ssr/css.js';
import { getViteResolve, getViteLoad } from './resolve.js';
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
import * as path from 'path';
const PLUGIN_NAME = '@astrojs/rollup-plugin-build-css';
// This is a virtual module that represents the .astro <style> usage on a page
const ASTRO_STYLE_PREFIX = '@astro-inline-style';
const ASTRO_PAGE_STYLE_PREFIX = '@astro-page-all-styles';
const isCSSRequest = (request: string) => STYLE_EXTENSIONS.has(path.extname(request));
export function getAstroPageStyleId(pathname: string) {
let styleId = ASTRO_PAGE_STYLE_PREFIX + pathname;
if(styleId.endsWith('/')) {
styleId += 'index';
}
styleId += '.js';
return styleId;
}
export function getAstroStyleId(pathname: string) {
let styleId = ASTRO_STYLE_PREFIX + pathname;
if(styleId.endsWith('/')) {
styleId += 'index';
}
styleId += '.css';
return styleId;
}
export function getAstroStylePathFromId(id: string) {
return id.substr(ASTRO_STYLE_PREFIX.length + 1);
}
function isStyleVirtualModule(id: string) {
return id.startsWith(ASTRO_STYLE_PREFIX);
}
function isPageStyleVirtualModule(id: string) {
return id.startsWith(ASTRO_PAGE_STYLE_PREFIX);
}
interface PluginOptions {
astroStyleMap: Map<string, string>;
astroPageStyleMap: Map<string, string>;
chunkToReferenceIdMap: Map<string, string>;
pureCSSChunks: Set<RenderedChunk>;
}
export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {
const { astroPageStyleMap, astroStyleMap, chunkToReferenceIdMap, pureCSSChunks } = options;
const styleSourceMap = new Map<string, string>();
let viteTransform: TransformHook;
return {
name: PLUGIN_NAME,
enforce: 'pre',
configResolved(resolvedConfig) {
viteTransform = getViteTransform(resolvedConfig);
},
async resolveId(id) {
if(isPageStyleVirtualModule(id)) {
return id;
}
if(isStyleVirtualModule(id)) {
return id;
}
return undefined;
},
async load(id) {
if(isPageStyleVirtualModule(id)) {
const source = astroPageStyleMap.get(id)!;
return source;
}
if(isStyleVirtualModule(id)) {
return astroStyleMap.get(id)!;
}
return null;
},
async transform(value, id) {
if(isStyleVirtualModule(id)) {
styleSourceMap.set(id, value);
return null;
}
if(isCSSRequest(id)) {
let result = await viteTransform(value, id);
if(result) {
styleSourceMap.set(id, result.code);
} else {
styleSourceMap.set(id, value);
}
return result;
}
return null;
},
renderChunk(_code, chunk) {
let chunkCSS = '';
let isPureCSS = true;
for(const [id] of Object.entries(chunk.modules)) {
if(!isCSSRequest(id) && !isPageStyleVirtualModule(id)) {
isPureCSS = false;
}
if(styleSourceMap.has(id)) {
chunkCSS += styleSourceMap.get(id)!;
}
}
if(isPureCSS) {
const referenceId = this.emitFile({
name: chunk.name + '.css',
type: 'asset',
source: chunkCSS
});
pureCSSChunks.add(chunk);
chunkToReferenceIdMap.set(chunk.fileName, referenceId);
}
return null;
},
// Delete CSS chunks so JS is not produced for them.
generateBundle(_options, bundle) {
for(const [chunkId, chunk] of Object.entries(bundle)) {
if(chunk.type === 'chunk' && pureCSSChunks.has(chunk)) {
delete bundle[chunkId];
}
}
}
}
}

View file

@ -0,0 +1,27 @@
import type { ResolveIdHook, LoadHook } from 'rollup';
import type { ResolvedConfig, Plugin as VitePlugin } from 'vite';
export function getVitePluginByName(viteConfig: ResolvedConfig, pluginName: string): VitePlugin {
const plugin = viteConfig.plugins.find(({ name }) => name === pluginName);
if (!plugin) throw new Error(`${pluginName} plugin couldnt be found`);
return plugin;}
export function getViteResolvePlugin(viteConfig: ResolvedConfig): VitePlugin {
return getVitePluginByName(viteConfig, 'vite:resolve');
}
export function getViteLoadFallbackPlugin(viteConfig: ResolvedConfig): VitePlugin {
return getVitePluginByName(viteConfig, 'vite:load-fallback');
}
export function getViteResolve(viteConfig: ResolvedConfig): ResolveIdHook {
const plugin = getViteResolvePlugin(viteConfig);
if (!plugin.resolveId) throw new Error(`vite:resolve has no resolveId() hook`);
return plugin.resolveId.bind(null as any) as any;
}
export function getViteLoad(viteConfig: ResolvedConfig): LoadHook {
const plugin = getViteLoadFallbackPlugin(viteConfig);
if (!plugin.load) throw new Error(`vite:load-fallback has no load() hook`);
return plugin.load.bind(null as any) as any;
}

View file

@ -0,0 +1,49 @@
import { InputOptions } from 'rollup';
function fromEntries<V>(entries: [string, V][]) {
const obj: Record<string, V> = {};
for (const [k, v] of entries) {
obj[k] = v;
}
return obj;
}
export function addRollupInput(
inputOptions: InputOptions,
newInputs: string[]
): InputOptions {
// Add input module ids to existing input option, whether it's a string, array or object
// this way you can use multiple html plugins all adding their own inputs
if (!inputOptions.input) {
return { ...inputOptions, input: newInputs };
}
if (typeof inputOptions.input === 'string') {
return {
...inputOptions,
input: [inputOptions.input, ...newInputs],
};
}
if (Array.isArray(inputOptions.input)) {
return {
...inputOptions,
input: [...inputOptions.input, ...newInputs],
};
}
if (typeof inputOptions.input === 'object') {
return {
...inputOptions,
input: {
...inputOptions.input,
...fromEntries(
newInputs
.map(i => [i.split('/').slice(-1)[0].split('.')[0], i]),
),
},
};
}
throw new Error(`Unknown rollup input type. Supported inputs are string, array and object.`);
}

View file

@ -0,0 +1,204 @@
import { Document, Element, Node } from 'parse5';
import npath from 'path';
import { findElements, getTagName, getAttribute, findNodes } from '@web/parse5-utils';
import adapter from 'parse5/lib/tree-adapters/default.js';
const hashedLinkRels = ['stylesheet', 'preload'];
const linkRels = [...hashedLinkRels, 'icon', 'manifest', 'apple-touch-icon', 'mask-icon'];
function getSrcSetUrls(srcset: string) {
if (!srcset) {
return [];
}
const srcsetParts = srcset.includes(',') ? srcset.split(',') : [srcset];
const urls = srcsetParts
.map(url => url.trim())
.map(url => (url.includes(' ') ? url.split(' ')[0] : url));
return urls;
}
function extractFirstUrlOfSrcSet(node: Element) {
const srcset = getAttribute(node, 'srcset');
if (!srcset) {
return '';
}
const urls = getSrcSetUrls(srcset);
return urls[0];
}
function isAsset(node: Element) {
let path = '';
switch (getTagName(node)) {
case 'img':
path = getAttribute(node, 'src') ?? '';
break;
case 'source':
path = extractFirstUrlOfSrcSet(node) ?? '';
break;
case 'link':
if (linkRels.includes(getAttribute(node, 'rel') ?? '')) {
path = getAttribute(node, 'href') ?? '';
}
break;
case 'meta':
if (getAttribute(node, 'property') === 'og:image' && getAttribute(node, 'content')) {
path = getAttribute(node, 'content') ?? '';
}
break;
case 'script':
if (getAttribute(node, 'type') !== 'module' && getAttribute(node, 'src')) {
path = getAttribute(node, 'src') ?? '';
}
break;
default:
return false;
}
if (!path) {
return false;
}
try {
new URL(path);
return false;
} catch (e) {
return true;
}
}
function isInlineScript(node: Element): boolean {
switch (getTagName(node)) {
case 'script':
if (getAttribute(node, 'type') === 'module' && !getAttribute(node, 'src')) {
return true;
}
return false;
default:
return false;
}
}
function isInlineStyle(node: Element): boolean {
return getTagName(node) === 'style';
}
export function isStylesheetLink(node: Element): boolean {
return getTagName(node) === 'link' && getAttribute(node, 'rel') === 'stylesheet';
}
export function isHashedAsset(node: Element) {
switch (getTagName(node)) {
case 'img':
return true;
case 'source':
return true;
case 'script':
return true;
case 'link':
return hashedLinkRels.includes(getAttribute(node, 'rel')!);
case 'meta':
return true;
default:
return false;
}
}
export function resolveAssetFilePath(
browserPath: string,
htmlDir: string,
projectRootDir: string,
absolutePathPrefix?: string,
) {
const _browserPath =
absolutePathPrefix && browserPath[0] === '/'
? '/' + npath.posix.relative(absolutePathPrefix, browserPath)
: browserPath;
return npath.join(
_browserPath.startsWith('/') ? projectRootDir : htmlDir,
_browserPath.split('/').join(npath.sep),
);
}
export function getSourceAttribute(node: Element) {
switch (getTagName(node)) {
case 'img': {
return 'src';
}
case 'source': {
return 'srcset';
}
case 'link': {
return 'href';
}
case 'script': {
return 'src';
}
case 'meta': {
return 'content';
}
default:
throw new Error(`Unknown node with tagname ${getTagName(node)}`);
}
}
export interface Location {
start: number;
end: number;
}
export function getSourcePaths(node: Element) {
const key = getSourceAttribute(node);
let location: Location = { start: 0, end: 0 };
const src = getAttribute(node, key);
if(node.sourceCodeLocation) {
let loc = node.sourceCodeLocation.attrs[key];
if(loc) {
location.start = loc.startOffset;
location.end = loc.endOffset;
}
}
if (typeof key !== 'string' || src === '') {
throw new Error(`Missing attribute ${key} in element ${node.nodeName}`);
}
let paths: {path: string, location: Location}[] = [];
if(src && key === 'srcset') {
paths = getSrcSetUrls(src).map(path => ({
path,
location
}));
} else if(src) {
paths.push({
path: src,
location
});
}
return paths;
}
export function getTextContent(node: Node): string {
if (adapter.isCommentNode(node)) {
return node.data || '';
}
if (adapter.isTextNode(node)) {
return node.value || '';
}
const subtree = findNodes(node, n => adapter.isTextNode(n));
return subtree.map(getTextContent).join('');
}
export function findAssets(document: Document) {
return findElements(document, isAsset);
}
export function findInlineScripts(document: Document) {
return findElements(document, isInlineScript);
}
export function findInlineStyles(document: Document) {
return findElements(document, isInlineStyle);
}
export function findStyleLinks(document: Document) {
return findElements(document, isStylesheetLink);
}

View file

@ -0,0 +1,391 @@
import type { AstroConfig, RouteCache } from '../@types/astro-core';
import type { LogOptions } from '../core/logger';
import type { ViteDevServer, Plugin as VitePlugin } from 'vite';
import type { OutputChunk, PreRenderedChunk, RenderedChunk } from 'rollup';
import type { AllPagesData } from '../core/build/types';
import parse5 from 'parse5';
import srcsetParse from 'srcset-parse';
import * as npath from 'path';
import { promises as fs } from 'fs';
import { getAttribute, getTagName, insertBefore, remove, createScript, createElement, setAttribute } from '@web/parse5-utils';
import { addRollupInput } from './add-rollup-input.js';
import { findAssets, findInlineScripts, findInlineStyles, getTextContent, isStylesheetLink } from './extract-assets.js';
import { render as ssrRender } from '../core/ssr/index.js';
import { getAstroStyleId, getAstroPageStyleId } from '../vite-plugin-build-css/index.js';
import { viteifyPath } from '../core/util.js';
// This package isn't real ESM, so have to coerce it
const matchSrcset: typeof srcsetParse = (srcsetParse as any).default;
const PLUGIN_NAME = '@astro/rollup-plugin-build';
const ASTRO_PAGE_PREFIX = '@astro-page';
const ASTRO_SCRIPT_PREFIX = '@astro-script';
const ASTRO_EMPTY = '@astro-empty';
const tagsWithSrcSet = new Set(['img', 'source']);
const isAstroInjectedLink = (node: parse5.Element) => isStylesheetLink(node) && getAttribute(node, 'data-astro-injected') === '';
const isBuildableLink = (node: parse5.Element, srcRoot: string) => isAstroInjectedLink(node) || getAttribute(node, 'href')?.startsWith(srcRoot);
const isBuildableImage = (node: parse5.Element, srcRoot: string) => getTagName(node) === 'img' && getAttribute(node, 'src')?.startsWith(srcRoot);
const hasSrcSet = (node: parse5.Element) => tagsWithSrcSet.has(getTagName(node)) && !!getAttribute(node, 'srcset');
interface PluginOptions {
astroConfig: AstroConfig;
astroStyleMap: Map<string, string>;
astroPageStyleMap: Map<string, string>;
chunkToReferenceIdMap: Map<string, string>;
logging: LogOptions;
allPages: AllPagesData;
pageNames: string[];
pureCSSChunks: Set<RenderedChunk>;
origin: string;
routeCache: RouteCache;
viteServer: ViteDevServer;
}
export function rollupPluginAstroBuildHTML(options: PluginOptions): VitePlugin {
const { astroConfig, astroStyleMap, astroPageStyleMap, chunkToReferenceIdMap, pureCSSChunks, logging, origin, allPages, routeCache, viteServer, pageNames } = options;
const srcRoot = astroConfig.src.pathname;
// A map of pages to rendered HTML
const renderedPageMap = new Map<string, string>();
//
const astroScriptMap = new Map<string, string>();
const astroPageMap = new Map<string, string>();
const astroAssetMap = new Map<string, Promise<Buffer>>();
const cssChunkMap = new Map<string, string[]>();
return {
name: PLUGIN_NAME,
enforce: 'pre',
async options(inputOptions) {
const htmlInput: Set<string> = new Set();
const assetInput: Set<string> = new Set(); // TODO remove?
const jsInput: Set<string> = new Set();
for(const [component, pageData] of Object.entries(allPages)) {
const [renderers, mod] = pageData.preload;
for(const path of mod.$$metadata.getAllHydratedComponentPaths()) {
jsInput.add(path);
}
for(const pathname of pageData.paths) {
pageNames.push(pathname.replace(/\/?$/, '/index.html').replace(/^\//, ''));
const id = ASTRO_PAGE_PREFIX + pathname;
const html = await ssrRender(renderers, mod, {
astroConfig,
filePath: new URL(`./${component}`, astroConfig.projectRoot),
logging,
mode: 'production',
origin,
pathname,
route: pageData.route,
routeCache,
viteServer,
});
renderedPageMap.set(id, html);
const document = parse5.parse(html, {
sourceCodeLocationInfo: true
});
const frontEndImports = [];
for(const script of findInlineScripts(document)) {
const astroScript = getAttribute(script, 'astro-script');
if(astroScript) {
const js = getTextContent(script);
const scriptId = ASTRO_SCRIPT_PREFIX + astroScript;
frontEndImports.push(scriptId);
astroScriptMap.set(scriptId, js);
}
}
let styles = '';
for(const node of findInlineStyles(document)) {
if(getAttribute(node, 'astro-style')) {
styles += getTextContent(node);
}
}
const assetImports = [];
for(let node of findAssets(document)) {
if(isBuildableLink(node, srcRoot)) {
const href = getAttribute(node, 'href')!;
const linkId = viteifyPath(href);
assetImports.push(linkId);
}
if(isBuildableImage(node, srcRoot)) {
const src = getAttribute(node, 'src');
if(src?.startsWith(srcRoot) && !astroAssetMap.has(src)) {
astroAssetMap.set(src, fs.readFile(new URL(`file://${src}`)));
}
}
if(hasSrcSet(node)) {
const candidates = matchSrcset(getAttribute(node, 'srcset')!);
for(const {url} of candidates) {
if(url.startsWith(srcRoot) && !astroAssetMap.has(url)) {
astroAssetMap.set(url, fs.readFile(new URL(`file://${url}`)));
}
}
}
}
if(styles) {
const styleId = getAstroStyleId(pathname);
astroStyleMap.set(styleId, styles);
// Put this at the front of imports
assetImports.unshift(styleId);
}
if(frontEndImports.length) {
htmlInput.add(id);
const jsSource = frontEndImports.map(sid => `import '${sid}';`).join('\n');
astroPageMap.set(id, jsSource);
}
if(assetImports.length) {
const pageStyleId = getAstroPageStyleId(pathname);
const jsSource = assetImports.map(sid => `import '${sid}';`).join('\n');
astroPageStyleMap.set(pageStyleId, jsSource);
assetInput.add(pageStyleId);
}
}
}
const allInputs = new Set([...jsInput, ...htmlInput, ...assetInput]);
// You always need at least 1 input, so add an placeholder just so we can build HTML/CSS
if(!allInputs.size) {
allInputs.add(ASTRO_EMPTY);
}
const outOptions = addRollupInput(inputOptions, Array.from(allInputs));
return outOptions;
},
async resolveId(id) {
switch(true) {
case astroScriptMap.has(id):
case astroPageMap.has(id):
case id === ASTRO_EMPTY: {
return id;
}
}
return undefined;
},
async load(id) {
// Load pages
if(astroPageMap.has(id)) {
return astroPageMap.get(id)!;
}
// Load scripts
if(astroScriptMap.has(id)) {
return astroScriptMap.get(id)!;
}
// Give this module actual code so it doesnt warn about an empty chunk
if(id === ASTRO_EMPTY) {
return 'console.log("empty");';
}
return null;
},
outputOptions(outputOptions) {
Object.assign(outputOptions, {
entryFileNames(chunk: PreRenderedChunk) {
// Removes the `@astro-page` prefix from JS chunk names.
if(chunk.name.startsWith(ASTRO_PAGE_PREFIX)) {
let pageName = chunk.name.substr(ASTRO_PAGE_PREFIX.length + 1);
if(!pageName) {
pageName = 'index';
}
return `assets/${pageName}.[hash].js`;
}
return 'assets/[name].[hash].js';
}
});
return outputOptions;
},
async generateBundle(_options, bundle) {
const facadeIdMap = new Map<string, string>();
for(const [chunkId, output] of Object.entries(bundle)) {
if(output.type === 'chunk') {
const chunk = output as OutputChunk;
const id = chunk.facadeModuleId;
if(id === ASTRO_EMPTY) {
delete bundle[chunkId];
} else if(id) {
facadeIdMap.set(id, chunk.fileName);
}
}
}
// Emit assets (images, etc)
const assetIdMap = new Map<string, string>();
for(const [assetPath, dataPromise] of astroAssetMap) {
const referenceId = this.emitFile({
type: 'asset',
name: npath.basename(assetPath),
source: await dataPromise
});
assetIdMap.set(assetPath, referenceId);
}
// Create a mapping of chunks to dependent chunks, used to add the proper
// link tags for CSS.
for(const chunk of pureCSSChunks) {
const referenceId = chunkToReferenceIdMap.get(chunk.fileName)!;
const chunkReferenceIds = [referenceId];
for(const imp of chunk.imports) {
if(chunkToReferenceIdMap.has(imp)) {
chunkReferenceIds.push(chunkToReferenceIdMap.get(imp)!);
}
}
for(const [id] of Object.entries(chunk.modules)) {
cssChunkMap.set(id, chunkReferenceIds);
}
}
// Keep track of links added so we don't do so twice.
const linkChunksAdded = new Set<string>();
const appendStyleChunksBefore = (ref: parse5.Element, pathname: string, referenceIds: string[] | undefined, attrs: Record<string, any> = {}) => {
let added = false;
if(referenceIds) {
const lastNode = ref;
for(const referenceId of referenceIds) {
const chunkFileName = this.getFileName(referenceId);
const relPath = npath.posix.relative(pathname, '/' + chunkFileName);
// This prevents added links more than once per type.
const key = pathname + relPath + attrs.rel || 'stylesheet';
if(!linkChunksAdded.has(key)) {
linkChunksAdded.add(key);
insertBefore(lastNode.parentNode, createElement('link', {
rel: 'stylesheet',
...attrs,
href: relPath
}), lastNode);
added = true;
}
}
}
return added;
}
for(const [id, html] of renderedPageMap) {
const pathname = id.substr(ASTRO_PAGE_PREFIX.length);
const document = parse5.parse(html, {
sourceCodeLocationInfo: true
});
if(facadeIdMap.has(id)) {
const bundleId = facadeIdMap.get(id)!;
const bundlePath = '/' + bundleId;
// Update scripts
let i = 0;
for(let script of findInlineScripts(document)) {
if(getAttribute(script, 'astro-script')) {
if(i === 0) {
const relPath = npath.posix.relative(pathname, bundlePath);
insertBefore(script.parentNode, createScript({
type: 'module',
src: relPath
}), script);
}
remove(script);
}
i++;
}
}
const styleId = getAstroPageStyleId(pathname);
let pageCSSAdded = false;
for(const node of findAssets(document)) {
if(isBuildableLink(node, srcRoot)) {
const rel = getAttribute(node, 'rel');
switch(rel) {
case 'stylesheet': {
if(!pageCSSAdded) {
const attrs = Object.fromEntries(node.attrs.map(attr => [attr.name, attr.value]));
delete attrs['data-astro-injected'];
pageCSSAdded = appendStyleChunksBefore(node, pathname, cssChunkMap.get(styleId), attrs);
}
remove(node);
break;
}
case 'preload': {
if(getAttribute(node, 'as') === 'style') {
const attrs = Object.fromEntries(node.attrs.map(attr => [attr.name, attr.value]));
appendStyleChunksBefore(node, pathname, cssChunkMap.get(styleId), attrs);
remove(node);
}
}
}
}
if(isBuildableImage(node, srcRoot)) {
const src = getAttribute(node, 'src')!;
const referenceId = assetIdMap.get(src);
if(referenceId) {
const fileName = this.getFileName(referenceId);
const relPath = npath.posix.relative(pathname, '/' + fileName);
setAttribute(node, 'src', relPath);
}
}
// Could be a `source` or an `img`.
if(hasSrcSet(node)) {
const srcset = getAttribute(node, 'srcset')!;
let changedSrcset = srcset;
const urls = matchSrcset(srcset).map(c => c.url);
for(const url of urls) {
if(assetIdMap.has(url)) {
const referenceId = assetIdMap.get(url)!;
const fileName = this.getFileName(referenceId);
const relPath = npath.posix.relative(pathname, '/' + fileName);
changedSrcset = changedSrcset.replace(url, relPath);
}
}
// If anything changed, update it
if(changedSrcset !== srcset) {
setAttribute(node, 'srcset', changedSrcset);
}
}
}
// Page styles for <style> usage, if not already appended via links.
for(const style of findInlineStyles(document)) {
if(getAttribute(style, 'astro-style')) {
if(!pageCSSAdded) {
pageCSSAdded = appendStyleChunksBefore(style, pathname, cssChunkMap.get(styleId));
}
remove(style);
}
}
const outHTML = parse5.serialize(document);
const outPath = npath.posix.join(pathname.substr(1), 'index.html');
this.emitFile({
fileName: outPath,
source: outHTML,
type: 'asset'
});
}
}
}
}

View file

@ -1,29 +1,45 @@
/**
* UNCOMMENT: add support for automatic <img> and srcset in build
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
import srcsetParse from 'srcset-parse';
let fixture;
// This package isn't real ESM, so have to coerce it
const matchSrcset = (srcsetParse).default;
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-assets/' });
await fixture.build();
});
// TODO: add automatic asset bundling
// Asset bundling
describe('Assets', () => {
let fixture;
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-assets/' });
await fixture.build();
});
it('built the base image', async () => {
await fixture.readFile('/images/twitter.png');
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const imgPath = $('img').attr('src');
const data = await fixture.readFile('/' + imgPath);
expect(!!data).to.equal(true);
});
it('built the 2x image', async () => {
await fixture.readFile('/images/twitter@2x.png');
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const srcset = $('img').attr('srcset');
const candidates = matchSrcset(srcset);
const match = candidates.find(a => a.density === 2);
const data = await fixture.readFile('/' + match.url);
expect(!!data).to.equal(true);
});
it('built the 3x image', async () => {
await fixture.readFile('/images/twitter@3x.png');
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const srcset = $('img').attr('srcset');
const candidates = matchSrcset(srcset);
const match = candidates.find(a => a.density === 3);
const data = await fixture.readFile('/' + match.url);
expect(!!data).to.equal(true);
});
});
*/
it.skip('is skipped', () => {});
});

View file

@ -1,28 +1,26 @@
/**
* UNCOMMENT: implement CSS bundling
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils';
import { loadFixture } from './test-utils.js';
// note: the hashes should be deterministic, but updating the file contents will change hashes
// be careful not to test that the HTML simply contains CSS, because it always will! filename and quanity matter here (bundling).
const EXPECTED_CSS = {
'/index.html': ['/_astro/common-', '/_astro/index-'], // dont match hashes, which change based on content
'/one/index.html': ['/_astro/common-', '/_astro/one/index-'],
'/two/index.html': ['/_astro/common-', '/_astro/two/index-'],
'/preload/index.html': ['/_astro/common-', '/_astro/preload/index-'],
'/preload-merge/index.html': ['/_astro/preload-merge/index-'],
'/index.html': ['assets/index', 'assets/typography'], // dont match hashes, which change based on content
'/one/index.html': ['../assets/one'],
'/two/index.html': ['../assets/two', '../assets/typography'],
'/preload/index.html': ['../assets/preload'],
'/preload-merge/index.html': ['../assets/preload-merge'],
};
const UNEXPECTED_CSS = ['/_astro/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css'];
const UNEXPECTED_CSS = ['/src/components/nav.css', '../css/typography.css', '../css/colors.css', '../css/page-index.css', '../css/page-one.css', '../css/page-two.css'];
let fixture;
describe('CSS Bundling', function() {
let fixture;
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-css-bundling/' });
await fixture.build({ mode: 'production' });
});
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-css-bundling/' });
await fixture.build({ mode: 'production' });
});
describe('CSS Bundling', () => {
it('Bundles CSS', async () => {
const builtCSS = new Set();
@ -35,7 +33,8 @@ describe('CSS Bundling', () => {
for (const href of css) {
const link = $(`link[rel="stylesheet"][href^="${href}"]`);
expect(link).to.have.lengthOf(1);
builtCSS.add(link.attr('href'));
const outHref = link.attr('href');
builtCSS.add(outHref.startsWith('../') ? outHref.substr(2) : outHref);
}
// test 2: assert old CSS was removed
@ -46,8 +45,8 @@ describe('CSS Bundling', () => {
// test 3: preload tags was not removed and attributes was preserved
if (filepath === '/preload/index.html') {
const stylesheet = $('link[rel="stylesheet"][href^="/_astro/preload/index-"]');
const preload = $('link[rel="preload"][href^="/_astro/preload/index-"]');
const stylesheet = $('link[rel="stylesheet"][href^="../assets/preload"]');
const preload = $('link[rel="preload"][href^="../assets/preload"]');
expect(stylesheet[0].attribs.media).to.equal('print');
expect(preload).to.have.lengthOf(1); // Preload tag was removed
}
@ -60,33 +59,9 @@ describe('CSS Bundling', () => {
// test 5: assert all bundled CSS was built and contains CSS
for (const url of builtCSS.keys()) {
const css = await context.readFile(url);
const css = await fixture.readFile(url);
expect(css).to.be.ok;
}
// test 6: assert ordering is preserved (typography.css before colors.css)
const bundledLoc = [...builtCSS].find((k) => k.startsWith('/_astro/common-'));
const bundledContents = await context.readFile(bundledLoc);
const typographyIndex = bundledContents.indexOf('body{');
const colorsIndex = bundledContents.indexOf(':root{');
expect(typographyIndex).toBeLessThan(colorsIndex);
// test 7: assert multiple style blocks were bundled (Nav.astro includes 2 scoped style blocks)
const scopedNavStyles = [...bundledContents.matchAll('.nav.astro-')];
expect(scopedNavStyles).to.have.lengthOf(2);
// test 8: assert <style global> was not scoped (in Nav.astro)
const globalStyles = [...bundledContents.matchAll('html{')];
expect(globalStyles).to.have.lengthOf(1);
// test 9: assert keyframes are only scoped for non-global styles (from Nav.astro)
const scopedKeyframes = [...bundledContents.matchAll('nav-scoped-fade-astro')];
const globalKeyframes = [...bundledContents.matchAll('nav-global-fade{')];
expect(scopedKeyframes.length).toBeGreaterThan(0);
expect(globalKeyframes.length).toBeGreaterThan(0);
}
});
});
*/
it.skip('is skipped', () => {});
});

View file

@ -14,7 +14,7 @@ describe('Dynamic components', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
expect($('script').length).to.eq(2);
expect($('script').length).to.eq(1);
});
it('Loads pages using client:media hydrator', async () => {
@ -23,12 +23,10 @@ describe('Dynamic components', () => {
const $ = cheerio.load(html);
// test 1: static value rendered
let js = await fixture.readFile(new URL($('script').attr('src'), root).pathname);
expect(js).to.include(`value:"(max-width: 700px)"`);
// test 2: dynamic value rendered
js = await fixture.readFile(new URL($('script').eq(1).attr('src'), root).pathname);
expect(js).to.include(`value:"(max-width: 600px)"`);
});

View file

@ -48,10 +48,10 @@ describe('Astro.*', () => {
expect($('#site').attr('href')).to.equal('https://mysite.dev/blog/');
});
it('Astro.resolve in development', async () => {
it('Astro.resolve built', async () => {
const html = await fixture.readFile('/resolve/index.html');
const $ = cheerio.load(html);
expect($('img').attr('src')).to.include('/src/images/penguin.png');
expect($('#inner-child img').attr('src')).to.include('/src/components/nested/images/penguin.png');
expect($('img').attr('src')).to.include('assets/penguin.ccd44411.png'); // Main src/images
expect($('#inner-child img').attr('src')).to.include('assets/penguin.b9ab122a.png');
});
});

View file

@ -2,22 +2,17 @@ import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
let fixture;
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' });
await fixture.build();
});
describe('Styles SSR', () => {
let fixture;
before(async () => {
fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' });
await fixture.build();
});
it('Has <link> tags', async () => {
const MUST_HAVE_LINK_TAGS = [
'/src/components/ReactCSS.css',
'/src/components/ReactModules.module.css',
'/src/components/SvelteScoped.svelte',
'/src/components/VueCSS.vue',
'/src/components/VueModules.vue',
'/src/components/VueScoped.vue',
'assets/index'
];
const html = await fixture.readFile('/index.html');
@ -70,17 +65,19 @@ describe('Styles SSR', () => {
const html = await fixture.readFile('/index.html');
const $ = cheerio.load(html);
const href = '/' + $('link').attr('href');
const raw = await fixture.readFile(href);
let scopedClass;
// test 1: <style> tag in <head> is transformed
const css = $('style')
.html()
const css = raw
.replace(/\.astro-[A-Za-z0-9-]+/, (match) => {
scopedClass = match; // get class hash from result
return match;
});
expect(css).to.equal(`.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px;}.outer${scopedClass}{color:red;}`);
expect(css).to.include(`.wrapper${scopedClass}{margin-left:auto;margin-right:auto;max-width:1200px;}.outer${scopedClass}{color:red;}`);
// test 2: element received .astro-XXXXXX class (this selector will succeed if transformed correctly)
const wrapper = $(`.wrapper${scopedClass}`);
@ -110,10 +107,8 @@ describe('Styles SSR', () => {
expect(el1.attr('class')).to.equal(`blue ${scopedClass}`);
expect(el2.attr('class')).to.equal(`visible ${scopedClass}`);
let css = '';
$('style').each((_, el) => {
css += $(el).html();
});
const href = '/' + $('link').attr('href');
const css = await fixture.readFile(href);
// test 4: CSS generates as expected
expect(css).to.include(`.blue.${scopedClass}{color:powderblue;}.color\\:blue.${scopedClass}{color:powderblue;}.visible.${scopedClass}{display:block;}`);

View file

@ -5,7 +5,7 @@
</style>
<body>
<h1>Icons</h1>
<img src="../images/twitter.png" srcset="../images/twitter.png 1x, ../images/twitter@2x.png 2x, ../images/twitter@3x.png 3x" />
<img src={Astro.resolve('../images/twitter.png')} srcset={`${Astro.resolve('../images/twitter.png')} 1x, ${Astro.resolve('../images/twitter@2x.png')} 2x, ${Astro.resolve('../images/twitter@3x.png')} 3x`} />
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 600w, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 800w">
<img srcset="https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg, https://ik.imagekit.io/demo/tr:w-450,h-450/medium_cafe_B1iTdD0C.jpg 1.5x, https://ik.imagekit.io/demo/tr:w-600,h-600/medium_cafe_B1iTdD0C.jpg 2x">
<!--

View file

@ -4,9 +4,9 @@ import Nav from '../components/Nav.astro';
<html>
<head>
<link rel="stylesheet" href="../css/typography.css" />
<link rel="stylesheet" href="../css/colors.css" />
<link rel="stylesheet" href="../css/page-index.css" />
<link rel="stylesheet" href={Astro.resolve('../css/typography.css')}>
<link rel="stylesheet" href={Astro.resolve('../css/colors.css')}>
<link rel="stylesheet" href={Astro.resolve('../css/page-index.css')}>
</head>
<body>
<Nav />

View file

@ -4,8 +4,8 @@ import Nav from '../components/Nav.astro';
<html>
<head>
<link rel="stylesheet" href="../css/typography.css" />
<link rel="stylesheet" href="../css/page-one.css" />
<link rel="stylesheet" href={Astro.resolve('../css/typography.css')} />
<link rel="stylesheet" href={Astro.resolve('../css/page-one.css')} />
</head>
<body>
<Nav />

View file

@ -4,11 +4,11 @@ import Nav from '../components/Nav.astro';
<html>
<head>
<link rel="preload" as="style" href="../css/page-preload-merge.css" />
<link rel="preload" as="style" href="../css/page-preload-merge-2.css" />
<link rel="preload" as="style" href={Astro.resolve('../css/page-preload-merge.css')} />
<link rel="preload" as="style" href={Astro.resolve('../css/page-preload-merge-2.css')} />
<link rel="stylesheet" href="../css/page-preload-merge.css" />
<link rel="stylesheet" href="../css/page-preload-merge-2.css" />
<link rel="stylesheet" href={Astro.resolve('../css/page-preload-merge.css')} />
<link rel="stylesheet" href={Astro.resolve('../css/page-preload-merge-2.css')} />
</head>
<body>
<Nav />

View file

@ -4,8 +4,8 @@ import Nav from '../components/Nav.astro';
<html>
<head>
<link rel="preload" href="../css/page-preload.css" />
<link rel="stylesheet" href="../css/page-preload.css" media="print" onload="this.media='all'" />
<link rel="preload" as="style" href={Astro.resolve('../css/page-preload.css')} />
<link rel="stylesheet" href={Astro.resolve('../css/page-preload.css')} media="print" onload="this.media='all'" />
</head>
<body>
<Nav />

View file

@ -4,9 +4,9 @@ import Nav from '../components/Nav.astro';
<html>
<head>
<link rel="stylesheet" href="../css/typography.css" />
<link rel="stylesheet" href="../css/colors.css" />
<link rel="stylesheet" href="../css/page-two.css" />
<link rel="stylesheet" href={Astro.resolve('../css/typography.css')} />
<link rel="stylesheet" href={Astro.resolve('../css/colors.css')} />
<link rel="stylesheet" href={Astro.resolve('../css/page-two.css')} />
</head>
<body>
<Nav />

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 MiB

125
yarn.lock
View file

@ -2214,16 +2214,6 @@
"@types/parse5" "^6.0.1"
parse5 "^6.0.1"
"@web/rollup-plugin-html@^1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@web/rollup-plugin-html/-/rollup-plugin-html-1.10.1.tgz#7995d3aff436f6b5c1a365830a9ff525388b40d8"
integrity sha512-XYJxHtdllwA5l4X8wh8CailrOykOl3YY+BRqO8+wS/I1Kq0JFISg3EUHdWAyVcw0TRDnHNLbOBJTm2ptAM+eog==
dependencies:
"@web/parse5-utils" "^1.3.0"
glob "^7.1.6"
html-minifier-terser "^6.0.0"
parse5 "^6.0.1"
"@webcomponents/template-shadowroot@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@webcomponents/template-shadowroot/-/template-shadowroot-0.1.0.tgz#adb3438d0d9a18e8fced08abc253f56b7eadab00"
@ -2901,14 +2891,6 @@ calmcard@~0.1.1:
resolved "https://registry.yarnpkg.com/calmcard/-/calmcard-0.1.1.tgz#35ac2b66492b0ed39ad06a893a0ff6e61124e449"
integrity sha1-NawrZkkrDtOa0GqJOg/25hEk5Ek=
camel-case@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
dependencies:
pascal-case "^3.1.2"
tslib "^2.0.3"
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
@ -3151,13 +3133,6 @@ ci-info@^3.2.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
clean-css@^5.1.5:
version "5.1.5"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.1.5.tgz#3b0af240dcfc9a3779a08c2332df3ebd4474f232"
integrity sha512-9dr/cU/LjMpU57PXlSvDkVRh0rPxJBXiBtD0+SgYt8ahTCsXtfKjCkNYgIoTC6mBg8CFr5EKhW3DKCaGMUbUfQ==
dependencies:
source-map "~0.6.0"
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@ -3330,20 +3305,15 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
integrity sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==
commander@^2.20.0, commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^6.0.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^8.1.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.2.0.tgz#37fe2bde301d87d47a53adeff8b5915db1381ca8"
integrity sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@~3.0.2:
version "3.0.2"
@ -3922,14 +3892,6 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0, domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
dot-prop@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@ -5655,7 +5617,7 @@ hastscript@^7.0.0:
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
he@1.2.0, he@^1.2.0:
he@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@ -5697,19 +5659,6 @@ html-entities@2.3.2, html-entities@^2.3.2:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==
html-minifier-terser@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.0.2.tgz#14059ad64b69bf9f8b8a33f25b53411d8321e75d"
integrity sha512-AgYO3UGhMYQx2S/FBJT3EM0ZYcKmH6m9XL9c1v77BeK/tYJxGPxT1/AtsdUi4FcP8kZGmqqnItCcjFPcX9hk6A==
dependencies:
camel-case "^4.1.2"
clean-css "^5.1.5"
commander "^8.1.0"
he "^1.2.0"
param-case "^3.0.4"
relateurl "^0.2.7"
terser "^5.7.2"
html-tags@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
@ -6861,13 +6810,6 @@ loose-envify@^1.1.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
dependencies:
tslib "^2.0.3"
lru-cache@4.1.x, lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
@ -7797,14 +7739,6 @@ nlcst-to-string@^2.0.0:
resolved "https://registry.yarnpkg.com/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz#9315dfab80882bbfd86ddf1b706f53622dc400cc"
integrity sha512-3x3jwTd6UPG7vi5k4GEzvxJ5rDA7hVUIRNHPblKuMVP9Z3xmlsd9cgLcpAMkc5uPOBna82EeshROFhsPkbnTZg==
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
dependencies:
lower-case "^2.0.2"
tslib "^2.0.3"
node-emoji@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c"
@ -8491,14 +8425,6 @@ pacote@^11.2.6:
ssri "^8.0.1"
tar "^6.1.0"
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -8611,14 +8537,6 @@ parseurl@^1.3.2, parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
pascal-case@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
dependencies:
no-case "^3.0.4"
tslib "^2.0.3"
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
@ -9348,11 +9266,6 @@ rehype-toc@^3.0.2:
dependencies:
"@jsdevtools/rehype-toc" "3.0.2"
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
remark-code-titles@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/remark-code-titles/-/remark-code-titles-0.1.2.tgz#ae41b47c517eae4084c761a59a60df5f0bd54aa8"
@ -9935,25 +9848,17 @@ source-map-js@^0.6.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
source-map-support@~0.5.20:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.5.0:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3, source-map@~0.7.2:
source-map@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@ -10043,6 +9948,11 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
srcset-parse@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/srcset-parse/-/srcset-parse-1.1.0.tgz#73f787f38b73ede2c5af775e0a3465579488122b"
integrity sha512-JWp4cG2eybkvKA1QUHGoNK6JDEYcOnSuhzNGjZuYUPqXreDl/VkkvP2sZW7Rmh+icuCttrR9ccb2WPIazyM/Cw==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -10448,15 +10358,6 @@ term-size@^2.1.0:
resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
terser@^5.7.2:
version "5.8.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.8.0.tgz#c6d352f91aed85cc6171ccb5e84655b77521d947"
integrity sha512-f0JH+6yMpneYcRJN314lZrSwu9eKkUFEHLN/kNy8ceh8gaRiLgFPJqrB9HsXjhEGdv4e/ekjTOFxIlL6xlma8A==
dependencies:
commander "^2.20.0"
source-map "~0.7.2"
source-map-support "~0.5.20"
text-extensions@^1.0.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"
@ -10630,7 +10531,7 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.2.0:
tslib@^2.0.1, tslib@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==