File size: 3,597 Bytes
99a44ac
 
ec6b346
99a44ac
 
 
 
 
 
 
 
704a62b
 
 
 
99a44ac
 
 
 
 
 
704a62b
99a44ac
 
 
ed7ee81
 
 
 
99a44ac
ed7ee81
 
 
 
 
 
 
99a44ac
ec6b346
 
 
 
 
 
735a30f
 
 
 
 
 
 
 
 
 
 
 
704a62b
ec6b346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704a62b
ec6b346
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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)
}