Ana içeriğe atla
Sürüm: 11.x

pnpm pack-app

Added in: v11.0.0

Experimental

pnpm pack-app is experimental. Its flags, pnpm.app configuration schema, and output layout may change in future releases.

Pack a CommonJS entry file into a standalone executable for one or more target platforms, using the Node.js Single Executable Applications API under the hood.

pnpm pack-app --entry <path> --target <triplet> [--target <triplet> ...]

Each target produces an executable under <output-dir>/<target>/ (default dist-app/<target>/). On Windows targets the output is suffixed with .exe; macOS outputs are ad-hoc signed automatically (via codesign on macOS hosts or ldid on Linux hosts) because SEA injection invalidates the existing code signature.

Requirements

  • The host must run Node.js v25.5+ to perform the SEA injection. If the running Node.js is older (or does not match the embedded runtime version — SEA blobs are not compatible across minor releases), pnpm downloads a matching builder automatically.
  • Cross-compiling macOS targets from Linux requires ldid on $PATH. Windows hosts cannot ad-hoc sign macOS outputs; build macOS targets on macOS or Linux.

Supported targets

Targets use the format <os>-<arch>[-<libc>]:

  • linux-x64, linux-x64-musl, linux-arm64, linux-arm64-musl
  • darwin-x64, darwin-arm64
  • win32-x64, win32-arm64

The -musl suffix is only valid for linux targets. The <os> segment matches process.platform values so the flag is consistent with pnpm's --os flag and with supportedArchitectures.os in pnpm-workspace.yaml.

Known limitations

darwin-x64 binaries crash on Intel Macs

darwin-x64 outputs segfault at startup on Intel Macs because of an upstream Node.js bug in the --build-sea injection step. LIEF's Mach-O surgery for x64 leaves LC_DYLD_CHAINED_FIXUPS chain entries pointing at stale targets after the SEA segment is inserted; dyld then dereferences a raw chain-encoded value as a pointer and the binary crashes in __cxx_global_var_init before any user code runs. This is reproducible with the canonical node --build-sea + codesign --sign - flow with no pnpm involvement.

The Node.js team has opted not to fix this on the grounds that x64 macOS is being phased out. Signature-related workarounds do not help — the corruption happens in the injection step, before signing, so swapping ldid for codesign (or vice versa) makes no difference. Re-signing produces a valid signature over already-broken bytes.

Tracking:

If you need to ship a CLI that runs on Intel Macs, build the darwin-x64 artifact with a non-SEA tool such as @yao-pkg/pkg (which appends to the binary tail rather than mutating Mach-O sections). Note that Rosetta is not an escape hatch — it only translates x64 → arm64 (for Apple Silicon Macs running Intel binaries), not the other direction, so Intel Macs cannot run a darwin-arm64 build.

Examples

Build for Linux and Windows at once:

pnpm pack-app --entry dist/index.cjs --target linux-x64 --target win32-x64

Embed a specific Node.js version:

pnpm pack-app --entry dist/index.cjs --target linux-x64-musl --runtime node@25.5.0

Parametreler

--entry <path>

Path to the CJS entry file to embed in the executable. Required unless pnpm.app.entry is set in package.json. A bare positional argument is also accepted (e.g. pnpm pack-app dist/index.cjs).

--target, -t <triplet>

Target to build for. May be specified multiple times. See Supported targets for the accepted values. Required unless pnpm.app.targets is set. When passed on the CLI, --target entirely replaces the configured list so you can narrow the build at invocation time.

--runtime <spec>

Runtime to embed in the output executables, as a <name>@<version> spec (e.g. node@25, node@25.5.0). Only node is supported today; the <name>@ prefix leaves room for future runtimes (bun, deno). The version must be >= v25.5 (the minimum that supports --build-sea). Defaults to the running Node.js version.

--output-dir, -o <dir>

Output directory for the built executables. Defaults to dist-app.

--output-name <name>

Name for the output executable (without extension). Defaults to the unscoped name from package.json (e.g. my-cli for @acme/my-cli).

Yapılandırma

Defaults for every flag can be set in package.json under pnpm.app. CLI flags override the config:

package.json
{
"name": "my-cli",
"pnpm": {
"app": {
"entry": "dist/index.cjs",
"targets": [
"linux-x64",
"linux-arm64",
"darwin-x64",
"darwin-arm64",
"win32-x64"
],
"runtime": "node@25.5.0",
"outputDir": "dist-app",
"outputName": "my-cli"
}
}
}

With this config in place, pnpm pack-app can be run with no arguments. --target on the CLI replaces the configured targets list, which is useful for narrowing a build (e.g. pnpm pack-app --target linux-x64).