testStaticSitesAndPwas

tcmts.cli.test.e2e.prod.testStaticSitesAndPwas · #qvz2k2er · term

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");
  })

uses