Hooks

Presets are the only place you can register hooks — extension points that run a JavaScript module through netpack’s Node bridge at a specific point in the build. They live under the hooks key of a preset:

// netpack.json
{
  "presets": ["@myorg/base"],
  "hooks": {
    "afterBundling": ["./transform.mjs", "@myorg/tools/stamp.js"]
  }
}

Each hook name maps to an array of module references, so several callbacks can attach, and the arrays merge across the whole preset chain. Hook modules are resolved with the same mechanism as presets (a path, or a package reference through node_modules).

Entry shape

An entry can be a bare string (the module to run) or an object that also scopes and parameterizes it:

{
  "hooks": {
    "afterBundling": [
      "./transform.mjs",                       // shorthand
      {
        "source": "./stamp.mjs",               // the module (required)
        "test": "\\.js$",                        // only .js files/modules
        "exclude": "\\.min\\.js$",               // …but not minified ones
        "mode": "prod",                          // only in optimized builds
        "order": -1,                             // run before default hooks
        "name": "stamp",                         // label for diagnostics
        "options": { "year": 2026 }              // passed to the hook
      }
    ]
  }
}
  • source — the hook module (path or package reference). A plain string entry is exactly { "source": "…" }.
  • test — a regular expression matched against the name. For asset hooks it filters which files the hook receives (and may rewrite); for per-module hooks it skips modules whose path doesn’t match. Omitted means “everything”. A filtered asset hook with no matching files isn’t invoked at all — the Node bridge only spins up when there’s work to do. An invalid regex is reported and ignored.
  • exclude — a regular expression for names to skip, applied after test.
  • modedev (dev server only), prod (optimized builds only), or both (default). A hook that doesn’t apply to the current build is never invoked.
  • order — an integer that shifts the hook earlier (negative) or later (positive) among the hooks for the same phase. The default (0) keeps the base-first order presets already give; use order only to override it.
  • name — a label surfaced in diagnostics and passed to the hook as payload.name.
  • options — any JSON value, handed to the hook function as payload.options (default {}). Use it to reuse one hook module with different settings.

The two forms mix freely in the same array, and dedup is by the whole entry (module plus its test/exclude/mode/order/name/options), so the same module with different settings runs more than once by design.

Behaviour

  • Merged, not overridden. Every preset’s hooks contribute; nothing shadows anything.
  • Base-first order. Hooks run in the reverse of option precedence — the deepest referenced (base) presets execute first, the entry preset last — so a base can set things up before a more specific preset finishes.
  • Deduplicated. The same module reached through two presets runs once, at its earliest position.
  • You only pay when you use them. Resolution happens up front in native code; the Node bridge is engaged only for hooks that actually have modules registered. A build with no hooks is exactly as fast as one with no config at all.

The module contract

A hook module default-exports (or module.exports) an async function. It receives { hook, root, dev, options, name } — plus module for the per-module hooks, and files for the asset hooks — and may return a value the bundler applies. options is the entry’s options value (or {}), and name its label (if set). Unknown hook names are ignored with a warning.

// transform.mjs — strip // line comments from every JS bundle
export default async ({ files }) => ({
  files: files
    .filter((f) => f.name.endsWith(".js"))
    .map((f) => ({ name: f.name, text: f.text.replace(/^\s*\/\/.*$/gm, "") })),
});

Modules run over the Node bridge, so @babel/core, terser, or any npm package they import must be installed in the project.

Two kinds carry extra payload and can return a value:

  • Asset hooks (additionalAssets, processAssets, afterProcessAssets, afterEmit / the afterBundling alias) receive files: [{ name, text }] — text outputs (.js, .css, .html, .json, .map, …) as text, binary assets by name only. Return a files array to replace an asset’s contents (or add a new one). This is your post-transformation slot.
  • shouldEmit may return { emit: false } to skip writing entirely.

The per-module hooks additionally receive the module’s path as module.

Lifecycle points

Every point in netpack’s build maps to a hook name, mirroring the webpack/rspack lifecycle. The build runs left to right; your post-transformation hooks most commonly tap the Emit phase:

The netpack build lifecycle Build phases from compiler start through per-module builds, graph completion, optimization, sealing, emit, and finish, each mapping to hook names. Compiler Modules Graph Optimize Seal Emit Finish initialize compilation buildModule succeedModule finishModules optimizeModules moduleIds seal processAssets afterBundling done Post-transformation hooks tap here — replace or add output files before they're written.
PhaseHooks (in order)
Compiler startinitialize, beforeRun, run / watchRun, beforeCompile (alias beforeCompilation), compile, thisCompilation, compilation, make
Per modulebuildModule, stillValidModule, succeedModule, failedModule
After the graphfinishMake, finishModules
Optimize (optimized builds)optimize, optimizeDependencies, afterOptimizeDependencies, optimizeModules, afterOptimizeModules, optimizeChunks, afterOptimizeChunks, optimizeTree, optimizeChunkModules
Ids & sealmoduleIds, chunkIds, seal, contentHash, afterCodeGeneration
EmitshouldEmit, emit, additionalAssets, processAssets, afterProcessAssets, afterEmit (alias afterBundling)
FinishafterSeal, afterCompile, done

The run-level hooks invalid, watchClose, shutdown and failed are recognized (you can register them) but reserved — they aren’t fired yet.

.NET

Hooks map onto the CompilerHooks / CompilationHooks tap system in NetPack.Core, which .NET plugins can tap directly (no Node bridge). See .NET libraries.