// Builds dist/index.html: one self-contained page (ttyd --index serves a single // file and nothing else), with CSS, JS, fonts and the favicon all inlined. import { build, transform } from 'esbuild'; import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const at = (...parts) => join(root, ...parts); const read = (...parts) => readFile(at(...parts), 'utf8'); // Version: $DEVDESK_VERSION (CI) or the top entry of debian/changelog. async function version() { if (process.env.DEVDESK_VERSION) return process.env.DEVDESK_VERSION; const changelog = await read('debian', 'changelog'); return /^devdesk \(([^)]+)\)/.exec(changelog)?.[1] ?? 'dev'; } // JetBrains Mono (OFL-1.1) from @fontsource: keep the latin + latin-ext faces // of the given weights and inline their woff2 files. async function fontFaces(weights) { const pkg = at('node_modules', '@fontsource', 'jetbrains-mono'); const faces = []; for (const weight of weights) { const css = await readFile(join(pkg, `${weight}.css`), 'utf8'); for (const block of css.match(/\/\* jetbrains-mono-latin(-ext)?-\d+-normal \*\/\s*@font-face\s*{[^}]*}/g) ?? []) { const file = /url\(\.\/files\/([^)]+\.woff2)\)/.exec(block)[1]; const data = (await readFile(join(pkg, 'files', file))).toString('base64'); faces.push(block.replace(/src:[^;]*;/, `src: url(data:font/woff2;base64,${data}) format('woff2');`)); } } if (!faces.length) throw new Error('no JetBrains Mono faces found'); return faces.join('\n'); } // The file viewer's editor (devdesk#19): its own IIFE, served over the ttyd // channel by `devdesk attach --asset editor.js` and cached by the browser. The // page carries its SHA-256, which is both the integrity check and the cache key. async function editorBundle() { const out = await build({ entryPoints: [at('web', 'editor', 'index.js')], bundle: true, minify: true, format: 'iife', globalName: 'DevDeskEditor', target: ['es2020'], write: false, legalComments: 'eof', }); const code = out.outputFiles[0].text; await mkdir(at('dist', 'assets'), { recursive: true }); await writeFile(at('dist', 'assets', 'editor.js'), code); return { sha: createHash('sha256').update(code).digest('hex'), size: Buffer.byteLength(code) }; } async function main() { const ver = await version(); const editor = await editorBundle(); const js = await build({ entryPoints: [at('web', 'src', 'main.js')], bundle: true, minify: true, format: 'iife', target: ['es2020'], write: false, legalComments: 'eof', define: { __DEVDESK_VERSION__: JSON.stringify(ver), __EDITOR_SHA__: JSON.stringify(editor.sha), }, }); const css = await transform([ await read('vendor', 'web_template', 'base.css'), await read('node_modules', '@xterm', 'xterm', 'css', 'xterm.css'), await fontFaces([400, 700]), await read('web', 'styles.css'), ].join('\n'), { loader: 'css', minify: true, legalComments: 'eof' }); const { FAVICON: favicon } = await import('./src/icon.js'); const script = js.outputFiles[0].text.replace(/<\/script/gi, '<\\/script'); const style = css.code.replace(/<\/style/gi, '<\\/style'); const template = await read('web', 'index.template.html'); const html = template .replace('__FAVICON__', () => favicon) .replace('', () => ``) .replace('', () => ``); await mkdir(at('dist'), { recursive: true }); await writeFile(at('dist', 'index.html'), html); console.log(`dist/index.html ${(html.length / 1024).toFixed(0)} KiB (devdesk ${ver})`); console.log(`dist/assets/editor.js ${(editor.size / 1024).toFixed(0)} KiB sha256 ${editor.sha.slice(0, 12)}`); } main().catch((err) => { console.error(err); process.exit(1); });