|
| 1 | +/** |
| 2 | + * Bundle a stub entry into an injectable IIFE, wrapped as a TypeScript constant. |
| 3 | + * |
| 4 | + * Two artifacts are built this way — the audio-FX runtime and the position-edits |
| 5 | + * render — and the engine injects both into the headless browser as a script tag. |
| 6 | + * They were two copies of this file differing in five names, which is a poor |
| 7 | + * place for a divergence to hide: whichever copy stopped being edited would go on |
| 8 | + * producing a subtly different artifact with nothing to say so. |
| 9 | + */ |
| 10 | + |
| 11 | +import { execFileSync } from "node:child_process"; |
| 12 | +import { mkdirSync, writeFileSync } from "node:fs"; |
| 13 | +import { dirname, resolve } from "node:path"; |
| 14 | +import { fileURLToPath } from "node:url"; |
| 15 | +import { buildSync } from "esbuild"; |
| 16 | + |
| 17 | +export interface InjectedArtifact { |
| 18 | + /** The build script's own `import.meta.url`, so paths resolve beside it. */ |
| 19 | + scriptUrl: string; |
| 20 | + /** Entry stub, relative to the package root. */ |
| 21 | + entry: string; |
| 22 | + /** Output file name inside `src/generated`. */ |
| 23 | + out: string; |
| 24 | + /** SCREAMING_CASE name for the string constant holding the IIFE. */ |
| 25 | + constName: string; |
| 26 | + /** The accessor the rest of the codebase imports. */ |
| 27 | + fnName: string; |
| 28 | + /** What that accessor returns, for its doc comment: "the pre-built X". */ |
| 29 | + what: string; |
| 30 | + /** Structured log event name. */ |
| 31 | + event: string; |
| 32 | +} |
| 33 | + |
| 34 | +export function buildInjectedArtifact(spec: InjectedArtifact): void { |
| 35 | + const scriptDir = dirname(fileURLToPath(spec.scriptUrl)); |
| 36 | + const scriptName = spec.scriptUrl.split("/").pop() ?? ""; |
| 37 | + const repoRoot = resolve(scriptDir, ".."); |
| 38 | + const entry = resolve(repoRoot, spec.entry); |
| 39 | + const generatedDir = resolve(repoRoot, "src/generated"); |
| 40 | + const outPath = resolve(generatedDir, spec.out); |
| 41 | + |
| 42 | + const result = buildSync({ |
| 43 | + entryPoints: [entry], |
| 44 | + bundle: true, |
| 45 | + write: false, |
| 46 | + platform: "browser", |
| 47 | + format: "iife", |
| 48 | + target: ["es2020"], |
| 49 | + minify: true, |
| 50 | + legalComments: "none", |
| 51 | + }); |
| 52 | + const iife = result.outputFiles[0]?.text ?? ""; |
| 53 | + if (!iife) throw new Error(`esbuild produced no output for ${spec.entry.split("/").pop()}`); |
| 54 | + |
| 55 | + mkdirSync(generatedDir, { recursive: true }); |
| 56 | + writeFileSync( |
| 57 | + outPath, |
| 58 | + [ |
| 59 | + `// AUTO-GENERATED by scripts/${scriptName} - do not edit`, |
| 60 | + `const ${spec.constName}: string = ${JSON.stringify(iife)};`, |
| 61 | + "", |
| 62 | + `/** Returns the pre-built ${spec.what} as a string constant. */`, |
| 63 | + `export function ${spec.fnName}(): string {`, |
| 64 | + ` return ${spec.constName};`, |
| 65 | + "}", |
| 66 | + "", |
| 67 | + ].join("\n"), |
| 68 | + "utf8", |
| 69 | + ); |
| 70 | + |
| 71 | + try { |
| 72 | + execFileSync("bun", ["x", "oxfmt", outPath], { stdio: "ignore" }); |
| 73 | + } catch { |
| 74 | + // Formatting is best effort when the generator runs in a minimal environment. |
| 75 | + } |
| 76 | + |
| 77 | + console.log(JSON.stringify({ event: spec.event, outPath, bytes: iife.length })); |
| 78 | +} |
0 commit comments