testAssetsKeepTheirUrlsAcrossDeploys

tcmts.cli.test.e2e.prod.testAssetsKeepTheirUrlsAcrossDeploys · #oyyzdj72 · term

A build's browser code is a few chunks, named by their content, in a directory builds share: a deploy that changes the app's code leaves tcm's runtime chunk at the same URL (a browser keeps it), and assets are served compressed.

import { plainEnv, tcmBin, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { get, loadTab, setUp, waitFor } from "#codebase/tcmts/cli/test/e2e/prod";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import type { ChildProcess } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

export const testAssetsKeepTheirUrlsAcrossDeploys = () =>
  withChildren(async (keep) => {
    const { dir, tcm, scratch } = setUp();
    const app = (word: string) =>
      `// tcm: namespace app\nimport { hostname } from "node:os";\n` +
      `export async function greeting(name: string): Promise<string> { return "${word} " + name + " from " + hostname(); }\n` +
      `export async function main() { document.querySelector("#app")!.textContent = await greeting("ada"); }\n`;
    scratch(app("hello"));
    tcm("add");
    tcm("build", "app.main");
    const child: ChildProcess = keep(
      spawn(process.execPath, [tcmBin(), "start", "app.main", "--port", "0"], {
        cwd: dir,
        env: plainEnv(),
      }),
    );
    let out = "";
    child.stdout!.setEncoding("utf8").on("data", (d: string) => (out += d));
    child.stderr!.setEncoding("utf8").on("data", (d: string) => (out += d));
    await waitFor(() => /url\s+http:\/\/\S+/.test(out));
    const url = /url\s+(http:\/\/\S+)/.exec(out)![1]!;
    const v1 = await loadTab(url);
    assert.ok(v1.assets.length >= 2, `a few chunks: ${v1.assets.join(", ")}`);
    assert.match(await (await get(url)).text(), /<script type="importmap">/);

    // compressed for a browser that takes it, as is for one that doesn't
    const file = path.join(dir, ".tcm", "apps", "assets", path.basename(v1.asset));
    const br = await get(new URL(v1.asset, url), { headers: { "accept-encoding": "br" } });
    assert.equal(br.headers.get("content-encoding"), "br");
    assert.match(br.headers.get("cache-control") ?? "", /immutable/);
    assert.equal(await br.text(), fs.readFileSync(file, "utf8"));
    const plain = await get(new URL(v1.asset, url), { headers: { "accept-encoding": "identity" } });
    assert.equal(plain.headers.get("content-encoding"), null);
    assert.equal(await plain.text(), fs.readFileSync(file, "utf8"));
    assert.equal((await get(new URL("/_app/assets/nothing.js", url))).status, 404);

    // a deploy: the app's chunk is new, the rest are where they were
    scratch(app("hi"));
    tcm("update");
    await waitFor(async () => (await loadTab(url)).asset !== v1.asset);
    const v2 = await loadTab(url);
    const kept = v1.assets.filter((a) => v2.assets.includes(a));
    assert.ok(
      kept.length >= 1 && kept.length < v1.assets.length,
      `kept ${kept.join(", ")} of ${v1.assets.join(", ")}`,
    );
    assert.ok(!kept.includes(v1.asset), "the app's own chunk changed");
    // and the old one is still there, for a tab opened before
    assert.equal((await get(new URL(v1.asset, url))).status, 200);
  })

uses