@pnpm/releasing.commands 1100.5.1 → 1100.5.3
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/deploy/deploy.js +131 -1
- package/lib/pack-app/packApp.js +130 -8
- package/lib/publish/pack.d.ts +1 -1
- package/lib/publish/pack.js +46 -23
- package/lib/publish/recursivePublish.d.ts +1 -1
- package/lib/publish/recursivePublish.js +2 -2
- package/package.json +36 -35
package/lib/deploy/deploy.js
CHANGED
|
@@ -12,6 +12,7 @@ import { install } from '@pnpm/installing.commands';
|
|
|
12
12
|
import { getLockfileImporterId, readWantedLockfile, writeWantedLockfile } from '@pnpm/lockfile.fs';
|
|
13
13
|
import { globalWarn, logger } from '@pnpm/logger';
|
|
14
14
|
import { rimraf } from '@zkochan/rimraf';
|
|
15
|
+
import { isSubdir } from 'is-subdir';
|
|
15
16
|
import { pick } from 'ramda';
|
|
16
17
|
import { renderHelp } from 'render-help';
|
|
17
18
|
import { writeYamlFile } from 'write-yaml-file';
|
|
@@ -91,14 +92,35 @@ export async function handler(opts, params) {
|
|
|
91
92
|
const selectedProject = selectedProjects[0].package;
|
|
92
93
|
const deployDirParam = params[0];
|
|
93
94
|
const deployDir = path.isAbsolute(deployDirParam) ? deployDirParam : path.join(opts.dir, deployDirParam);
|
|
95
|
+
validateDeployTarget(deployDir, {
|
|
96
|
+
dir: opts.dir,
|
|
97
|
+
force: opts.force,
|
|
98
|
+
projectDir: selectedProject.rootDir,
|
|
99
|
+
workspaceDir: opts.workspaceDir,
|
|
100
|
+
});
|
|
101
|
+
const normalizedDeployDir = path.resolve(deployDir);
|
|
102
|
+
const normalizedWorkspaceDir = path.resolve(opts.workspaceDir);
|
|
103
|
+
const workspaceChildTarget = isChildPath(normalizedDeployDir, normalizedWorkspaceDir);
|
|
104
|
+
if (workspaceChildTarget) {
|
|
105
|
+
createWorkspaceChildTargetParents(normalizedWorkspaceDir, normalizedDeployDir);
|
|
106
|
+
}
|
|
94
107
|
if (!isEmptyDirOrNothing(deployDir)) {
|
|
95
108
|
if (!opts.force) {
|
|
96
109
|
throw new PnpmError('DEPLOY_DIR_NOT_EMPTY', `Deploy path ${deployDir} is not empty`);
|
|
97
110
|
}
|
|
98
111
|
logger.warn({ message: 'using --force, deleting deploy path', prefix: deployDir });
|
|
99
112
|
}
|
|
113
|
+
if (workspaceChildTarget) {
|
|
114
|
+
validateWorkspaceChildTargetComponents(normalizedWorkspaceDir, normalizedDeployDir);
|
|
115
|
+
}
|
|
100
116
|
await rimraf(deployDir);
|
|
101
|
-
|
|
117
|
+
if (workspaceChildTarget) {
|
|
118
|
+
createWorkspaceChildTargetParents(normalizedWorkspaceDir, normalizedDeployDir);
|
|
119
|
+
createWorkspaceChildTargetDir(normalizedWorkspaceDir, normalizedDeployDir);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await fs.promises.mkdir(deployDir, { recursive: true });
|
|
123
|
+
}
|
|
102
124
|
const includeOnlyPackageFiles = !opts.deployAllFiles;
|
|
103
125
|
await copyProject(selectedProject.rootDir, deployDir, { includeOnlyPackageFiles });
|
|
104
126
|
if (opts.sharedWorkspaceLockfile) {
|
|
@@ -181,6 +203,114 @@ async function copyProject(src, dest, opts) {
|
|
|
181
203
|
const importPkg = createIndexedPkgImporter('clone-or-copy');
|
|
182
204
|
importPkg(dest, { filesMap, force: true, resolvedFrom: 'local-dir' });
|
|
183
205
|
}
|
|
206
|
+
function validateDeployTarget(deployDir, opts) {
|
|
207
|
+
const normalizedDeployDir = path.resolve(deployDir);
|
|
208
|
+
const workspaceDir = path.resolve(opts.workspaceDir);
|
|
209
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
210
|
+
const dir = path.resolve(opts.dir);
|
|
211
|
+
if (samePath(normalizedDeployDir, workspaceDir)) {
|
|
212
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target is the workspace root');
|
|
213
|
+
}
|
|
214
|
+
if (isAncestorPath(normalizedDeployDir, workspaceDir)) {
|
|
215
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target contains the workspace root');
|
|
216
|
+
}
|
|
217
|
+
if (samePath(normalizedDeployDir, projectDir)) {
|
|
218
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target is the selected project root');
|
|
219
|
+
}
|
|
220
|
+
if (isAncestorPath(normalizedDeployDir, projectDir)) {
|
|
221
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target contains the selected project');
|
|
222
|
+
}
|
|
223
|
+
if (samePath(normalizedDeployDir, dir)) {
|
|
224
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target is the current directory');
|
|
225
|
+
}
|
|
226
|
+
if (isAncestorPath(normalizedDeployDir, dir)) {
|
|
227
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target contains the current directory');
|
|
228
|
+
}
|
|
229
|
+
if (opts.force && !isChildPath(normalizedDeployDir, workspaceDir)) {
|
|
230
|
+
throw unsafeDeployTarget(normalizedDeployDir, 'target is outside the workspace');
|
|
231
|
+
}
|
|
232
|
+
if (isChildPath(normalizedDeployDir, workspaceDir)) {
|
|
233
|
+
validateWorkspaceChildTargetComponents(workspaceDir, normalizedDeployDir);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function validateWorkspaceChildTargetComponents(workspaceDir, deployDir) {
|
|
237
|
+
const relative = path.relative(workspaceDir, deployDir);
|
|
238
|
+
let current = workspaceDir;
|
|
239
|
+
for (const component of relative.split(path.sep)) {
|
|
240
|
+
if (!component)
|
|
241
|
+
continue;
|
|
242
|
+
current = path.join(current, component);
|
|
243
|
+
let stat;
|
|
244
|
+
try {
|
|
245
|
+
stat = fs.lstatSync(current);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (isENOENT(error))
|
|
249
|
+
return;
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
if (stat.isSymbolicLink()) {
|
|
253
|
+
throw unsafeDeployTarget(current, 'target path contains a symlink');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function createWorkspaceChildTargetParents(workspaceDir, deployDir) {
|
|
258
|
+
const parent = path.dirname(deployDir);
|
|
259
|
+
const relative = path.relative(workspaceDir, parent);
|
|
260
|
+
let current = workspaceDir;
|
|
261
|
+
for (const component of relative.split(path.sep)) {
|
|
262
|
+
if (!component)
|
|
263
|
+
continue;
|
|
264
|
+
current = path.join(current, component);
|
|
265
|
+
createWorkspaceChildTargetComponent(current);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function createWorkspaceChildTargetDir(workspaceDir, deployDir) {
|
|
269
|
+
try {
|
|
270
|
+
fs.mkdirSync(deployDir);
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (isEEXIST(error)) {
|
|
274
|
+
throw unsafeDeployTarget(deployDir, 'target changed during deploy preparation');
|
|
275
|
+
}
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
validateWorkspaceChildTargetComponents(workspaceDir, deployDir);
|
|
279
|
+
}
|
|
280
|
+
function createWorkspaceChildTargetComponent(component) {
|
|
281
|
+
try {
|
|
282
|
+
fs.mkdirSync(component);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (!isEEXIST(error))
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
const stat = fs.lstatSync(component);
|
|
289
|
+
if (stat.isSymbolicLink()) {
|
|
290
|
+
throw unsafeDeployTarget(component, 'target path contains a symlink');
|
|
291
|
+
}
|
|
292
|
+
if (!stat.isDirectory()) {
|
|
293
|
+
throw unsafeDeployTarget(component, 'target path contains a non-directory');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function unsafeDeployTarget(deployDir, reason) {
|
|
297
|
+
return new PnpmError('INVALID_DEPLOY_TARGET', `Refusing to deploy to unsafe target ${deployDir}: ${reason}`);
|
|
298
|
+
}
|
|
299
|
+
function samePath(left, right) {
|
|
300
|
+
return path.normalize(left) === path.normalize(right);
|
|
301
|
+
}
|
|
302
|
+
function isAncestorPath(parent, child) {
|
|
303
|
+
return isChildPath(child, parent);
|
|
304
|
+
}
|
|
305
|
+
function isChildPath(child, parent) {
|
|
306
|
+
return !samePath(child, parent) && isSubdir(parent, child);
|
|
307
|
+
}
|
|
308
|
+
function isENOENT(error) {
|
|
309
|
+
return typeof error === 'object' && error != null && 'code' in error && error.code === 'ENOENT';
|
|
310
|
+
}
|
|
311
|
+
function isEEXIST(error) {
|
|
312
|
+
return typeof error === 'object' && error != null && 'code' in error && error.code === 'EEXIST';
|
|
313
|
+
}
|
|
184
314
|
async function deployFromSharedLockfile(opts, selectedProject, deployDir) {
|
|
185
315
|
if (!opts.injectWorkspacePackages) {
|
|
186
316
|
throw new PnpmError('DEPLOY_NONINJECTED_WORKSPACE', 'By default, starting from pnpm v10, we only deploy from workspaces that have "inject-workspace-packages=true" set', {
|
package/lib/pack-app/packApp.js
CHANGED
|
@@ -94,6 +94,13 @@ export async function handler(opts, params) {
|
|
|
94
94
|
if (!entryPath) {
|
|
95
95
|
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.');
|
|
96
96
|
}
|
|
97
|
+
// `entry` may come from a repo-controlled package.json, so reject absolute
|
|
98
|
+
// paths and `..` traversal before touching the filesystem: the entry's
|
|
99
|
+
// contents get embedded into the produced executable, so an escaping path
|
|
100
|
+
// could exfiltrate a host file (e.g. an SSH key) into a distributable binary.
|
|
101
|
+
if (pathEscapesProject(entryPath)) {
|
|
102
|
+
throw new PnpmError('PACK_APP_ENTRY_OUTSIDE_PROJECT', `The entry path "${entryPath}" resolves outside the project directory.`, { hint: 'The entry must be a relative path inside the project directory, not an absolute path or one that escapes via "..".' });
|
|
103
|
+
}
|
|
97
104
|
const resolvedEntry = path.resolve(opts.dir, entryPath);
|
|
98
105
|
let entryStat;
|
|
99
106
|
try {
|
|
@@ -105,6 +112,12 @@ export async function handler(opts, params) {
|
|
|
105
112
|
if (!entryStat.isFile()) {
|
|
106
113
|
throw new PnpmError('PACK_APP_ENTRY_NOT_FILE', `Entry path must be a regular file: ${resolvedEntry}`);
|
|
107
114
|
}
|
|
115
|
+
// Defense in depth against a same-name symlink that points out of the
|
|
116
|
+
// project: resolve symlinks and require the real path to stay within the
|
|
117
|
+
// (also symlink-resolved) project directory.
|
|
118
|
+
if (!isWithinDir(resolvedEntry, opts.dir)) {
|
|
119
|
+
throw new PnpmError('PACK_APP_ENTRY_OUTSIDE_PROJECT', `The entry path "${entryPath}" resolves outside the project directory.`, { hint: 'The entry must be a relative path inside the project directory, not an absolute path or one that escapes via "..".' });
|
|
120
|
+
}
|
|
108
121
|
const cliTargets = opts.target == null
|
|
109
122
|
? undefined
|
|
110
123
|
: Array.isArray(opts.target) ? opts.target : [opts.target];
|
|
@@ -118,9 +131,29 @@ export async function handler(opts, params) {
|
|
|
118
131
|
// masked by later problems (missing package.json name, registry lookup, etc.).
|
|
119
132
|
const runtimeSpec = opts.runtime ?? project.app?.runtime ?? `node@${process.version.slice(1)}`;
|
|
120
133
|
const { version: requestedNodeSpec } = parseRuntime(runtimeSpec);
|
|
121
|
-
|
|
134
|
+
// `outputDir` is likewise repo-controllable; reject absolute paths and `..`
|
|
135
|
+
// traversal so build artifacts cannot be written outside the project directory.
|
|
136
|
+
const outputDirRaw = opts.outputDir ?? project.app?.outputDir ?? 'dist-app';
|
|
137
|
+
if (pathEscapesProject(outputDirRaw)) {
|
|
138
|
+
throw new PnpmError('PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT', `The output directory "${outputDirRaw}" resolves outside the project directory.`, { hint: 'The output directory must be a relative path inside the project directory, not an absolute path or one that escapes via "..".' });
|
|
139
|
+
}
|
|
140
|
+
const outputDir = path.resolve(opts.dir, outputDirRaw);
|
|
122
141
|
await mkdir(outputDir, { recursive: true });
|
|
142
|
+
// Defense in depth against a symlinked output directory that points out of
|
|
143
|
+
// the project: the lexical check above can't see through a symlink, so
|
|
144
|
+
// re-check containment once the real path exists.
|
|
145
|
+
if (!isWithinDir(outputDir, opts.dir)) {
|
|
146
|
+
throw new PnpmError('PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT', `The output directory "${outputDirRaw}" resolves outside the project directory.`, { hint: 'The output directory must be a relative path inside the project directory, not an absolute path or one that escapes via "..".' });
|
|
147
|
+
}
|
|
123
148
|
const outputName = validateOutputName(opts.outputName ?? project.app?.outputName ?? deriveOutputNameFromPackage(project, opts.dir));
|
|
149
|
+
// Reject a pre-existing symlink (or any non-regular file) at any target's
|
|
150
|
+
// final output path before downloading anything: a repo could commit
|
|
151
|
+
// `dist-app/<target>/<name>` as a symlink pointing outside the project, and
|
|
152
|
+
// `node --build-sea` would follow it to overwrite an arbitrary file. The
|
|
153
|
+
// directory containment checks above do not cover the leaf file.
|
|
154
|
+
for (const target of targets) {
|
|
155
|
+
rejectNonRegularOutputFile(path.join(outputDir, target.raw, outputFileName(outputName, target.platform)));
|
|
156
|
+
}
|
|
124
157
|
const fetch = createFetchFromRegistry(opts);
|
|
125
158
|
const buildRoot = path.join(opts.pnpmHomeDir, 'pack-app');
|
|
126
159
|
// Resolve the embedded target version first so the builder can be pinned to
|
|
@@ -147,9 +180,16 @@ export async function handler(opts, params) {
|
|
|
147
180
|
const targetOutputDir = path.join(outputDir, target.raw);
|
|
148
181
|
// eslint-disable-next-line no-await-in-loop
|
|
149
182
|
await mkdir(targetOutputDir, { recursive: true });
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
183
|
+
// A repo could symlink `dist-app/<target>` out of the project even when
|
|
184
|
+
// `dist-app` itself is contained; re-check the real path before any
|
|
185
|
+
// binary is written into it.
|
|
186
|
+
if (!isWithinDir(targetOutputDir, opts.dir)) {
|
|
187
|
+
throw new PnpmError('PACK_APP_OUTPUT_DIR_OUTSIDE_PROJECT', `The output directory "${targetOutputDir}" resolves outside the project directory.`, { hint: 'The output directory must be a relative path inside the project directory, not an absolute path or one that escapes via "..".' });
|
|
188
|
+
}
|
|
189
|
+
const outputFile = path.join(targetOutputDir, outputFileName(outputName, target.platform));
|
|
190
|
+
// Re-check the leaf path right before the build in case it became a
|
|
191
|
+
// symlink after the upfront pass.
|
|
192
|
+
rejectNonRegularOutputFile(outputFile);
|
|
153
193
|
const seaConfig = {
|
|
154
194
|
main: resolvedEntry,
|
|
155
195
|
output: outputFile,
|
|
@@ -175,7 +215,7 @@ export async function handler(opts, params) {
|
|
|
175
215
|
await rm(tmpConfigDir, { recursive: true, force: true }).catch(() => { });
|
|
176
216
|
}
|
|
177
217
|
// eslint-disable-next-line no-await-in-loop
|
|
178
|
-
await adHocSignMacBinary(target, outputFile);
|
|
218
|
+
await adHocSignMacBinary(target, outputFile, opts.dir);
|
|
179
219
|
results.push(` ${target.raw}: ${outputFile} (Node.js ${resolvedTargetVersion})`);
|
|
180
220
|
}
|
|
181
221
|
return `Built ${targets.length} executable${targets.length === 1 ? '' : 's'}:\n${results.join('\n')}`;
|
|
@@ -408,6 +448,53 @@ function deriveOutputNameFromPackage(project, dir) {
|
|
|
408
448
|
function isObject(value) {
|
|
409
449
|
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
410
450
|
}
|
|
451
|
+
// `entry` and `outputDir` can come from a repo-controlled package.json, so a
|
|
452
|
+
// malicious repo must not be able to read/write outside the project via an
|
|
453
|
+
// absolute path or `..` traversal. An absolute path replaces the base on join;
|
|
454
|
+
// a `..` segment climbs out. Checked lexically before any filesystem access so
|
|
455
|
+
// the failure is fast and side-effect-free.
|
|
456
|
+
function pathEscapesProject(rawPath) {
|
|
457
|
+
// `path.parse().root` is non-empty for any host-rooted form: a POSIX
|
|
458
|
+
// absolute path (`/x`), and on Windows also the drive-relative (`C:x`) and
|
|
459
|
+
// root-relative (`\x`) forms that `path.isAbsolute()` reports as relative
|
|
460
|
+
// yet still resolve outside the project. Platform-specific, matching the
|
|
461
|
+
// pacquet port (which rejects `Component::RootDir` / `Component::Prefix`).
|
|
462
|
+
if (path.parse(rawPath).root !== '')
|
|
463
|
+
return true;
|
|
464
|
+
return rawPath.split(/[/\\]/).includes('..');
|
|
465
|
+
}
|
|
466
|
+
// Defense in depth beyond `pathEscapesProject`: resolve symlinks and require
|
|
467
|
+
// the real target to stay inside the (also symlink-resolved) project dir, so a
|
|
468
|
+
// same-name symlink pointing out of the project is caught even though the
|
|
469
|
+
// lexical join looked contained. Fails closed on any realpath error.
|
|
470
|
+
function isWithinDir(target, dir) {
|
|
471
|
+
let realDir;
|
|
472
|
+
let realTarget;
|
|
473
|
+
try {
|
|
474
|
+
realDir = fs.realpathSync(dir);
|
|
475
|
+
realTarget = fs.realpathSync(target);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const rel = path.relative(realDir, realTarget);
|
|
481
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
482
|
+
}
|
|
483
|
+
// The on-disk file name of the produced executable for a target: a bare name
|
|
484
|
+
// on POSIX, suffixed with `.exe` on Windows.
|
|
485
|
+
function outputFileName(outputName, platform) {
|
|
486
|
+
return platform === 'win32' ? `${outputName}.exe` : outputName;
|
|
487
|
+
}
|
|
488
|
+
// Refuse to write to `outputFile` when it already exists and is not a regular
|
|
489
|
+
// file — most importantly a symlink, which `node --build-sea` would follow to
|
|
490
|
+
// overwrite a file outside the project. `lstatSync` does not traverse the final
|
|
491
|
+
// component, so a symlink reports `isFile() === false`. A missing path is fine.
|
|
492
|
+
function rejectNonRegularOutputFile(outputFile) {
|
|
493
|
+
const existing = fs.lstatSync(outputFile, { throwIfNoEntry: false });
|
|
494
|
+
if (existing && !existing.isFile()) {
|
|
495
|
+
throw new PnpmError('PACK_APP_OUTPUT_FILE_NOT_REGULAR', `The output file "${outputFile}" already exists and is not a regular file (e.g. a symlink); refusing to write through it.`, { hint: 'Remove the existing path, or choose a different --output-name or --output-dir.' });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
411
498
|
/**
|
|
412
499
|
* SEA injection invalidates the existing code signature on macOS binaries, so
|
|
413
500
|
* the output must be re-signed. Native macOS hosts use `codesign`; Linux hosts
|
|
@@ -415,16 +502,20 @@ function isObject(value) {
|
|
|
415
502
|
* available ad-hoc signer, so we refuse to produce an unsigned output silently
|
|
416
503
|
* and tell the user to re-sign on macOS or Linux.
|
|
417
504
|
*/
|
|
418
|
-
async function adHocSignMacBinary(target, outputFile) {
|
|
505
|
+
async function adHocSignMacBinary(target, outputFile, dir) {
|
|
419
506
|
if (target.platform !== 'darwin')
|
|
420
507
|
return;
|
|
421
508
|
if (process.platform === 'darwin') {
|
|
422
|
-
|
|
509
|
+
// `codesign` is a macOS system tool; spawn it by absolute path so a
|
|
510
|
+
// repo-controlled `node_modules/.bin/codesign` on PATH can't be run in its
|
|
511
|
+
// place.
|
|
512
|
+
await execa('/usr/bin/codesign', ['--sign', '-', outputFile], { stdio: 'inherit' });
|
|
423
513
|
return;
|
|
424
514
|
}
|
|
425
515
|
if (process.platform === 'linux') {
|
|
516
|
+
const ldid = resolveTrustedSigner('ldid', dir, outputFile);
|
|
426
517
|
try {
|
|
427
|
-
await execa(
|
|
518
|
+
await execa(ldid, ['-S', outputFile], { stdio: 'inherit' });
|
|
428
519
|
}
|
|
429
520
|
catch {
|
|
430
521
|
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>".' });
|
|
@@ -433,4 +524,35 @@ async function adHocSignMacBinary(target, outputFile) {
|
|
|
433
524
|
}
|
|
434
525
|
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.' });
|
|
435
526
|
}
|
|
527
|
+
// Resolve an external signer (`ldid`) to an absolute path via PATH, skipping
|
|
528
|
+
// any match that resolves inside the project directory — a repo could ship
|
|
529
|
+
// `node_modules/.bin/ldid` and, if that directory is on the developer's PATH,
|
|
530
|
+
// get an attacker-controlled binary executed when packaging a darwin target.
|
|
531
|
+
// Returns the first match outside the project, or throws PACK_APP_MACOS_SIGN_FAILED.
|
|
532
|
+
function resolveTrustedSigner(name, dir, outputFile) {
|
|
533
|
+
for (const entry of (process.env.PATH ?? '').split(path.delimiter)) {
|
|
534
|
+
if (entry === '')
|
|
535
|
+
continue;
|
|
536
|
+
const candidate = path.join(entry, name);
|
|
537
|
+
let stat;
|
|
538
|
+
try {
|
|
539
|
+
stat = fs.statSync(candidate);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!stat.isFile())
|
|
545
|
+
continue;
|
|
546
|
+
if (isWithinDir(candidate, dir))
|
|
547
|
+
continue;
|
|
548
|
+
try {
|
|
549
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
return candidate;
|
|
555
|
+
}
|
|
556
|
+
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>".' });
|
|
557
|
+
}
|
|
436
558
|
//# sourceMappingURL=packApp.js.map
|
package/lib/publish/pack.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function rcOptionsTypes(): Record<string, unknown>;
|
|
|
4
4
|
export declare function cliOptionsTypes(): Record<string, unknown>;
|
|
5
5
|
export declare const commandNames: string[];
|
|
6
6
|
export declare function help(): string;
|
|
7
|
-
export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs' | 'ignoreScripts' | 'embedReadme' | 'packGzipLevel' | 'nodeLinker' | 'skipManifestObfuscation' | 'userAgent'> & Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'recursive' | 'workspaceConcurrency' | 'workspaceDir'>> & Partial<Pick<ConfigContext, 'hooks' | 'selectedProjectsGraph'>> & {
|
|
7
|
+
export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs' | 'ignoreScripts' | 'embedReadme' | 'packGzipLevel' | 'nodeLinker' | 'skipManifestObfuscation' | 'userAgent'> & Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'recursive' | 'workspaceConcurrency' | 'workspaceDir'>> & Partial<Pick<ConfigContext, 'hooks' | 'selectedProjectsGraph' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
|
|
8
8
|
argv: {
|
|
9
9
|
original: string[];
|
|
10
10
|
};
|
package/lib/publish/pack.js
CHANGED
|
@@ -9,7 +9,7 @@ import { PnpmError } from '@pnpm/error';
|
|
|
9
9
|
import { packlist } from '@pnpm/fs.packlist';
|
|
10
10
|
import { logger } from '@pnpm/logger';
|
|
11
11
|
import { createExportableManifest } from '@pnpm/releasing.exportable-manifest';
|
|
12
|
-
import {
|
|
12
|
+
import { sortFilteredProjects } from '@pnpm/workspace.projects-sorter';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import pLimit from 'p-limit';
|
|
15
15
|
import { pick } from 'ramda';
|
|
@@ -104,7 +104,12 @@ export async function handler(opts) {
|
|
|
104
104
|
prefix: opts.dir,
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
|
-
const chunks =
|
|
107
|
+
const chunks = sortFilteredProjects({
|
|
108
|
+
selectedProjectsGraph,
|
|
109
|
+
allProjectsGraph: opts.allProjectsGraph,
|
|
110
|
+
prodAllProjectsGraph: opts.prodAllProjectsGraph,
|
|
111
|
+
prodOnlySelectedProjectDirs: opts.prodOnlySelectedProjectDirs,
|
|
112
|
+
});
|
|
108
113
|
const limitPack = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency));
|
|
109
114
|
const resolvedOpts = { ...opts };
|
|
110
115
|
if (opts.out) {
|
|
@@ -215,16 +220,44 @@ export async function api(opts) {
|
|
|
215
220
|
const filesMap = Object.fromEntries(files.map((file) => [`package/${file}`, path.join(dir, file)]));
|
|
216
221
|
// cspell:disable-next-line
|
|
217
222
|
if (opts.workspaceDir != null && dir !== opts.workspaceDir && !files.some((file) => /LICEN[CS]E(?:\..+)?/i.test(file))) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
const { workspaceDir } = opts;
|
|
224
|
+
const licenses = await glob([LICENSE_GLOB], { cwd: workspaceDir, expandDirectories: false });
|
|
225
|
+
await Promise.all(licenses.map(async (license) => {
|
|
226
|
+
const licensePath = path.join(workspaceDir, license);
|
|
227
|
+
// Only inject a regular file. A symlink could point outside the workspace and leak its
|
|
228
|
+
// target's bytes into the published tarball, so `lstat()` (which does not follow symlinks)
|
|
229
|
+
// rejects it — matching pacquet's inject_workspace_license.
|
|
230
|
+
const stats = await fs.promises.lstat(licensePath);
|
|
231
|
+
if (stats.isFile()) {
|
|
232
|
+
filesMap[`package/${license}`] = licensePath;
|
|
233
|
+
}
|
|
234
|
+
}));
|
|
222
235
|
}
|
|
223
236
|
const destDir = packDestination
|
|
224
237
|
? (path.isAbsolute(packDestination) ? packDestination : path.join(dir, packDestination ?? '.'))
|
|
225
238
|
: dir;
|
|
226
239
|
if (!opts.dryRun) {
|
|
227
240
|
await fs.promises.mkdir(destDir, { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
// Derive `contents` and `unpackedSize` from `filesMap` (the full set of tar entries) rather than
|
|
243
|
+
// from `files` (the packlist subset) so that:
|
|
244
|
+
// - workspace LICENSE files appended to `filesMap` after the packlist call are included; and
|
|
245
|
+
// - `package.yaml` / `package.json5` entries are reported under the name they actually have in
|
|
246
|
+
// the tar (`package.json`), since `packPkg()` rewrites them.
|
|
247
|
+
// The `stat()` pass must run before `postpack`, which may delete prepack-generated files that
|
|
248
|
+
// were packed. See https://github.com/pnpm/pnpm/issues/12775.
|
|
249
|
+
const sizes = await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
|
|
250
|
+
if (isManifestEntry(name)) {
|
|
251
|
+
return Buffer.byteLength(JSON.stringify(publishManifest, null, 2));
|
|
252
|
+
}
|
|
253
|
+
const stat = await fs.promises.stat(source);
|
|
254
|
+
return stat.size;
|
|
255
|
+
}));
|
|
256
|
+
const unpackedSize = sizes.reduce((acc, size) => acc + size, 0);
|
|
257
|
+
const packedContents = Array.from(new Set(Object.keys(filesMap).map((name) => isManifestEntry(name)
|
|
258
|
+
? 'package.json'
|
|
259
|
+
: name.replace(/^package\//, '')))).sort((a, b) => a.localeCompare(b, 'en'));
|
|
260
|
+
if (!opts.dryRun) {
|
|
228
261
|
await packPkg({
|
|
229
262
|
destFile: path.join(destDir, tarballName),
|
|
230
263
|
filesMap,
|
|
@@ -248,22 +281,6 @@ export async function api(opts) {
|
|
|
248
281
|
else {
|
|
249
282
|
packedTarballPath = path.relative(opts.dir, path.join(dir, tarballName));
|
|
250
283
|
}
|
|
251
|
-
// Derive `contents` and `unpackedSize` from `filesMap` (the full set of tar entries) rather than
|
|
252
|
-
// from `files` (the packlist subset) so that:
|
|
253
|
-
// - workspace LICENSE files appended to `filesMap` after the packlist call are included; and
|
|
254
|
-
// - `package.yaml` / `package.json5` entries are reported under the name they actually have in
|
|
255
|
-
// the tar (`package.json`), since `packPkg()` rewrites them.
|
|
256
|
-
const sizes = await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
|
|
257
|
-
if (/^package\/package\.(?:json|json5|yaml)$/.test(name)) {
|
|
258
|
-
return Buffer.byteLength(JSON.stringify(publishManifest, null, 2));
|
|
259
|
-
}
|
|
260
|
-
const stat = await fs.promises.stat(source);
|
|
261
|
-
return stat.size;
|
|
262
|
-
}));
|
|
263
|
-
const unpackedSize = sizes.reduce((acc, size) => acc + size, 0);
|
|
264
|
-
const packedContents = Array.from(new Set(Object.keys(filesMap).map((name) => /^package\/package\.(?:json|json5|yaml)$/.test(name)
|
|
265
|
-
? 'package.json'
|
|
266
|
-
: name.replace(/^package\//, '')))).sort((a, b) => a.localeCompare(b, 'en'));
|
|
267
284
|
return {
|
|
268
285
|
publishedManifest: publishManifest,
|
|
269
286
|
contents: packedContents,
|
|
@@ -271,6 +288,12 @@ export async function api(opts) {
|
|
|
271
288
|
unpackedSize,
|
|
272
289
|
};
|
|
273
290
|
}
|
|
291
|
+
// True when a `package/<path>` tar key names the package manifest, which is
|
|
292
|
+
// packed as a single serialized `package/package.json` entry and reported as
|
|
293
|
+
// `package.json` in the contents listing regardless of the source file name.
|
|
294
|
+
function isManifestEntry(name) {
|
|
295
|
+
return name === 'package/package.json' || name === 'package/package.json5' || name === 'package/package.yaml';
|
|
296
|
+
}
|
|
274
297
|
function stripBuildMetadata(version) {
|
|
275
298
|
const plusIndex = version.indexOf('+');
|
|
276
299
|
return plusIndex === -1 ? version : version.slice(0, plusIndex);
|
|
@@ -294,7 +317,7 @@ async function packPkg(opts) {
|
|
|
294
317
|
await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
|
|
295
318
|
const isExecutable = bins.some((bin) => path.relative(bin, source) === '');
|
|
296
319
|
const mode = isExecutable ? 0o755 : 0o644;
|
|
297
|
-
if (
|
|
320
|
+
if (isManifestEntry(name)) {
|
|
298
321
|
pack.entry({ mode, mtime, name: 'package/package.json' }, JSON.stringify(manifest, null, 2));
|
|
299
322
|
return;
|
|
300
323
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Config, ConfigContext } from '@pnpm/config.reader';
|
|
2
2
|
import type { PublishPackedPkgOptions, PublishSummary } from './publishPackedPkg.js';
|
|
3
|
-
export type PublishRecursiveOpts = Required<Pick<Config, 'bin' | 'cacheDir' | 'dir' | 'pnpmHomeDir' | 'configByUri' | 'registries' | 'workspaceDir'>> & Required<Pick<ConfigContext, 'cliOptions'>> & Partial<Pick<Config, 'tag' | 'ca' | 'catalogs' | 'cert' | 'fetchTimeout' | 'force' | 'dryRun' | 'extraBinPaths' | 'extraEnv' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'key' | 'httpProxy' | 'httpsProxy' | 'localAddress' | 'lockfileDir' | 'noProxy' | 'npmPath' | 'offline' | 'strictSsl' | 'unsafePerm' | 'userAgent' | 'verifyStoreIntegrity'>> & Partial<Pick<ConfigContext, 'selectedProjectsGraph'>> & {
|
|
3
|
+
export type PublishRecursiveOpts = Required<Pick<Config, 'bin' | 'cacheDir' | 'dir' | 'pnpmHomeDir' | 'configByUri' | 'registries' | 'workspaceDir'>> & Required<Pick<ConfigContext, 'cliOptions'>> & Partial<Pick<Config, 'tag' | 'ca' | 'catalogs' | 'cert' | 'fetchTimeout' | 'force' | 'dryRun' | 'extraBinPaths' | 'extraEnv' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'key' | 'httpProxy' | 'httpsProxy' | 'localAddress' | 'lockfileDir' | 'noProxy' | 'npmPath' | 'offline' | 'strictSsl' | 'unsafePerm' | 'userAgent' | 'verifyStoreIntegrity'>> & Partial<Pick<ConfigContext, 'selectedProjectsGraph' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
|
|
4
4
|
access?: 'public' | 'restricted';
|
|
5
5
|
argv: {
|
|
6
6
|
original: string[];
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
3
3
|
import { createResolver } from '@pnpm/installing.client';
|
|
4
4
|
import { logger } from '@pnpm/logger';
|
|
5
|
-
import {
|
|
5
|
+
import { sortFilteredProjects } from '@pnpm/workspace.projects-sorter';
|
|
6
6
|
import pFilter from 'p-filter';
|
|
7
7
|
import { pick } from 'ramda';
|
|
8
8
|
import { writeJsonFile } from 'write-json-file';
|
|
@@ -55,7 +55,7 @@ export async function recursivePublish(opts) {
|
|
|
55
55
|
if (opts.cliOptions['otp']) {
|
|
56
56
|
appendedArgs.push(`--otp=${opts.cliOptions['otp']}`);
|
|
57
57
|
}
|
|
58
|
-
const chunks =
|
|
58
|
+
const chunks = sortFilteredProjects(opts);
|
|
59
59
|
const tag = opts.tag ?? 'latest';
|
|
60
60
|
if (opts.batch) {
|
|
61
61
|
const sortedPkgs = chunks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnpm/releasing.commands",
|
|
3
|
-
"version": "1100.5.
|
|
3
|
+
"version": "1100.5.3",
|
|
4
4
|
"description": "Commands for deploy, pack, and publish",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pnpm",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"funding": "https://opencollective.com/pnpm",
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/pnpm/pnpm/tree/main/releasing/commands"
|
|
13
|
+
"url": "https://github.com/pnpm/pnpm/tree/main/pnpm11/releasing/commands"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://github.com/pnpm/pnpm/tree/main/releasing/commands#readme",
|
|
15
|
+
"homepage": "https://github.com/pnpm/pnpm/tree/main/pnpm11/releasing/commands#readme",
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/pnpm/pnpm/issues"
|
|
18
18
|
},
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"ci-info": "^4.4.0",
|
|
36
36
|
"detect-libc": "^2.1.2",
|
|
37
37
|
"execa": "npm:safe-execa@0.3.0",
|
|
38
|
+
"is-subdir": "^2.0.0",
|
|
38
39
|
"libnpmpublish": "^11.2.0",
|
|
39
40
|
"normalize-path": "^3.0.0",
|
|
40
41
|
"normalize-registry-url": "2.0.1",
|
|
@@ -51,35 +52,35 @@
|
|
|
51
52
|
"validate-npm-package-name": "7.0.2",
|
|
52
53
|
"write-json-file": "^7.0.0",
|
|
53
54
|
"write-yaml-file": "^6.0.0",
|
|
54
|
-
"@pnpm/
|
|
55
|
-
"@pnpm/cli.common-cli-options-help": "1100.0.2",
|
|
56
|
-
"@pnpm/config.reader": "1101.10.0",
|
|
57
|
-
"@pnpm/catalogs.types": "1100.0.0",
|
|
58
|
-
"@pnpm/constants": "1100.0.0",
|
|
55
|
+
"@pnpm/bins.resolver": "1100.0.8",
|
|
59
56
|
"@pnpm/config.pick-registry-for-package": "1100.0.9",
|
|
57
|
+
"@pnpm/config.reader": "1101.11.0",
|
|
58
|
+
"@pnpm/constants": "1100.0.0",
|
|
59
|
+
"@pnpm/cli.common-cli-options-help": "1100.0.2",
|
|
60
60
|
"@pnpm/deps.path": "1100.0.8",
|
|
61
|
-
"@pnpm/
|
|
62
|
-
"@pnpm/engine.runtime.
|
|
63
|
-
"@pnpm/
|
|
64
|
-
"@pnpm/
|
|
65
|
-
"@pnpm/exec.lifecycle": "1100.1.
|
|
61
|
+
"@pnpm/catalogs.types": "1100.0.0",
|
|
62
|
+
"@pnpm/engine.runtime.commands": "1100.1.8",
|
|
63
|
+
"@pnpm/error": "1100.0.1",
|
|
64
|
+
"@pnpm/engine.runtime.node-resolver": "1101.1.10",
|
|
65
|
+
"@pnpm/exec.lifecycle": "1100.1.2",
|
|
66
|
+
"@pnpm/fetching.directory-fetcher": "1100.0.19",
|
|
67
|
+
"@pnpm/cli.utils": "1101.0.13",
|
|
66
68
|
"@pnpm/exec.pnpm-cli-runner": "1100.0.1",
|
|
67
|
-
"@pnpm/fs.
|
|
68
|
-
"@pnpm/fs.indexed-pkg-importer": "1100.0.15",
|
|
69
|
-
"@pnpm/installing.commands": "1100.10.0",
|
|
70
|
-
"@pnpm/lockfile.fs": "1100.1.6",
|
|
71
|
-
"@pnpm/installing.client": "1100.2.9",
|
|
69
|
+
"@pnpm/fs.indexed-pkg-importer": "1100.0.17",
|
|
72
70
|
"@pnpm/fs.packlist": "1100.0.1",
|
|
73
|
-
"@pnpm/
|
|
74
|
-
"@pnpm/
|
|
71
|
+
"@pnpm/installing.client": "1100.2.11",
|
|
72
|
+
"@pnpm/installing.commands": "1100.10.2",
|
|
73
|
+
"@pnpm/fs.is-empty-dir-or-nothing": "1100.0.0",
|
|
74
|
+
"@pnpm/lockfile.fs": "1100.1.8",
|
|
75
|
+
"@pnpm/network.fetch": "1100.1.4",
|
|
76
|
+
"@pnpm/lockfile.types": "1100.0.13",
|
|
77
|
+
"@pnpm/network.auth-header": "1101.1.3",
|
|
75
78
|
"@pnpm/network.git-utils": "1100.0.2",
|
|
76
|
-
"@pnpm/
|
|
77
|
-
"@pnpm/
|
|
78
|
-
"@pnpm/
|
|
79
|
+
"@pnpm/releasing.exportable-manifest": "1100.1.9",
|
|
80
|
+
"@pnpm/resolving.resolver-base": "1100.5.1",
|
|
81
|
+
"@pnpm/network.web-auth": "1101.2.0",
|
|
79
82
|
"@pnpm/types": "1101.3.2",
|
|
80
|
-
"@pnpm/workspace.projects-sorter": "1100.0.
|
|
81
|
-
"@pnpm/resolving.resolver-base": "1100.4.2",
|
|
82
|
-
"@pnpm/bins.resolver": "1100.0.8"
|
|
83
|
+
"@pnpm/workspace.projects-sorter": "1100.0.8"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
85
86
|
"@pnpm/logger": "^1100.0.0"
|
|
@@ -102,17 +103,17 @@
|
|
|
102
103
|
"tar": "^7.5.15",
|
|
103
104
|
"undici": "^7.27.2",
|
|
104
105
|
"write-yaml-file": "^6.0.0",
|
|
105
|
-
"@pnpm/assert-project": "1100.0.
|
|
106
|
-
"@pnpm/
|
|
107
|
-
"@pnpm/
|
|
106
|
+
"@pnpm/assert-project": "1100.0.18",
|
|
107
|
+
"@pnpm/catalogs.config": "1100.0.2",
|
|
108
|
+
"@pnpm/hooks.pnpmfile": "1100.0.17",
|
|
109
|
+
"@pnpm/prepare": "1100.0.18",
|
|
110
|
+
"@pnpm/releasing.commands": "1100.5.3",
|
|
111
|
+
"@pnpm/logger": "1100.0.0",
|
|
108
112
|
"@pnpm/test-fixtures": "1100.0.0",
|
|
109
113
|
"@pnpm/test-ipc-server": "1100.0.0",
|
|
110
|
-
"@pnpm/
|
|
111
|
-
"@pnpm/
|
|
112
|
-
"@pnpm/
|
|
113
|
-
"@pnpm/logger": "1100.0.0",
|
|
114
|
-
"@pnpm/testing.registry-mock": "1100.0.6",
|
|
115
|
-
"@pnpm/workspace.projects-filter": "1100.0.22"
|
|
114
|
+
"@pnpm/testing.command-defaults": "1100.0.8",
|
|
115
|
+
"@pnpm/testing.registry-mock": "1100.0.8",
|
|
116
|
+
"@pnpm/workspace.projects-filter": "1100.0.24"
|
|
116
117
|
},
|
|
117
118
|
"engines": {
|
|
118
119
|
"node": ">=22.13"
|