Add bundle size information to the build output (#169)

This commit is contained in:
Matthew Phillips 2021-05-04 14:06:38 -04:00 committed by GitHub
parent 75b05b393a
commit 0054f78e42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 163 additions and 14 deletions

View file

@ -54,6 +54,7 @@
"find-up": "^5.0.0",
"github-slugger": "^1.3.0",
"gray-matter": "^4.0.2",
"gzip-size": "^6.0.0",
"hast-to-hyperscript": "~9.0.0",
"kleur": "^4.1.4",
"locate-character": "^2.0.5",
@ -77,6 +78,7 @@
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.32.8",
"snowpack": "^3.3.7",
"string-width": "^5.0.0",
"source-map-support": "^0.5.19",
"svelte": "^3.35.0",
"unified": "^9.2.1",

View file

@ -4,7 +4,7 @@ import type { LogOptions } from './logger';
import type { AstroRuntime, LoadResult } from './runtime';
import { existsSync, promises as fsPromises } from 'fs';
import { bold, green, yellow, underline } from 'kleur/colors';
import { bold, green, yellow } from 'kleur/colors';
import path from 'path';
import cheerio from 'cheerio';
import { fileURLToPath } from 'url';
@ -16,6 +16,7 @@ import { generateRSS } from './build/rss.js';
import { generateSitemap } from './build/sitemap.js';
import { collectStatics } from './build/static.js';
import { canonicalURL } from './build/util.js';
import { createURLStats, mapBundleStatsToURLStats, logURLStats } from './build/stats.js';
const { mkdir, readFile, writeFile } = fsPromises;
@ -205,8 +206,11 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const statics = new Set<string>();
const collectImportsOptions = { astroConfig, logging, resolvePackageUrl, mode };
const pages = await allPages(pageRoot);
let builtURLs: string[] = [];
let urlStats = createURLStats();
let importsToUrl = new Map<string, Set<string>>();
const pages = await allPages(pageRoot);
try {
info(logging, 'build', yellow('! building pages...'));
@ -218,8 +222,10 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
const pageType = getPageType(filepath);
const pageOptions: PageBuildOptions = { astroRoot, dist, filepath, runtime, site: astroConfig.buildOptions.site, sitemap: astroConfig.buildOptions.sitemap, statics };
let urls: string[];
if (pageType === 'collection') {
const { canonicalURLs, rss } = await buildCollectionPage(pageOptions);
urls = canonicalURLs;
builtURLs.push(...canonicalURLs);
if (rss) {
const basename = path
@ -230,10 +236,27 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
}
} else {
const { canonicalURLs } = await buildStaticPage(pageOptions);
urls = canonicalURLs;
builtURLs.push(...canonicalURLs);
}
mergeSet(imports, await collectDynamicImports(filepath, collectImportsOptions));
const dynamicImports = await collectDynamicImports(filepath, collectImportsOptions);
mergeSet(imports, dynamicImports);
// Keep track of urls and dynamic imports for stats.
for(const url of urls) {
urlStats.set(url, {
dynamicImports,
stats: []
});
}
for(let imp of dynamicImports) {
if(!importsToUrl.has(imp)) {
importsToUrl.set(imp, new Set<string>());
}
mergeSet(importsToUrl.get(imp)!, new Set(urls));
}
})
);
info(logging, 'build', green('✔'), 'pages built.');
@ -253,7 +276,8 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
if (imports.size > 0) {
try {
info(logging, 'build', yellow('! bundling client-side code.'));
await bundle(imports, { dist, runtime, astroConfig });
const bundleStats = await bundle(imports, { dist, runtime, astroConfig });
mapBundleStatsToURLStats(urlStats, importsToUrl, bundleStats);
info(logging, 'build', green('✔'), 'bundling complete.');
} catch (err) {
error(logging, 'build', err);
@ -297,13 +321,8 @@ export async function build(astroConfig: AstroConfig): Promise<0 | 1> {
info(logging, 'tip', `Set "buildOptions.site" in astro.config.mjs to generate a sitemap.xml`);
}
builtURLs.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
info(logging, 'build', underline('Pages'));
const lastIndex = builtURLs.length - 1;
builtURLs.forEach((url, index) => {
const sep = index === 0 ? '┌' : index === lastIndex ? '└' : '├';
info(logging, null, ' ' + sep, url === '/' ? url : url + '/');
});
// Log in a table-like view.
logURLStats(logging, urlStats, builtURLs);
await runtime.shutdown();
info(logging, 'build', bold(green('▶ Build Complete!')));

View file

@ -1,6 +1,6 @@
import type { AstroConfig, RuntimeMode, ValidExtensionPlugins } from '../@types/astro';
import type { ImportDeclaration } from '@babel/types';
import type { InputOptions, OutputOptions } from 'rollup';
import type { InputOptions, OutputOptions, OutputChunk } from 'rollup';
import type { AstroRuntime } from '../runtime';
import type { LogOptions } from '../logger';
@ -16,6 +16,7 @@ import babelParser from '@babel/parser';
import path from 'path';
import { rollup } from 'rollup';
import { terser } from 'rollup-plugin-terser';
import { createBundleStats, addBundleStats } from './stats.js';
const { transformSync } = esbuild;
const { readFile } = fsPromises;
@ -309,5 +310,12 @@ export async function bundle(imports: Set<string>, { runtime, dist }: BundleOpti
],
};
await build.write(outputOptions);
const stats = createBundleStats();
const {output} = await build.write(outputOptions);
await Promise.all(output.map(async chunk => {
const code = (chunk as OutputChunk).code || '';
await addBundleStats(stats, code, chunk.fileName);
}));
return stats;
}

