Update build output (#1814)
This commit is contained in:
parent
b49f9a525e
commit
3b8f201c4b
5 changed files with 41 additions and 170 deletions
5
.changeset/many-donkeys-report.md
Normal file
5
.changeset/many-donkeys-report.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add build output
|
|
@ -97,7 +97,6 @@
|
||||||
"strip-ansi": "^7.0.1",
|
"strip-ansi": "^7.0.1",
|
||||||
"strip-indent": "^4.0.0",
|
"strip-indent": "^4.0.0",
|
||||||
"supports-esm": "^1.0.0",
|
"supports-esm": "^1.0.0",
|
||||||
"tiny-glob": "^0.2.8",
|
|
||||||
"tsconfig-resolver": "^3.0.1",
|
"tsconfig-resolver": "^3.0.1",
|
||||||
"vite": "^2.6.10",
|
"vite": "^2.6.10",
|
||||||
"yargs-parser": "^20.2.9",
|
"yargs-parser": "^20.2.9",
|
||||||
|
|
|
@ -6,19 +6,17 @@ import type { RenderedChunk } from 'rollup';
|
||||||
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
|
import { rollupPluginAstroBuildHTML } from '../../vite-plugin-build-html/index.js';
|
||||||
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
import { rollupPluginAstroBuildCSS } from '../../vite-plugin-build-css/index.js';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { bold, cyan, green } from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
import vite, { ViteDevServer } from '../vite.js';
|
import vite, { ViteDevServer } from '../vite.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { createVite } from '../create-vite.js';
|
import { createVite } from '../create-vite.js';
|
||||||
import { pad } from '../dev/util.js';
|
import { debug, defaultLogOptions, info, levels, timerMessage, warn } from '../logger.js';
|
||||||
import { debug, defaultLogOptions, levels, timerMessage, warn } from '../logger.js';
|
|
||||||
import { preload as ssrPreload } from '../ssr/index.js';
|
import { preload as ssrPreload } from '../ssr/index.js';
|
||||||
import { generatePaginateFunction } from '../ssr/paginate.js';
|
import { generatePaginateFunction } from '../ssr/paginate.js';
|
||||||
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
import { createRouteManifest, validateGetStaticPathsModule, validateGetStaticPathsResult } from '../ssr/routing.js';
|
||||||
import { generateRssFunction } from '../ssr/rss.js';
|
import { generateRssFunction } from '../ssr/rss.js';
|
||||||
import { generateSitemap } from '../ssr/sitemap.js';
|
import { generateSitemap } from '../ssr/sitemap.js';
|
||||||
import { kb, profileHTML, profileJS } from './stats.js';
|
|
||||||
|
|
||||||
export interface BuildOptions {
|
export interface BuildOptions {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
@ -55,7 +53,9 @@ class AstroBuilder {
|
||||||
|
|
||||||
async build() {
|
async build() {
|
||||||
const { logging, origin } = this;
|
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(
|
const viteConfig = await createVite(
|
||||||
vite.mergeConfig(
|
vite.mergeConfig(
|
||||||
{
|
{
|
||||||
|
@ -97,12 +97,30 @@ class AstroBuilder {
|
||||||
route,
|
route,
|
||||||
routeCache: this.routeCache,
|
routeCache: this.routeCache,
|
||||||
viteServer,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// dynamic route:
|
// 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) {
|
if (result.rss?.xml) {
|
||||||
const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), this.config.dist);
|
const rssFile = new URL(result.rss.url.replace(/^\/?/, './'), this.config.dist);
|
||||||
if (assets[fileURLToPath(rssFile)]) {
|
if (assets[fileURLToPath(rssFile)]) {
|
||||||
|
@ -212,7 +230,7 @@ class AstroBuilder {
|
||||||
// You're done! Time to clean up.
|
// You're done! Time to clean up.
|
||||||
await viteServer.close();
|
await viteServer.close();
|
||||||
if (logging.level && levels[logging.level] <= levels['info']) {
|
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 */
|
/** Stats */
|
||||||
private async printStats({ cwd, pageCount }: { cwd: URL; pageCount: number }) {
|
private async printStats({ logging, timeStart, pageCount }: { logging: LogOptions; timeStart: number; pageCount: number }) {
|
||||||
const [js, html] = await Promise.all([profileJS({ cwd, entryHTML: new URL('./index.html', cwd) }), profileHTML({ cwd })]);
|
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(`${bold(cyan('Done'))}
|
debug(logging, ''); // empty line for debug
|
||||||
Pages (${pageCount} total)
|
const buildTime = performance.now() - timeStart;
|
||||||
${green(`✔ All pages under ${kb(html.maxSize)}`)}
|
const total = buildTime < 750 ? `${Math.round(buildTime)}ms` : `${(buildTime / 1000).toFixed(2)}s`;
|
||||||
JS
|
const perPage = `${Math.round(buildTime / pageCount)}ms`;
|
||||||
${pad('initial load', 50)}${pad(kb(js.entryHTML || 0), 8, 'left')}
|
info(logging, 'build', `${pageCount} pages built in ${colors.bold(total)} ${colors.dim(`(${perPage}/page)`)}`);
|
||||||
${pad('total size', 50)}${pad(kb(js.total), 8, 'left')}
|
info(logging, 'build', `🚀 ${colors.cyan(colors.bold('Done'))}`);
|
||||||
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`)}
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { CompileError } from '@astrojs/parser';
|
import type { CompileError } from '@astrojs/parser';
|
||||||
|
|
||||||
import { bold, blue, dim, red, grey, underline, yellow } from 'kleur/colors';
|
import { bold, blue, dim, red, grey, underline, yellow } from 'kleur/colors';
|
||||||
|
import { performance } from 'perf_hooks';
|
||||||
import { Writable } from 'stream';
|
import { Writable } from 'stream';
|
||||||
import stringWidth from 'string-width';
|
import stringWidth from 'string-width';
|
||||||
import { format as utilFormat } from 'util';
|
import { format as utilFormat } from 'util';
|
||||||
|
@ -36,7 +37,7 @@ export const defaultLogDestination = new Writable({
|
||||||
dest.write(dim(dt.format(new Date()) + ' '));
|
dest.write(dim(dt.format(new Date()) + ' '));
|
||||||
|
|
||||||
let type = event.type;
|
let type = event.type;
|
||||||
if (type !== null) {
|
if (type) {
|
||||||
if (event.level === 'info') {
|
if (event.level === 'info') {
|
||||||
type = bold(blue(type));
|
type = bold(blue(type));
|
||||||
} else if (event.level === 'warn') {
|
} else if (event.level === 'warn') {
|
||||||
|
@ -190,5 +191,5 @@ if (process.argv.includes('--verbose')) {
|
||||||
export function timerMessage(message: string, startTime: number = performance.now()) {
|
export function timerMessage(message: string, startTime: number = performance.now()) {
|
||||||
let timeDiff = performance.now() - startTime;
|
let timeDiff = performance.now() - startTime;
|
||||||
let timeDisplay = timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`;
|
let timeDisplay = timeDiff < 750 ? `${Math.round(timeDiff)}ms` : `${(timeDiff / 1000).toFixed(1)}s`;
|
||||||
return `${message}: ${dim(timeDisplay)}]`;
|
return `${message} ${dim(timeDisplay)}`;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue