esm.sh's degraded storage backend served an unpinned transitive @codemirror/view@^6.27.0 range URL as text/plain, which browsers block for a disallowed MIME type. A failed top-level module import aborts the whole <script type=module>, so the language/model dropdowns came up empty despite a healthy backend — and top-level version pinning can't fix a transitive range. Add a frontend/ esbuild stage (Node stage in the Dockerfile) that vendors everything into static/vendor and serve only from there: - CodeMirror (view/state/commands) bundled into one self-contained ESM file - cropperjs (+ CSS) copied from npm dist - Tesseract.js engine + wasm core + Dutch (nld) language pack; OCR assets stay lazy (fetched only when a scan runs) and fully local - Drop vim mode (@replit/codemirror-vim); keep document-level Shift-H/L Google Fonts remains the only external <link> (degrades gracefully, not in the failing path). Bump to 0.5.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.4 KiB
JavaScript
78 lines
3.4 KiB
JavaScript
// 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/<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);
|