testStaticSitesAndPwas
tcm build --static, and PWAs: an app with a pwa beside it gets a manifest, its icons and a service worker, which
tcm start serves too; changing the pwa alone is a new build; and an app that calls the server can't be a static
site, which has none
import { plainEnv, tcmBin, tcmIn, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { get, setUp } from "#codebase/tcmts/cli/test/e2e/prod"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; export const testStaticSitesAndPwas = () => withChildren(async (keep) => { const { dir, tcm, scratch } = setUp(); scratch(`// tcm: namespace app export async function serverTime(): Promise<string> { return new Date(Number(process.env.T ?? 0)).toISOString(); } export async function main() { document.querySelector("#app")!.textContent = await serverTime(); } export function local() { document.querySelector("#app")!.textContent = "here"; } export const pwa = { name: "Here", theme_color: "#123456", icons: [{ src: '<svg xmlns="http://www.w3.org/2000/svg"/>' }] }; `); tcm("add"); const refused = tcmIn(dir, "build", "app.main", "--static", "out"); assert.equal(refused.status, 1); assert.match( refused.out, /app\.main calls functions on the server \(app\.serverTime\), and a static site has no server/, ); assert.ok(!fs.existsSync(path.join(dir, "out")), "nothing is written"); // a PWA: what calls no server opens from its cache first const built = tcm("build", "app.local", "--static", "out"); assert.match( built, /pwa\s+installable; its service worker keeps \d+ file\(s\) and opens it from them first/, ); assert.match(built, /warning\s+the pwa has no \.png icon/); const page = fs.readFileSync(path.join(dir, "out", "index.html"), "utf8"); assert.doesNotMatch(page, /var __TCM_BASE__/, "at the site's root"); assert.match(page, /<meta name="theme-color" content="#123456">/); assert.match(page, /navigator\.serviceWorker\.register\(base \+ "\/sw\.js"/); assert.match( fs.readFileSync(path.join(dir, "out", "sw.js"), "utf8"), /const OFFLINE_FIRST = true;/, ); // the pwa is part of what the build is: changing it alone builds again const builds = () => fs.readdirSync(path.join(dir, ".tcm", "apps")).filter((f) => /^[a-z2-7]{52}$/.test(f)); const before = builds(); tcm("patch", "app.pwa", "--old", "#123456", "--new", "#654321"); tcm("build", "app.local"); assert.equal(builds().length, before.length + 1); assert.match(tcm("apps"), /\* app\.local/); // tcm start serves the service worker of the build it serves, never cached const url = await new Promise<string>((resolve, reject) => { const child = keep( spawn(process.execPath, [tcmBin(), "start", "app.local", "--port", "0"], { cwd: dir, env: plainEnv(), }), ); let out = ""; child.stdout.setEncoding("utf8").on("data", (d: string) => { out += d; const m = /url\s+(http:\/\/\S+)/.exec(out); if (m) resolve(m[1]!); }); child.on("exit", (code) => reject(new Error(`tcm start exited ${code}:\n${out}`))); }); const sw = await get(new URL("sw.js", url)); assert.equal(sw.status, 200); assert.equal(sw.headers.get("cache-control"), "no-cache"); assert.match(await sw.text(), /the service worker of build/); const served = await (await get(url, { headers: { accept: "text/html" } })).text(); assert.match(served, /<meta name="theme-color" content="#654321">/); const manifest = /<link rel="manifest" href="([^"]+)">/.exec(served)![1]!; const m = await get(new URL(manifest, url)); assert.equal(m.headers.get("content-type"), "application/manifest+json"); assert.equal(((await m.json()) as { name: string }).name, "Here"); })