napi-postinstall
The install-time and runtime helper for handling native bindings in npm
versions affected by npm/cli#4828. This
is a reimplementation of the node-install functionality from
esbuild for napi-rs ecosystem packages like
rollup, @swc/core and unrs-resolver.
For more details, please refer to the following issues:
- npm/cli#4828 -- root cause
- napi-rs/napi-rs#2569
- unrs/unrs-resolver#56
TOC
Usage
TL;DR for package users
You usually don't need to install napi-postinstall yourself — it is an
install-time or runtime helper used by some napi-rs packages.
- For new packages, prefer the runtime fallback: no install script is needed, and it works on all native platforms as well as in WebContainer.
- It works around npm/cli#4828, where
npmcan fail to install the correct platform-specific optional dependency. - When it has to install a missing binding by itself, it uses the registry,
credentials and CA settings from the project
.npmrc, so private registries work without extra configuration. The file is read from the project that owns the installed package; one that only exists at a workspace root is not picked up yet, so keep a.npmrcnext to that project. If npm itself cannot be queried, it warns and uses the public registry, which the two lookups done in JavaScript (reading a manifest and downloading a tarball) then fall back to. - A binding that lives on another filesystem is copied rather than renamed. On
Node before 16.15 that copy resolves relative symlinks, such as
node_modules/.binentries, against the source it then removes, so those links can break. pnpmandyarnresolve optional dependencies correctly in most cases, so the helper is usually a no-op there. Leaving install scripts enabled is the safe default.- If your package manager warns about ignored build scripts, allow the build
script for the dependency that needs it. For
pnpm10+, runpnpm approve-builds, or add that dependency (for exampleunrs-resolver, notnapi-postinstall) toignoredBuiltDependenciesto silence the warning when installation already works. - If the native binding fails to load, use the runtime fallback (or re-enable install scripts and reinstall).
Notes for package authors
For new packages, prefer the runtime fallback: it works on all native platforms and in WebContainer, and does not require an install script. The postinstall path remains supported for compatibility.
If your package must use postinstall, prefer a postinstall.js that calls the
API instead of invoking the napi-postinstall binary; see
Postinstall script. You can copy this note into your own
README:
This package uses
napi-postinstallso the correct native binding is installed onnpmversions affected by npm/cli#4828.pnpmandyarnnormally handle optional dependencies themselves; if your package manager reports ignored build scripts, you can allow the script or safely ignore it when the native binding already loads.
Install
# pnpm
pnpm add napi-postinstall
# yarn
yarn add napi-postinstall
# npm
npm i napi-postinstall
# bun
bun add napi-postinstall
Runtime fallback
A runtime fallback in the package loader is the preferred integration for new packages. It needs no install script, works on every native platform, and automatically uses the WASI build in WebContainer:
// index.js — the block napi-patch inserts into a generated napi-rs loader
let nativeBinding = null
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
// ...napi-rs generated platform loading...
if (!nativeBinding && process.env['SKIP_<PACKAGE>_FALLBACK'] !== '1') {
try {
nativeBinding = require('napi-postinstall/fallback')(
require.resolve('./package.json'),
true,
)
if (
typeof __napiLoadedBindingTarget !== 'undefined' &&
typeof nativeBinding.__napiBindingTarget === 'string'
) {
__napiLoadedBindingTarget = nativeBinding.__napiBindingTarget
}
} catch (err) {
loadErrors.push(err)
}
}
fallback() is synchronous and only runs when the normal binding lookup fails.
It uses the locally installed napi-postinstall CLI, so it inherits the project
registry and authentication configuration. The block must run before the
loader's final if (!nativeBinding) throw; napi-patch places it there
automatically and guards it with SKIP_<PACKAGE>_FALLBACK (<PACKAGE> strips
a leading @ and replaces runs of non-alphanumeric characters with _, then
uppercases; @scope/my-package becomes SKIP_SCOPE_MY_PACKAGE_FALLBACK). If that env is set and no
binding was loaded, the loader's final check throws instead of starting another
nested install.
Patching a generated loader
For a loader generated by napi-rs, use the napi-patch bin instead of copying
the snippet into a build script:
napi-patch # index.js, name from package.json
napi-patch my-package # index.js, explicit name
napi-patch napi/index.js # nested entry, name from package.json
napi-patch napi/index.js my-package
With two arguments, the first is always the entry and the second is the package
name. With one argument, a name containing . is treated as the entry; otherwise
it is the package name and the entry defaults to index.js. This keeps dotted
package names unambiguous:
napi-patch index.js @scope.whatever/package
The name falls back to package.json in the current directory. The inserted
require.resolve('./package.json') assumes the published loader sits next to the
manifest, which is the normal napi-rs layout. It adds the fallback before the
loader's final if (!nativeBinding) check, guards it with
SKIP_<PACKAGE>_FALLBACK, and adds no webcontainer condition: the fallback is
attempted on every platform when the normal binding is missing.
Project-specific additions, such as a Yarn PnP environment flag, can be passed through the API:
import { patch } from 'napi-postinstall/patch'
patch({
entry: 'napi/index.js',
name: 'my-package',
append: `
if (process.versions.pnp) {
process.env.MY_PACKAGE_YARN_PNP = '1'
}
`,
})
Postinstall script
For packages that still use a postinstall hook, prefer a postinstall.js that
calls the API over invoking the CLI directly from postinstall. With
npm --no-bin-links, the bin shim may not exist
(unrs-resolver#190). Use a
JavaScript file that calls the API instead:
// postinstall.js
const { checkAndPreparePackage } = require('napi-postinstall')
checkAndPreparePackage('unrs-resolver').catch(error => {
console.error(error)
process.exitCode = 1
})
{
"scripts": {
"postinstall": "node postinstall.js"
}
}
This will check and prepare the napi binding packages for you automatically.
CLI
The CLI is available for manual use:
napi-postinstall unrs-resolver #<napi-package-name>
API
Types
// napi-postinstall
export interface PackageJson {
name: string
version: string
}
export declare function checkAndPreparePackage(
packageNameOrPackageJson: PackageJson | string,
checkVersion?: boolean,
): Promise<void>
// napi-postinstall/fallback
declare function fallback<T = unknown>(
packageJsonPath: string,
checkVersion?: boolean,
): T
export = fallback
// napi-postinstall/patch
export interface PatchOptions {
entry?: string
name: string
append?: string
}
export declare function patch(options: PatchOptions): void
The installed CLI scripts are exposed as napi-postinstall and napi-patch.
Example
Use napi-patch after generating index.js:
napi-patch
The inserted block is shown in Runtime fallback. If you
maintain a custom loader instead, call fallback() from its final
if (!nativeBinding) branch.
// postinstall.js — legacy compatibility path
const { checkAndPreparePackage, isNpm } = require('napi-postinstall')
if (isNpm()) {
void checkAndPreparePackage('unrs-resolver' /* <napi-package-name> */)
}
Sponsors and Backers
Sponsors
| 1stG | RxTS | UnTS |
|---|---|---|
Backers
| 1stG | RxTS | UnTS |
|---|---|---|
Changelog
Detailed changes for each release are documented in CHANGELOG.md.