Skip to content

feat: build hooks (preBuild, postBuild, per-file transform)#273

Open
robertsLando wants to merge 3 commits intomainfrom
worktree-issue-252-build-hooks
Open

feat: build hooks (preBuild, postBuild, per-file transform)#273
robertsLando wants to merge 3 commits intomainfrom
worktree-issue-252-build-hooks

Conversation

@robertsLando
Copy link
Copy Markdown
Member

Closes #252.

Summary

Adds three first-class build hooks that turn the shell-script wrappers users had to surround pkg with (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 via PKG_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; pkg deliberately ships no minifier so the runtime dependency footprint stays minimal — drop in terser, swc, javascript-obfuscator, etc.

Lifecycle:

preBuild → walk → transform (per file) → bytecode/compression → write → postBuild (per binary)

Hooks run identically in the traditional and enhanced SEA pipelines. Simple SEA mode (--sea without a package.json) supports preBuild/postBuild but skips transform (no walker output to apply rewrites to).

Configuration surface

Per the issue: no CLI flags (function hooks can't live on the CLI).

Source preBuild postBuild transform
Node.js API (exec() opts) string | fn string | fn fn
pkg.config.{js,cjs,mjs} string | fn string | fn fn
package.json#pkg / .pkgrc string string — (function-only)

Validation rejects empty shell strings, wrong types, and (for transform) non-function values, with the standard wasReported error path so failures look like every other pkg config error.

Implementation

  • New lib/hooks.ts (~150 lines): runPreBuild, runPostBuild, runTransform. Shell hooks via spawn(shell:true, stdio:'inherit'); non-zero exit throws.
  • lib/index.ts: runPreBuild after pkgOptions.set(pkg) (covers SEA + traditional in one place); runTransform after refine and before packer; runPostBuild per target after producer.
  • lib/sea.ts: seaEnhanced runs runTransform after refine and runPostBuild after each bake. Simple sea runs runPostBuild after each bake.
  • lib/config.ts: 'preBuild', 'postBuild', 'transform' added to NON_FLAG_PKG_KEYS; per-hook validation (string-or-fn for shell hooks, fn-only for transform); programmatic-only fields threaded through a new parsed.apiPkg slot that resolveConfig merges into rawPkg so config-file and API hooks compose.
  • lib/types.ts: PreBuildHook, PostBuildHook, TransformHook exports + new fields on PkgOptions and PkgExecOptions.
  • Docs: "Build hooks" section added to the Node.js API guide with full examples (function and shell forms for each hook, terser recipe for transform); configuration schema updated; ARCHITECTURE.md pipeline diagrams updated for both traditional and enhanced SEA.

Notes on the transform pipeline

When transform is configured, hook bodies are loaded eagerly for every record with STORE_BLOB or STORE_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 through record.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 — clean
  • yarn lint — clean (style + eslint)
  • yarn test:unit — 227 tests pass, including:
    • validatePkgConfig accepts preBuild/postBuild as string or function, rejects empty / wrong-type values, requires transform to be a function
    • parseInput (PkgExecOptions): hook fields land in apiPkg, type-checked at the boundary, absent when no hooks set
    • runPreBuild / 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 path
  • test/test-46-hooks/ (new e2e): runs pkg.exec() programmatically with all three hooks (shell preBuild, function postBuild, function transform), then spawns the produced binary and asserts the transformed marker appears (proves the transform mutation reached bytecode generation and the final binary)
  • Adjacent regression checks: test-46-input and test-85-sea-enhanced still pass

🤖 Generated with Claude Code

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
Copy link
Copy Markdown

codecov Bot commented Apr 27, 2026

Codecov Report

❌ Patch coverage is 95.32967% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.93%. Comparing base (26f10a3) to head (f52b1a9).

Files with missing lines Patch % Lines
lib/hooks.ts 92.35% 12 Missing ⚠️
lib/sea.ts 79.16% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            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              
Files with missing lines Coverage Δ
lib/config.ts 96.48% <100.00%> (+0.32%) ⬆️
lib/index.ts 83.33% <100.00%> (+0.85%) ⬆️
lib/types.ts 100.00% <100.00%> (ø)
lib/sea.ts 67.22% <79.16%> (+0.30%) ⬆️
lib/hooks.ts 92.35% <92.35%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts with runPreBuild, runPostBuild, and runTransform, 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.

Comment thread docs-site/guide/api.md Outdated
Comment thread test/test-46-hooks/main.js
Comment thread test/unit/hooks.test.ts Outdated
robertsLando and others added 2 commits April 27, 2026 17:36
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]>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and transform(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.

Comment thread lib/hooks.ts
import type { FileRecords, PkgOptions } from './types';

/**
* Run a shell command synchronously to-completion. stdio is inherited so the
Comment thread docs-site/guide/api.md
Comment on lines +182 to +187
```json [package.json#pkg]
{
"pkg": {
"postBuild": "\"$PKG_OUTPUT\" --version"
}
}
Comment thread test/unit/hooks.test.ts
Comment on lines +119 to +121
// 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: build hooks (pre/post + per-file transform)

2 participants