testDevPlacementAndCallsByHash
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
- tcmts.cli.test.browser.CHROME
- tcmts.cli.test.browser.pageText
- tcmts.cli.test.e2e.harness.okIn
- tcmts.cli.test.e2e.harness.tempDir
- tcmts.cli.test.e2e.harness.withChildren
- tcmts.cli.test.e2e.web.crawl
- tcmts.cli.test.e2e.web.get
- tcmts.cli.test.e2e.web.hashOf
- tcmts.cli.test.e2e.web.rpc
- tcmts.cli.test.e2e.web.startDev
- tcmts.cli.test.e2e.web.waitFor