Spaces:
Running
Running
| 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) | |
| } | |