@pnpm/releasing.commands 1100.6.4 → 1100.7.1
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/CHANGELOG.md +80 -0
- package/lib/change/index.d.ts +42 -0
- package/lib/change/index.js +248 -0
- package/lib/deploy/createDeployFiles.d.ts +26 -0
- package/lib/deploy/createDeployFiles.js +255 -0
- package/lib/deploy/deploy.d.ts +12 -0
- package/lib/deploy/deploy.js +398 -0
- package/lib/deploy/deployHook.d.ts +2 -0
- package/lib/deploy/deployHook.js +15 -0
- package/lib/deploy/index.d.ts +2 -0
- package/lib/deploy/index.js +3 -0
- package/lib/index.d.ts +7 -0
- package/lib/lane/index.d.ts +24 -0
- package/lib/lane/index.js +157 -0
- package/lib/pack-app/index.d.ts +2 -0
- package/lib/pack-app/index.js +3 -0
- package/lib/pack-app/packApp.d.ts +22 -0
- package/lib/pack-app/packApp.js +558 -0
- package/lib/publish/FailedToPublishError.d.ts +22 -0
- package/lib/publish/FailedToPublishError.js +40 -0
- package/lib/publish/batchPublish.d.ts +20 -0
- package/lib/publish/batchPublish.js +201 -0
- package/lib/publish/displayError.d.ts +1 -0
- package/lib/publish/displayError.js +23 -0
- package/lib/publish/executeTokenHelper.d.ts +4 -0
- package/lib/publish/executeTokenHelper.js +17 -0
- package/lib/publish/extractManifestFromPacked.d.ts +19 -0
- package/lib/publish/extractManifestFromPacked.js +110 -0
- package/lib/publish/index.d.ts +3 -0
- package/lib/publish/index.js +4 -0
- package/lib/publish/oidc/authToken.d.ts +73 -0
- package/lib/publish/oidc/authToken.js +97 -0
- package/lib/publish/oidc/idToken.d.ts +76 -0
- package/lib/publish/oidc/idToken.js +90 -0
- package/lib/publish/oidc/provenance.d.ts +73 -0
- package/lib/publish/oidc/provenance.js +91 -0
- package/lib/publish/otp.d.ts +38 -0
- package/lib/publish/otp.js +41 -0
- package/lib/publish/otpEnv.d.ts +7 -0
- package/lib/publish/otpEnv.js +5 -0
- package/lib/publish/pack.d.ts +34 -0
- package/lib/publish/pack.js +408 -0
- package/lib/publish/previousChangelog.d.ts +25 -0
- package/lib/publish/previousChangelog.js +194 -0
- package/lib/publish/publish.d.ts +40 -0
- package/lib/publish/publish.js +256 -0
- package/lib/publish/publishPackedPkg.d.ts +79 -0
- package/lib/publish/publishPackedPkg.js +298 -0
- package/lib/publish/recursivePublish.d.ts +18 -0
- package/lib/publish/recursivePublish.js +132 -0
- package/lib/publish/registryConfigKeys.d.ts +30 -0
- package/lib/publish/registryConfigKeys.js +50 -0
- package/lib/publish/utils/shared-context.d.ts +7 -0
- package/lib/publish/utils/shared-context.js +20 -0
- package/lib/resolveUnpublishedDirs.d.ts +10 -0
- package/lib/resolveUnpublishedDirs.js +14 -0
- package/lib/stage/approve.d.ts +2 -0
- package/lib/stage/approve.js +14 -0
- package/lib/stage/context.d.ts +14 -0
- package/lib/stage/context.js +25 -0
- package/lib/stage/download.d.ts +2 -0
- package/lib/stage/download.js +32 -0
- package/lib/stage/errors.d.ts +15 -0
- package/lib/stage/errors.js +17 -0
- package/lib/stage/help.d.ts +1 -0
- package/lib/stage/help.js +84 -0
- package/lib/stage/index.d.ts +13 -0
- package/lib/stage/index.js +57 -0
- package/lib/stage/list.d.ts +2 -0
- package/lib/stage/list.js +49 -0
- package/lib/stage/parsing.d.ts +6 -0
- package/lib/stage/parsing.js +27 -0
- package/lib/stage/publish.d.ts +7 -0
- package/lib/stage/publish.js +38 -0
- package/lib/stage/reject.d.ts +2 -0
- package/lib/stage/reject.js +16 -0
- package/lib/stage/rendering.d.ts +11 -0
- package/lib/stage/rendering.js +50 -0
- package/lib/stage/request.d.ts +29 -0
- package/lib/stage/request.js +110 -0
- package/lib/stage/types.d.ts +38 -0
- package/lib/stage/types.js +3 -0
- package/lib/stage/view.d.ts +2 -0
- package/lib/stage/view.js +14 -0
- package/lib/tarball/index.d.ts +2 -0
- package/lib/tarball/index.js +3 -0
- package/lib/tarball/publishSummary.d.ts +47 -0
- package/lib/tarball/publishSummary.js +36 -0
- package/lib/tarball/safeTarballFilename.d.ts +8 -0
- package/lib/tarball/safeTarballFilename.js +21 -0
- package/lib/tarball/summarizeTarball.d.ts +12 -0
- package/lib/tarball/summarizeTarball.js +84 -0
- package/lib/version/index.d.ts +35 -0
- package/lib/version/index.js +360 -0
- package/package.json +44 -44
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createGzip } from 'node:zlib';
|
|
4
|
+
import { getBinsFromPackageManifest } from '@pnpm/bins.resolver';
|
|
5
|
+
import { FILTERING } from '@pnpm/cli.common-cli-options-help';
|
|
6
|
+
import { readProjectManifest } from '@pnpm/cli.utils';
|
|
7
|
+
import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency, types as allTypes } from '@pnpm/config.reader';
|
|
8
|
+
import { PnpmError } from '@pnpm/error';
|
|
9
|
+
import { packlist } from '@pnpm/fs.packlist';
|
|
10
|
+
import { logger } from '@pnpm/logger';
|
|
11
|
+
import { createExportableManifest, readReadmeFile } from '@pnpm/releasing.exportable-manifest';
|
|
12
|
+
import { changelogStorage, readPendingChangelog, renderChangelog } from '@pnpm/releasing.versioning';
|
|
13
|
+
import { sortFilteredProjects } from '@pnpm/workspace.projects-sorter';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import pLimit from 'p-limit';
|
|
16
|
+
import { pick } from 'ramda';
|
|
17
|
+
import { realpathMissing } from 'realpath-missing';
|
|
18
|
+
import { renderHelp } from 'render-help';
|
|
19
|
+
import tar from 'tar-stream';
|
|
20
|
+
import { glob } from 'tinyglobby';
|
|
21
|
+
import validateNpmPackageName from 'validate-npm-package-name';
|
|
22
|
+
import { fetchPreviousChangelog } from './previousChangelog.js';
|
|
23
|
+
import { runScriptsIfPresent } from './publish.js';
|
|
24
|
+
const LICENSE_GLOB = 'LICEN{S,C}E{,.*}'; // cspell:disable-line
|
|
25
|
+
export function rcOptionsTypes() {
|
|
26
|
+
return {
|
|
27
|
+
...cliOptionsTypes(),
|
|
28
|
+
...pick([
|
|
29
|
+
'npm-path',
|
|
30
|
+
'skip-manifest-obfuscation',
|
|
31
|
+
], allTypes),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function cliOptionsTypes() {
|
|
35
|
+
return {
|
|
36
|
+
out: String,
|
|
37
|
+
recursive: Boolean,
|
|
38
|
+
...pick([
|
|
39
|
+
'dry-run',
|
|
40
|
+
'pack-destination',
|
|
41
|
+
'pack-gzip-level',
|
|
42
|
+
'json',
|
|
43
|
+
'skip-manifest-obfuscation',
|
|
44
|
+
'workspace-concurrency',
|
|
45
|
+
], allTypes),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const commandNames = ['pack'];
|
|
49
|
+
export function help() {
|
|
50
|
+
return renderHelp({
|
|
51
|
+
description: 'Create a tarball from a package',
|
|
52
|
+
usages: ['pnpm pack'],
|
|
53
|
+
descriptionLists: [
|
|
54
|
+
{
|
|
55
|
+
title: 'Options',
|
|
56
|
+
list: [
|
|
57
|
+
{
|
|
58
|
+
description: 'Does everything `pnpm pack` would do except actually writing the tarball to disk.',
|
|
59
|
+
name: '--dry-run',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Directory in which `pnpm pack` will save tarballs. The default is the current working directory.',
|
|
63
|
+
name: '--pack-destination <dir>',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
description: 'Prints the packed tarball and contents in the json format.',
|
|
67
|
+
name: '--json',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: 'Customizes the output path for the tarball. Use `%s` and `%v` to include the package name and version, e.g., `%s.tgz` or `some-dir/%s-%v.tgz`. By default, the tarball is saved in the current working directory with the name `<package-name>-<version>.tgz`.',
|
|
71
|
+
name: '--out <path>',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
description: 'Pack all packages from the workspace',
|
|
75
|
+
name: '--recursive',
|
|
76
|
+
shortAlias: '-r',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
description: 'Skip pnpm\'s manifest obfuscation: keep the original `packageManager` field and publish lifecycle scripts in the packed manifest instead of stripping them. The pnpm-specific `pnpm` field is still omitted.',
|
|
80
|
+
name: '--skip-manifest-obfuscation',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
description: `Set the maximum number of concurrency. Default is ${getDefaultWorkspaceConcurrency()}. For unlimited concurrency use Infinity.`,
|
|
84
|
+
name: '--workspace-concurrency <number>',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
FILTERING,
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function handler(opts) {
|
|
93
|
+
const packedPackages = [];
|
|
94
|
+
if (opts.recursive) {
|
|
95
|
+
const selectedProjectsGraph = opts.selectedProjectsGraph;
|
|
96
|
+
const pkgsToPack = [];
|
|
97
|
+
for (const { package: pkg } of Object.values(selectedProjectsGraph)) {
|
|
98
|
+
if (pkg.manifest.name && pkg.manifest.version) {
|
|
99
|
+
pkgsToPack.push(pkg);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const packedPkgDirs = new Set(pkgsToPack.map(({ rootDir }) => rootDir));
|
|
103
|
+
if (packedPkgDirs.size === 0) {
|
|
104
|
+
logger.info({
|
|
105
|
+
message: 'There are no packages that should be packed',
|
|
106
|
+
prefix: opts.dir,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const chunks = sortFilteredProjects({
|
|
110
|
+
selectedProjectsGraph,
|
|
111
|
+
allProjectsGraph: opts.allProjectsGraph,
|
|
112
|
+
prodAllProjectsGraph: opts.prodAllProjectsGraph,
|
|
113
|
+
prodOnlySelectedProjectDirs: opts.prodOnlySelectedProjectDirs,
|
|
114
|
+
});
|
|
115
|
+
const limitPack = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency));
|
|
116
|
+
const resolvedOpts = { ...opts };
|
|
117
|
+
if (opts.out) {
|
|
118
|
+
resolvedOpts.out = path.resolve(opts.dir, opts.out);
|
|
119
|
+
}
|
|
120
|
+
else if (opts.packDestination) {
|
|
121
|
+
resolvedOpts.packDestination = path.resolve(opts.dir, opts.packDestination);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
resolvedOpts.packDestination = path.resolve(opts.dir);
|
|
125
|
+
}
|
|
126
|
+
for (const chunk of chunks) {
|
|
127
|
+
// eslint-disable-next-line no-await-in-loop
|
|
128
|
+
await Promise.all(chunk.map(pkgDir => limitPack(async () => {
|
|
129
|
+
if (!packedPkgDirs.has(pkgDir))
|
|
130
|
+
return;
|
|
131
|
+
const pkg = selectedProjectsGraph[pkgDir].package;
|
|
132
|
+
const packResult = await api({
|
|
133
|
+
...resolvedOpts,
|
|
134
|
+
dir: pkg.rootDir,
|
|
135
|
+
});
|
|
136
|
+
packedPackages.push(toPackResultJson(packResult));
|
|
137
|
+
})));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const packResult = await api(opts);
|
|
142
|
+
packedPackages.push(toPackResultJson(packResult));
|
|
143
|
+
}
|
|
144
|
+
if (opts.json) {
|
|
145
|
+
return JSON.stringify(packedPackages.length > 1 ? packedPackages : packedPackages[0], null, 2);
|
|
146
|
+
}
|
|
147
|
+
return packedPackages.map(({ name, version, filename, files }) => `${opts.unicode ? '📦 ' : 'package:'} ${name}@${version}
|
|
148
|
+
${chalk.blueBright('Tarball Contents')}
|
|
149
|
+
${files.map(({ path }) => path).join('\n')}
|
|
150
|
+
${chalk.blueBright('Tarball Details')}
|
|
151
|
+
${filename}`).join('\n\n');
|
|
152
|
+
}
|
|
153
|
+
export async function api(opts) {
|
|
154
|
+
const { manifest: entryManifest, fileName: manifestFileName } = await readProjectManifest(opts.dir, opts);
|
|
155
|
+
preventBundledDependenciesWithoutHoistedNodeLinker(opts.nodeLinker, entryManifest);
|
|
156
|
+
const _runScriptsIfPresent = runScriptsIfPresent.bind(null, {
|
|
157
|
+
depPath: opts.dir,
|
|
158
|
+
extraBinPaths: opts.extraBinPaths,
|
|
159
|
+
extraEnv: opts.extraEnv,
|
|
160
|
+
pkgRoot: opts.dir,
|
|
161
|
+
rootModulesDir: await realpathMissing(path.join(opts.dir, 'node_modules')),
|
|
162
|
+
stdio: 'inherit',
|
|
163
|
+
unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
|
|
164
|
+
userAgent: opts.userAgent,
|
|
165
|
+
});
|
|
166
|
+
if (!opts.ignoreScripts) {
|
|
167
|
+
await _runScriptsIfPresent([
|
|
168
|
+
'prepack',
|
|
169
|
+
'prepare',
|
|
170
|
+
], entryManifest);
|
|
171
|
+
}
|
|
172
|
+
const dir = entryManifest.publishConfig?.directory
|
|
173
|
+
? path.join(opts.dir, entryManifest.publishConfig.directory)
|
|
174
|
+
: opts.dir;
|
|
175
|
+
// always read the latest manifest, as "prepack" or "prepare" script may modify package manifest.
|
|
176
|
+
const { manifest } = await readProjectManifest(dir, opts);
|
|
177
|
+
preventBundledDependenciesWithoutHoistedNodeLinker(opts.nodeLinker, manifest);
|
|
178
|
+
if (!manifest.name) {
|
|
179
|
+
throw new PnpmError('PACKAGE_NAME_NOT_FOUND', `Package name is not defined in the ${manifestFileName}.`);
|
|
180
|
+
}
|
|
181
|
+
if (!validateNpmPackageName(manifest.name).validForOldPackages) {
|
|
182
|
+
throw new PnpmError('INVALID_PACKAGE_NAME', `Invalid package name "${manifest.name}".`);
|
|
183
|
+
}
|
|
184
|
+
if (!manifest.version) {
|
|
185
|
+
throw new PnpmError('PACKAGE_VERSION_NOT_FOUND', `Package version is not defined in the ${manifestFileName}.`);
|
|
186
|
+
}
|
|
187
|
+
const publishManifest = await createPublishManifest({
|
|
188
|
+
projectDir: dir,
|
|
189
|
+
modulesDir: path.join(opts.dir, 'node_modules'),
|
|
190
|
+
manifest,
|
|
191
|
+
embedReadme: opts.embedReadme,
|
|
192
|
+
catalogs: opts.catalogs ?? {},
|
|
193
|
+
hooks: opts.hooks,
|
|
194
|
+
skipManifestObfuscation: opts.skipManifestObfuscation,
|
|
195
|
+
});
|
|
196
|
+
// Strip semver build metadata (the `+<build>` segment) from the published version so that
|
|
197
|
+
// the tarball, the manifest packed inside it, and the metadata sent to the registry all agree.
|
|
198
|
+
// libnpmpublish runs `semver.clean()` on `manifest.version` before computing the provenance
|
|
199
|
+
// subject, which removes build metadata. Leaving it in here would mismatch the version embedded
|
|
200
|
+
// in the tarball's package.json and cause the registry to reject the publish with a 422 when
|
|
201
|
+
// verifying the sigstore provenance bundle. See https://github.com/pnpm/pnpm/issues/11518.
|
|
202
|
+
publishManifest.version = stripBuildMetadata(publishManifest.version);
|
|
203
|
+
let tarballName;
|
|
204
|
+
let packDestination;
|
|
205
|
+
const normalizedName = manifest.name.replace('@', '').replace('/', '-');
|
|
206
|
+
if (opts.out) {
|
|
207
|
+
if (opts.packDestination) {
|
|
208
|
+
throw new PnpmError('INVALID_OPTION', 'Cannot use --pack-destination and --out together');
|
|
209
|
+
}
|
|
210
|
+
const preparedOut = opts.out.replaceAll('%s', normalizedName).replaceAll('%v', publishManifest.version);
|
|
211
|
+
const parsedOut = path.parse(preparedOut);
|
|
212
|
+
packDestination = parsedOut.dir ? parsedOut.dir : opts.packDestination;
|
|
213
|
+
tarballName = parsedOut.base;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
tarballName = `${normalizedName}-${publishManifest.version}.tgz`;
|
|
217
|
+
packDestination = opts.packDestination;
|
|
218
|
+
}
|
|
219
|
+
const files = await packlist(dir, {
|
|
220
|
+
manifest: publishManifest,
|
|
221
|
+
workspaceDir: opts.workspaceDir,
|
|
222
|
+
});
|
|
223
|
+
const filesMap = Object.fromEntries(files.map((file) => [`package/${file}`, path.join(dir, file)]));
|
|
224
|
+
// cspell:disable-next-line
|
|
225
|
+
if (opts.workspaceDir != null && dir !== opts.workspaceDir && !files.some((file) => /LICEN[CS]E(?:\..+)?/i.test(file))) {
|
|
226
|
+
const { workspaceDir } = opts;
|
|
227
|
+
const licenses = await glob([LICENSE_GLOB], { cwd: workspaceDir, expandDirectories: false });
|
|
228
|
+
await Promise.all(licenses.map(async (license) => {
|
|
229
|
+
const licensePath = path.join(workspaceDir, license);
|
|
230
|
+
// Only inject a regular file. A symlink could point outside the workspace and leak its
|
|
231
|
+
// target's bytes into the published tarball, so `lstat()` (which does not follow symlinks)
|
|
232
|
+
// rejects it — matching pacquet's inject_workspace_license.
|
|
233
|
+
const stats = await fs.promises.lstat(licensePath);
|
|
234
|
+
if (stats.isFile()) {
|
|
235
|
+
filesMap[`package/${license}`] = licensePath;
|
|
236
|
+
}
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
// In `registry` changelog storage the package carries no committed
|
|
240
|
+
// CHANGELOG.md; its section was parked at `pnpm version -r` time and is
|
|
241
|
+
// composed here on top of the previously published version's changelog and
|
|
242
|
+
// packed in. A composed entry supersedes any stale committed CHANGELOG.md.
|
|
243
|
+
const injectedEntries = {};
|
|
244
|
+
const composedChangelog = await composeRegistryChangelog(opts, manifest.name, manifest.version);
|
|
245
|
+
if (composedChangelog != null) {
|
|
246
|
+
delete filesMap['package/CHANGELOG.md'];
|
|
247
|
+
injectedEntries['package/CHANGELOG.md'] = composedChangelog;
|
|
248
|
+
}
|
|
249
|
+
const destDir = packDestination
|
|
250
|
+
? (path.isAbsolute(packDestination) ? packDestination : path.join(dir, packDestination ?? '.'))
|
|
251
|
+
: dir;
|
|
252
|
+
if (!opts.dryRun) {
|
|
253
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
// Derive `contents` and `unpackedSize` from `filesMap` (the full set of tar entries) rather than
|
|
256
|
+
// from `files` (the packlist subset) so that:
|
|
257
|
+
// - workspace LICENSE files appended to `filesMap` after the packlist call are included; and
|
|
258
|
+
// - `package.yaml` / `package.json5` entries are reported under the name they actually have in
|
|
259
|
+
// the tar (`package.json`), since `packPkg()` rewrites them.
|
|
260
|
+
// The `stat()` pass must run before `postpack`, which may delete prepack-generated files that
|
|
261
|
+
// were packed. See https://github.com/pnpm/pnpm/issues/12775.
|
|
262
|
+
const sizes = await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
|
|
263
|
+
if (isManifestEntry(name)) {
|
|
264
|
+
return Buffer.byteLength(JSON.stringify(publishManifest, null, 2));
|
|
265
|
+
}
|
|
266
|
+
const stat = await fs.promises.stat(source);
|
|
267
|
+
return stat.size;
|
|
268
|
+
}));
|
|
269
|
+
const injectedSize = Object.values(injectedEntries).reduce((acc, content) => acc + Buffer.byteLength(content), 0);
|
|
270
|
+
const unpackedSize = sizes.reduce((acc, size) => acc + size, 0) + injectedSize;
|
|
271
|
+
const packedContents = Array.from(new Set([
|
|
272
|
+
...Object.keys(filesMap).map((name) => isManifestEntry(name)
|
|
273
|
+
? 'package.json'
|
|
274
|
+
: name.replace(/^package\//, '')),
|
|
275
|
+
...Object.keys(injectedEntries).map((name) => name.replace(/^package\//, '')),
|
|
276
|
+
])).sort((a, b) => a.localeCompare(b, 'en'));
|
|
277
|
+
if (!opts.dryRun) {
|
|
278
|
+
await packPkg({
|
|
279
|
+
destFile: path.join(destDir, tarballName),
|
|
280
|
+
filesMap,
|
|
281
|
+
injectedEntries,
|
|
282
|
+
modulesDir: path.join(opts.dir, 'node_modules'),
|
|
283
|
+
packGzipLevel: opts.packGzipLevel,
|
|
284
|
+
manifest: publishManifest,
|
|
285
|
+
bins: [
|
|
286
|
+
...(await getBinsFromPackageManifest(publishManifest, dir)).map(({ path }) => path),
|
|
287
|
+
...(manifest.publishConfig?.executableFiles ?? [])
|
|
288
|
+
.map((executableFile) => path.join(dir, executableFile)),
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
if (!opts.ignoreScripts) {
|
|
292
|
+
await _runScriptsIfPresent(['postpack'], entryManifest);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
let packedTarballPath;
|
|
296
|
+
if (opts.dir !== destDir) {
|
|
297
|
+
packedTarballPath = path.join(destDir, tarballName);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
packedTarballPath = path.relative(opts.dir, path.join(dir, tarballName));
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
publishedManifest: await withRegistryReadme(publishManifest, dir),
|
|
304
|
+
contents: packedContents,
|
|
305
|
+
tarballPath: packedTarballPath,
|
|
306
|
+
unpackedSize,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The readme is always sent to the registry as package metadata, matching the npm CLI, so that
|
|
311
|
+
* registries can render it on the package page. The `embed-readme` setting only controls whether
|
|
312
|
+
* the readme is additionally written into the `package.json` inside the tarball (via
|
|
313
|
+
* `createExportableManifest`), which is why it is added to the returned manifest here rather than
|
|
314
|
+
* to the packed one.
|
|
315
|
+
*/
|
|
316
|
+
async function withRegistryReadme(manifest, projectDir) {
|
|
317
|
+
if (manifest.readme != null)
|
|
318
|
+
return manifest;
|
|
319
|
+
const readme = await readReadmeFile(projectDir);
|
|
320
|
+
if (readme == null)
|
|
321
|
+
return manifest;
|
|
322
|
+
return { ...manifest, readme };
|
|
323
|
+
}
|
|
324
|
+
// True when a `package/<path>` tar key names the package manifest, which is
|
|
325
|
+
// packed as a single serialized `package/package.json` entry and reported as
|
|
326
|
+
// `package.json` in the contents listing regardless of the source file name.
|
|
327
|
+
function isManifestEntry(name) {
|
|
328
|
+
return name === 'package/package.json' || name === 'package/package.json5' || name === 'package/package.yaml';
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The CHANGELOG.md to pack for a `registry`-storage release: its parked
|
|
332
|
+
* section (written at `pnpm version -r` time) rendered on top of the
|
|
333
|
+
* previously published version's changelog. `undefined` when storage is
|
|
334
|
+
* `repository`, there is no workspace, or the release has no parked section
|
|
335
|
+
* (an ordinary `pnpm pack` of a package that is not mid-release).
|
|
336
|
+
*/
|
|
337
|
+
async function composeRegistryChangelog(opts, pkgName, version) {
|
|
338
|
+
if (changelogStorage(opts.versioning) !== 'registry' || opts.workspaceDir == null)
|
|
339
|
+
return undefined;
|
|
340
|
+
const section = await readPendingChangelog(opts.workspaceDir, pkgName, version);
|
|
341
|
+
if (section == null)
|
|
342
|
+
return undefined;
|
|
343
|
+
const previous = opts.registries != null
|
|
344
|
+
? await fetchPreviousChangelog(opts, pkgName, version)
|
|
345
|
+
: undefined;
|
|
346
|
+
return renderChangelog(previous ?? null, pkgName, section);
|
|
347
|
+
}
|
|
348
|
+
function stripBuildMetadata(version) {
|
|
349
|
+
const plusIndex = version.indexOf('+');
|
|
350
|
+
return plusIndex === -1 ? version : version.slice(0, plusIndex);
|
|
351
|
+
}
|
|
352
|
+
function preventBundledDependenciesWithoutHoistedNodeLinker(nodeLinker, manifest) {
|
|
353
|
+
if (nodeLinker === 'hoisted')
|
|
354
|
+
return;
|
|
355
|
+
for (const key of ['bundledDependencies', 'bundleDependencies']) {
|
|
356
|
+
const bundledDependencies = manifest[key];
|
|
357
|
+
if (bundledDependencies) {
|
|
358
|
+
throw new PnpmError('BUNDLED_DEPENDENCIES_WITHOUT_HOISTED', `${key} does not work with "nodeLinker: ${nodeLinker}"`, {
|
|
359
|
+
hint: `Add "nodeLinker: hoisted" to pnpm-workspace.yaml or delete ${key} from the root package.json to resolve this error`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function packPkg(opts) {
|
|
365
|
+
const { destFile, filesMap, injectedEntries, bins, manifest, } = opts;
|
|
366
|
+
const mtime = new Date('1985-10-26T08:15:00.000Z');
|
|
367
|
+
const pack = tar.pack();
|
|
368
|
+
await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
|
|
369
|
+
const isExecutable = bins.some((bin) => path.relative(bin, source) === '');
|
|
370
|
+
const mode = isExecutable ? 0o755 : 0o644;
|
|
371
|
+
if (isManifestEntry(name)) {
|
|
372
|
+
pack.entry({ mode, mtime, name: 'package/package.json' }, JSON.stringify(manifest, null, 2));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
pack.entry({ mode, mtime, name }, fs.readFileSync(source));
|
|
376
|
+
}));
|
|
377
|
+
for (const [name, content] of Object.entries(injectedEntries ?? {})) {
|
|
378
|
+
pack.entry({ mode: 0o644, mtime, name }, content);
|
|
379
|
+
}
|
|
380
|
+
const tarball = fs.createWriteStream(destFile);
|
|
381
|
+
pack.pipe(createGzip({ level: opts.packGzipLevel })).pipe(tarball);
|
|
382
|
+
pack.finalize();
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
tarball.on('close', () => {
|
|
385
|
+
resolve();
|
|
386
|
+
}).on('error', reject);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function createPublishManifest(opts) {
|
|
390
|
+
const { projectDir, embedReadme, modulesDir, manifest, catalogs, hooks, skipManifestObfuscation } = opts;
|
|
391
|
+
return createExportableManifest(projectDir, manifest, {
|
|
392
|
+
catalogs,
|
|
393
|
+
hooks,
|
|
394
|
+
embedReadme,
|
|
395
|
+
modulesDir,
|
|
396
|
+
skipManifestObfuscation,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function toPackResultJson(packResult) {
|
|
400
|
+
const { publishedManifest, contents, tarballPath } = packResult;
|
|
401
|
+
return {
|
|
402
|
+
name: publishedManifest.name,
|
|
403
|
+
version: publishedManifest.version,
|
|
404
|
+
filename: tarballPath,
|
|
405
|
+
files: contents.map((file) => ({ path: file })),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=pack.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Config } from '@pnpm/config.reader';
|
|
2
|
+
import { type CreateFetchFromRegistryOptions } from '@pnpm/network.fetch';
|
|
3
|
+
export type PreviousChangelogOptions = CreateFetchFromRegistryOptions & Pick<Config, 'registries' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchTimeout'>;
|
|
4
|
+
/**
|
|
5
|
+
* The `CHANGELOG.md` packed into the highest published version of `pkgName`
|
|
6
|
+
* that is semver-lower than `version` — the changelog the section for
|
|
7
|
+
* `version` is prepended onto in `registry` storage. `undefined` when the
|
|
8
|
+
* package is new, has no earlier published version, or that version's tarball
|
|
9
|
+
* carried no changelog.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fetchPreviousChangelog(opts: PreviousChangelogOptions, pkgName: string, version: string): Promise<string | undefined>;
|
|
12
|
+
/**
|
|
13
|
+
* The `CHANGELOG.md` packed into the published `pkgName@version` exactly, or
|
|
14
|
+
* `undefined` when that version is not published (or carried no changelog).
|
|
15
|
+
* Used to confirm a release actually carries its composed section before the
|
|
16
|
+
* intents behind it are garbage-collected.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchPublishedChangelog(opts: PreviousChangelogOptions, pkgName: string, version: string): Promise<string | undefined>;
|
|
19
|
+
export declare function changelogHasSection(changelog: string, section: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Whether `pkgName@version` is published, reusing one client across the batch.
|
|
22
|
+
* `fetchPackument` returning `undefined` always means the 404 (unpublished): the
|
|
23
|
+
* probe sends no cache validator, so it never yields a 304 not-modified.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createVersionPublishedChecker(opts: PreviousChangelogOptions): (pkgName: string, version: string) => Promise<boolean>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
3
|
+
import { PnpmError } from '@pnpm/error';
|
|
4
|
+
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header';
|
|
5
|
+
import { createFetchFromRegistry } from '@pnpm/network.fetch';
|
|
6
|
+
import { fetchMetadataFromFromRegistry } from '@pnpm/resolving.npm-resolver';
|
|
7
|
+
import { lt, rsort, valid } from 'semver';
|
|
8
|
+
import tar from 'tar-stream';
|
|
9
|
+
const CHANGELOG_ENTRY = 'package/CHANGELOG.md';
|
|
10
|
+
/**
|
|
11
|
+
* Caps the previous tarball we buffer and decompress to compose the changelog.
|
|
12
|
+
* The bytes come from a registry/proxy, so an unbounded read or a highly
|
|
13
|
+
* compressible ("gzip bomb") tarball could OOM release automation. A composed
|
|
14
|
+
* changelog is best-effort — exceeding the cap just skips the history prepend —
|
|
15
|
+
* so this bound can be generous while still defeating the amplification attack.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_TARBALL_BYTES = 256 * 1024 * 1024;
|
|
18
|
+
function createRegistryClient(opts) {
|
|
19
|
+
return {
|
|
20
|
+
fetch: createFetchFromRegistry(opts),
|
|
21
|
+
getAuthHeader: createGetAuthHeaderByURI(opts.configByUri ?? {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `CHANGELOG.md` packed into the highest published version of `pkgName`
|
|
26
|
+
* that is semver-lower than `version` — the changelog the section for
|
|
27
|
+
* `version` is prepended onto in `registry` storage. `undefined` when the
|
|
28
|
+
* package is new, has no earlier published version, or that version's tarball
|
|
29
|
+
* carried no changelog.
|
|
30
|
+
*/
|
|
31
|
+
export async function fetchPreviousChangelog(opts, pkgName, version) {
|
|
32
|
+
const client = createRegistryClient(opts);
|
|
33
|
+
const meta = await fetchPackument(client, opts, pkgName);
|
|
34
|
+
if (meta == null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const previousVersion = pickPreviousVersion(meta, version);
|
|
37
|
+
if (previousVersion == null)
|
|
38
|
+
return undefined;
|
|
39
|
+
return downloadTarballChangelog(client, pkgName, meta, previousVersion);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The `CHANGELOG.md` packed into the published `pkgName@version` exactly, or
|
|
43
|
+
* `undefined` when that version is not published (or carried no changelog).
|
|
44
|
+
* Used to confirm a release actually carries its composed section before the
|
|
45
|
+
* intents behind it are garbage-collected.
|
|
46
|
+
*/
|
|
47
|
+
export async function fetchPublishedChangelog(opts, pkgName, version) {
|
|
48
|
+
const client = createRegistryClient(opts);
|
|
49
|
+
const meta = await fetchPackument(client, opts, pkgName);
|
|
50
|
+
if (meta?.versions[version] == null)
|
|
51
|
+
return undefined;
|
|
52
|
+
return downloadTarballChangelog(client, pkgName, meta, version);
|
|
53
|
+
}
|
|
54
|
+
export function changelogHasSection(changelog, section) {
|
|
55
|
+
return changelog.includes(section.trim());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether `pkgName@version` is published, reusing one client across the batch.
|
|
59
|
+
* `fetchPackument` returning `undefined` always means the 404 (unpublished): the
|
|
60
|
+
* probe sends no cache validator, so it never yields a 304 not-modified.
|
|
61
|
+
*/
|
|
62
|
+
export function createVersionPublishedChecker(opts) {
|
|
63
|
+
const client = createRegistryClient(opts);
|
|
64
|
+
return async (pkgName, version) => {
|
|
65
|
+
const meta = await fetchPackument(client, opts, pkgName);
|
|
66
|
+
return meta?.versions[version] != null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function fetchPackument(client, opts, pkgName) {
|
|
70
|
+
const registry = pickRegistryForPackage(opts.registries, pkgName);
|
|
71
|
+
let fetchResult;
|
|
72
|
+
try {
|
|
73
|
+
fetchResult = await fetchMetadataFromFromRegistry({
|
|
74
|
+
fetch: client.fetch,
|
|
75
|
+
retry: {
|
|
76
|
+
factor: opts.fetchRetryFactor,
|
|
77
|
+
maxTimeout: opts.fetchRetryMaxtimeout,
|
|
78
|
+
minTimeout: opts.fetchRetryMintimeout,
|
|
79
|
+
retries: opts.fetchRetries,
|
|
80
|
+
},
|
|
81
|
+
timeout: opts.fetchTimeout ?? 60000,
|
|
82
|
+
fetchWarnTimeoutMs: 10000,
|
|
83
|
+
}, pkgName, {
|
|
84
|
+
registry,
|
|
85
|
+
authHeaderValue: client.getAuthHeader(registry, { pkgName }),
|
|
86
|
+
fullMetadata: false,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// A package with no published version yet has no changelog to build on.
|
|
91
|
+
if (err != null && typeof err === 'object' && 'code' in err && err.code === 'ERR_PNPM_FETCH_404') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
return fetchResult.notModified ? undefined : fetchResult.meta;
|
|
97
|
+
}
|
|
98
|
+
/** Highest published version of the package that is semver-lower than `version`. */
|
|
99
|
+
function pickPreviousVersion(meta, version) {
|
|
100
|
+
const candidates = Object.keys(meta.versions).filter((candidate) => valid(candidate) != null && lt(candidate, version));
|
|
101
|
+
return candidates.length > 0 ? rsort(candidates)[0] : undefined;
|
|
102
|
+
}
|
|
103
|
+
async function downloadTarballChangelog(client, pkgName, meta, version) {
|
|
104
|
+
const tarballUrl = meta.versions[version]?.dist?.tarball;
|
|
105
|
+
if (tarballUrl == null)
|
|
106
|
+
return undefined;
|
|
107
|
+
const response = await client.fetch(tarballUrl, { authHeaderValue: client.getAuthHeader(tarballUrl, { pkgName }) });
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new PnpmError('CHANGELOG_TARBALL_FETCH_FAILED', `Failed to download ${pkgName}@${version} tarball (${response.status}) to compose the changelog: ${tarballUrl}`);
|
|
110
|
+
}
|
|
111
|
+
const tarballData = await readCapped(response, MAX_TARBALL_BYTES);
|
|
112
|
+
if (tarballData == null)
|
|
113
|
+
return undefined;
|
|
114
|
+
return extractTarballEntry(tarballData, CHANGELOG_ENTRY);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reads a response body into a buffer, stopping (and returning `undefined`) as
|
|
118
|
+
* soon as it exceeds `maxBytes` — bounding the actual download rather than
|
|
119
|
+
* trusting a `content-length` header, which may be absent or lie.
|
|
120
|
+
*/
|
|
121
|
+
async function readCapped(response, maxBytes) {
|
|
122
|
+
const reader = response.body?.getReader();
|
|
123
|
+
if (reader == null) {
|
|
124
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
125
|
+
return buffer.byteLength > maxBytes ? undefined : buffer;
|
|
126
|
+
}
|
|
127
|
+
const chunks = [];
|
|
128
|
+
let total = 0;
|
|
129
|
+
for (;;) {
|
|
130
|
+
// Streaming a body is inherently sequential — each read must await the
|
|
131
|
+
// previous chunk — so the successive awaits here are intentional.
|
|
132
|
+
// eslint-disable-next-line no-await-in-loop
|
|
133
|
+
const { done, value } = await reader.read();
|
|
134
|
+
if (done)
|
|
135
|
+
break;
|
|
136
|
+
total += value.byteLength;
|
|
137
|
+
if (total > maxBytes) {
|
|
138
|
+
// Best-effort cleanup: a cancel failure must not turn the over-cap path
|
|
139
|
+
// into a hard error — the caller just gets no changelog either way.
|
|
140
|
+
// eslint-disable-next-line no-await-in-loop
|
|
141
|
+
await reader.cancel().catch(() => { });
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
chunks.push(Buffer.from(value));
|
|
145
|
+
}
|
|
146
|
+
return Buffer.concat(chunks);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Reads one entry's contents out of a (optionally gzipped) tarball buffer.
|
|
150
|
+
* Composing the changelog is best-effort, so any decode/parse failure —
|
|
151
|
+
* including a gzip bomb tripping the inflate cap — resolves to `undefined`
|
|
152
|
+
* (no history prepend) rather than throwing.
|
|
153
|
+
*/
|
|
154
|
+
async function extractTarballEntry(tarballData, entryName) {
|
|
155
|
+
const tarData = gunzipCapped(tarballData);
|
|
156
|
+
if (tarData == null)
|
|
157
|
+
return undefined;
|
|
158
|
+
const extract = tar.extract();
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let contents;
|
|
161
|
+
extract.on('entry', (header, stream, next) => {
|
|
162
|
+
if (header.name !== entryName) {
|
|
163
|
+
stream.resume();
|
|
164
|
+
stream.on('end', next);
|
|
165
|
+
stream.on('error', () => resolve(undefined));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const chunks = [];
|
|
169
|
+
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
170
|
+
stream.on('error', () => resolve(undefined));
|
|
171
|
+
stream.on('end', () => {
|
|
172
|
+
contents = Buffer.concat(chunks).toString('utf8');
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
extract.on('error', () => resolve(undefined));
|
|
177
|
+
extract.on('finish', () => resolve(contents));
|
|
178
|
+
extract.end(tarData);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Inflates a published `.tgz`, capped at `MAX_TARBALL_BYTES` of output so a
|
|
183
|
+
* gzip bomb throws rather than ballooning memory. Returns `undefined` on that
|
|
184
|
+
* cap or any other decode failure — the caller treats it as no changelog.
|
|
185
|
+
*/
|
|
186
|
+
function gunzipCapped(tarballData) {
|
|
187
|
+
try {
|
|
188
|
+
return gunzipSync(tarballData, { maxOutputLength: MAX_TARBALL_BYTES });
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=previousChangelog.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Config, type ConfigContext } from '@pnpm/config.reader';
|
|
2
|
+
import { type RunLifecycleHookOptions } from '@pnpm/exec.lifecycle';
|
|
3
|
+
import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest';
|
|
4
|
+
import type { ProjectManifest } from '@pnpm/types';
|
|
5
|
+
import { type PublishSummary } from './publishPackedPkg.js';
|
|
6
|
+
import { type PublishRecursiveOpts, type RecursivePublishedPackage } from './recursivePublish.js';
|
|
7
|
+
export declare function rcOptionsTypes(): Record<string, unknown>;
|
|
8
|
+
export declare function cliOptionsTypes(): Record<string, unknown>;
|
|
9
|
+
export declare const commandNames: string[];
|
|
10
|
+
export declare function help(): string;
|
|
11
|
+
export declare function handler(opts: Omit<PublishRecursiveOpts, 'workspaceDir'> & {
|
|
12
|
+
argv: {
|
|
13
|
+
original: string[];
|
|
14
|
+
};
|
|
15
|
+
engineStrict?: boolean;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
recursive?: boolean;
|
|
18
|
+
workspaceDir?: string;
|
|
19
|
+
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'skipManifestObfuscation' | 'versioning'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<{
|
|
20
|
+
exitCode?: number;
|
|
21
|
+
output?: string;
|
|
22
|
+
} | undefined>;
|
|
23
|
+
export interface PublishResult {
|
|
24
|
+
exitCode?: number;
|
|
25
|
+
manifest?: ProjectManifest;
|
|
26
|
+
publishedManifest?: ExportedManifest;
|
|
27
|
+
/** Per-package summary in the npm-CLI `--json` shape; only populated for single-package publish. */
|
|
28
|
+
publishSummary?: PublishSummary;
|
|
29
|
+
/** Per-package summaries collected by recursive publish. */
|
|
30
|
+
publishedPackages?: RecursivePublishedPackage[];
|
|
31
|
+
}
|
|
32
|
+
export declare function publish(opts: Omit<PublishRecursiveOpts, 'workspaceDir'> & {
|
|
33
|
+
argv: {
|
|
34
|
+
original: string[];
|
|
35
|
+
};
|
|
36
|
+
engineStrict?: boolean;
|
|
37
|
+
recursive?: boolean;
|
|
38
|
+
workspaceDir?: string;
|
|
39
|
+
} & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel' | 'skipManifestObfuscation' | 'versioning'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<PublishResult>;
|
|
40
|
+
export declare function runScriptsIfPresent(opts: RunLifecycleHookOptions, scriptNames: string[], manifest: ProjectManifest): Promise<void>;
|