import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { build } from "esbuild"; import { writeWebLicenseNotices } from "./package-licenses.mjs"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { tsImport } from "tsx/esm/api"; import { buildDocumentation } from "../site/docs/build.mjs"; const source = new URL("../site/", import.meta.url); const output = new URL("../build/site/", import.meta.url); await rm(output, { recursive: true, force: true }); await mkdir(output, { recursive: true }); await cp(source, output, { recursive: true, filter: (path) => path !== fileURLToPath(new URL("docs", source)) && !path.endsWith(".ts") && !path.endsWith(".tsx") && !path.endsWith("tsconfig.json") && !path.endsWith("README.md"), }); await cp(new URL("../LICENSE", import.meta.url), new URL("LICENSE", output)); const [{ locales }, { Glyph }] = await Promise.all([ "../src/client/locales/index.ts", "../src/client/ui/icons.tsx", ].map(path => tsImport(path, { parentURL: import.meta.url, tsconfig: fileURLToPath(new URL("../src/tsconfig.json", import.meta.url)), }))); // Independent locale pools stay inert HTML; the first entry is also the no-JS fallback. const welcomePools = [locales.en, locales.zh].map(locale => locale.playful.welcome.map(({ text }) => renderToStaticMarkup(createElement("span", { lang: locale.tag }, `“${text}”`)))); const homepage = (await readFile(new URL("index.html", source), "utf8")) .replace(/

.*?<\/p>/, `

${welcomePools.map(pool => pool[0] ?? "").join("")}

`); await writeFile(new URL("index.html", output), homepage.replace( '', ``, )); // The page shares only the small, DOM-based rotation owner with the product UI. await build({ entryPoints: [fileURLToPath(new URL("main.js", source))], outfile: fileURLToPath(new URL("main.js", output)), bundle: true, format: "esm", minify: true, }); // The opening shot uses the current homepage, with only its film clock adapter // substituted for the ordinary page script. Keep its layout and copy in one place. await writeFile(new URL("film/ui/website.html", output), homepage .replace('', '') .replace('', '') .replace('', (await readFile(new URL("film/ui/index.html", source), "utf8")).match(//)[0] + ''), ); // Static controls use the product icon owner without shipping another React // runtime to the film page. The live demonstration remains its isolated bundle. const filmPage = new URL("film/index.html", output); await writeFile(filmPage, (await readFile(filmPage, "utf8")).replace( /<\/span>/g, (_, name) => renderToStaticMarkup(createElement(Glyph, { name, size: 21 })), )); await cp( new URL("../src/client/components/living/playback-controls.css", import.meta.url), new URL("film/playback-controls.css", output), ); await cp( new URL("../src/client/components/living/brand-mark.css", import.meta.url), new URL("film/brand-mark.css", output), ); writeWebLicenseNotices( fileURLToPath(new URL("../", import.meta.url)), fileURLToPath(new URL("film/ui/third-party-licenses.txt", output)), ); await build({ entryPoints: { ui: fileURLToPath(new URL("../site/film/ui/main.tsx", import.meta.url)), website: fileURLToPath(new URL("../site/film/ui/website.ts", import.meta.url)), }, outdir: fileURLToPath(new URL("film/ui/", output)), bundle: true, format: "iife", jsx: "automatic", minify: true, legalComments: "linked", define: { "import.meta": "piikFilmBuild", "process.env.NODE_ENV": '"production"', }, banner: { js: "const piikFilmBuild = { url: document.currentScript.src, env: { DEV: false } };", }, }); await buildDocumentation(); console.log( "Website built in build/site (static files, including the shared product UI).", );