// Serves the locally-vendored React ESM (client/vendor/react, generated by // scripts/build-vendor.ts) behind the stable `/vendor/react/*` URLs the // importmap in client/index.html points at. This is what makes moi run offline // — no esm.sh fetch. // // The importmap URLs carry no dev/prod distinction; we pick the build here so // developers get React's warnings (invalid hook calls, key warnings) while // production gets the smaller build. `client/` ships in the published package // (see package.json `files`), so this resolves in a global install too. import { join } from 'node:path' import { prebuilt } from './static' const VENDOR_DIR = join(import.meta.dir, '..', 'client', 'vendor', 'react') const EMOJIBASE_DIR = join(import.meta.dir, '..', 'client', 'vendor', 'emojibase') const MODE = prebuilt ? 'production' : 'development' // A single flat file (`react.js`) or one nested under `_impl/` — both `.js`, // no traversal. Matches exactly the layout build-vendor.ts emits. const ALLOWED = /^(_impl\/)?[a-zA-Z0-9_-]+\.js$/ export async function serveVendorReact(req: Request): Promise { const rel = new URL(req.url).pathname.replace(/^\/vendor\/react\//, '') if (!ALLOWED.test(rel) || rel.includes('..')) { return new Response('Not found', { status: 404 }) } const file = Bun.file(join(VENDOR_DIR, MODE, rel)) if (!(await file.exists())) { return new Response('Not found', { status: 404 }) } // Revalidate on every load (no max-age): the same URLs serve different bytes // when the mode flips (prebuilt `moi` vs `bun run dev` on one port) or after // a `vendor:react` regen. The ETag encodes both, so browsers keep the bytes // (304) but never reuse a stale mode — prod jsx-dev-runtime has no `jsxDEV` // and would crash dev. const etag = `"${MODE}-${file.lastModified}-${file.size}"` const headers = { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-cache', ETag: etag } if (req.headers.get('if-none-match') === etag) { return new Response(null, { status: 304, headers }) } return new Response(file, { headers }) } // `/.json` only — mirrors the emojibase-data package layout the // emoji picker expects (`${emojibaseUrl}/${locale}/data.json`). const EMOJIBASE_ALLOWED = /^[a-z-]+\/[a-z]+\.json$/ // Serves the vendored emojibase-data JSON (client/vendor/emojibase) behind // `/vendor/emojibase/*`, so the settings emoji picker works offline — no // jsdelivr fetch at runtime. export async function serveVendorEmojibase(req: Request): Promise { const rel = new URL(req.url).pathname.replace(/^\/vendor\/emojibase\//, '') if (!EMOJIBASE_ALLOWED.test(rel) || rel.includes('..')) { return new Response('Not found', { status: 404 }) } const file = Bun.file(join(EMOJIBASE_DIR, rel)) if (!(await file.exists())) { return new Response('Not found', { status: 404 }) } return new Response(file, { headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'public, max-age=3600' } }) }