exportStatic
uses node:path
Write a build out as a static site, for any static host (tcm build --static <dir>). A component app's pages are
rendered as it's built (#4ydmpw3v): each page it links to, from /, is a file (pageFile) with its
first paint, server components included, and what they rendered is kept in _app/trees/, for the app to read when
it goes to another page, or before. Each tree's file (Flight's text) is named by its content (t.<sha256>.txt), and so is the map
of them the pages name (_app/site.<sha256>.json: the file each tree is in, and the trees each page shows), so a
browser keeps them for good: only the pages are asked for again. Also: 404.html, the page that renders any other
path in the browser (GitHub Pages, Cloudflare Pages and others serve it for a path with no file), the assets in _app/assets/, a PWA's service worker
(sw.js), which keeps every page, and .nojekyll (so GitHub Pages serves _app). It's for the path the site is
served under (base). An app that calls server functions can't be one: there's no server to answer. Files already in
dir stay, so building into the last build's directory keeps the assets a tab still running it may load.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { appsDir, assetsDir, pageFile, pageUnder, prerender, serviceWorkerSource } from "#codebase/tcmts/cli/web"; import type { BuildManifest } from "#codebase/tcmts/cli/web"; import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; export async function exportStatic( cb: Codebase, build: BuildManifest, dir: string, base: string, ): Promise<{ files: string[]; pages: number; warnings: string[] }> { const calls = build.endpoints.filter((h) => !build.components?.includes(h)); if (calls.length) { const names = calls.map((h) => cb.displayRef(`#${h}`)).join(", "); throw new Error( `${build.app} calls functions on the server (${names}), and a static site has no server: run it with \`tcm start\``, ); } const from = path.join(appsDir(cb), build.hash); // (said to the page first: server components read what the build rendered) const shell = pageUnder(fs.readFileSync(path.join(from, "index.html"), "utf8"), base).replace( /<head>/, "<head>\n<script>var __TCM_STATIC__ = true;</script>", ); const site = build.ssr ? await prerender(cb, build, shell, base) : { pages: new Map([["/", shell]]), trees: new Map<string, string>(), uses: new Map<string, string[]>(), warnings: [], }; const written: string[] = []; const write = (rel: string, content: string | Uint8Array) => { const file = path.join(dir, rel); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, content); written.push(rel); }; // each tree in a file named by its content (two the same are one), and the map of them named so too, which every // page names: what a page reads, a browser keeps for good const named = (text: string) => createHash("sha256").update(text).digest("hex").slice(0, 32); const files = new Map<string, string>(); const treeFiles: Record<string, string> = {}; for (const [name, tree] of site.trees) { const file = `t.${named(tree)}`; treeFiles[name] = file; files.set(`_app/trees/${file}.txt`, tree); } const map = JSON.stringify({ trees: treeFiles, pages: Object.fromEntries(site.uses) }); const mapFile = `site.${named(map)}`; if (site.trees.size) files.set(`_app/${mapFile}.json`, map); const said = (html: string) => site.trees.size ? html.replace( "var __TCM_STATIC__ = true;", () => `var __TCM_STATIC__ = ${JSON.stringify(mapFile)};`, ) : html; for (const [p, html] of site.pages) write(pageFile(p), said(html)); write("404.html", said(shell)); write(".nojekyll", ""); for (const [rel, text] of files) if (!fs.existsSync(path.join(dir, rel))) write(rel, text); else written.push(rel); for (const f of build.assets) { const rel = `_app/assets/${f}`; // (a name is its content: one there already is this one) if (fs.existsSync(path.join(dir, rel))) written.push(rel); else write(rel, fs.readFileSync(path.join(assetsDir(cb), f))); } if (build.pwa) { // it keeps every page and what they rendered, and is new whenever any of it is (a browser updates when it changes) const kept = [ ...[...site.pages.keys()].map((p) => `.${p}`), "./404.html", ...[...files.keys()].map((rel) => `./${rel}`), ...build.assets.map((f) => `./_app/assets/${f}`), ]; const version = createHash("sha256").update(build.hash); for (const html of site.pages.values()) version.update(html); version.update(map); // (a static site has no server: it opens from the device first) write( "sw.js", serviceWorkerSource(version.digest("hex").slice(0, 52), kept, true, "./404.html"), ); } return { files: written, pages: site.pages.size, warnings: site.warnings }; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.web.BuildManifest
- tcmts.cli.web.appsDir
- tcmts.cli.web.assetsDir
- tcmts.cli.web.pageFile
- tcmts.cli.web.pageUnder
- tcmts.cli.web.prerender
- tcmts.cli.web.serviceWorkerSource