diff options
29 files changed, 1203 insertions, 395 deletions
diff --git a/.changeset/silly-peas-battle.md b/.changeset/silly-peas-battle.md new file mode 100644 index 000000000..f25be96fa --- /dev/null +++ b/.changeset/silly-peas-battle.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Update the build to build/bundle assets diff --git a/packages/astro/package.json b/packages/astro/package.json index 19f473c9d..7c9c69049 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -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", diff --git a/packages/astro/src/@types/astro-core.ts b/packages/astro/src/@types/astro-core.ts index 29eb7bba4..204c94a30 100644 --- a/packages/astro/src/@types/astro-core.ts +++ b/packages/astro/src/@types/astro-core.ts @@ -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; } diff --git a/packages/astro/src/@types/astro-runtime.ts b/packages/astro/src/@types/astro-runtime.ts index fa312f840..f1984c27c 100644 --- a/packages/astro/src/@types/astro-runtime.ts +++ b/packages/astro/src/@types/astro-runtime.ts @@ -48,6 +48,7 @@ export interface TopLevelAstro { export interface SSRMetadata { renderers: Renderer[]; + pathname: string; } export interface SSRResult { diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index d00c8c8bf..b4b8831a2 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -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 don’t 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 }); } } diff --git a/packages/astro/src/core/build/types.d.ts b/packages/astro/src/core/build/types.d.ts new file mode 100644 index 000000000..c165f9ce3 --- /dev/null +++ b/packages/astro/src/core/build/types.d.ts @@ -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>; + diff --git a/packages/astro/src/core/ssr/index.ts b/packages/astro/src/core/ssr/index.ts index 8823c483e..1a472b808 100644 --- a/packages/astro/src/core/ssr/index.ts +++ b/packages/astro/src/core/ssr/index.ts @@ -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; } +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) + 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 = anyError.stack; + if (pluginName) err.plugin = pluginName; + throw err; + } + + // 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 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(); +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); } - 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, + validateGetStaticPathsModule(mod); + if (!routeCache[route.component]) { + routeCache[route.component] = await ( + await mod.getStaticPaths!({ + paginate: generatePaginateFunction(route), + rss: () => { + /* noop */ }, - 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', - }); + }) + ).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 } || {}; + } - // inject CSS - [...getStylesForID(filePath.pathname, viteServer)].forEach((href) => { - tags.push({ - tag: 'link', - attrs: { type: 'text/css', rel: 'stylesheet', href }, - injectTo: 'head', - }); + // 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', }); + } - // add injected tags - html = injectTags(html, tags); + // inject CSS + [...getStylesForID(filePath.pathname, viteServer)].forEach((href) => { + tags.push({ + tag: 'link', + attrs: { + rel: 'stylesheet', + href, + 'data-astro-injected': true + }, + injectTo: 'head', + }); + }); - // run transformIndexHtml() in dev to run Vite dev transformations - if (mode === 'development') { - html = await viteServer.transformIndexHtml(filePath.pathname, html, pathname); - } + // add injected tags + html = injectTags(html, tags); - return html; - } catch (e: any) { - 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} + // run transformIndexHtml() in dev to run Vite dev transformations + if (mode === 'development') { + html = await viteServer.transformIndexHtml(filePath.pathname, html, pathname); + } -${frame} -`; - err.stack = e.stack; - if (pluginName) err.plugin = pluginName; - throw err; - } + return html; +} - // Generic error (probably from Vite, and already formatted) +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; } -} +}
\ No newline at end of file diff --git a/packages/astro/src/core/util.ts b/packages/astro/src/core/util.ts index 4fb32634c..b43a04700 100644 --- a/packages/astro/src/core/util.ts +++ b/packages/astro/src/core/util.ts @@ -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}`; +}
\ No newline at end of file diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index f748e73ff..dd9043097 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -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>'); } diff --git a/packages/astro/src/runtime/server/metadata.ts b/packages/astro/src/runtime/server/metadata.ts index 2b318ad80..4dcfc73c9 100644 --- a/packages/astro/src/runtime/server/metadata.ts +++ b/packages/astro/src/runtime/server/metadata.ts @@ -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); } diff --git a/packages/astro/src/vite-plugin-build-css/index.ts b/packages/astro/src/vite-plugin-build-css/index.ts new file mode 100644 index 000000000..e54c04bed --- /dev/null +++ b/packages/astro/src/vite-plugin-build-css/index.ts @@ -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]; + } + } + } + } +} diff --git a/packages/astro/src/vite-plugin-build-css/resolve.ts b/packages/astro/src/vite-plugin-build-css/resolve.ts new file mode 100644 index 000000000..7c55107d3 --- /dev/null +++ b/packages/astro/src/vite-plugin-build-css/resolve.ts @@ -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 couldn’t 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; +} diff --git a/packages/astro/src/vite-plugin-build-html/add-rollup-input.ts b/packages/astro/src/vite-plugin-build-html/add-rollup-input.ts new file mode 100644 index 000000000..0da25ecf9 --- /dev/null +++ b/packages/astro/src/vite-plugin-build-html/add-rollup-input.ts @@ -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.`); +} diff --git a/packages/astro/src/vite-plugin-build-html/extract-assets.ts b/packages/astro/src/vite-plugin-build-html/extract-assets.ts new file mode 100644 index 000000000..3f38828a4 --- /dev/null +++ b/packages/astro/src/vite-plugin-build-html/extract-assets.ts @@ -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); +}
\ No newline at end of file diff --git a/packages/astro/src/vite-plugin-build-html/index.ts b/packages/astro/src/vite-plugin-build-html/index.ts new file mode 100644 index 000000000..16085e707 --- /dev/null +++ b/packages/astro/src/vite-plugin-build-html/index.ts @@ -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' + }); + } + } + } +} diff --git a/packages/astro/test/astro-assets.test.js b/packages/astro/test/astro-assets.test.js index 2e75f3920..df279f140 100644 --- a/packages/astro/test/astro-assets.test.js +++ b/packages/astro/test/astro-assets.test.js @@ -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', () => {}); +});
\ No newline at end of file diff --git a/packages/astro/test/astro-css-bundling.test.js b/packages/astro/test/astro-css-bundling.test.js index fddb258e6..0ff7cadbd 100644 --- a/packages/astro/test/astro-css-bundling.test.js +++ b/packages/astro/test/astro-css-bundling.test.js @@ -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-'], // don’t 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'], // don’t 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', () => {}); +});
\ No newline at end of file diff --git a/packages/astro/test/astro-dynamic.test.js b/packages/astro/test/astro-dynamic.test.js index 82fdc3818..0e68db2db 100644 --- a/packages/astro/test/astro-dynamic.test.js +++ b/packages/astro/test/astro-dynamic.test.js @@ -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)"`); }); diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js index ad002b985..75d44e743 100644 --- a/packages/astro/test/astro-global.test.js +++ b/packages/astro/test/astro-global.test.js @@ -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'); }); }); diff --git a/packages/astro/test/astro-styles-ssr.test.js b/packages/astro/test/astro-styles-ssr.test.js index 31f643d17..6b223ebc9 100644 --- a/packages/astro/test/astro-styles-ssr.test.js +++ b/packages/astro/test/astro-styles-ssr.test.js @@ -2,22 +2,17 @@ import { expect } from 'chai'; import cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; -let fixture; +describe('Styles SSR', () => { + let fixture; -before(async () => { - fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' }); - await fixture.build(); -}); + before(async () => { + fixture = await loadFixture({ projectRoot: './fixtures/astro-styles-ssr/' }); + await fixture.build(); + }); -describe('Styles SSR', () => { 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;}`); diff --git a/packages/astro/test/fixtures/astro-assets/src/pages/index.astro b/packages/astro/test/fixtures/astro-assets/src/pages/index.astro index 18760ab3b..0932c66d4 100644 --- a/packages/astro/test/fixtures/astro-assets/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-assets/src/pages/index.astro @@ -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"> <!-- diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro index 9479981c8..f776de2e8 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/index.astro @@ -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 /> diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro index cd47f0b27..935db26c4 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/one.astro @@ -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 /> diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload-merge.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload-merge.astro index 0f256c166..34b080966 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload-merge.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload-merge.astro @@ -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 /> diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload.astro index 6d421acbe..535b5f3e3 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/preload.astro @@ -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 /> diff --git a/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro b/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro index cc730f0d1..2c733d7f0 100644 --- a/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro +++ b/packages/astro/test/fixtures/astro-css-bundling/src/pages/two.astro @@ -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 /> diff --git a/packages/astro/test/fixtures/astro-global/src/components/nested/images/penguin.png b/packages/astro/test/fixtures/astro-global/src/components/nested/images/penguin.png Binary files differnew file mode 100644 index 000000000..bc9523bd4 --- /dev/null +++ b/packages/astro/test/fixtures/astro-global/src/components/nested/images/penguin.png diff --git a/packages/astro/test/fixtures/astro-global/src/images/penguin.png b/packages/astro/test/fixtures/astro-global/src/images/penguin.png Binary files differnew file mode 100644 index 000000000..74cb5e8f3 --- /dev/null +++ b/packages/astro/test/fixtures/astro-global/src/images/penguin.png @@ -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== |