cowrite / scripts /build.js
lvwerra's picture
lvwerra HF Staff
LaTeX math via lazily-loaded KaTeX ($x$ inline, $$...$$ display), agent-proposable; Enter sends a comment, Shift+Enter adds a line
735a30f verified
Raw
History Blame Contribute Delete
3.6 kB
import esbuild from 'esbuild'
import fs from 'node:fs'
import zlib from 'node:zlib'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
const pub = path.join(root, 'public')
fs.mkdirSync(pub, { recursive: true })
// version stamp: the server rejects websocket clients from other builds so a
// stale tab can never silently corrupt a doc with its old state
const buildId = String(Date.now())
await esbuild.build({
entryPoints: [path.join(root, 'client/src/doc.js'), path.join(root, 'client/src/home.js')],
bundle: true,
minify: true,
format: 'iife',
outdir: pub,
define: { 'process.env.NODE_ENV': '"production"', __BUILD_ID__: JSON.stringify(buildId) },
logLevel: 'info',
})
// Asset URLs carry the build id. Without it a cached bundle keeps losing the
// websocket build-check on every reload, and the tab is stuck outdated forever:
// it still talks HTTP, so edits and accepts look like they work while the
// collaboration socket is dead. Versioned URLs make a reload always escape that.
for (const f of ['doc.html', 'index.html', 'app.css']) {
let body = fs.readFileSync(path.join(root, 'client', f), 'utf8')
if (f.endsWith('.html')) {
body = body
.replace(/(src|href)="\/(doc|home)\.js"/g, `$1="/$2.js?v=${buildId}"`)
.replace(/href="\/app\.css"/g, `href="/app.css?v=${buildId}"`)
}
fs.writeFileSync(path.join(pub, f), body)
}
// self-hosted webfont
fs.mkdirSync(path.join(pub, 'fonts'), { recursive: true })
for (const f of fs.readdirSync(path.join(root, 'client/fonts'))) {
fs.copyFileSync(path.join(root, 'client/fonts', f), path.join(pub, 'fonts', f))
}
// KaTeX, kept OUT of the main bundle: it is only fetched by a page that
// actually has math on it. The prebuilt UMD file already assigns window.katex,
// so there is nothing to re-bundle. Its stylesheet points at fonts/KaTeX_*,
// which resolves against /fonts — the same directory, already immutable-cached.
// Only woff2 is copied; every browser that runs this app prefers it.
const katexDist = path.join(root, 'node_modules/katex/dist')
fs.copyFileSync(path.join(katexDist, 'katex.min.js'), path.join(pub, 'katex.js'))
fs.copyFileSync(path.join(katexDist, 'katex.min.css'), path.join(pub, 'katex.css'))
for (const f of fs.readdirSync(path.join(katexDist, 'fonts'))) {
if (f.endsWith('.woff2')) fs.copyFileSync(path.join(katexDist, 'fonts', f), path.join(pub, 'fonts', f))
}
fs.writeFileSync(path.join(pub, 'build-id'), buildId)
// Pre-compress the bundles. The editor bundle is ~640KB raw and ~160KB brotli,
// and that transfer was most of a cold page load. Doing it here rather than in
// a runtime middleware costs nothing per request and gets brotli's slowest,
// smallest setting for free.
const compressed = []
for (const f of fs.readdirSync(pub)) {
if (!/\.(js|css)$/.test(f)) continue
const raw = fs.readFileSync(path.join(pub, f))
const br = zlib.brotliCompressSync(raw, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: raw.length,
},
})
const gz = zlib.gzipSync(raw, { level: zlib.constants.Z_BEST_COMPRESSION })
fs.writeFileSync(path.join(pub, f + '.br'), br)
fs.writeFileSync(path.join(pub, f + '.gz'), gz)
compressed.push(`${f} ${kb(raw.length)}K -> br ${kb(br.length)}K / gz ${kb(gz.length)}K`)
}
console.log('build done ->', pub, 'build-id', buildId)
for (const line of compressed) console.log(' ', line)
function kb(n) {
return Math.round(n / 1024)
}