previewInScratch
uses node:fs
A scratch file's watch (the statement beginning on line, text being what the file holds) that comes to a React
element, built to run in the browser: its module and what it uses, bundled with the React DOM its React goes with
(one script, React DOM's createRoot rendering it), for a notebook to run in a frame of its own; kept by the
statement's hash (previewBuilds), so it's built again only when what it runs changes. Code that only runs on the
server fails there, saying why.
import { exportId } from "#codebase/tcmts/core/hash"; import { materialize, moduleFile } from "#codebase/tcmts/loader"; import { HttpError } from "#codebase/tcmts/server"; import { analyzedScratch, editableCodebase, previewBuilds, scratchPath } from "#codebase/tcmts/studio"; import fs from "node:fs"; import path from "node:path"; export async function previewInScratch(file: string, text: string, line: number): Promise<string> { const cb = editableCodebase(); const abs = scratchPath(cb, file, text); const a = analyzedScratch(cb, abs, text); if (a.diagnostics.length || a.errors.length) throw new HttpError(409, `${file} has problems: fix them first.`); const w = a.watches.find((x) => x.line === line); if (!w) throw new HttpError(404, `There's no statement on line ${line} to run.`); const src = cb.source([...a.defs, ...a.watches]); materialize([w.hash], src, cb.lay); const dir = cb.lay.cacheDir; const mod = moduleFile(w.componentHash, src); // (the React DOM of the module's own React: its JSX names it) const react = /@jsxImportSource (npm__react__[\w.-]+)/.exec( fs.readFileSync(path.join(dir, mod), "utf8"), )?.[1]; const doms = fs .readdirSync(path.join(dir, "node_modules")) .filter((d) => d.startsWith("npm__react-dom__")); const dom = (react && doms.find((d) => d === react.replace("npm__react__", "npm__react-dom__"))) ?? doms.sort().at(-1); if (!dom) throw new HttpError(409, "The codebase runs no React DOM to render it with."); // (built before: the same code, all it runs the same) const key = `${w.hash} ${dom}`; const kept = previewBuilds.get(key); if (kept) return kept; const entry = path.join(dir, `_preview_${w.hash}.tsx`); fs.writeFileSync( entry, `import { createRoot } from "${dom}/client";\nimport * as m from "./${mod}";\ncreateRoot(document.getElementById("root")!).render((m as Record<string, any>)[${JSON.stringify(exportId(w.hash))}]);\n`, ); try { const { build } = await import("vite"); const out = (await build({ configFile: false, root: dir, logLevel: "silent", mode: "production", define: { "process.env.NODE_ENV": JSON.stringify("production") }, esbuild: { jsx: "automatic", jsxDev: false }, oxc: { jsx: { runtime: "automatic", development: false } }, build: { write: false, modulePreload: false, minify: true, target: "es2022", rollupOptions: { input: entry, output: { inlineDynamicImports: true, format: "es" } }, }, } as never)) as | { output: { type: string; code?: string }[] } | { output: { type: string; code?: string }[] }[]; const chunk = (Array.isArray(out) ? out[0]! : out).output.find((o) => o.type === "chunk"); if (!chunk?.code) throw new Error("nothing was built"); previewBuilds.set(key, chunk.code); for (const k of previewBuilds.keys()) { if (previewBuilds.size <= 40) break; previewBuilds.delete(k); } return chunk.code; } catch (e) { throw new HttpError( 400, `It couldn't be built to run in the browser: ${(e as Error).message.split("\n")[0]}`, ); } finally { fs.rmSync(entry, { force: true }); } }
uses
- tcmts.core.hash.exportId
- tcmts.loader.materialize
- tcmts.loader.moduleFile
- tcmts.server.HttpError
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.previewBuilds
- tcmts.studio.scratchPath