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