123/**4 * The `deno task build` command: builds the static PWA into `dist/` with a5 * source-hash build id, a service-worker precache manifest, and a scan that6 * fails the build if server secrets leak into client output.7 *8 * @module9 */1011import { extname, join, relative } from "node:path";12import { LOFI_VERSION } from "../version.ts";13import { prepareLofiAstroConfig } from "../astro/mod.ts";14import { loadEnvironment, serverEnvironmentNames } from "../tooling/environment.ts";15import {16 duplicateBuildAssets,17 precacheUrls,18 scanSecrets,19 shellWeightBytes,20 sourceFingerprint,21 walkFiles,22} from "../tooling/project.ts";23import {24 precachePaths,25 productionPwaIssues,26 screenshotAssetPaths,27} from "../tooling/pwa-validation.ts";28import { runDeno } from "../tooling/process.ts";29import { exitOnFailure, validatedCommandEnvironment } from "./shared.ts";3031// A fresh install downloads the whole precache, so every build reports the32// shell's weight, and an unusually heavy shell (the lofi runtime with the Jazz33// WASM engine accounts for ~10 MB of it) earns a warning. How much an app34// ships is the author's call — the warning is information, not a gate.35const shellWeightNoticeBytes = 20 * 1024 * 1024;3637function megabytes(bytes: number): string {38 return (bytes / (1024 * 1024)).toFixed(1);39}4041const environment = await validatedCommandEnvironment();42environment.LOFI_SKIP_JAZZ_MANAGED = "1";43environment.JAZZ_ADMIN_SECRET = "";44environment.BACKEND_SECRET = "";45const sourceHash = (await sourceFingerprint()).slice(0, 12);46const astroConfig = relative(Deno.cwd(), await prepareLofiAstroConfig());4748async function readPackageAsset(path: string): Promise<string> {49 const url = new URL(path, import.meta.url);50 if (url.protocol === "file:") return await Deno.readTextFile(url);51 const response = await fetch(url);52 if (!response.ok) {53 throw new Error(`failed to read package asset ${path}: HTTP ${response.status}`);54 }55 return await response.text();56}5758exitOnFailure(59 await runDeno(60 ["run", "-A", "npm:astro@7.0.9", "build", "--config", astroConfig],61 environment,62 ),63 "production build",64);6566await Deno.writeTextFile(67 "dist/lofi-build.json",68 `${69 JSON.stringify({70 lofiVersion: LOFI_VERSION,71 sourceHash,72 basePath: environment.LOFI_BASE_PATH,73 builtAt: new Date().toISOString(),74 })75 }\n`,76);77const serviceWorkerPath = "dist/sw.js";78const serviceWorker = await readPackageAsset("../runtime/sw.js");79await Deno.writeTextFile(80 serviceWorkerPath,81 serviceWorker.replace("__LOFI_BUILD_REVISION__", sourceHash),82);83const productionManifest = JSON.parse(await Deno.readTextFile("dist/manifest.webmanifest"));84// One walk serves the precache manifest, the shell checks, and the route85// count below.86const distFiles = await walkFiles("dist", { includeDist: true });87const screenshotPaths = screenshotAssetPaths(productionManifest, environment.LOFI_BASE_PATH);88const shellPaths = precachePaths(distFiles, screenshotPaths);8990const duplicateAssets = await duplicateBuildAssets(shellPaths);91if (duplicateAssets.length > 0) {92 for (const group of duplicateAssets) {93 console.error(`error: byte-identical build assets: ${group.join(", ")}`);94 }95 console.error(96 "error: two build contexts emitted the same payload under different names and every copy is precached; unify the asset naming, then rerun `deno task build`",97 );98 Deno.exit(1);99}100101const shellBytes = await shellWeightBytes(shellPaths);102if (shellBytes > shellWeightNoticeBytes) {103 console.warn(104 `warning: the app shell weighs ${105 megabytes(shellBytes)106 } MB; a fresh install downloads every precached byte, so heavy assets may belong outside the precache (served on demand instead)`,107 );108}109110const precache = precacheUrls(distFiles, screenshotPaths);111await Deno.writeTextFile("dist/lofi-precache.json", `${JSON.stringify(precache.sort())}\n`);112113const pwaIssues = await productionPwaIssues(Deno.cwd(), environment.LOFI_BASE_PATH);114if (pwaIssues.length > 0) {115 for (const pwaIssue of pwaIssues) console.error(`error: ${pwaIssue.detail}`);116 console.error(`error: ${pwaIssues[0].remediation}`);117 Deno.exit(1);118}119120const loadedEnvironment = await loadEnvironment();121const serverSecrets = Object.fromEntries(122 serverEnvironmentNames.map((name) => [name, loadedEnvironment[name] ?? ""]),123);124const secretResult = await scanSecrets(serverSecrets);125if (secretResult.leaks.length > 0) {126 for (const leak of secretResult.leaks) {127 console.error(`error: ${leak.name} value found in ${leak.path}`);128 }129 console.error(130 "error: build output may expose a server secret; remove it and rerun `deno task build`",131 );132 Deno.exit(1);133}134135const routes = distFiles.filter((path) => extname(path) === ".html").length;136console.log(137 `lofi build: ${join(Deno.cwd(), "dist")} (${routes} routes, ${sourceHash}, ${138 megabytes(shellBytes)139 } MB shell, secret scan passed)`,140);