* docs: lint prose with Vale Add Vale with an in-repo style, .vale/styles/pnpm, that turns the writing rules in AGENTS.md and the Google developer documentation style guide into lint rules. The stock Vale packages (Google, Microsoft, RedHat, write-good) were too noisy to block on, for example 1,300 passive-voice hits in the v12 docs alone, so the rules are written for this repo, following the GitLab model. Each one links the guideline it enforces. Levels: - error, fails CI: dashes, plain-word substitutions, condescending words, "please", "note that", hedges, puffery, optional plurals, exclamation marks, ellipses, curly quotes, generic link text, product spellings, sentence-case headings, repeated words, double spaces, implementation history in comments; in changesets, bold or italic emphasis, internal names and release-line names. - warning: future tense, sentences over 30 words, "instead of" and "so that" tails in changesets. - suggestion: passive voice. .vale.ini scopes the rules. Docs get every rule. Contributor Markdown and code comments keep dashes and headings as warnings until their roughly 8,100 em dashes are rewritten. Released changesets stay in .changeset/ and are not rewritten, so lint-prose.mjs lints only the changesets added since PROSE_BASE (the PR base in CI). The prose workflow installs a pinned, checksum-verified Vale release and runs `pnpm lint:prose`. The rest of this change rewrites the existing docs, READMEs, contributor Markdown, and code comments to pass the error-level rules. Only prose and comment text changed. Renamed headings keep their anchors. Spelling stays with cspell, which checks identifiers that Vale does not see. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: keep prose lint from flattening meaning --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
5.5 KiB
id, title
| id | title |
|---|---|
| pack-app | pnpm pack-app |
Added in: v11.0.0
:::warning 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, since SEA blobs are not compatible across minor releases), pnpm downloads a matching builder automatically.
- Cross-compiling macOS targets from Linux requires
ldidon$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-musldarwin-x64,darwin-arm64win32-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:
- nodejs/node#62893: minimal
node --build-searepro - nodejs/node#59553: long-running SEA test failures on macOS x64 with the same root cause
- nodejs/node#60250: Node.js skipping the SEA tests on x64 macOS rather than fixing them
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). 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
Options
--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).
Configuration
Defaults for every flag can be set in package.json under pnpm.app. CLI flags override the config:
{
"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).