#!/usr/bin/env node /** * Idempotent GLB optimization gate. Runs from deploy.sh BEFORE vite build. * * Why this exists: the farm emits 1–8 MB PBR GLBs. Shipped raw, 27 of them were * 27.3 MB; through `gltf-transform optimize` they are 7.0 MB with the same PBR. * That step used to live in my head, which is not a build system. An asset added * by hand — or by a farm run that skipped the post-step — would silently ship raw. * * Skips anything already carrying EXT_meshopt_compression, so it is safe to run * every deploy and costs ~0s when there is nothing new. */ import { execFileSync } from 'node:child_process'; import { readdirSync, readFileSync, statSync, renameSync, rmSync } from 'node:fs'; import { join } from 'node:path'; const DIR = new URL('../public/assets/models/', import.meta.url).pathname; const MAX_KB = 900; // per-asset ceiling; a farm regression should fail the build /** Read the GLB JSON chunk without a glTF library. */ function meta(path) { const b = readFileSync(path); if (b.length < 20 || b.subarray(0, 4).toString() !== 'glTF') return null; const jsonLen = b.readUInt32LE(12); try { return JSON.parse(b.subarray(20, 20 + jsonLen).toString()); } catch { return null; } } const files = readdirSync(DIR).filter((f) => f.endsWith('.glb')); let optimized = 0, skipped = 0, before = 0, after = 0; const oversize = []; for (const f of files) { const p = join(DIR, f); const sizeBefore = statSync(p).size; before += sizeBefore; const m = meta(p); if (m?.extensionsRequired?.includes('EXT_meshopt_compression')) { skipped++; after += sizeBefore; } else { const tmp = `${p}.opt`; execFileSync('npx', ['--yes', '@gltf-transform/cli@latest', 'optimize', p, tmp, '--texture-compress', 'webp', '--texture-size', '1024', '--simplify-error', '0.002', '--compress', 'meshopt'], { stdio: 'pipe', timeout: 900_000 }); rmSync(p); renameSync(tmp, p); optimized++; after += statSync(p).size; console.log(` optimized ${f}: ${(sizeBefore / 1024) | 0} KB -> ${(statSync(p).size / 1024) | 0} KB`); } const kb = (statSync(p).size / 1024) | 0; if (kb > MAX_KB) oversize.push(`${f} (${kb} KB)`); } console.log(`assets: ${files.length} files, ${optimized} optimized, ${skipped} already lean — ` + `${(before / 1048576).toFixed(1)} MB -> ${(after / 1048576).toFixed(1)} MB`); if (oversize.length) { console.error(`\nFAIL: ${oversize.length} asset(s) over ${MAX_KB} KB after optimization:`); for (const o of oversize) console.error(` ${o}`); console.error('A mesh this heavy usually means the bake used too dense a pipeline ' + '(trellis2 1024_cascade produces ~487k tris; hunyuan3d_mlx is the machine default).'); process.exit(1); }