diff options
-rw-r--r-- | .changeset/many-donkeys-report.md | 5 | ||||
-rw-r--r-- | packages/astro/package.json | 1 | ||||
-rw-r--r-- | packages/astro/src/core/build/index.ts | 56 | ||||
-rw-r--r-- | packages/astro/src/core/build/stats.ts | 144 | ||||
-rw-r--r-- | packages/astro/src/core/logger.ts | 5 |
5 files changed, 41 insertions, 170 deletions
diff --git a/.changeset/many-donkeys-report.md b/.changeset/many-donkeys-report.md new file mode 100644 index 000000000..86be61150 --- /dev/null +++ b/.changeset/many-donkeys-report.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Add build output diff --git a/packages/astro/package.json b/packages/astro/package.json index 71a7f9b4a..6729aad1c 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -97,7 +97,6 @@ "strip-ansi": "^7.0.1", "strip-indent": "^4.0.0", "supports-esm": "^1.0.0", - "tiny-glob": "^0.2.8", "tsconfig-resolver": "^3.0.1", "vite": "^2.6.10", "yargs-parser": "^20.2.9", diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts index d7adb4745..4a50f85b7 100644 --- a/packages/astro/src/core/build/index.ts +++ b/packages/astro/src/core/build/index.ts @@ -6,19 +6,17 @@ import type { RenderedChunk } from 'rollup'; 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 } from 'kleur/colors'; +import * as colors 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 { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.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'; import { generateSitemap } from '../ssr/sitemap.js'; -import { kb, profileHTML, profileJS } from './stats.js'; export interface BuildOptions { mode?: string; @@ -55,7 +53,9 @@ class AstroBuilder { async build() { const { logging, origin } = this; - const timer: Record<string, number> = { viteStart: performance.now() }; + const timer: Record<string, number> = {}; + timer.init = performance.now(); + timer.viteStart = performance.now(); const viteConfig = await createVite( vite.mergeConfig( { @@ -97,12 +97,30 @@ class AstroBuilder { route, routeCache: this.routeCache, viteServer, - }), + }) + .then((routes) => { + const html = `${route.pathname}`.replace(/\/?$/, '/index.html'); + debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.yellow(html)}`); + return routes; + }) + .catch((err) => { + debug(logging, 'build', `├── ${colors.bold(colors.red(' '))} ${route.component}`); + throw err; + }), }; return; } // dynamic route: - const result = await this.getStaticPathsForRoute(route); + const result = await this.getStaticPathsForRoute(route) + .then((routes) => { + const label = routes.paths.length === 1 ? 'page' : 'pages'; + debug(logging, 'build', `├── ${colors.bold(colors.green('✔'))} ${route.component} → ${colors.magenta(`[${routes.paths.length} ${label}]`)}`); + return routes; + }) + .catch((err) => { + debug(logging, 'build', `├── ${colors.bold(colors.red('✗'))} ${route.component}`); + throw err; + }); if (result.rss?.xml) { const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), this.config.dist); if (assets[fileURLToPath(rssFile)]) { @@ -212,7 +230,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: pageNames.length }); + await this.printStats({ logging, timeStart: timer.init, pageCount: pageNames.length }); } } @@ -233,21 +251,13 @@ class AstroBuilder { } /** Stats */ - private async printStats({ cwd, pageCount }: { cwd: URL; pageCount: number }) { - const [js, html] = await Promise.all([profileJS({ cwd, entryHTML: new URL('./index.html', cwd) }), profileHTML({ cwd })]); - + private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) { /* eslint-disable no-console */ - console.log(`${bold(cyan('Done'))} -Pages (${pageCount} total) - ${green(`✔ All pages under ${kb(html.maxSize)}`)} -JS - ${pad('initial load', 50)}${pad(kb(js.entryHTML || 0), 8, 'left')} - ${pad('total size', 50)}${pad(kb(js.total), 8, 'left')} -CSS - ${pad('initial load', 50)}${pad('0 kB', 8, 'left')} - ${pad('total size', 50)}${pad('0 kB', 8, 'left')} -Images - ${green(`✔ All images under 50 kB`)} -`); + debug(logging, ''); // empty line for debug + const buildTime = performance.now() - timeStart; + const total = buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`; + const perPage = `${Math.round(buildTime / pageCount)}ms`; + info(logging, 'build', `${pageCount} pages built in ${colors.bold(total)} ${colors.dim(`(${perPage}/page)`)}`); + info(logging, 'build', `🚀 ${colors.cyan(colors.bold('Done'))}`); } } diff --git a/packages/astro/src/core/build/stats.ts b/packages/astro/src/core/build/stats.ts deleted file mode 100644 index 853f91e9d..000000000 --- a/packages/astro/src/core/build/stats.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as eslexer from 'es-module-lexer'; -import fetch from 'node-fetch'; -import fs from 'fs'; -import slash from 'slash'; -import glob from 'tiny-glob'; -import { fileURLToPath } from 'url'; - -type FileSizes = { [file: string]: number }; - -// Feel free to modify output to whatever’s needed in display. If it’s not needed, kill it and improve stat speeds! - -/** JS: prioritize entry HTML, but also show total */ -interface JSOutput { - /** breakdown of JS per-file */ - js: FileSizes; - /** weight of index.html */ - entryHTML?: number; - /** total bytes of [js], added for convenience */ - total: number; -} - -/** HTML: total isn’t important, because those are broken up requests. However, surface any anomalies / bloated HTML */ -interface HTMLOutput { - /** breakdown of HTML per-file */ - html: FileSizes; - /** biggest HTML file */ - maxSize: number; -} - -/** Scan any directory */ -async function scan(cwd: URL, pattern: string): Promise<URL[]> { - const results = await glob(pattern, { cwd: fileURLToPath(cwd) }); - return results.map((filepath) => new URL(slash(filepath), cwd)); -} - -/** get total HTML size */ -export async function profileHTML({ cwd }: { cwd: URL }): Promise<HTMLOutput> { - const sizes: FileSizes = {}; - const html = await scan(cwd, '**/*.html'); - let maxSize = 0; - await Promise.all( - html.map(async (file) => { - const relPath = file.pathname.replace(cwd.pathname, ''); - const size = (await fs.promises.stat(file)).size; - sizes[relPath] = size; - if (size > maxSize) maxSize = size; - }) - ); - return { - html: sizes, - maxSize, - }; -} - -/** get total JS size (note: .wasm counts as JS!) */ -export async function profileJS({ cwd, entryHTML }: { cwd: URL; entryHTML?: URL }): Promise<JSOutput> { - const sizes: FileSizes = {}; - let htmlSize = 0; - - // profile HTML entry (do this first, before all JS in a project is scanned) - if (entryHTML) { - let entryScripts: URL[] = []; - let visitedEntry = false; // note: a quirk of Vite is that the entry file is async-loaded. Count that, but don’t count subsequent async loads - - // Note: this function used cheerio to scan HTML, read deps, and build - // an accurate, “production-ready” benchmark for how much HTML, JS, and CSS - // you shipped. Disabled for now, because we have a post-merge cleanup item - // to revisit these build stats. - // - // let $ = cheerio.load(await fs.promises.readFile(entryHTML)); - // scan <script> files, keep adding to total until done - // $('script').each((n, el) => { - // const src = $(el).attr('src'); - // const innerHTML = $(el).html(); - // // if inline script, add to overall JS weight - // if (innerHTML) { - // htmlSize += Buffer.byteLength(innerHTML); - // } - // // otherwise if external script, load & scan it - // if (src) { - // entryScripts.push(new URL(src, entryHTML)); - // } - // }); - - let scanPromises: Promise<void>[] = []; - - await Promise.all(entryScripts.map(parseJS)); - - /** parse JS for imports, and add to total size */ - async function parseJS(url: URL): Promise<void> { - const relPath = url.pathname.replace(cwd.pathname, ''); - if (sizes[relPath]) return; - try { - let code = url.protocol === 'file:' ? await fs.promises.readFile(url, 'utf8') : await fetch(url.href).then((body) => body.text()); - sizes[relPath] = Buffer.byteLength(code); - const staticImports = eslexer.parse(code)[0].filter(({ d }) => { - if (!visitedEntry) return true; // if we’re on the entry file, count async imports, too - return d === -1; // subsequent runs: don’t count deferred code toward total - }); - for (const { n } of staticImports) { - if (!n) continue; - let nextURL: URL | undefined; - // external import - if (n.startsWith('http://') || n.startsWith('https://') || n.startsWith('//')) nextURL = new URL(n); - // relative import - else if (n[0] === '.') nextURL = new URL(n, url); - // absolute import (note: make sure "//" is already handled!) - else if (n[0] === '/') nextURL = new URL(`.${n}`, cwd); - if (!nextURL) continue; // unknown format: skip - if (sizes[nextURL.pathname.replace(cwd.pathname, '')]) continue; // already scanned: skip - scanPromises.push(parseJS(nextURL)); - } - } catch (err) { - console.warn(`Could not access ${url.href} to include in bundle size`); // eslint-disable-line no-console - } - visitedEntry = true; // after first run, stop counting async imports toward total - } - - await Promise.all(scanPromises); - - htmlSize = Object.values(sizes).reduce((sum, next) => sum + next, 0); - } - - // collect size of all JS in project (note: some may have already been scanned; skip when possible) - const js = await scan(cwd, '**/*.(js|mjs|wasm)'); - await Promise.all( - js.map(async (file) => { - const relPath = file.pathname.replace(cwd.pathname, ''); - if (!sizes[relPath]) sizes[relPath] = (await fs.promises.stat(file)).size; // only scan if new - }) - ); - - return { - js: sizes, - entryHTML: htmlSize || undefined, - total: Object.values(sizes).reduce((sum, acc) => sum + acc, 0), - }; -} - -/** b -> kB */ -export function kb(bytes: number): string { - if (bytes === 0) return `0 kB`; - return (Math.round(bytes / 1000) || 1) + ' kB'; // if this is between 0.1–0.4, round up to 1 -} diff --git a/packages/astro/src/core/logger.ts b/packages/astro/src/core/logger.ts index 78a84eba7..eb8f6fef2 100644 --- a/packages/astro/src/core/logger.ts +++ b/packages/astro/src/core/logger.ts @@ -1,6 +1,7 @@ import type { CompileError } from '@astrojs/parser'; import { bold, blue, dim, red, grey, underline, yellow } from 'kleur/colors'; +import { performance } from 'perf_hooks'; import { Writable } from 'stream'; import stringWidth from 'string-width'; import { format as utilFormat } from 'util'; @@ -36,7 +37,7 @@ export const defaultLogDestination = new Writable({ dest.write(dim(dt.format(new Date()) + ' ')); let type = event.type; - if (type !== null) { + if (type) { if (event.level === 'info') { type = bold(blue(type)); } else if (event.level === 'warn') { @@ -190,5 +191,5 @@ if (process.argv.includes('--verbose')) { export function timerMessage(message: string, startTime: number = performance.now()) { let timeDiff = performance.now() - startTime; let timeDisplay = timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`; - return `${message}: ${dim(timeDisplay)}]`; + return `${message} ${dim(timeDisplay)}`; } |