testDevPlacementAndCallsByHash

tcmts.cli.test.e2e.web.testDevPlacementAndCallsByHash · #lkkhhcms · term

tcm dev: placement is inferred, server code never reaches the browser, calls cross by hash

import { CHROME, pageText } from "#codebase/tcmts/cli/test/browser";
import { okIn, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { crawl, get, hashOf, rpc, startDev, waitFor } from "#codebase/tcmts/cli/test/e2e/web";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

export const testDevPlacementAndCallsByHash = () =>
  withChildren(async (keep) => {
    const dir = tempDir("web");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    const scratch = (text: string) => fs.writeFileSync(path.join(dir, "scratch.ts"), text);
    // a file for the server function to read
    const lines = path.join(tempDir("web lines"), "lines.txt");
    fs.writeFileSync(lines, "one\ntwo\nthree\n");
    tcm("init");
    fs.rmSync(path.join(dir, "scratch.ts"));
    scratch(`// tcm: namespace app
import { readFileSync } from "node:fs";
const visits = new Map<string, number>();
export async function lineCount(): Promise<number> { return readFileSync(${JSON.stringify(lines)}, "utf8").split("\\n").length; }
export async function visit(who: string): Promise<{ who: string; n: number; pid: number; at: Date }> {
  const n = (visits.get(who) ?? 0) + 1;
  visits.set(who, n);
  return { who, n, pid: process.pid, at: new Date(0) };
}
export const render = (lines: number, n: number) => \`lines \${lines}, visit \${n}\`;
export async function main() {
  const v = await visit("ada");
  document.querySelector("#app")!.textContent = render(await lineCount(), v.n);
}
`);
    tcm("add");
    assert.match(tcm("view", "app.lineCount"), /runs on the server: it uses node:fs; browser code calls it by hash/);
    assert.match(tcm("view", "app.main"), /runs in the browser: it uses document/);
    assert.match(tcm("view", "app.render"), /runs anywhere/);

    const { url, output } = await startDev(keep, dir, "app.main");
    // the report is written in several pieces after the url line: wait for it rather than racing it
    await waitFor(() => /server\s+2 function\(s\) the browser calls by hash: app\.(visit|lineCount)/.test(output()));

    // what the browser receives: the browser half, and stubs in place of the server half
    const modules = await crawl(url);
    const shipped = [...modules.values()].join("\n");
    for (const secret of ["readFileSync", "node:fs", "process.pid", "visits.set"]) assert.ok(!shipped.includes(secret), `${secret} must not reach the browser`);
    assert.ok([...modules.keys()].some((u) => u.includes("tcm-stub")), "server components arrive as stubs");
    assert.ok(shipped.includes("lines ${lines}"), "browser code is shipped as code");

    // the endpoints: exactly the server functions the app calls; state stays on the server
    const first = await rpc(url, hashOf(dir, "app.visit"), ["ada"]);
    const second = await rpc(url, hashOf(dir, "app.visit"), ["ada"]);
    assert.equal(first.status, 200);
    assert.match(second.body, /"n":2/);
    assert.match(first.body, /"at":\{"\$d":"1970-01-01T00:00:00.000Z"\}/, "dates cross as dates");
    assert.equal((await rpc(url, hashOf(dir, "app.render"), [1, 2])).status, 403, "browser code is not an endpoint");

    if (CHROME) {
      // the visit count depends on how often the headless page reloads; that it came from the server is what matters
      const text = await pageText(url, "#app", (t) => /^lines \d+, visit \d+$/.test(t));
      assert.match(text, /^lines \d+, visit \d+$/, "the app ran in a real browser and called the server");
    }

    // an update in another process reloads the app
    scratch(`// tcm: namespace app\nexport const render = (lines: number, n: number) => \`\${lines} lines (visit \${n})\`;\n`);
    tcm("update");
    const updated = hashOf(dir, "app.main");
    let entry = "";
    for (const end = Date.now() + 8000; Date.now() < end && !entry.includes(updated); await new Promise((r) => setTimeout(r, 150))) {
      entry = await (await get(new URL("/entry.ts", url))).text();
    }
    assert.ok(entry.includes(updated), "the entry now imports the updated app");
    assert.match(output(), /reloaded app\.main/);
    if (CHROME) {
      const text = await pageText(url, "#app", (t) => /^\d+ lines \(visit \d+\)$/.test(t));
      assert.match(text, /^\d+ lines \(visit \d+\)$/, "the reloaded app renders with the updated code");
    }

    // a synchronous server function can't be called across the network; the error names it
    scratch(`// tcm: namespace app
import { hostname } from "node:os";
export const machine = () => hostname();
export async function main() { document.body.textContent = machine(); }
`);
    tcm("update");
    await waitFor(() => /calls app\.machine, which runs on the server .* isn't async/.test(output()));
    assert.match(output(), /Make app\.machine async\./);
  })

uses