feat: build hooks (preBuild, postBuild, per-file transform)#273
feat: build hooks (preBuild, postBuild, per-file transform)#273robertsLando wants to merge 3 commits intomainfrom
Conversation
Add three first-class build hooks to replace the shell-script wrappers
that previously had to surround pkg invocations:
- preBuild — shell command or JS function run once before the walker
- postBuild — shell command or JS function run once per produced binary;
shell form receives the output path via PKG_OUTPUT
- transform — JS-only per-file content rewrite, applied between the
walker and bytecode/compression. Enables minify/obfuscate recipes
without bundling them into pkg's runtime deps.
Lifecycle: preBuild → walk → transform (per file) → bytecode/compression
→ write → postBuild (per binary). Hooks run identically in traditional
and enhanced SEA pipelines; simple SEA mode supports preBuild/postBuild
but skips transform (no walker output).
Configurable via the typed Node.js API (all three hooks, function form
included), package.json#pkg / .pkgrc (shell form for preBuild/postBuild)
and pkg.config.{js,cjs,mjs} (function form for any hook).
Closes #252.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #273 +/- ##
==========================================
+ Coverage 85.51% 85.93% +0.42%
==========================================
Files 22 23 +1
Lines 7297 7629 +332
Branches 1039 1103 +64
==========================================
+ Hits 6240 6556 +316
- Misses 1049 1065 +16
Partials 8 8
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds first-class build hooks to pkg (preBuild, postBuild, and per-file transform) so users can integrate pre/post steps and in-memory content rewrites into the packaging pipeline across traditional and SEA modes.
Changes:
- Introduces
lib/hooks.tswithrunPreBuild,runPostBuild, andrunTransform, and wires them into traditional + SEA build flows. - Extends config parsing/validation (
lib/config.ts) and public types (lib/types.ts) to support hook configuration from the Node API and JS-based config files. - Adds unit + e2e tests and updates documentation/architecture diagrams to describe the new lifecycle.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
lib/hooks.ts |
Implements hook execution (shell + function) and per-file transform over FileRecords. |
lib/index.ts |
Runs preBuild before pipeline selection; runs transform before packing; runs postBuild after each produced binary (traditional mode). |
lib/sea.ts |
Runs transform in enhanced SEA before asset generation; runs postBuild after baking/signing outputs (enhanced + simple SEA). |
lib/config.ts |
Validates hook types; threads programmatic API hook fields via parsed.apiPkg and merges into resolved config. |
lib/types.ts |
Exposes hook type aliases and adds hook fields to PkgOptions / PkgExecOptions. |
test/unit/hooks.test.ts |
Unit coverage for hook runners (shell/function + transform behaviors). |
test/unit/config-parse.test.ts |
Unit coverage for parsing/validation of hook fields in exec() options and pkg config. |
test/test-46-hooks/main.js |
New e2e test proving hook ordering and that transform mutations reach the final executable output. |
test/test-46-hooks/index.js |
Entry file for the new hooks e2e test. |
docs/ARCHITECTURE.md |
Updates pipeline diagrams to include preBuild/transform/postBuild steps. |
docs-site/guide/configuration.md |
Documents new config keys and their types/constraints. |
docs-site/guide/api.md |
Documents new API fields and adds a “Build hooks” guide section with examples. |
PkgOptions and PkgExecOptions had drifted into a 12-field overlap that duplicated identical names, types, and JSDoc — most visibly across the new build hooks. Pull those shared fields into a `PkgBaseOptions` base interface and have both shapes extend it. Listy fields where typing intentionally differs (`targets`, `publicPackages`, `noDictionary` are lenient `string | string[]` in config files and strict `string[]` at the API boundary) and the `options`/`bakeOptions` rename stay on the leaf interfaces — pulling those up would need generic gymnastics for no net win. Public surface: PkgBaseOptions is exported from the package entry so downstream tooling can type-derive shared build-shaping options without enumerating fields by hand. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copilot review: - docs-site/guide/api.md: tighten the type strings for preBuild/postBuild (parens around the function arms so `string | (() => ...)` parses as intended), and reflect that `transform` returns may be Promise-wrapped. - test/test-46-hooks/main.js: add `assert(!module.parent)` to match the e2e convention used by sibling tests. - test/unit/hooks.test.ts: replace `'true'` / `'exit 7'` with portable `node -e` invocations — `true` is a POSIX shell builtin and Windows cmd.exe doesn't recognize it, so the unit suite would fail on win32 CI. Self-review: - lib/config.ts: switch from `Object.assign(rawPkg, parsed.apiPkg)` to a spread, so the source `configJson.pkg` / `inputJson.pkg` objects are not mutated by API-injected hooks bleeding back into the parsed config. - lib/hooks.ts: drop the redundant `as PreBuildHook` / `PostBuildHook` / `TransformHook` casts — TypeScript already narrows the union after the `typeof === 'string'` checks. Trim `void | undefined` on the transform result to plain `void` (`void` covers `undefined` for return types). - lib/hooks.ts + lib/sea.ts: extract a `runPostBuildForTargets` helper to dedupe the per-target loop the two SEA paths shared, and to give the loop direct unit-test coverage (closes the lib/sea.ts coverage gap flagged by codecov on the previous push). - lib/sea.ts: cache `pkgOptions.get()` once in `seaEnhanced` instead of fetching it twice. - docs-site/guide/api.md: warn that `transform` receives every embedded file (including binaries / `.node` addons) — users must filter by extension before rewriting, since returning a string for binary content would corrupt it. Also document the SEA-vs-traditional postBuild timing difference (parallel-bake-then-sequential-postBuild in SEA vs. interleaved per-target in the traditional pipeline). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds first-class build hooks to pkg so users can run pre/post build steps and per-file transformations (minify/obfuscate/etc.) without wrapping pkg in external scripts, across both traditional and SEA pipelines.
Changes:
- Introduces
preBuild,postBuild, andtransform(filePath, contents)hooks with validation and typed API/config support. - Wires hook execution into traditional pipeline (
lib/index.ts) and SEA pipelines (lib/sea.ts) at defined lifecycle points. - Adds unit + e2e coverage and updates documentation/architecture diagrams to describe the new lifecycle.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
lib/hooks.ts |
Implements hook runners (shell + function forms) and per-file transform application. |
lib/index.ts |
Invokes preBuild once, transform after refine, postBuild per produced binary in traditional pipeline. |
lib/sea.ts |
Adds transform before SEA asset generation and postBuild after baking/signing for SEA modes. |
lib/config.ts |
Validates hooks; threads API-only hook fields via parsed.apiPkg and merges into resolved pkg config. |
lib/types.ts |
Adds hook types and factors shared fields into PkgBaseOptions to expose hooks in PkgExecOptions. |
test/unit/hooks.test.ts |
Unit tests for hook runners and transform behavior. |
test/unit/config-parse.test.ts |
Unit tests for hook parsing/validation in parseInput and validatePkgConfig. |
test/test-46-hooks/main.js |
New e2e that exercises preBuild (shell), transform (fn), postBuild (fn) and asserts mutation reached the final binary. |
test/test-46-hooks/index.js |
Fixture printing a marker that transform rewrites. |
docs/ARCHITECTURE.md |
Updates pipeline diagrams to include hook lifecycle points. |
docs-site/guide/configuration.md |
Updates schema table to include new hook fields. |
docs-site/guide/api.md |
Adds “Build hooks” section with examples and lifecycle notes. |
| import type { FileRecords, PkgOptions } from './types'; | ||
|
|
||
| /** | ||
| * Run a shell command synchronously to-completion. stdio is inherited so the |
| ```json [package.json#pkg] | ||
| { | ||
| "pkg": { | ||
| "postBuild": "\"$PKG_OUTPUT\" --version" | ||
| } | ||
| } |
| // Throws if the helper iterates anyway: postBuild is undefined, so the | ||
| // function-form path inside runPostBuild would crash. Reaching the end | ||
| // proves the early-return short-circuit fires. |
Closes #252.
Summary
Adds three first-class build hooks that turn the shell-script wrappers users had to surround
pkgwith (pre-bundling, smoke tests, minification, obfuscation) into typed config:preBuild— shell command or JS function run once before the walker. Setup work, codegen, pre-bundling.postBuild— shell command or JS function run once per produced binary. Smoke tests, signing, notarization, upload. Shell form sees the output path viaPKG_OUTPUT; function form receives it as an argument.transform(filePath, contents)— JS-function-only per-file content rewrite, applied after the walker collects files and after refinement, but before bytecode compilation and compression. This is the hook for minify/obfuscate;pkgdeliberately ships no minifier so the runtime dependency footprint stays minimal — drop in terser, swc, javascript-obfuscator, etc.Lifecycle:
Hooks run identically in the traditional and enhanced SEA pipelines. Simple SEA mode (
--seawithout apackage.json) supportspreBuild/postBuildbut skipstransform(no walker output to apply rewrites to).Configuration surface
Per the issue: no CLI flags (function hooks can't live on the CLI).
exec()opts)pkg.config.{js,cjs,mjs}package.json#pkg/.pkgrcValidation rejects empty shell strings, wrong types, and (for
transform) non-function values, with the standardwasReportederror path so failures look like every other pkg config error.Implementation
lib/hooks.ts(~150 lines):runPreBuild,runPostBuild,runTransform. Shell hooks viaspawn(shell:true, stdio:'inherit'); non-zero exit throws.lib/index.ts:runPreBuildafterpkgOptions.set(pkg)(covers SEA + traditional in one place);runTransformafterrefineand beforepacker;runPostBuildper target after producer.lib/sea.ts:seaEnhancedrunsrunTransformafter refine andrunPostBuildafter eachbake. SimplesearunsrunPostBuildafter each bake.lib/config.ts:'preBuild','postBuild','transform'added toNON_FLAG_PKG_KEYS; per-hook validation (string-or-fn for shell hooks, fn-only for transform); programmatic-only fields threaded through a newparsed.apiPkgslot thatresolveConfigmerges intorawPkgso config-file and API hooks compose.lib/types.ts:PreBuildHook,PostBuildHook,TransformHookexports + new fields onPkgOptionsandPkgExecOptions.transform); configuration schema updated; ARCHITECTURE.md pipeline diagrams updated for both traditional and enhanced SEA.Notes on the transform pipeline
When
transformis configured, hook bodies are loaded eagerly for every record withSTORE_BLOBorSTORE_CONTENT— without preloading, packer and sea-assets re-read disk and the transform would be bypassed. This trades some peak memory for correctness and only applies to builds that opt into a transform. The mutated body flows throughrecord.body, which packer already prefers over the file path, so the transformed source feeds bytecode compilation, compression, and SEA archive generation without further changes.Test plan
yarn build— cleanyarn lint— clean (style + eslint)yarn test:unit— 227 tests pass, including:validatePkgConfigacceptspreBuild/postBuildas string or function, rejects empty / wrong-type values, requirestransformto be a functionparseInput(PkgExecOptions): hook fields land inapiPkg, type-checked at the boundary, absent when no hooks setrunPreBuild/runPostBuild/runTransform: function form awaited, shell form throws on non-zero exit, transform replaces / preserves / rejects bad return types, async transform awaited, errors surface with the file pathtest/test-46-hooks/(new e2e): runspkg.exec()programmatically with all three hooks (shellpreBuild, functionpostBuild, functiontransform), then spawns the produced binary and asserts the transformed marker appears (proves the transform mutation reached bytecode generation and the final binary)test-46-inputandtest-85-sea-enhancedstill pass🤖 Generated with Claude Code