Skip to content

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

@robertsLando

Description

@robertsLando

Users often need to run extra steps around a pkg build — bundling with esbuild/webpack beforehand, minifying/obfuscating the files pkg is about to embed, or smoke-testing the produced binary afterwards. Today this lives in external shell scripts, and there's no way to transform walker output without mutating the user's source tree.

Blocked on #242 — the per-file transform hook is inherently a JS function, so it needs the typed Node.js API options object to land first. Shipping the whole hooks surface together (rather than shell-only first, functions later) avoids an awkward intermediate config shape.

Proposal

Split hooks into two kinds with clearly different roles.

1. Shell hooks — preBuild, postBuild

For setup/teardown and smoke tests. Config-file friendly (strings).

{
  "pkg": {
    "hooks": {
      "preBuild":  "esbuild src/index.js --bundle --outfile=dist/bundle.js",
      "postBuild": "./dist/my-app-linux --version"
    }
  }
}
  • Spawned via shell, stdio inherited, non-zero exit fails the build
  • postBuild runs per produced binary; path exposed as PKG_OUTPUT env var
  • JS API also accepts function form (() => Promise<void>)

2. Transformer hook — transform(filePath, contents) (JS API only)

For per-file content transformation after the walker collects files, before they're written into the VFS / SEA archive. This is the hook for minification / obfuscation.

await pkg.exec([...], {
  transform: (filePath, contents) => {
    if (filePath.endsWith('.js')) return minify(contents).code;
    return contents;
  }
});

Why a dedicated transformer rather than a shell hook:

  • Operates on the exact set of files pkg is embedding (walker output), not the whole source tree
  • Never touches the user's source files on disk
  • Per-file — users apply their own matching rules
  • Runs before bytecode compilation and compression, so it composes cleanly with --public-packages and Brotli/GZip

Minification / Obfuscation

Deliberately not a built-in pkg feature — keeps runtime deps minimal. Instead, document recipes using the transform hook with the user's tool of choice (terser, javascript-obfuscator, swc, esbuild, etc.). Users pick the tool that fits their project; pkg stays out of the transform business.

Scope

  • Lifecycle order: preBuild → walk → transform (per file) → bytecode/compression → write → postBuild (per binary)
  • Config surface: CLI flags not needed (function hooks can't live on the CLI); configure via package.json#pkg / .pkgrc (feat: support a .pkgrc configuration file #238) for shell hooks, and via the typed Node.js API (feat: typed options object for the Node.js API #242) for the transform function
  • Documentation: add a "Recipes" section covering minify/obfuscate via transform, and pre-bundling via preBuild

Part of #235

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions