pagez

The build report

What build prints when it's done. Each page's row has its HTML, gzipped, as it's sent, the time it took to compile, and the elements that give it behavior. So a page that grows heavy or slow shows where the cost is.

What pages share is one common row, the CSS and JavaScript Vite bundled, since the browser downloads it once. The last line is the whole build and the version of Pagez that ran.

page           html   time
/            1.2 KB  12 ms
/essays/     1.4 KB   9 ms  fold-out
common       0.8 KB css, 1.1 KB js
2 pages and 1 redirect in 240 ms → dist · pagez 0.1.0
import { createLogger, type Logger } from "vite-plus";

/** A page as the report shows it. */
export interface Reported {
  /** Its path from the root, `/about/`, or `/404.html`. */
  path: string;
  /** Its HTML's size, gzipped, in bytes. */
  html: number;
  /** How long it took to compile, in milliseconds. */
  ms: number;
  behavior: string[];
}

export interface Report {
  pages: Reported[];
  /** The bundled CSS and JavaScript, gzipped, in bytes. */
  css: number;
  js: number;
  redirects: number;
  ms: number;
  out: string;
  version: string;
}

A logger that prints the report and Vite's warnings and errors, and none of Vite's other lines. Vite lists each file it wrote, which the report says page by page, so pagez build prints the report alone. The build also runs at logLevel: "warn", which stops Vite's progress lines.

export function reportOnly(): Logger {
  const logger = createLogger();
  const info = logger.info.bind(logger);
  logger.info = (message, options) => {
    if (/ · pagez \S+$/.test(message)) info(message, options);
  };
  return logger;
}

export function report({ pages, css, js, redirects, ms, out, version }: Report): string {
  const rows = [
    ["page", "html", "time", ""],
    ...pages.map((page) => [page.path, size(page.html), time(page.ms), page.behavior.join(", ")]),
  ];
  const widths = [0, 1, 2].map((i) => Math.max(...rows.map((row) => row[i]!.length)));
  const lines = rows.map((row) =>
    [row[0]!.padEnd(widths[0]!), row[1]!.padStart(widths[1]!), row[2]!.padStart(widths[2]!), row[3]]
      .join("  ")
      .trimEnd(),
  );
  const common = [...(css > 0 ? [`${size(css)} css`] : []), ...(js > 0 ? [`${size(js)} js`] : [])];
  if (common.length > 0) lines.push(`${"common".padEnd(widths[0]!)}  ${common.join(", ")}`);
  const counted = [
    plural(pages.length, "page"),
    ...(redirects > 0 ? [plural(redirects, "redirect")] : []),
  ];
  lines.push(`${counted.join(" and ")} in ${time(ms)} → ${out} · pagez ${version}`);
  return lines.join("\n");
}

const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? "" : "s"}`;

/** Kilobytes, to one decimal below 10 KB, where a tenth still means something. */
export function size(bytes: number): string {
  const kb = bytes / 1024;
  return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
}

export function time(ms: number): string {
  return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`;
}