Files
zeekay 48f9b8faf0 Serve Zen, and stop serving Inter's bytes
The Geist->Zen rename reached the stylesheets and stopped there. Four layers
were still handing users another vendor's face.

frontend/public still carried Inter.woff2/woff, the real upstream binaries
signed RSMS, and fonts.scss declared three static Inter weights on top of the
variable face; one variable file spans 100-900, so those three collapse into it.
The Zen pair here named only Hanzo in its copyright, which OFL does not allow
for a cut of Geist — @hanzo/font's identity check reads family, vendor tag and
both notices out of the name table and refuses to publish a face that fails, so
the bytes now come from there.

common/tailwind/dist/tailwind.css is a build artifact. .gitignore names it,
`insightscli doctor` deletes it, nothing imports it (the app imports the source
tailwind.css), and a single upstream merge dragged it in. Its
--default-font-family still said MatterSQ, two renames behind the config it is
supposed to come from. Untracked rather than hand-edited.

@hanzo/quill's tokens set --font-sans behind [data-quill], a later declaration
than base.scss's :root, so every component inside a quill subtree resolved to
the token's own Inter stack instead of the app's. The token now leads with Zen,
and each package's own build regenerates its dist.

The preload lookup in build.mjs matched /Inter(-hash)?\.woff2/ and throws when
it resolves empty, so it had to move with the file. It was also warming the
second family in --font-sans rather than the first.

Two smaller ones: the region picker fetched Inter from fonts.googleapis.com on
an unauthenticated page, and quill's storybook depended on
@fontsource-variable/inter, which declares the family "Inter Variable" — a name
no stack in this repo asks for, so it downloaded and painted nothing.
2026-08-28 17:12:24 -07:00