View file

@ -0,0 +1,61 @@
import type { LogOptions } from '../logger';
import { info, table } from '../logger.js';
import { underline } from 'kleur/colors';
import gzipSize from 'gzip-size';
interface BundleStats {
size: number;
gzipSize: number;
}
interface URLStats {
dynamicImports: Set<string>;
stats: BundleStats[];
}
export type BundleStatsMap = Map<string, BundleStats>;
export type URLStatsMap = Map<string, URLStats>;
export function createURLStats(): URLStatsMap {
return new Map<string, URLStats>();
}
export function createBundleStats(): BundleStatsMap {
return new Map<string, BundleStats>();
}
export async function addBundleStats(bundleStatsMap: BundleStatsMap, code: string, filename: string) {
const gzsize = await gzipSize(code);
bundleStatsMap.set(filename, {
size: Buffer.byteLength(code),
gzipSize: gzsize
});
}
export function mapBundleStatsToURLStats(urlStats: URLStatsMap, importsToUrl: Map<string, Set<string>>, bundleStats: BundleStatsMap) {
for(let [imp, stats] of bundleStats) {
for(let url of importsToUrl.get('/' + imp) || []) {
urlStats.get(url)?.stats.push(stats);
}
}
}
export function logURLStats(logging: LogOptions, urlStats: URLStatsMap, builtURLs: string[]) {
builtURLs.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
info(logging, null, '');
const log = table(logging, [60, 20]);
log(info, ' ' + underline('Pages'), underline('GZip Size'));
const lastIndex = builtURLs.length - 1;
builtURLs.forEach((url, index) => {
const sep = index === 0 ? '┌' : index === lastIndex ? '└' : '├';
const urlPart = (' ' + sep + ' ') + (url === '/' ? url : url + '/');
const bytes = urlStats.get(url)?.stats.map(s => s.gzipSize).reduce((a, b) => a + b, 0) || 0;
const kb = (bytes * 0.001).toFixed(2);
const sizePart = kb + ' kB';
log(info, urlPart, sizePart);
});
}

View file

@ -3,6 +3,7 @@ import type { CompileError } from 'astro-parser';
import { bold, blue, red, grey, underline } from 'kleur/colors';
import { Writable } from 'stream';
import { format as utilFormat } from 'util';
import stringWidth from 'string-width';
type ConsoleStream = Writable & {
fd: 1 | 2;
@ -102,6 +103,15 @@ export function error(opts: LogOptions, type: string | null, ...messages: Array<
return log(opts, 'error', type, ...messages);
}
type LogFn = typeof debug | typeof info | typeof warn | typeof error;
export function table(opts: LogOptions, columns: number[]) {
return function logTable(logFn: LogFn, ...input: Array<any>) {
const messages = columns.map((len, i) => padStr(input[i].toString(), len));
logFn(opts, null, ...messages);
};
}
/** Pretty format error for display */
export function parseError(opts: LogOptions, err: CompileError) {
let frame = err.frame
@ -142,3 +152,14 @@ export function trapWarn(cb: (...args: any[]) => void = () => {}) {
};
return () => (console.warn = warn);
}
function padStr(str: string, len: number) {
const strLen = stringWidth(str);
if(strLen > len) {
return str.substring(0, len - 3) + '...';
}
const spaces = Array.from({ length: len - strLen }, () => ' ').join('');
return str + spaces;
}

View file

@ -2019,6 +2019,11 @@ ansi-regex@^5.0.0:
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
ansi-regex@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.0.tgz#ecc7f5933cbe5ac7b33e209a5ff409ab1669c6b2"
integrity sha512-tAaOSrWCHF+1Ear1Z4wnJCXA9GGox4K6Ic85a5qalES2aeEwQGr7UC93mwef49536PkCYjzkp0zIxfFvexJ6zQ==
ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
@ -3972,7 +3977,7 @@ duplexer3@^0.1.4:
resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
duplexer@^0.1.1:
duplexer@^0.1.1, duplexer@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
@ -4059,6 +4064,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz"
@ -5263,6 +5273,13 @@ gray-matter@^4.0.2:
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
gzip-size@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==
dependencies:
duplexer "^0.1.2"
hamljs@^0.6.2:
version "0.6.2"
resolved "https://registry.npmjs.org/hamljs/-/hamljs-0.6.2.tgz"
@ -6054,6 +6071,11 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-fullwidth-code-point@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88"
integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==
is-glob@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz"
@ -10453,6 +10475,15 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.0.0.tgz#19191f152f937b96f4ec54ba0986a5656660c5a2"
integrity sha512-zwXcRmLUdiWhMPrHz6EXITuyTgcEnUqDzspTkCLhQovxywWz6NP9VHgqfVg20V/1mUg0B95AKbXxNT+ALRmqCw==
dependencies:
emoji-regex "^9.2.2"
is-fullwidth-code-point "^4.0.0"
strip-ansi "^7.0.0"
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz"
@ -10521,6 +10552,13 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.0.tgz#1dc49b980c3a4100366617adac59327eefdefcb0"
integrity sha512-UhDTSnGF1dc0DRbUqr1aXwNoY3RgVkSWG8BrpnuFIxhP57IqbS7IRta2Gfiavds4yCxc5+fEAVVOgBZWnYkvzg==
dependencies:
ansi-regex "^6.0.0"
strip-ansi@~0.1.0:
version "0.1.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz"