// Vendors finrod's browser dependencies into ../static/vendor/ so the running // container serves everything locally — no esm.sh / CDN dependency at runtime. // // node build.mjs // // Outputs: // static/vendor/codemirror.js — esbuild bundle (view+state+commands) // static/vendor/cropper.esm.js — cropperjs ESM (dependency-free) // static/vendor/cropper.min.css — cropperjs stylesheet // static/vendor/tesseract/tesseract.esm.min.js — Tesseract.js main thread // static/vendor/tesseract/worker.min.js — Tesseract.js worker // static/vendor/tesseract/tesseract-core*.{js,wasm} — wasm OCR core // static/vendor/tesseract/lang/.traineddata.gz — default language packs import * as esbuild from "esbuild"; import { createRequire } from "node:module"; import { cp, mkdir, rm, readdir, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const here = dirname(fileURLToPath(import.meta.url)); const OUT = join(here, "..", "static", "vendor"); // Only Dutch is baked in — finrod's real use is OCR'ing Dutch source text. // Any other OCR language falls back to on-demand download from Tesseract's // default CDN. These packs are served locally but only fetched by the browser // when OCR is actually used (the OCR code path dynamic-imports Tesseract). const LANGS = ["nld"]; const TESSDATA_BASE = "https://tessdata.projectnaptha.com/4.0.0"; const pkgDir = (name) => dirname(require.resolve(`${name}/package.json`)); await rm(OUT, { recursive: true, force: true }); await mkdir(join(OUT, "tesseract", "lang"), { recursive: true }); // 1. CodeMirror — bundle the whole graph into one ESM file. await esbuild.build({ entryPoints: [join(here, "src", "codemirror.js")], bundle: true, format: "esm", minify: true, legalComments: "none", outfile: join(OUT, "codemirror.js"), }); console.log("✓ codemirror.js"); // 2. cropperjs — ESM build + CSS, both dependency-free, just copy. const cropper = pkgDir("cropperjs"); await cp(join(cropper, "dist", "cropper.esm.js"), join(OUT, "cropper.esm.js")); await cp(join(cropper, "dist", "cropper.min.css"), join(OUT, "cropper.min.css")); console.log("✓ cropperjs"); // 3. Tesseract.js — ship the prebuilt worker + wasm core (do NOT bundle; it // spawns a Web Worker and streams wasm, which expects real dist files). const tess = pkgDir("tesseract.js"); await cp(join(tess, "dist", "tesseract.esm.min.js"), join(OUT, "tesseract", "tesseract.esm.min.js")); await cp(join(tess, "dist", "worker.min.js"), join(OUT, "tesseract", "worker.min.js")); const core = pkgDir("tesseract.js-core"); for (const f of await readdir(core)) { if (f.endsWith(".js") || f.endsWith(".wasm")) { await cp(join(core, f), join(OUT, "tesseract", f)); } } console.log("✓ tesseract engine + core"); // Language packs — fetched at build time so the runtime image is self-contained. for (const lang of LANGS) { const url = `${TESSDATA_BASE}/${lang}.traineddata.gz`; const res = await fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); const buf = Buffer.from(await res.arrayBuffer()); await writeFile(join(OUT, "tesseract", "lang", `${lang}.traineddata.gz`), buf); console.log(`✓ tessdata ${lang} (${(buf.length / 1e6).toFixed(1)} MB)`); } console.log("vendor build complete →", OUT);