223 lines
7.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
import * as fs from 'fs'
import * as path from 'path'
import { fileURLToPath } from 'url'
import {
buildInParallel,
copyIndexHtml,
copyPublicFolder,
copyRRWebWorkerFiles,
copySnappyWASMFile,
createHashlessEntrypoints,
isDev,
reportTopChunks,
startDevServer,
} from '@hanzo/esbuilder'
import { finalizeToolbarBuild, getToolbarAppBuildConfig } from './toolbar-config.mjs'
export const __dirname = path.dirname(fileURLToPath(import.meta.url))
// Chunks below this size aren't worth a preload tag in the HTML document
const PRELOAD_MIN_CHUNK_BYTES = 50 * 1024
startDevServer(__dirname)
copyPublicFolder(path.resolve(__dirname, 'public'), path.resolve(__dirname, 'dist'))
copySnappyWASMFile(__dirname)
copyRRWebWorkerFiles(__dirname)
writeIndexHtml()
writeExporterHtml()
writeRenderQueryHtml()
await import('./build-products.mjs')
const common = {
absWorkingDir: __dirname,
bundle: true,
writeMetaFile: !isDev,
}
await buildInParallel(
[
{
name: 'Insights App',
globalName: 'insightsApp',
entryPoints: ['src/index.tsx', 'src/sharedChunkAnchors.ts'],
splitting: true,
format: 'esm',
outdir: path.resolve(__dirname, 'dist'),
heavy: true,
...common,
},
{
name: 'Decompression Worker',
entryPoints: ['src/scenes/session-recordings/player/snapshot-processing/decompressionWorker.ts'],
format: 'esm',
outfile: path.resolve(__dirname, 'dist', 'decompressionWorker.js'),
...common,
},
{
name: 'Monaco Editor Worker',
entryPoints: ['src/lib/monaco/workers/editor.worker.ts'],
format: 'esm',
outfile: path.resolve(__dirname, 'dist', 'monacoEditorWorker.js'),
...common,
},
{
name: 'Monaco JSON Worker',
entryPoints: ['src/lib/monaco/workers/json.worker.ts'],
format: 'esm',
outfile: path.resolve(__dirname, 'dist', 'monacoJsonWorker.js'),
...common,
},
{
name: 'Monaco TypeScript Worker',
entryPoints: ['src/lib/monaco/workers/ts.worker.ts'],
format: 'esm',
outfile: path.resolve(__dirname, 'dist', 'monacoTsWorker.js'),
...common,
},
{
name: 'Exporter',
entryPoints: {
exporter: 'src/exporter/index.tsx',
exporterSharedChunkAnchors: 'src/sharedChunkAnchors.ts',
},
splitting: true,
format: 'esm',
outdir: path.resolve(__dirname, 'dist'),
heavy: true,
...common,
},
{
name: 'Render Query',
globalName: 'insightsRenderQuery',
entryPoints: ['src/render-query/index.tsx'],
format: 'iife',
outfile: path.resolve(__dirname, 'dist', 'render-query.js'),
...common,
},
{
...getToolbarAppBuildConfig(__dirname),
...common,
},
],
{
async onBuildComplete(config, buildResponse) {
if (!buildResponse) {
return
}
const { chunks, entrypoints } = buildResponse
if (config.name === 'Insights App') {
if (Object.keys(chunks).length === 0) {
console.error('Could not get chunk metadata for bundle "Insights App."')
throw new Error('Could not get chunk metadata for bundle "Insights App."')
}
if (!isDev && Object.keys(entrypoints).length === 0) {
console.error('Could not get entrypoint for bundle "Insights App."')
throw new Error('Could not get entrypoint for bundle "Insights App."')
}
if (!isDev) {
reportTopChunks(buildResponse.outputs, { label: 'Insights App chunks' })
writePreloadManifest(buildResponse.outputs)
}
writeIndexHtml(chunks, entrypoints)
}
if (config.name === 'Exporter') {
if (!isDev) {
reportTopChunks(buildResponse.outputs, { label: 'Exporter chunks' })
}
writeExporterHtml(chunks, entrypoints)
}
if (config.name === 'Render Query') {
writeRenderQueryHtml(chunks, entrypoints)
}
if (config.name === 'Toolbar') {
await finalizeToolbarBuild(__dirname, buildResponse)
}
createHashlessEntrypoints(__dirname, entrypoints)
},
}
)
/**
* Write dist/preload-manifest.json, read by the Django backend (insights/utils.py) to emit
* <link rel="preload"/"modulepreload"> tags so the boot chain (CSS, font, App and
* AuthenticatedShell chunks) is fetched in parallel instead of discovered as a waterfall.
* Paths are URL suffixes appended to `JS_URL + '/'`.
*/
export function writePreloadManifest(outputs = {}) {
const distDir = path.resolve(__dirname, 'dist')
// esbuild metafile paths are relative to absWorkingDir (= __dirname), not the invoking cwd
const toUrl = (outputPath) => `static/${path.relative(distDir, path.resolve(__dirname, outputPath))}`
const findEntryJs = (entryPoint) =>
Object.entries(outputs).find(([out, meta]) => meta.entryPoint === entryPoint && out.endsWith('.js'))
const collectChunkUrls = (entryPoint) => {
const found = findEntryJs(entryPoint)
if (!found) {
return []
}
const [outputPath, meta] = found
const urls = [toUrl(outputPath)]
for (const imp of meta.imports || []) {
if (imp.kind === 'import-statement' && (outputs[imp.path]?.bytes || 0) > PRELOAD_MIN_CHUNK_BYTES) {
urls.push(toUrl(imp.path))
}
}
return urls
}
const cssOutput = Object.keys(outputs).find((out) => /\/index(-[^./-]+)?\.css$/.test(out))
const fontOutput = Object.keys(outputs).find((out) => /\/Zen-Variable(-[^./]+)?\.woff2$/.test(out))
const dedupe = (urls) => [...new Set(urls)]
const manifest = {
css: cssOutput ? toUrl(cssOutput) : '',
font: fontOutput ? toUrl(fontOutput) : '',
// Entry first: its modulepreload starts the fetch at preload-scan time, before the
// loader script at the end of <head> gets to import() it.
js: dedupe([...collectChunkUrls('src/index.tsx'), ...collectChunkUrls('src/scenes/App.tsx')]),
// The backend emits these only for authenticated requests
authenticatedJs: dedupe(collectChunkUrls('src/scenes/AuthenticatedShell.tsx')),
}
// An empty field means an entry point, chunk, or asset name drifted from the lookups above —
// failing the build beats shipping green with the optimization silently off.
for (const [key, value] of Object.entries(manifest)) {
if (value.length === 0) {
console.error(`preload-manifest.json field "${key}" resolved empty.`)
throw new Error(`preload-manifest.json field "${key}" resolved empty.`)
}
}
fs.writeFileSync(path.resolve(distDir, 'preload-manifest.json'), JSON.stringify(manifest, null, 2))
}
export function writeIndexHtml(chunks = {}, entrypoints = []) {
copyIndexHtml(__dirname, 'src/index.html', 'dist/index.html', 'index', chunks, entrypoints)
copyIndexHtml(__dirname, 'src/layout.html', 'dist/layout.html', 'index', chunks, entrypoints)
}
export function writeExporterHtml(chunks = {}, entrypoints = []) {
copyIndexHtml(__dirname, 'src/exporter/index.html', 'dist/exporter.html', 'exporter', chunks, entrypoints)
}
export function writeRenderQueryHtml(chunks = {}, entrypoints = []) {
copyIndexHtml(
__dirname,
'src/render-query/index.html',
'dist/render_query.html',
'render-query',
chunks,
entrypoints
)
}