@pnpm/releasing.commands 1100.0.2 → 1100.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { deploy } from './deploy/index.js';
2
+ export { packApp } from './pack-app/index.js';
2
3
  export { pack, publish } from './publish/index.js';
3
4
  export { version } from './version/index.js';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { deploy } from './deploy/index.js';
2
+ export { packApp } from './pack-app/index.js';
2
3
  export { pack, publish } from './publish/index.js';
3
4
  export { version } from './version/index.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,2 @@
1
+ import * as packApp from './packApp.js';
2
+ export { packApp };
@@ -0,0 +1,3 @@
1
+ import * as packApp from './packApp.js';
2
+ export { packApp };
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import type { Config } from '@pnpm/config.reader';
2
+ export declare const commandNames: string[];
3
+ export declare function rcOptionsTypes(): Record<string, unknown>;
4
+ export declare function cliOptionsTypes(): Record<string, unknown>;
5
+ export declare const shorthands: Record<string, string>;
6
+ export declare function help(): string;
7
+ export type PackAppOptions = Pick<Config, 'dir' | 'pnpmHomeDir'> & Partial<Pick<Config, 'ca' | 'cert' | 'configByUri' | 'httpProxy' | 'httpsProxy' | 'key' | 'localAddress' | 'nodeDownloadMirrors' | 'noProxy' | 'strictSsl' | 'userAgent'>> & {
8
+ entry?: string;
9
+ target?: string | string[];
10
+ nodeVersion?: string;
11
+ outputDir?: string;
12
+ outputName?: string;
13
+ };
14
+ export declare function handler(opts: PackAppOptions, params: string[]): Promise<string>;
15
+ /** Fields pack-app reads from `pnpm.app` in package.json. */
16
+ export interface ProjectAppConfig {
17
+ entry?: string;
18
+ targets?: string[];
19
+ nodeVersion?: string;
20
+ outputDir?: string;
21
+ outputName?: string;
22
+ }
@@ -0,0 +1,398 @@
1
+ import fs from 'node:fs';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { docsUrl } from '@pnpm/cli.utils';
6
+ import { getNodeMirror, parseNodeSpecifier, resolveNodeVersion, } from '@pnpm/engine.runtime.node-resolver';
7
+ import { PnpmError } from '@pnpm/error';
8
+ import { runPnpmCli } from '@pnpm/exec.pnpm-cli-runner';
9
+ import { createFetchFromRegistry } from '@pnpm/network.fetch';
10
+ import { familySync } from 'detect-libc';
11
+ import { safeExeca as execa } from 'execa';
12
+ import { renderHelp } from 'render-help';
13
+ /** Minimum Node.js version that supports `node --build-sea`. */
14
+ const MIN_BUILDER_VERSION = { major: 25, minor: 5 };
15
+ // Range to download when the running Node is too old. Constrained to the
16
+ // current major so we don't silently jump majors across releases, and pinned
17
+ // above MIN_BUILDER_VERSION.minor so older point releases (e.g. 25.0.x) that
18
+ // don't support `--build-sea` aren't picked.
19
+ const DEFAULT_BUILDER_SPEC = `>=${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}.0 <${MIN_BUILDER_VERSION.major + 1}.0.0`;
20
+ // Target OS names match `process.platform`. That keeps the CLI surface
21
+ // consistent with pnpm's own `--os` flag (which also takes platform constants)
22
+ // and with `supportedArchitectures.os` in pnpm-workspace.yaml.
23
+ const SUPPORTED_OS = ['linux', 'darwin', 'win32'];
24
+ const SUPPORTED_TARGETS = 'linux-x64, linux-x64-musl, linux-arm64, linux-arm64-musl, darwin-x64, darwin-arm64, win32-x64, win32-arm64';
25
+ export const commandNames = ['pack-app'];
26
+ export function rcOptionsTypes() {
27
+ return {};
28
+ }
29
+ export function cliOptionsTypes() {
30
+ return {
31
+ entry: String,
32
+ target: [String, Array],
33
+ 'node-version': String,
34
+ 'output-dir': String,
35
+ 'output-name': String,
36
+ };
37
+ }
38
+ export const shorthands = {
39
+ t: '--target',
40
+ o: '--output-dir',
41
+ };
42
+ export function help() {
43
+ return renderHelp({
44
+ description: 'Pack a CommonJS entry file into a standalone executable for one or more target platforms.\n\n' +
45
+ 'The executable embeds a Node.js binary via the Node.js Single Executable Applications API.\n' +
46
+ `Requires Node.js v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}+ to perform ` +
47
+ 'the injection. The running Node.js is used when it is new enough; otherwise, the ' +
48
+ `latest Node.js v${MIN_BUILDER_VERSION.major}.${MIN_BUILDER_VERSION.minor}+ in the ` +
49
+ `v${MIN_BUILDER_VERSION.major}.x line is downloaded automatically.\n\n` +
50
+ 'Defaults for --entry, --target, --node-version, --output-dir, and --output-name can be ' +
51
+ 'set in the package.json under "pnpm.app". CLI flags override the config; --target entirely ' +
52
+ 'replaces the configured list so you can narrow it at invocation time.',
53
+ url: docsUrl('pack-app'),
54
+ usages: [
55
+ 'pnpm pack-app --entry dist/index.cjs --target linux-x64 --target win32-x64',
56
+ 'pnpm pack-app --entry dist/index.cjs --target linux-x64-musl --node-version 22',
57
+ ],
58
+ descriptionLists: [
59
+ {
60
+ title: 'Options',
61
+ list: [
62
+ {
63
+ description: 'Path to the CJS entry file to embed in the executable',
64
+ name: '--entry',
65
+ },
66
+ {
67
+ description: `Target to build for. May be specified multiple times. Supported: ${SUPPORTED_TARGETS}`,
68
+ name: '--target',
69
+ shortAlias: '-t',
70
+ },
71
+ {
72
+ description: 'Node.js version to embed in the output executables (e.g. "22", "22.0.0", "lts"). ' +
73
+ 'Defaults to the running Node.js version.',
74
+ name: '--node-version',
75
+ },
76
+ {
77
+ description: 'Output directory for the built executables. Defaults to "dist-app".',
78
+ name: '--output-dir',
79
+ shortAlias: '-o',
80
+ },
81
+ {
82
+ description: 'Name for the output executable (without extension). Defaults to the unscoped package name.',
83
+ name: '--output-name',
84
+ },
85
+ ],
86
+ },
87
+ ],
88
+ });
89
+ }
90
+ export async function handler(opts, params) {
91
+ // pnpm.app in package.json supplies defaults for every flag. CLI flags win,
92
+ // but `--target` entirely replaces the config list (additive merging would
93
+ // prevent narrowing from the CLI). See ProjectAppConfig below for the shape.
94
+ const project = await readProjectAppConfig(opts.dir);
95
+ const entryPath = opts.entry ?? params[0] ?? project.app?.entry;
96
+ if (!entryPath) {
97
+ throw new PnpmError('PACK_APP_MISSING_ENTRY', '"pnpm pack-app" requires a CJS entry file — pass --entry <path> or set "pnpm.app.entry" in package.json.');
98
+ }
99
+ const resolvedEntry = path.resolve(opts.dir, entryPath);
100
+ let entryStat;
101
+ try {
102
+ entryStat = fs.statSync(resolvedEntry);
103
+ }
104
+ catch {
105
+ throw new PnpmError('PACK_APP_ENTRY_NOT_FOUND', `Entry file not found: ${resolvedEntry}`);
106
+ }
107
+ if (!entryStat.isFile()) {
108
+ throw new PnpmError('PACK_APP_ENTRY_NOT_FILE', `Entry path must be a regular file: ${resolvedEntry}`);
109
+ }
110
+ const cliTargets = opts.target == null
111
+ ? undefined
112
+ : Array.isArray(opts.target) ? opts.target : [opts.target];
113
+ const rawTargets = cliTargets ?? project.app?.targets ?? [];
114
+ if (rawTargets.length === 0) {
115
+ throw new PnpmError('PACK_APP_MISSING_TARGET', `"pnpm pack-app" requires at least one target — pass --target <triplet> or set "pnpm.app.targets" in package.json. Supported: ${SUPPORTED_TARGETS}`);
116
+ }
117
+ const targets = rawTargets.map(parseTarget);
118
+ const outputDir = path.resolve(opts.dir, opts.outputDir ?? project.app?.outputDir ?? 'dist-app');
119
+ await mkdir(outputDir, { recursive: true });
120
+ const outputName = validateOutputName(opts.outputName ?? project.app?.outputName ?? deriveOutputNameFromPackage(project, opts.dir));
121
+ const requestedNodeSpec = opts.nodeVersion ?? project.app?.nodeVersion ?? process.version.slice(1);
122
+ const fetch = createFetchFromRegistry(opts);
123
+ const buildRoot = path.join(opts.pnpmHomeDir, 'pack-app');
124
+ const builderBin = await resolveBuilderBinary({ fetch, nodeDownloadMirrors: opts.nodeDownloadMirrors, buildRoot });
125
+ const resolvedTargetVersion = await resolveVersion(fetch, requestedNodeSpec, opts.nodeDownloadMirrors);
126
+ const results = [];
127
+ for (const target of targets) {
128
+ // eslint-disable-next-line no-await-in-loop
129
+ const embeddedNodeBin = await ensureNodeRuntime({
130
+ buildRoot,
131
+ version: resolvedTargetVersion,
132
+ platform: target.platform,
133
+ arch: target.arch,
134
+ libc: target.libc,
135
+ });
136
+ const targetOutputDir = path.join(outputDir, target.raw);
137
+ // eslint-disable-next-line no-await-in-loop
138
+ await mkdir(targetOutputDir, { recursive: true });
139
+ const outputFile = target.platform === 'win32'
140
+ ? path.join(targetOutputDir, `${outputName}.exe`)
141
+ : path.join(targetOutputDir, outputName);
142
+ const seaConfig = {
143
+ main: resolvedEntry,
144
+ output: outputFile,
145
+ executable: embeddedNodeBin,
146
+ disableExperimentalSEAWarning: true,
147
+ useCodeCache: false,
148
+ useSnapshot: false,
149
+ };
150
+ // Write the SEA config into a fresh, unpredictable temp directory (0700
151
+ // by default) rather than a predictable path under os.tmpdir(). Avoids
152
+ // TOCTOU/symlink attacks on multi-user systems.
153
+ // eslint-disable-next-line no-await-in-loop
154
+ const tmpConfigDir = await mkdtemp(path.join(os.tmpdir(), 'pnpm-pack-app-'));
155
+ const configPath = path.join(tmpConfigDir, 'sea-config.json');
156
+ // eslint-disable-next-line no-await-in-loop
157
+ await writeFile(configPath, JSON.stringify(seaConfig, null, 2), { flag: 'wx' });
158
+ try {
159
+ // eslint-disable-next-line no-await-in-loop
160
+ await execa(builderBin, ['--build-sea', configPath], { stdio: 'inherit' });
161
+ }
162
+ finally {
163
+ // eslint-disable-next-line no-await-in-loop
164
+ await rm(tmpConfigDir, { recursive: true, force: true }).catch(() => { });
165
+ }
166
+ // eslint-disable-next-line no-await-in-loop
167
+ await adHocSignMacBinary(target, outputFile);
168
+ results.push(` ${target.raw}: ${outputFile} (Node.js ${resolvedTargetVersion})`);
169
+ }
170
+ return `Built ${targets.length} executable${targets.length === 1 ? '' : 's'}:\n${results.join('\n')}`;
171
+ }
172
+ /**
173
+ * Returns a Node.js binary that supports `--build-sea`. Prefers the running
174
+ * interpreter to avoid a download; falls back to downloading Node.js v25.
175
+ */
176
+ async function resolveBuilderBinary(ctx) {
177
+ if (runningNodeCanBuildSea()) {
178
+ return process.execPath;
179
+ }
180
+ const version = await resolveVersion(ctx.fetch, DEFAULT_BUILDER_SPEC, ctx.nodeDownloadMirrors);
181
+ return ensureNodeRuntime({
182
+ buildRoot: ctx.buildRoot,
183
+ version,
184
+ platform: process.platform,
185
+ arch: process.arch,
186
+ // Pin libc to the host's. Otherwise a caller that had set
187
+ // supportedArchitectures.libc=musl in their config would cause the
188
+ // glibc host to download a musl Node that it cannot execute.
189
+ libc: hostLinuxLibc(),
190
+ });
191
+ }
192
+ function hostLinuxLibc() {
193
+ if (process.platform !== 'linux')
194
+ return undefined;
195
+ const family = familySync();
196
+ return family === 'musl' ? 'musl' : 'glibc';
197
+ }
198
+ function runningNodeCanBuildSea() {
199
+ const [majorStr, minorStr] = process.version.slice(1).split('.');
200
+ const major = Number(majorStr);
201
+ const minor = Number(minorStr);
202
+ return (major > MIN_BUILDER_VERSION.major ||
203
+ (major === MIN_BUILDER_VERSION.major && minor >= MIN_BUILDER_VERSION.minor));
204
+ }
205
+ /**
206
+ * Fetches a Node.js runtime into a dedicated per-target directory under the
207
+ * pnpm home, reusing the cached binary if already present. Actual files are
208
+ * hardlinked from pnpm's content-addressable store, so repeated calls are
209
+ * cheap and `pnpm store prune` can reclaim them.
210
+ */
211
+ async function ensureNodeRuntime(opts) {
212
+ // Linux variants always need a libc pin (glibc or musl) so that variant
213
+ // selection is deterministic and doesn't depend on the host's detected
214
+ // libc or the user's supportedArchitectures.libc config.
215
+ const libc = opts.platform === 'linux' ? opts.libc ?? 'glibc' : opts.libc;
216
+ const targetId = [opts.platform, opts.arch, libc].filter(Boolean).join('-');
217
+ const installDir = path.join(opts.buildRoot, `${targetId}-${opts.version}`);
218
+ const nodeDir = path.join(installDir, 'node_modules', 'node');
219
+ const binaryPath = nodeBinaryPath(nodeDir, opts.platform);
220
+ if (fs.existsSync(binaryPath))
221
+ return binaryPath;
222
+ await mkdir(installDir, { recursive: true });
223
+ await writeFile(path.join(installDir, 'package.json'), `${JSON.stringify({ name: `pnpm-pack-app-${targetId}`, private: true }, null, 2)}\n`);
224
+ // Flags that select the target variant must come before the positional
225
+ // package spec; otherwise `pnpm add` silently installs the host variant.
226
+ const args = [
227
+ 'add',
228
+ '--ignore-scripts',
229
+ '--ignore-workspace',
230
+ `--os=${opts.platform}`,
231
+ `--cpu=${opts.arch}`,
232
+ ];
233
+ if (libc != null) {
234
+ args.push(`--libc=${libc}`);
235
+ }
236
+ args.push(`node@runtime:${opts.version}`);
237
+ runPnpmCli(args, { cwd: installDir });
238
+ if (!fs.existsSync(binaryPath)) {
239
+ throw new PnpmError('PACK_APP_NODE_BINARY_MISSING', `Expected Node.js binary at ${binaryPath} after installing node@runtime:${opts.version}, but it was not found.`);
240
+ }
241
+ return binaryPath;
242
+ }
243
+ function nodeBinaryPath(nodeDir, platform) {
244
+ return platform === 'win32'
245
+ ? path.join(nodeDir, 'node.exe')
246
+ : path.join(nodeDir, 'bin', 'node');
247
+ }
248
+ async function resolveVersion(fetch, specifier, nodeDownloadMirrors) {
249
+ const { releaseChannel, versionSpecifier } = parseNodeSpecifier(specifier);
250
+ const nodeMirrorBaseUrl = getNodeMirror(nodeDownloadMirrors, releaseChannel);
251
+ const version = await resolveNodeVersion(fetch, versionSpecifier, nodeMirrorBaseUrl);
252
+ if (!version) {
253
+ throw new PnpmError('PACK_APP_NODE_VERSION_NOT_FOUND', `Could not find a Node.js version that satisfies "${specifier}"`);
254
+ }
255
+ return version;
256
+ }
257
+ // Parsed triplet must match this shape exactly. We anchor and constrain each
258
+ // segment so that inputs like `linux-x64-musl-../../outside` are rejected
259
+ // outright — otherwise `target.raw` would later flow into path.join for the
260
+ // output directory and could escape it.
261
+ const TARGET_PATTERN = /^(linux|darwin|win32)-(x64|arm64)(?:-(musl))?$/;
262
+ function parseTarget(raw) {
263
+ const match = TARGET_PATTERN.exec(raw);
264
+ if (!match) {
265
+ throw new PnpmError('PACK_APP_INVALID_TARGET', `Invalid target: "${raw}". Expected format: <os>-<arch>[-<libc>] where <os> is ${SUPPORTED_OS.join('|')}, <arch> is x64|arm64, optional <libc> is musl (linux only).`);
266
+ }
267
+ const [, platform, arch, libc] = match;
268
+ if (libc === 'musl' && platform !== 'linux') {
269
+ throw new PnpmError('PACK_APP_INVALID_TARGET', `The "musl" libc suffix is only valid for linux targets (got "${raw}").`);
270
+ }
271
+ return { raw, platform, arch, libc: libc || undefined };
272
+ }
273
+ // Characters that Win32 rejects in filenames, plus NUL. Path separators are
274
+ // checked separately via `path.basename` so the message is crisp.
275
+ const INVALID_FILENAME_CHARS = /[<>:"|?*\0]/;
276
+ // Win32 reserved device names (case-insensitive, with or without an extension).
277
+ const RESERVED_WINDOWS_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
278
+ // Reject anything that would let the output escape its target directory, or
279
+ // that would fail filesystem-level validation on any supported host. This
280
+ // surfaces problems at `pack-app` invocation time instead of letting them
281
+ // blow up later in `writeFile(outputFile, …)`.
282
+ function validateOutputName(name) {
283
+ if (name !== path.basename(name) ||
284
+ name === '' || name === '.' || name === '..' ||
285
+ name.includes('/') || name.includes('\\') ||
286
+ INVALID_FILENAME_CHARS.test(name) ||
287
+ RESERVED_WINDOWS_NAME.test(name) ||
288
+ /[. ]$/.test(name)) {
289
+ throw new PnpmError('PACK_APP_INVALID_OUTPUT_NAME', `Invalid --output-name "${name}". The name must be a plain filename without path separators, Windows-reserved names (e.g. CON, NUL), characters like <>:"|?* or NUL, and must not end in a dot or space.`);
290
+ }
291
+ return name;
292
+ }
293
+ // A narrow reader just for this command. Using readProjectManifest from
294
+ // @pnpm/cli.utils would pull in the installable/engine checks, which are
295
+ // irrelevant here: pack-app doesn't need the current project to be installable
296
+ // under the running Node, just to have a package.json with optional settings.
297
+ async function readProjectAppConfig(dir) {
298
+ let raw;
299
+ try {
300
+ raw = await readFile(path.join(dir, 'package.json'), 'utf8');
301
+ }
302
+ catch {
303
+ return {};
304
+ }
305
+ let manifest;
306
+ try {
307
+ manifest = JSON.parse(raw);
308
+ }
309
+ catch (err) {
310
+ throw new PnpmError('PACK_APP_INVALID_PACKAGE_JSON', `Failed to parse ${path.join(dir, 'package.json')}: ${err.message}`);
311
+ }
312
+ if (!isObject(manifest))
313
+ return {};
314
+ const name = typeof manifest.name === 'string' && manifest.name !== '' ? manifest.name : undefined;
315
+ const pnpmField = isObject(manifest.pnpm) ? manifest.pnpm : undefined;
316
+ const appField = pnpmField && isObject(pnpmField.app) ? pnpmField.app : undefined;
317
+ if (!appField)
318
+ return { name };
319
+ return { name, app: validateAppConfig(appField) };
320
+ }
321
+ function validateAppConfig(raw) {
322
+ const known = new Set(['entry', 'targets', 'nodeVersion', 'outputDir', 'outputName']);
323
+ for (const key of Object.keys(raw)) {
324
+ if (!known.has(key)) {
325
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', `Unknown "pnpm.app.${key}" setting in package.json. Allowed keys: ${Array.from(known).join(', ')}.`);
326
+ }
327
+ }
328
+ const config = {};
329
+ if (raw.entry != null) {
330
+ if (typeof raw.entry !== 'string') {
331
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', '"pnpm.app.entry" must be a string.');
332
+ }
333
+ config.entry = raw.entry;
334
+ }
335
+ if (raw.targets != null) {
336
+ if (!Array.isArray(raw.targets) || !raw.targets.every((t) => typeof t === 'string')) {
337
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', '"pnpm.app.targets" must be an array of strings.');
338
+ }
339
+ config.targets = raw.targets;
340
+ }
341
+ if (raw.nodeVersion != null) {
342
+ if (typeof raw.nodeVersion !== 'string') {
343
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', '"pnpm.app.nodeVersion" must be a string.');
344
+ }
345
+ config.nodeVersion = raw.nodeVersion;
346
+ }
347
+ if (raw.outputDir != null) {
348
+ if (typeof raw.outputDir !== 'string') {
349
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', '"pnpm.app.outputDir" must be a string.');
350
+ }
351
+ config.outputDir = raw.outputDir;
352
+ }
353
+ if (raw.outputName != null) {
354
+ if (typeof raw.outputName !== 'string') {
355
+ throw new PnpmError('PACK_APP_INVALID_CONFIG', '"pnpm.app.outputName" must be a string.');
356
+ }
357
+ config.outputName = raw.outputName;
358
+ }
359
+ return config;
360
+ }
361
+ function deriveOutputNameFromPackage(project, dir) {
362
+ if (!project.name) {
363
+ throw new PnpmError('PACK_APP_NO_OUTPUT_NAME', `Could not determine the output name: package.json in ${dir} has no "name" field.`, { hint: 'Pass --output-name <name> or set "pnpm.app.outputName" in package.json.' });
364
+ }
365
+ // Strip @scope/ prefix from scoped packages so the binary name is a plain
366
+ // filename instead of "scope/name". The second validateOutputName() pass
367
+ // downstream rejects any leftover path separators.
368
+ return project.name.replace(/^@[^/]+\//, '');
369
+ }
370
+ function isObject(value) {
371
+ return value != null && typeof value === 'object' && !Array.isArray(value);
372
+ }
373
+ /**
374
+ * SEA injection invalidates the existing code signature on macOS binaries, so
375
+ * the output must be re-signed. Native macOS hosts use `codesign`; Linux hosts
376
+ * cross-signing a darwin target use `ldid`. Windows hosts have no readily
377
+ * available ad-hoc signer, so we refuse to produce an unsigned output silently
378
+ * and tell the user to re-sign on macOS or Linux.
379
+ */
380
+ async function adHocSignMacBinary(target, outputFile) {
381
+ if (target.platform !== 'darwin')
382
+ return;
383
+ if (process.platform === 'darwin') {
384
+ await execa('codesign', ['--sign', '-', outputFile], { stdio: 'inherit' });
385
+ return;
386
+ }
387
+ if (process.platform === 'linux') {
388
+ try {
389
+ await execa('ldid', ['-S', outputFile], { stdio: 'inherit' });
390
+ }
391
+ catch {
392
+ throw new PnpmError('PACK_APP_MACOS_SIGN_FAILED', `Cross-compiled macOS binary at ${outputFile} could not be ad-hoc signed with "ldid".`, { hint: 'Install ldid (https://github.com/ProcursusTeam/ldid) or re-sign the binary on macOS with "codesign --sign - <file>".' });
393
+ }
394
+ return;
395
+ }
396
+ throw new PnpmError('PACK_APP_MACOS_SIGN_UNSUPPORTED_HOST', `Cannot ad-hoc sign the macOS binary at ${outputFile} on a ${process.platform} host.`, { hint: 'Build macOS targets on a macOS or Linux host, or re-sign the produced binary yourself with "codesign --sign -" on macOS.' });
397
+ }
398
+ //# sourceMappingURL=packApp.js.map
@@ -5,14 +5,15 @@ export declare const commandNames: string[];
5
5
  export declare function help(): string;
6
6
  interface VersionHandlerOptions extends Config {
7
7
  allowSameVersion?: boolean;
8
- noGitChecks?: boolean;
9
- noCommitHooks?: boolean;
10
- noStrict?: boolean;
8
+ commitHooks?: boolean;
9
+ gitChecks?: boolean;
10
+ gitTagVersion?: boolean;
11
+ json?: boolean;
12
+ message?: string;
11
13
  preid?: string;
12
- tagVersionPrefix?: string;
13
14
  recursive?: boolean;
14
- json?: boolean;
15
- ignoredPackages?: string[];
15
+ signGitTag?: boolean;
16
+ tagVersionPrefix?: string;
16
17
  }
17
18
  export declare function handler(opts: VersionHandlerOptions, params: string[]): Promise<string | {
18
19
  output?: string;
@@ -1,34 +1,42 @@
1
+ import path from 'node:path';
1
2
  import { readProjectManifest } from '@pnpm/cli.utils';
2
3
  import { types as allTypes } from '@pnpm/config.reader';
3
4
  import { PnpmError } from '@pnpm/error';
4
5
  import { isGitRepo, isWorkingTreeClean } from '@pnpm/network.git-utils';
5
6
  import { filterProjectsFromDir } from '@pnpm/workspace.projects-filter';
7
+ import { safeExeca as execa } from 'execa';
6
8
  import { pick } from 'ramda';
7
9
  import { renderHelp } from 'render-help';
8
10
  import { inc, valid } from 'semver';
9
11
  export function rcOptionsTypes() {
10
12
  return pick([
13
+ 'allow-same-version',
14
+ 'commit-hooks',
11
15
  'git-checks',
16
+ 'git-tag-version',
17
+ 'message',
18
+ 'sign-git-tag',
19
+ 'tag-version-prefix',
12
20
  ], allTypes);
13
21
  }
14
22
  export function cliOptionsTypes() {
15
23
  return {
16
24
  ...rcOptionsTypes(),
17
- 'allow-same-version': Boolean,
18
- 'no-git-checks': Boolean,
19
- 'no-commit-hooks': Boolean,
20
- 'no-strict': Boolean,
21
- 'preid': String,
22
- 'tag-version-prefix': String,
23
- recursive: Boolean,
24
25
  json: Boolean,
26
+ preid: String,
27
+ recursive: Boolean,
25
28
  };
26
29
  }
27
30
  export const commandNames = ['version'];
31
+ const BUMP_TYPES = ['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', 'prerelease'];
32
+ function isBumpType(value) {
33
+ return BUMP_TYPES.includes(value);
34
+ }
28
35
  export function help() {
29
36
  return renderHelp({
30
37
  description: 'Bumps the version of a package.',
31
38
  usages: [
39
+ 'pnpm version <newversion>',
32
40
  'pnpm version <major|minor|patch|premajor|preminor|prepatch|prerelease>',
33
41
  ],
34
42
  descriptionLists: [
@@ -44,7 +52,7 @@ export function help() {
44
52
  name: '--preid <preid>',
45
53
  },
46
54
  {
47
- description: 'Sets the tag prefix (default: v)',
55
+ description: 'Sets the tag prefix. Default is "v". Set to empty string to remove the prefix.',
48
56
  name: '--tag-version-prefix <prefix>',
49
57
  },
50
58
  {
@@ -52,9 +60,21 @@ export function help() {
52
60
  name: '--allow-same-version',
53
61
  },
54
62
  {
55
- description: 'Skip running commit hooks',
63
+ description: 'Commit message. "%s" is replaced with the new version. Default is "%s".',
64
+ name: '--message <message>',
65
+ },
66
+ {
67
+ description: "Don't create a commit or tag for the version bump. Git commits and tags are always skipped in recursive mode.",
68
+ name: '--no-git-tag-version',
69
+ },
70
+ {
71
+ description: 'Skip running git commit hooks when committing the version bump',
56
72
  name: '--no-commit-hooks',
57
73
  },
74
+ {
75
+ description: 'Sign the generated git tag with GPG',
76
+ name: '--sign-git-tag',
77
+ },
58
78
  {
59
79
  description: 'Filter packages by name (glob pattern)',
60
80
  name: '--filter <pattern>',
@@ -73,19 +93,22 @@ export function help() {
73
93
  });
74
94
  }
75
95
  export async function handler(opts, params) {
76
- const bumpType = params[0];
77
- if (!bumpType || !['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', 'prerelease'].includes(bumpType)) {
78
- throw new PnpmError('INVALID_VERSION_BUMP', 'Invalid version bump type. Must be one of: major, minor, patch, premajor, preminor, prepatch, prerelease');
96
+ const rawBump = params[0];
97
+ if (!rawBump) {
98
+ throw new PnpmError('INVALID_VERSION_BUMP', 'A version argument is required. Must be a valid semver version (e.g. 1.2.3) or one of: major, minor, patch, premajor, preminor, prepatch, prerelease');
79
99
  }
80
- // Check git status if needed
81
- if (!opts.noGitChecks && await isGitRepo()) {
82
- if (!await isWorkingTreeClean()) {
100
+ const explicitVersion = valid(rawBump);
101
+ if (!explicitVersion && !isBumpType(rawBump)) {
102
+ throw new PnpmError('INVALID_VERSION_BUMP', `Invalid version argument: ${rawBump}. Must be a valid semver version (e.g. 1.2.3) or one of: major, minor, patch, premajor, preminor, prepatch, prerelease`);
103
+ }
104
+ const gitCwd = opts.workspaceDir ?? opts.dir;
105
+ if (opts.gitChecks !== false && await isGitRepo({ cwd: gitCwd })) {
106
+ if (!await isWorkingTreeClean({ cwd: gitCwd })) {
83
107
  throw new PnpmError('UNCLEAN_WORKING_TREE', 'Working tree is not clean. Commit or stash your changes.');
84
108
  }
85
109
  }
86
110
  const changes = [];
87
111
  if (opts.recursive) {
88
- // Handle workspace versioning
89
112
  const workspaceDir = opts.workspaceDir || opts.dir;
90
113
  const filters = [];
91
114
  if (opts.filter && opts.filter.length > 0) {
@@ -101,7 +124,7 @@ export async function handler(opts, params) {
101
124
  prefix: opts.dir,
102
125
  });
103
126
  const pkgDirs = Object.keys(result.selectedProjectsGraph);
104
- const bumpResults = await Promise.all(pkgDirs.map(pkgDir => bumpPackageVersion(pkgDir, bumpType, opts)));
127
+ const bumpResults = await Promise.all(pkgDirs.map(pkgDir => bumpPackageVersion(pkgDir, rawBump, explicitVersion, opts)));
105
128
  for (const change of bumpResults) {
106
129
  if (change) {
107
130
  changes.push(change);
@@ -109,8 +132,7 @@ export async function handler(opts, params) {
109
132
  }
110
133
  }
111
134
  else {
112
- // Handle single package versioning
113
- const change = await bumpPackageVersion(opts.dir, bumpType, opts);
135
+ const change = await bumpPackageVersion(opts.dir, rawBump, explicitVersion, opts);
114
136
  if (change) {
115
137
  changes.push(change);
116
138
  }
@@ -118,9 +140,14 @@ export async function handler(opts, params) {
118
140
  if (changes.length === 0) {
119
141
  throw new PnpmError('NO_PACKAGES_TO_VERSION', 'No packages to version');
120
142
  }
121
- // Output results
143
+ // In recursive mode, multiple packages can be bumped to different versions
144
+ // in a single run, and there is no obvious single version to tag the commit
145
+ // with. Skip the git commit and tag entirely in that case.
146
+ if (!opts.recursive && opts.gitTagVersion !== false && await isGitRepo({ cwd: gitCwd })) {
147
+ await commitAndTag(changes, { ...opts, cwd: gitCwd });
148
+ }
122
149
  if (opts.json) {
123
- return JSON.stringify(changes, null, 2);
150
+ return JSON.stringify(changes.map(({ manifestPath: _manifestPath, ...change }) => change), null, 2);
124
151
  }
125
152
  let output = 'Version bumped successfully:\n';
126
153
  for (const change of changes) {
@@ -128,8 +155,8 @@ export async function handler(opts, params) {
128
155
  }
129
156
  return output;
130
157
  }
131
- async function bumpPackageVersion(pkgDir, bumpType, opts) {
132
- const { manifest, writeProjectManifest } = await readProjectManifest(pkgDir);
158
+ async function bumpPackageVersion(pkgDir, rawBump, explicitVersion, opts) {
159
+ const { manifest, writeProjectManifest, fileName } = await readProjectManifest(pkgDir);
133
160
  if (!manifest.name || !manifest.version) {
134
161
  return null;
135
162
  }
@@ -137,9 +164,9 @@ async function bumpPackageVersion(pkgDir, bumpType, opts) {
137
164
  if (!valid(currentVersion)) {
138
165
  throw new PnpmError('INVALID_VERSION', `Invalid version in ${pkgDir}: ${currentVersion}`);
139
166
  }
140
- const newVersion = inc(currentVersion, bumpType, false, opts.preid);
167
+ const newVersion = explicitVersion ?? inc(currentVersion, rawBump, false, opts.preid);
141
168
  if (!newVersion) {
142
- throw new PnpmError('VERSION_BUMP_FAILED', `Failed to bump version from ${currentVersion} using ${bumpType}`);
169
+ throw new PnpmError('VERSION_BUMP_FAILED', `Failed to bump version from ${currentVersion} using ${rawBump}`);
143
170
  }
144
171
  if (newVersion === currentVersion && !opts.allowSameVersion) {
145
172
  throw new PnpmError('VERSION_NOT_CHANGED', `Version was not changed: ${currentVersion}`);
@@ -151,8 +178,49 @@ async function bumpPackageVersion(pkgDir, bumpType, opts) {
151
178
  currentVersion,
152
179
  newVersion,
153
180
  path: pkgDir,
181
+ manifestPath: path.join(pkgDir, fileName),
154
182
  };
155
183
  }
184
+ async function commitAndTag(changes, opts) {
185
+ const resolvedCwd = path.resolve(opts.cwd);
186
+ const [change] = changes;
187
+ const rawMessage = opts.message ?? '%s';
188
+ const message = rawMessage.replace(/%s/g, change.newVersion);
189
+ const tagPrefix = opts.tagVersionPrefix ?? 'v';
190
+ const tagName = `${tagPrefix}${change.newVersion}`;
191
+ const execOpts = { cwd: opts.cwd };
192
+ const resolvedManifestPath = path.resolve(change.manifestPath);
193
+ const relativeManifestPath = path.relative(resolvedCwd, resolvedManifestPath);
194
+ if (relativeManifestPath === '' ||
195
+ path.isAbsolute(relativeManifestPath) ||
196
+ relativeManifestPath.startsWith(`..${path.sep}`) ||
197
+ relativeManifestPath === '..') {
198
+ throw new PnpmError('INVALID_MANIFEST_PATH', `Cannot stage manifest outside of git cwd: ${change.manifestPath}`);
199
+ }
200
+ const manifestPath = relativeManifestPath.split(path.sep).join('/');
201
+ await execa('git', ['add', manifestPath], execOpts);
202
+ const commitArgs = ['commit', '-m', message];
203
+ if (opts.commitHooks === false) {
204
+ commitArgs.push('--no-verify');
205
+ }
206
+ // writeProjectManifest skips writing when the new content matches the existing
207
+ // file, so an --allow-same-version run can leave nothing staged and fail the
208
+ // commit. Pass --allow-empty in that case to let the tag point at the current
209
+ // HEAD as a deliberate marker.
210
+ if (opts.allowSameVersion) {
211
+ commitArgs.push('--allow-empty');
212
+ }
213
+ await execa('git', commitArgs, execOpts);
214
+ const tagArgs = ['tag'];
215
+ if (opts.signGitTag) {
216
+ tagArgs.push('-s');
217
+ }
218
+ else {
219
+ tagArgs.push('-a');
220
+ }
221
+ tagArgs.push(tagName, '-m', message);
222
+ await execa('git', tagArgs, execOpts);
223
+ }
156
224
  export const version = {
157
225
  handler,
158
226
  help,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/releasing.commands",
3
- "version": "1100.0.2",
3
+ "version": "1100.1.0",
4
4
  "description": "Commands for deploy, pack, and publish",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -29,6 +29,7 @@
29
29
  "@zkochan/rimraf": "^4.0.0",
30
30
  "chalk": "^5.6.0",
31
31
  "ci-info": "^4.3.0",
32
+ "detect-libc": "^2.0.3",
32
33
  "enquirer": "^2.4.1",
33
34
  "execa": "npm:safe-execa@0.3.0",
34
35
  "libnpmpublish": "^11.1.3",
@@ -47,32 +48,34 @@
47
48
  "write-json-file": "^7.0.0",
48
49
  "write-yaml-file": "^6.0.0",
49
50
  "@pnpm/bins.resolver": "1100.0.1",
51
+ "@pnpm/cli.utils": "1101.0.0",
50
52
  "@pnpm/cli.common-cli-options-help": "1100.0.0",
51
- "@pnpm/catalogs.types": "1100.0.0",
52
53
  "@pnpm/config.pick-registry-for-package": "1100.0.1",
53
- "@pnpm/cli.utils": "1101.0.0",
54
- "@pnpm/config.reader": "1101.0.0",
55
54
  "@pnpm/constants": "1100.0.0",
56
- "@pnpm/deps.path": "1100.0.1",
55
+ "@pnpm/engine.runtime.node-resolver": "1100.0.3",
57
56
  "@pnpm/error": "1100.0.0",
58
- "@pnpm/exec.lifecycle": "1100.0.2",
59
- "@pnpm/engine.runtime.commands": "1100.0.2",
60
- "@pnpm/fetching.directory-fetcher": "1100.0.2",
57
+ "@pnpm/fetching.directory-fetcher": "1100.0.3",
58
+ "@pnpm/exec.pnpm-cli-runner": "1100.0.0",
59
+ "@pnpm/exec.lifecycle": "1100.0.3",
60
+ "@pnpm/deps.path": "1100.0.1",
61
+ "@pnpm/installing.client": "1100.0.3",
61
62
  "@pnpm/fs.is-empty-dir-or-nothing": "1100.0.0",
62
- "@pnpm/installing.client": "1100.0.2",
63
63
  "@pnpm/fs.packlist": "1100.0.0",
64
- "@pnpm/lockfile.fs": "1100.0.1",
65
- "@pnpm/lockfile.types": "1100.0.1",
66
- "@pnpm/installing.commands": "1100.1.0",
67
- "@pnpm/network.git-utils": "1100.0.0",
68
- "@pnpm/network.fetch": "1100.0.1",
64
+ "@pnpm/catalogs.types": "1100.0.0",
65
+ "@pnpm/fs.indexed-pkg-importer": "1100.0.2",
66
+ "@pnpm/engine.runtime.commands": "1100.0.3",
67
+ "@pnpm/lockfile.types": "1100.0.2",
69
68
  "@pnpm/network.web-auth": "1101.0.0",
69
+ "@pnpm/network.git-utils": "1100.0.0",
70
70
  "@pnpm/releasing.exportable-manifest": "1100.0.2",
71
- "@pnpm/fs.indexed-pkg-importer": "1100.0.1",
72
- "@pnpm/resolving.resolver-base": "1100.0.1",
73
- "@pnpm/workspace.projects-filter": "1100.0.2",
71
+ "@pnpm/types": "1101.0.0",
72
+ "@pnpm/resolving.resolver-base": "1100.1.0",
73
+ "@pnpm/lockfile.fs": "1100.0.2",
74
+ "@pnpm/workspace.projects-filter": "1100.0.3",
74
75
  "@pnpm/workspace.projects-sorter": "1100.0.1",
75
- "@pnpm/types": "1101.0.0"
76
+ "@pnpm/network.fetch": "1100.0.1",
77
+ "@pnpm/config.reader": "1101.1.0",
78
+ "@pnpm/installing.commands": "1100.1.1"
76
79
  },
77
80
  "peerDependencies": {
78
81
  "@pnpm/logger": ">=1001.0.0 <1002.0.0"
@@ -95,15 +98,15 @@
95
98
  "load-json-file": "^7.0.1",
96
99
  "tar": "^7.5.10",
97
100
  "write-yaml-file": "^6.0.0",
98
- "@pnpm/assert-project": "1100.0.1",
99
- "@pnpm/catalogs.config": "1100.0.0",
100
- "@pnpm/hooks.pnpmfile": "1100.0.1",
101
- "@pnpm/prepare": "1100.0.1",
101
+ "@pnpm/hooks.pnpmfile": "1100.0.2",
102
+ "@pnpm/prepare": "1100.0.2",
103
+ "@pnpm/assert-project": "1100.0.2",
104
+ "@pnpm/releasing.commands": "1100.1.0",
102
105
  "@pnpm/logger": "1100.0.0",
103
- "@pnpm/releasing.commands": "1100.0.2",
104
106
  "@pnpm/test-fixtures": "1100.0.0",
105
- "@pnpm/test-ipc-server": "1100.0.0",
106
- "@pnpm/testing.command-defaults": "1100.0.0"
107
+ "@pnpm/testing.command-defaults": "1100.0.1",
108
+ "@pnpm/catalogs.config": "1100.0.0",
109
+ "@pnpm/test-ipc-server": "1100.0.0"
107
110
  },
108
111
  "engines": {
109
112
  "node": ">=22.13"