// rnx app-fonts stage — fetch the config-plugin fonts declared on an // engine `?appFonts=` wire, gzip them, and merge asset descriptors into a // build's asset manifest. // // the branch-build path (app/api/github/agent/run.sh) captures the metro // __packager_asset graph itself, but config-plugin fonts (expo-font etc.) only // live in the native binary — never the bundle — so that capture misses them. // rather than reimplement font fetch/gzip/hash in bash, run.sh calls this once // after the asset capture; the descriptors land in the same manifest the // upload step already PUTs, keyed by sha256(url) so the rendered build resolves // each font through /api/preview/share/fetch?url= identically to the // preview path (`rnx record upload`). both pipelines share fetchAppFontFiles. import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { gzipSync } from 'node:zlib' import { fetchAppFontFiles } from '../app-fonts' import { rnxExit } from '../run-rnx' type ManifestEntry = { url: string urlhash: string contentType: string encoding: 'gzip' sizeBytes: number rawBytes: number } function flag(args: string[], name: string): string | undefined { const idx = args.indexOf(`--${name}`) if (idx !== -1 && idx + 1 < args.length) return args[idx + 1] const inline = args.find((a) => a.startsWith(`--${name}=`)) return inline ? inline.slice(name.length + 3) : undefined } async function stage(args: string[]): Promise { const wire = flag(args, 'wire') || '' const outDir = flag(args, 'out-dir') const manifestPath = flag(args, 'manifest') if (!outDir || !manifestPath) { console.error( ' usage: rnx app-fonts stage --wire --out-dir --manifest ', ) rnxExit(1) } if (!wire.trim()) { console.log('no app fonts declared — nothing to stage') return } mkdirSync(outDir, { recursive: true }) const fonts = await fetchAppFontFiles(wire, { onStaged: ({ url, byteLength }) => console.log(` staged app font: ${url} (${(byteLength / 1024).toFixed(1)} KiB)`), onError: (url, err) => console.error( ` warning: failed to fetch app font ${url}: ${err instanceof Error ? err.message : err}`, ), }) const descriptors: ManifestEntry[] = [] for (const font of fonts) { const gz = gzipSync(font.bytes) writeFileSync(join(outDir, font.urlhash), gz) descriptors.push({ url: font.url, urlhash: font.urlhash, contentType: font.contentType, encoding: 'gzip', sizeBytes: gz.length, rawBytes: font.bytes.byteLength, }) } let existing: ManifestEntry[] = [] if (existsSync(manifestPath)) { try { const parsed: unknown = JSON.parse(readFileSync(manifestPath, 'utf8')) if (Array.isArray(parsed)) existing = parsed as ManifestEntry[] } catch {} } const seen = new Set(existing.map((e) => e.urlhash)) const merged = [...existing, ...descriptors.filter((d) => !seen.has(d.urlhash))] writeFileSync(manifestPath, JSON.stringify(merged)) console.log( `staged ${descriptors.length} app font(s); manifest now has ${merged.length} file(s)`, ) } export async function runAppFonts(args: string[]): Promise { const sub = args[0] if (sub === 'stage') { await stage(args.slice(1)) return } console.error(` unknown app-fonts subcommand: ${sub ?? '(none)'}`) console.error(' available: stage') rnxExit(1) }