fix(exe): restore @pnpm/exe startup on Node.js v25.7+ (#11330)

## Summary

`@pnpm/exe@11.0.0-rc.4` aborts on every invocation with:

```
node::sea::(anonymous namespace)::SeaDeserializer::Read() at ../src/node_sea.cc:174
Assertion failed: (format_value) <= (static_cast<uint8_t>(ModuleFormat::kModule))
```

Two independent Node.js v25.7+ SEA regressions are responsible, both surfaced by the rc.4 bump of the embedded runtime from 25.6.1 to 25.9.0. This PR fixes both and adds a prepublish smoke test so a broken binary can't reach npm again.

## Root cause

**1. SEA blob format changed in Node.js v25.7.0** ([nodejs/node#61813](https://github.com/nodejs/node/pull/61813) added ESM-entry-point support and inserted a new `ModuleFormat` byte into the blob header). SEA blobs carry no version marker, so a blob written by one Node.js version can only be deserialized by a matching one. In rc.4, the CI host Node.js (25.6.1, pre-change) wrote the blob and it was embedded in a 25.9.0 runtime (post-change) — the deserializer reads a misaligned byte as `format_value`, exceeds `kModule`, `CHECK_LE` fires, `SIGABRT`. `resolveBuilderBinary()` was preferring `process.execPath` whenever the running Node supported `--build-sea`, never checking that its version matched the embedded runtime.

**2. Node.js v25.7+ replaces the ambient `require` and `import()` inside a CJS SEA entry with embedder hooks** that only resolve built-in module names. The `pnpm.cjs` shim loaded `dist/pnpm.mjs` via `await import(pathToFileURL(...).href)`, which after the fix to (1) reached the CJS entry and then blew up with:

```
ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: file:///.../dist/pnpm.mjs
    at loadBuiltinModuleForEmbedder
    at importModuleDynamicallyForEmbedder
```

## Changes

- **`releasing/commands/src/pack-app/packApp.ts`** — `resolveBuilderBinary` now takes the resolved target runtime version and only reuses `process.execPath` when `process.version` exactly matches; otherwise it downloads a host-arch Node of the target version via the existing `ensureNodeRuntime` path. Added `PACK_APP_RUNTIME_TOO_OLD` for runtimes older than v25.5 (no `--build-sea`). Removed the now-unused `DEFAULT_BUILDER_SPEC` and the stale `fetch`/`nodeDownloadMirrors` args on the builder resolver. Help text / examples refreshed to drop `node@22` / `node@lts` references that would now be rejected.
- **`pnpm/pnpm.cjs`** — loads `dist/pnpm.mjs` through `Module.createRequire(process.execPath)` instead of `await import(fileURL)`. `createRequire` returns a regular CJS loader that bypasses the SEA embedder hooks, and the pnpm bundle has no top-level await so synchronous `require` of ESM (Node 22+) loads it cleanly. No build-time paths are baked in — `process.execPath` is evaluated at runtime, verified by relocation-testing the darwin-arm64 SEA under `/tmp/`.
- **`pnpm/artifacts/verify-binary.mjs`** (new) + `prepublishOnly` on every platform artifact — replaces the existence-only `test -f pnpm` gate with:
  1. A **relocation-sensitivity check**: run the binary without `dist/` staged and confirm the failure mentions a path derived from `process.execPath`, not a build-time constant. Catches any future regression of (2).
  2. A **smoke test**: stage a `dist → ../exe/dist` symlink (using `symlink-dir` so Windows junctions are handled transparently), exec `./pnpm -v`, assert the output is a SemVer 2 string.
  - Cross-platform targets (darwin/win32 artifacts on a Linux CI, or a libc mismatch) skip the exec with a log line and fall back to existence-only, so a musl artifact published from a glibc host still goes through.
  - Real `dist/` dirs (developer layout) are preserved; stale symlinks from aborted runs are replaced; created symlinks are cleaned up on exit.
- **`pnpm/artifacts/exe/test/setup.test.ts`** — new `pnpm -v` execution test gated on both the platform binary and the staged bundle being present, so ordinary `pn compile` test runs skip cleanly instead of failing on a missing `dist/`.
This commit is contained in:
Zoltan Kochan
2026-04-21 17:54:03 +02:00
committed by GitHub
parent fd437ded13
commit eb7e6ae8ab
14 changed files with 243 additions and 34 deletions

View File

@@ -0,0 +1,10 @@
---
"@pnpm/releasing.commands": patch
"pnpm": patch
---
Fix the `@pnpm/exe` SEA executable crashing at startup on Node.js v25.7+. Two separate regressions in `@pnpm/exe@11.0.0-rc.4` are addressed:
1. `pnpm pack-app` now pins the Node.js used to write the SEA blob to the exact embedded runtime version. The SEA blob format changed in Node.js v25.7 (ESM entry-point support added a `ModuleFormat` header byte), so a blob produced by a pre-25.7 builder cannot be deserialized by a 25.7+ runtime and vice versa. In rc.4 the CI host Node.js (v25.6.1) built blobs embedded in a v25.9.0 runtime, tripping `SeaDeserializer::Read() ... format_value <= kModule` on every invocation. `pack-app` now downloads a host-arch builder Node.js of the target version when the running Node.js doesn't already match.
2. The pnpm CJS SEA entry shim now loads `dist/pnpm.mjs` through `Module.createRequire(process.execPath)` instead of `await import(pathToFileURL(...).href)`. In Node.js v25.7+, the ambient `require` and `import()` inside a CJS SEA entry are replaced with embedder hooks that only resolve built-in module names, causing external `file://` loads to fail with `ERR_UNKNOWN_BUILTIN_MODULE`. An explicit `createRequire()` bypasses those hooks.

View File

@@ -74,6 +74,7 @@
"eisdir",
"elifecycle",
"elit",
"embedder",
"emfile",
"enametoolong",
"endregion",

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs darwin arm64"
},
"devDependencies": {
"@pnpm/macos-arm64": "workspace:*"

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs darwin x64"
},
"devDependencies": {
"@pnpm/macos-x64": "workspace:*"

View File

@@ -17,6 +17,11 @@ const platformBin = path.join(
isWindows ? 'pnpm.exe' : 'pnpm'
)
const hasPlatformBinary = fs.existsSync(platformBin)
// dist/ is staged by the build-artifacts flow (not by `pn compile`), so
// ordinary test runs don't have it. The hardlink test is fine without it
// (existence + inode only), but the -v test actually executes the SEA, which
// loads dist/pnpm.mjs from next to the binary and would fail here.
const hasStagedBundle = fs.existsSync(path.join(exeDir, 'dist', 'pnpm.mjs'))
describe('exePlatformPkgName', () => {
test('uses linuxstatic- prefix for linux + musl libc family', () => {
@@ -68,4 +73,18 @@ test('prepare writes correct content for all bin files', () => {
const pnpmBin = path.join(exeDir, isWindows ? 'pnpm.exe' : 'pnpm')
expect(fs.statSync(pnpmBin).ino).toBe(fs.statSync(platformBin).ino)
});
// Actually execute the hardlinked pnpm binary. Existence and inode-match are
// not enough — a SEA blob built by a Node.js version that differs from the
// embedded runtime deserializes on startup with a native assertion and an
// abort signal, not a clean error exit (see rc.4 regression). Running `-v`
// verifies the SEA payload is actually readable by the embedded Node.
(hasPlatformBinary && hasStagedBundle ? test : test.skip)('pnpm -v runs and prints a semver', () => {
execFileSync(process.execPath, [path.join(exeDir, 'prepare.js')], { cwd: exeDir })
execFileSync(process.execPath, [path.join(exeDir, 'setup.js')], { cwd: exeDir })
const pnpmBin = path.join(exeDir, isWindows ? 'pnpm.exe' : 'pnpm')
const stdout = execFileSync(pnpmBin, ['-v'], { encoding: 'utf8', timeout: 30_000 }).trim()
expect(stdout).toMatch(/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/)
})

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs linux arm64 musl"
},
"devDependencies": {
"@pnpm/linuxstatic-arm64": "workspace:*"

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs linux arm64 glibc"
},
"devDependencies": {
"@pnpm/linux-arm64": "workspace:*"

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs linux x64 musl"
},
"devDependencies": {
"@pnpm/linuxstatic-x64": "workspace:*"

View File

@@ -17,7 +17,7 @@
"pnpm"
],
"scripts": {
"prepublishOnly": "test -f pnpm || (echo 'Error: pnpm is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs linux x64 glibc"
},
"devDependencies": {
"@pnpm/linux-x64": "workspace:*"

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env node
// Prepublish gate for the @pnpm/<platform> artifact packages. Runs from the
// package directory (cwd contains the built pnpm binary). Verifies:
// 1. The binary exists with the expected filename for the target.
// 2. If the host can execute the target, `pnpm -v` returns a semver.
//
// Existence alone is not sufficient — @pnpm/exe@11.0.0-rc.4 shipped a binary
// that was present but crashed with a native SEA deserialization assertion on
// any invocation. Executing -v would have caught it on the Linux CI host.
//
// Each platform package ships only the SEA binary (no dist/ or node_modules),
// but the SEA's CJS entry (pnpm.cjs) loads dist/pnpm.mjs from
// dirname(process.execPath). To run the binary in place we symlink
// ./dist -> ../exe/dist (the sibling @pnpm/exe package's staged bundle) for
// the duration of the test, then remove the symlink on exit. The platform
// package's "files" whitelist is "pnpm" only, so a stale symlink would never
// reach the published tarball, but we clean up anyway to leave the tree
// untouched for subsequent tools.
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
// Resolves via the pnpm CLI's own node_modules (which always contains
// symlink-dir — the CLI depends on it directly). symlink-dir handles Windows
// junctions internally, so the verifier doesn't need its own elevation /
// link-type branching.
import { symlinkDirSync } from 'symlink-dir'
const [targetOs, targetArch, targetLibc] = process.argv.slice(2)
if (!targetOs || !targetArch) {
console.error('Usage: verify-binary.mjs <os> <arch> [libc]')
process.exit(2)
}
const binName = targetOs === 'win32' ? 'pnpm.exe' : 'pnpm'
if (!fs.existsSync(binName)) {
console.error(`Error: ${binName} is missing in ${process.cwd()}`)
process.exit(1)
}
// Node populates header.glibcVersionRuntime only on glibc hosts, so its
// presence is a reliable glibc/musl discriminator without shelling out.
function detectHostLibc () {
if (process.platform !== 'linux') return null
const header = process.report.getReport().header
return header.glibcVersionRuntime ? 'glibc' : 'musl'
}
const hostLibc = detectHostLibc()
// Cross-platform or cross-libc targets can't be executed from the publish
// host. Existence is the best we can verify — skip the -v check instead of
// failing, so a musl artifact published from a glibc CI still goes through.
const osMatches = process.platform === targetOs
const archMatches = process.arch === targetArch
const libcMatches = targetOs !== 'linux' || !targetLibc || targetLibc === hostLibc
if (!osMatches || !archMatches || !libcMatches) {
const targetLabel = [targetOs, targetArch, targetLibc].filter(Boolean).join('/')
const hostLabel = [process.platform, process.arch, hostLibc].filter(Boolean).join('/')
console.log(`Skipping ${binName} -v: host ${hostLabel} cannot execute target ${targetLabel}`)
process.exit(0)
}
const distLinkPath = path.resolve('dist')
const distLinkTarget = path.join('..', 'exe', 'dist')
let distLinkCreated = false
// Remove a prior symlink from an aborted run so cleanup ownership is always
// well-defined. A real dist/ directory (unlikely in a platform package, but
// possible during development) is preserved — we treat it as external and
// skip cleanup.
try {
if (fs.lstatSync(distLinkPath).isSymbolicLink()) fs.unlinkSync(distLinkPath)
} catch (err) {
if (err.code !== 'ENOENT') throw err
}
const distPreexists = fs.existsSync(distLinkPath)
process.on('exit', () => {
if (!distLinkCreated) return
try { fs.unlinkSync(distLinkPath) } catch { /* nothing to clean up */ }
})
// Relocation check: before staging dist/, confirm the binary reads its bundle
// path from process.execPath at runtime and not from a build-time constant.
// A pnpm.cjs shim that accidentally captured __filename or a cwd-relative
// path during packaging would keep working on the build machine but break on
// every end-user machine. Asserting the error references the *runtime* cwd
// catches that regression here instead of after publish.
//
// Skipped when a real dist/ is already present (developer layout); in that
// case we can't distinguish a correctly-resolved dist from a hardcoded one.
if (!distPreexists) {
const expectedRuntimeDist = path.join(fs.realpathSync(process.cwd()), 'dist', 'pnpm.mjs')
let sansDistStdout
try {
sansDistStdout = execFileSync(`./${binName}`, ['-v'], {
encoding: 'utf8',
timeout: 30_000,
stdio: ['ignore', 'pipe', 'pipe'],
})
} catch (err) {
const stderr = String(err?.stderr ?? '')
// Expected: binary tried to require the runtime dist path and failed
// because it isn't there. Anything else (a spawn error, crash signal,
// timeout) is NOT evidence of a non-relocatable pnpm.cjs — it's an
// unrelated failure that would hide the regression we actually care
// about. Surface it with the raw diagnostic so the operator can tell
// which one they're looking at.
if (!stderr.includes(expectedRuntimeDist)) {
const status = err?.status ?? 'none'
const signal = err?.signal ?? 'none'
const code = err?.code ?? 'none'
console.error(`Error: ${binName} -v without dist/ did not fail with a missing-runtime-dist error. Either pnpm.cjs regressed to a non-relocatable form, or the binary failed for an unrelated reason. status=${status} signal=${signal} code=${code}\nstderr:\n${stderr}`)
process.exit(1)
}
}
if (sansDistStdout !== undefined) {
console.error(`Error: ${binName} -v unexpectedly succeeded without dist/ alongside the binary. Output: ${JSON.stringify(sansDistStdout.trim())}. pnpm.cjs is loading a bundle from somewhere other than dirname(process.execPath); the published binary would ignore the dist shipped in @pnpm/exe.`)
process.exit(1)
}
}
// Only stage the symlink when nothing's there already — symlink-dir will
// atomically rename away any existing dir/file, which would silently drop a
// developer's staged dist/ directory.
if (!distPreexists) {
try {
symlinkDirSync(distLinkTarget, distLinkPath)
distLinkCreated = true
} catch (err) {
console.error(`Error: could not stage dist/ symlink: ${String(err)}`)
process.exit(1)
}
}
let stdout
try {
stdout = execFileSync(`./${binName}`, ['-v'], { encoding: 'utf8', timeout: 30_000 }).trim()
} catch (err) {
console.error(`Error: ${binName} -v failed: ${String(err)}`)
process.exit(1)
}
// Accept SemVer 2 with optional prerelease and build-metadata suffixes so a
// future `11.0.0-rc.4+sha.<hash>` release doesn't fail this gate spuriously.
if (!/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(stdout)) {
console.error(`Error: ${binName} -v produced unexpected output: ${JSON.stringify(stdout)}`)
process.exit(1)
}
console.log(`${binName} -v OK (${stdout})`)

View File

@@ -17,7 +17,7 @@
"pnpm.exe"
],
"scripts": {
"prepublishOnly": "test -f pnpm.exe || (echo 'Error: pnpm.exe is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs win32 arm64"
},
"devDependencies": {
"@pnpm/win-arm64": "workspace:*"

View File

@@ -17,7 +17,7 @@
"pnpm.exe"
],
"scripts": {
"prepublishOnly": "test -f pnpm.exe || (echo 'Error: pnpm.exe is missing' && exit 1)"
"prepublishOnly": "node ../verify-binary.mjs win32 x64"
},
"devDependencies": {
"@pnpm/win-x64": "workspace:*"

View File

@@ -3,10 +3,17 @@
// In a SEA binary, relative import specifiers resolve against the *build-time*
// path of the embedded script, not the runtime location of the executable.
// We must resolve against process.execPath so the import works on any machine.
//
// Goes through Module.createRequire() rather than the ambient require() or
// dynamic import(). In Node.js >=25.7, the ambient require() and import()
// inside a CJS SEA entry are replaced with embedder hooks that only know how
// to resolve built-in module names, so any attempt to load an external file
// fails with ERR_UNKNOWN_BUILTIN_MODULE. A createRequire() bound to the
// running binary returns a normal module loader that bypasses those hooks,
// and the pnpm bundle has no top-level await so synchronous require() of it
// (Node.js 22+ feature) loads cleanly.
const { join, dirname } = require('path')
const { pathToFileURL } = require('url')
const { createRequire } = require('module')
;(async () => {
const distPath = join(dirname(process.execPath), 'dist', 'pnpm.mjs')
await import(pathToFileURL(distPath).href)
})()
const distPath = join(dirname(process.execPath), 'dist', 'pnpm.mjs')
createRequire(process.execPath)(distPath)

View File

@@ -20,12 +20,6 @@ import { renderHelp } from 'render-help'
/** Minimum Node.js version that supports `node --build-sea`. */
const MIN_BUILDER_VERSION = { major: 25, minor: 5 } as const
// Range to download when the running Node is too old. Constrained to the
// current major so we don't silently jump majors across releases, and pinned
// above MIN_BUILDER_VERSION.minor so older point releases (e.g. 25.0.x) that
// don't support `--build-sea` aren't picked.
const DEFAULT_BUILDER_SPEC = `>=${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}.0 <${MIN_BUILDER_VERSION.major + 1}.0.0`
// Target OS names match `process.platform`. That keeps the CLI surface
// consistent with pnpm's own `--os` flag (which also takes platform constants)
// and with `supportedArchitectures.os` in pnpm-workspace.yaml.
@@ -61,16 +55,17 @@ export function help (): string {
'Pack a CommonJS entry file into a standalone executable for one or more target platforms.\n\n' +
'The executable embeds a Node.js binary via the Node.js Single Executable Applications API.\n' +
`Requires Node.js v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}+ to perform ` +
'the injection. The running Node.js is used when it is new enough; otherwise, the ' +
`latest Node.js v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}+ in the ` +
`v${MIN_BUILDER_VERSION.major}.x line is downloaded automatically.\n\n` +
'the injection. SEA blobs are not compatible across Node.js minor releases, so the ' +
'builder Node.js must match the embedded runtime version exactly. The running Node.js ' +
'is used when it already matches; otherwise a host-arch Node.js of the embedded runtime ' +
'version is downloaded automatically.\n\n' +
'Defaults for --entry, --target, --runtime, --output-dir, and --output-name can be ' +
'set in the package.json under "pnpm.app". CLI flags override the config; --target entirely ' +
'replaces the configured list so you can narrow it at invocation time.',
url: docsUrl('pack-app'),
usages: [
'pnpm pack-app --entry dist/index.cjs --target linux-x64 --target win32-x64',
'pnpm pack-app --entry dist/index.cjs --target linux-x64-musl --runtime node@22',
`pnpm pack-app --entry dist/index.cjs --target linux-x64-musl --runtime node@${MIN_BUILDER_VERSION.major}`,
],
descriptionLists: [
{
@@ -89,7 +84,8 @@ export function help (): string {
{
description:
'Runtime to embed in the output executables, as a "<name>@<version>" spec ' +
'(e.g. "node@22", "node@22.0.0", "node@lts"). Only "node" is supported today. ' +
`(e.g. "node@${MIN_BUILDER_VERSION.major}", "node@${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}.0"). ` +
`Only "node" is supported today, and the version must be >= v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor} (the minimum that supports --build-sea). ` +
'Defaults to the running Node.js version.',
name: '--runtime',
},
@@ -186,8 +182,17 @@ export async function handler (opts: PackAppOptions, params: string[]): Promise<
const fetch = createFetchFromRegistry(opts)
const buildRoot = path.join(opts.pnpmHomeDir, 'pack-app')
const builderBin = await resolveBuilderBinary({ fetch, nodeDownloadMirrors: opts.nodeDownloadMirrors, buildRoot })
// Resolve the embedded target version first so the builder can be pinned to
// the same version. SEA blobs carry no version header and the serialized
// format has changed across Node.js minor releases (e.g. v25.7 added a
// ModuleFormat byte for ESM entry points), so a blob produced by a builder
// of a different version than the embedded runtime will fail deserialization
// at startup with an opaque native assertion.
const resolvedTargetVersion = await resolveVersion(fetch, requestedNodeSpec, opts.nodeDownloadMirrors)
const builderBin = await resolveBuilderBinary({
buildRoot,
targetVersion: resolvedTargetVersion,
})
const results: string[] = []
for (const target of targets) {
@@ -243,21 +248,32 @@ export async function handler (opts: PackAppOptions, params: string[]): Promise<
}
/**
* Returns a Node.js binary that supports `--build-sea`. Prefers the running
* interpreter to avoid a download; falls back to downloading Node.js v25.
* Returns a Node.js binary that supports `--build-sea` AND produces a SEA
* blob the embedded runtime can deserialize. The second constraint forces the
* builder to match the target runtime version exactly: blobs are versioned by
* the writer's internal struct layout with no header, and Node bumps that
* layout in minor releases (e.g. v25.7 added a ModuleFormat byte for ESM
* entries), so a cross-version blob crashes at startup.
*
* Prefers the running interpreter when it already matches the target version;
* otherwise downloads the target version for the host platform.
*/
async function resolveBuilderBinary (ctx: {
fetch: ReturnType<typeof createFetchFromRegistry>
nodeDownloadMirrors?: Record<string, string>
buildRoot: string
targetVersion: string
}): Promise<string> {
if (runningNodeCanBuildSea()) {
if (runningNodeCanBuildSea() && process.version === `v${ctx.targetVersion}`) {
return process.execPath
}
const version = await resolveVersion(ctx.fetch, DEFAULT_BUILDER_SPEC, ctx.nodeDownloadMirrors)
if (!builderVersionCanBuildSea(ctx.targetVersion)) {
throw new PnpmError('PACK_APP_RUNTIME_TOO_OLD',
`The embedded runtime "node@${ctx.targetVersion}" is older than Node.js v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}, which is the minimum version that supports --build-sea.`,
{ hint: `Pass --runtime node@${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}.0 (or newer) or set "pnpm.app.runtime" in package.json.` }
)
}
return ensureNodeRuntime({
buildRoot: ctx.buildRoot,
version,
version: ctx.targetVersion,
platform: process.platform,
arch: process.arch,
// Pin libc to the host's. Otherwise a caller that had set
@@ -274,7 +290,11 @@ function hostLinuxLibc (): 'glibc' | 'musl' | undefined {
}
function runningNodeCanBuildSea (): boolean {
const [majorStr, minorStr] = process.version.slice(1).split('.')
return builderVersionCanBuildSea(process.version.slice(1))
}
function builderVersionCanBuildSea (version: string): boolean {
const [majorStr, minorStr] = version.split('.')
const major = Number(majorStr)
const minor = Number(minorStr)
return (
@@ -393,7 +413,7 @@ function parseRuntime (spec: string): ParsedRuntime {
const match = RUNTIME_PATTERN.exec(spec)
if (!match) {
throw new PnpmError('PACK_APP_INVALID_RUNTIME',
`Invalid runtime "${spec}". Expected format: <name>@<version> (supported runtimes: ${SUPPORTED_RUNTIMES.join(', ')}; e.g. "node@22.0.0", "node@lts").`)
`Invalid runtime "${spec}". Expected format: <name>@<version> (supported runtimes: ${SUPPORTED_RUNTIMES.join(', ')}; e.g. "node@${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}.0").`)
}
return { name: match[1] as ParsedRuntime['name'], version: match[2] }
}