@pnpm/releasing.commands 1100.5.4 → 1100.6.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.
@@ -6,6 +6,30 @@ import tar from 'tar-stream';
6
6
  const TARBALL_SUFFIXES = ['.tar.gz', '.tgz'];
7
7
  export const isTarballPath = (path) => TARBALL_SUFFIXES.some(suffix => path.endsWith(suffix));
8
8
  export async function extractManifestFromPacked(tarballPath) {
9
+ const { manifest } = await extractEntriesFromPacked(tarballPath, false);
10
+ return JSON.parse(manifest);
11
+ }
12
+ /**
13
+ * Read the publish manifest from a pre-built tarball, filling in its `readme` from the tarball's
14
+ * root README file when the manifest doesn't already declare one. This mirrors the npm CLI, which
15
+ * reads the readme out of the tarball (via pacote's `fullReadJson`) so the registry gets it as
16
+ * metadata even though it isn't stored in the packed `package.json`.
17
+ */
18
+ export async function extractPublishManifestFromPacked(tarballPath) {
19
+ const { manifest, readme } = await extractEntriesFromPacked(tarballPath, true);
20
+ const parsed = JSON.parse(manifest);
21
+ if (parsed.readme == null && readme != null) {
22
+ parsed.readme = readme;
23
+ }
24
+ return parsed;
25
+ }
26
+ /**
27
+ * Scan the tarball for `package/package.json` and, when `wantReadme` is set, the root
28
+ * `README.md`. The manifest-only path (`wantReadme` false) resolves as soon as the manifest
29
+ * entry is read and stops decompressing the rest of the archive; the publish path scans on
30
+ * because a README can appear after the manifest.
31
+ */
32
+ async function extractEntriesFromPacked(tarballPath, wantReadme) {
9
33
  const extract = tar.extract();
10
34
  const gunzip = createGunzip();
11
35
  const tarballStream = fs.createReadStream(tarballPath);
@@ -19,47 +43,62 @@ export async function extractManifestFromPacked(tarballPath) {
19
43
  tarballStream.destroy();
20
44
  }
21
45
  const promise = new Promise((resolve, reject) => {
46
+ let settled = false;
47
+ let manifest;
48
+ let readme;
22
49
  function handleError(error) {
23
50
  cleanup();
24
51
  reject(error);
25
52
  }
53
+ function settle() {
54
+ if (settled)
55
+ return;
56
+ settled = true;
57
+ cleanup();
58
+ if (manifest == null) {
59
+ reject(new PublishArchiveMissingManifestError(tarballPath));
60
+ return;
61
+ }
62
+ resolve({ manifest, readme });
63
+ }
26
64
  tarballStream.once('error', handleError);
27
65
  gunzip.once('error', handleError);
28
- let manifestFound = false;
29
66
  extract.on('entry', (header, stream, next) => {
30
67
  const normalizedPath = path.normalize(header.name).replaceAll('\\', '/');
31
- if (normalizedPath !== 'package/package.json') {
68
+ const isManifest = normalizedPath === 'package/package.json';
69
+ const isReadme = wantReadme && /^package\/readme\.md$/i.test(normalizedPath);
70
+ if (!isManifest && !isReadme) {
32
71
  stream.once('end', next);
33
72
  stream.resume();
34
73
  return;
35
74
  }
36
- manifestFound = true;
37
75
  const chunks = [];
38
76
  stream.on('data', (chunk) => {
39
77
  chunks.push(chunk);
40
78
  });
41
79
  stream.once('end', () => {
42
- try {
43
- const text = Buffer.concat(chunks).toString();
44
- cleanup();
45
- resolve(text);
80
+ const text = Buffer.concat(chunks).toString();
81
+ if (isManifest) {
82
+ manifest = text;
46
83
  }
47
- catch (error) {
48
- handleError(error);
84
+ else {
85
+ readme = text;
49
86
  }
87
+ // Stop early once every wanted entry has been captured, so the manifest-only
88
+ // path doesn't stream and decompress the remainder of the tarball.
89
+ if (manifest != null && (!wantReadme || readme != null)) {
90
+ settle();
91
+ return;
92
+ }
93
+ next();
50
94
  });
51
95
  stream.once('error', handleError);
52
96
  });
53
- extract.once('finish', () => {
54
- cleanup();
55
- if (!manifestFound) {
56
- reject(new PublishArchiveMissingManifestError(tarballPath));
57
- }
58
- });
97
+ extract.once('finish', settle);
59
98
  extract.once('error', handleError);
60
99
  });
61
100
  tarballStream.pipe(gunzip).pipe(extract);
62
- return JSON.parse(await promise);
101
+ return promise;
63
102
  }
64
103
  export class PublishArchiveMissingManifestError extends PnpmError {
65
104
  tarballPath;
@@ -28,11 +28,14 @@ export async function publishWithOtpHandling({ context = SHARED_CONTEXT, manifes
28
28
  return withOtpHandling({
29
29
  context,
30
30
  fetchOptions,
31
- // When otp is undefined (first attempt), { ...publishOptions, otp } adds
32
- // otp: undefined to the options. This is safe because libnpmpublish treats
33
- // undefined the same as absent (unlike HTTP headers, where undefined gets
34
- // coerced to the string "undefined").
35
- operation: otp => publish(manifest, tarballData, { ...publishOptions, otp }),
31
+ // withOtpHandling first calls this with no otp, then retries with the otp
32
+ // it obtained from an OTP challenge. On the first attempt we fall back to
33
+ // the configured --otp (publishOptions.otp) so it is actually sent; a retry
34
+ // otp takes precedence over it. When both are undefined, passing
35
+ // otp: undefined is safe because libnpmpublish treats undefined the same as
36
+ // absent (unlike HTTP headers, where undefined gets coerced to the string
37
+ // "undefined").
38
+ operation: otp => publish(manifest, tarballData, { ...publishOptions, otp: otp ?? publishOptions.otp }),
36
39
  });
37
40
  }
38
41
  //# sourceMappingURL=otp.js.map
@@ -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' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
7
+ export type PackOptions = Pick<UniversalOptions, 'dir'> & Pick<Config, 'catalogs' | 'ignoreScripts' | 'embedReadme' | 'packGzipLevel' | 'nodeLinker' | 'skipManifestObfuscation' | 'userAgent'> & Partial<Pick<Config, 'extraBinPaths' | 'extraEnv' | 'recursive' | 'workspaceConcurrency' | 'workspaceDir' | 'versioning' | 'registries' | 'configByUri' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchTimeout' | 'ca' | 'cert' | 'key' | 'strictSsl' | 'httpProxy' | 'httpsProxy' | 'noProxy' | 'localAddress'>> & Partial<Pick<ConfigContext, 'hooks' | 'selectedProjectsGraph' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
8
8
  argv: {
9
9
  original: string[];
10
10
  };
@@ -8,7 +8,8 @@ import { getDefaultWorkspaceConcurrency, getWorkspaceConcurrency, types as allTy
8
8
  import { PnpmError } from '@pnpm/error';
9
9
  import { packlist } from '@pnpm/fs.packlist';
10
10
  import { logger } from '@pnpm/logger';
11
- import { createExportableManifest } from '@pnpm/releasing.exportable-manifest';
11
+ import { createExportableManifest, readReadmeFile } from '@pnpm/releasing.exportable-manifest';
12
+ import { changelogStorage, readPendingChangelog, renderChangelog } from '@pnpm/releasing.versioning';
12
13
  import { sortFilteredProjects } from '@pnpm/workspace.projects-sorter';
13
14
  import chalk from 'chalk';
14
15
  import pLimit from 'p-limit';
@@ -18,6 +19,7 @@ import { renderHelp } from 'render-help';
18
19
  import tar from 'tar-stream';
19
20
  import { glob } from 'tinyglobby';
20
21
  import validateNpmPackageName from 'validate-npm-package-name';
22
+ import { fetchPreviousChangelog } from './previousChangelog.js';
21
23
  import { runScriptsIfPresent } from './publish.js';
22
24
  const LICENSE_GLOB = 'LICEN{S,C}E{,.*}'; // cspell:disable-line
23
25
  export function rcOptionsTypes() {
@@ -216,6 +218,7 @@ export async function api(opts) {
216
218
  }
217
219
  const files = await packlist(dir, {
218
220
  manifest: publishManifest,
221
+ workspaceDir: opts.workspaceDir,
219
222
  });
220
223
  const filesMap = Object.fromEntries(files.map((file) => [`package/${file}`, path.join(dir, file)]));
221
224
  // cspell:disable-next-line
@@ -233,6 +236,16 @@ export async function api(opts) {
233
236
  }
234
237
  }));
235
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
+ }
236
249
  const destDir = packDestination
237
250
  ? (path.isAbsolute(packDestination) ? packDestination : path.join(dir, packDestination ?? '.'))
238
251
  : dir;
@@ -253,14 +266,19 @@ export async function api(opts) {
253
266
  const stat = await fs.promises.stat(source);
254
267
  return stat.size;
255
268
  }));
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'));
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'));
260
277
  if (!opts.dryRun) {
261
278
  await packPkg({
262
279
  destFile: path.join(destDir, tarballName),
263
280
  filesMap,
281
+ injectedEntries,
264
282
  modulesDir: path.join(opts.dir, 'node_modules'),
265
283
  packGzipLevel: opts.packGzipLevel,
266
284
  manifest: publishManifest,
@@ -282,18 +300,51 @@ export async function api(opts) {
282
300
  packedTarballPath = path.relative(opts.dir, path.join(dir, tarballName));
283
301
  }
284
302
  return {
285
- publishedManifest: publishManifest,
303
+ publishedManifest: await withRegistryReadme(publishManifest, dir),
286
304
  contents: packedContents,
287
305
  tarballPath: packedTarballPath,
288
306
  unpackedSize,
289
307
  };
290
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
+ }
291
324
  // True when a `package/<path>` tar key names the package manifest, which is
292
325
  // packed as a single serialized `package/package.json` entry and reported as
293
326
  // `package.json` in the contents listing regardless of the source file name.
294
327
  function isManifestEntry(name) {
295
328
  return name === 'package/package.json' || name === 'package/package.json5' || name === 'package/package.yaml';
296
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
+ }
297
348
  function stripBuildMetadata(version) {
298
349
  const plusIndex = version.indexOf('+');
299
350
  return plusIndex === -1 ? version : version.slice(0, plusIndex);
@@ -311,7 +362,7 @@ function preventBundledDependenciesWithoutHoistedNodeLinker(nodeLinker, manifest
311
362
  }
312
363
  }
313
364
  async function packPkg(opts) {
314
- const { destFile, filesMap, bins, manifest, } = opts;
365
+ const { destFile, filesMap, injectedEntries, bins, manifest, } = opts;
315
366
  const mtime = new Date('1985-10-26T08:15:00.000Z');
316
367
  const pack = tar.pack();
317
368
  await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
@@ -323,6 +374,9 @@ async function packPkg(opts) {
323
374
  }
324
375
  pack.entry({ mode, mtime, name }, fs.readFileSync(source));
325
376
  }));
377
+ for (const [name, content] of Object.entries(injectedEntries ?? {})) {
378
+ pack.entry({ mode: 0o644, mtime, name }, content);
379
+ }
326
380
  const tarball = fs.createWriteStream(destFile);
327
381
  pack.pipe(createGzip({ level: opts.packGzipLevel })).pipe(tarball);
328
382
  pack.finalize();
@@ -0,0 +1,19 @@
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;
@@ -0,0 +1,182 @@
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
+ async function fetchPackument(client, opts, pkgName) {
58
+ const registry = pickRegistryForPackage(opts.registries, pkgName);
59
+ let fetchResult;
60
+ try {
61
+ fetchResult = await fetchMetadataFromFromRegistry({
62
+ fetch: client.fetch,
63
+ retry: {
64
+ factor: opts.fetchRetryFactor,
65
+ maxTimeout: opts.fetchRetryMaxtimeout,
66
+ minTimeout: opts.fetchRetryMintimeout,
67
+ retries: opts.fetchRetries,
68
+ },
69
+ timeout: opts.fetchTimeout ?? 60000,
70
+ fetchWarnTimeoutMs: 10000,
71
+ }, pkgName, {
72
+ registry,
73
+ authHeaderValue: client.getAuthHeader(registry, { pkgName }),
74
+ fullMetadata: false,
75
+ });
76
+ }
77
+ catch (err) {
78
+ // A package with no published version yet has no changelog to build on.
79
+ if (err != null && typeof err === 'object' && 'code' in err && err.code === 'ERR_PNPM_FETCH_404') {
80
+ return undefined;
81
+ }
82
+ throw err;
83
+ }
84
+ return fetchResult.notModified ? undefined : fetchResult.meta;
85
+ }
86
+ /** Highest published version of the package that is semver-lower than `version`. */
87
+ function pickPreviousVersion(meta, version) {
88
+ const candidates = Object.keys(meta.versions).filter((candidate) => valid(candidate) != null && lt(candidate, version));
89
+ return candidates.length > 0 ? rsort(candidates)[0] : undefined;
90
+ }
91
+ async function downloadTarballChangelog(client, pkgName, meta, version) {
92
+ const tarballUrl = meta.versions[version]?.dist?.tarball;
93
+ if (tarballUrl == null)
94
+ return undefined;
95
+ const response = await client.fetch(tarballUrl, { authHeaderValue: client.getAuthHeader(tarballUrl, { pkgName }) });
96
+ if (!response.ok) {
97
+ throw new PnpmError('CHANGELOG_TARBALL_FETCH_FAILED', `Failed to download ${pkgName}@${version} tarball (${response.status}) to compose the changelog: ${tarballUrl}`);
98
+ }
99
+ const tarballData = await readCapped(response, MAX_TARBALL_BYTES);
100
+ if (tarballData == null)
101
+ return undefined;
102
+ return extractTarballEntry(tarballData, CHANGELOG_ENTRY);
103
+ }
104
+ /**
105
+ * Reads a response body into a buffer, stopping (and returning `undefined`) as
106
+ * soon as it exceeds `maxBytes` — bounding the actual download rather than
107
+ * trusting a `content-length` header, which may be absent or lie.
108
+ */
109
+ async function readCapped(response, maxBytes) {
110
+ const reader = response.body?.getReader();
111
+ if (reader == null) {
112
+ const buffer = Buffer.from(await response.arrayBuffer());
113
+ return buffer.byteLength > maxBytes ? undefined : buffer;
114
+ }
115
+ const chunks = [];
116
+ let total = 0;
117
+ for (;;) {
118
+ // Streaming a body is inherently sequential — each read must await the
119
+ // previous chunk — so the successive awaits here are intentional.
120
+ // eslint-disable-next-line no-await-in-loop
121
+ const { done, value } = await reader.read();
122
+ if (done)
123
+ break;
124
+ total += value.byteLength;
125
+ if (total > maxBytes) {
126
+ // Best-effort cleanup: a cancel failure must not turn the over-cap path
127
+ // into a hard error — the caller just gets no changelog either way.
128
+ // eslint-disable-next-line no-await-in-loop
129
+ await reader.cancel().catch(() => { });
130
+ return undefined;
131
+ }
132
+ chunks.push(Buffer.from(value));
133
+ }
134
+ return Buffer.concat(chunks);
135
+ }
136
+ /**
137
+ * Reads one entry's contents out of a (optionally gzipped) tarball buffer.
138
+ * Composing the changelog is best-effort, so any decode/parse failure —
139
+ * including a gzip bomb tripping the inflate cap — resolves to `undefined`
140
+ * (no history prepend) rather than throwing.
141
+ */
142
+ async function extractTarballEntry(tarballData, entryName) {
143
+ const tarData = gunzipCapped(tarballData);
144
+ if (tarData == null)
145
+ return undefined;
146
+ const extract = tar.extract();
147
+ return new Promise((resolve) => {
148
+ let contents;
149
+ extract.on('entry', (header, stream, next) => {
150
+ if (header.name !== entryName) {
151
+ stream.resume();
152
+ stream.on('end', next);
153
+ stream.on('error', () => resolve(undefined));
154
+ return;
155
+ }
156
+ const chunks = [];
157
+ stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
158
+ stream.on('error', () => resolve(undefined));
159
+ stream.on('end', () => {
160
+ contents = Buffer.concat(chunks).toString('utf8');
161
+ next();
162
+ });
163
+ });
164
+ extract.on('error', () => resolve(undefined));
165
+ extract.on('finish', () => resolve(contents));
166
+ extract.end(tarData);
167
+ });
168
+ }
169
+ /**
170
+ * Inflates a published `.tgz`, capped at `MAX_TARBALL_BYTES` of output so a
171
+ * gzip bomb throws rather than ballooning memory. Returns `undefined` on that
172
+ * cap or any other decode failure — the caller treats it as no changelog.
173
+ */
174
+ function gunzipCapped(tarballData) {
175
+ try {
176
+ return gunzipSync(tarballData, { maxOutputLength: MAX_TARBALL_BYTES });
177
+ }
178
+ catch {
179
+ return undefined;
180
+ }
181
+ }
182
+ //# sourceMappingURL=previousChangelog.js.map
@@ -16,7 +16,7 @@ export declare function handler(opts: Omit<PublishRecursiveOpts, 'workspaceDir'>
16
16
  json?: boolean;
17
17
  recursive?: boolean;
18
18
  workspaceDir?: string;
19
- } & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'skipManifestObfuscation'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<{
19
+ } & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'skipManifestObfuscation' | 'versioning'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<{
20
20
  exitCode?: number;
21
21
  output?: string;
22
22
  } | undefined>;
@@ -36,5 +36,5 @@ export declare function publish(opts: Omit<PublishRecursiveOpts, 'workspaceDir'>
36
36
  engineStrict?: boolean;
37
37
  recursive?: boolean;
38
38
  workspaceDir?: string;
39
- } & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel' | 'skipManifestObfuscation'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<PublishResult>;
39
+ } & Pick<Config, 'bin' | 'gitChecks' | 'ignoreScripts' | 'pnpmHomeDir' | 'publishBranch' | 'embedReadme' | 'packGzipLevel' | 'skipManifestObfuscation' | 'versioning'> & Pick<ConfigContext, 'allProjects'>, params: string[]): Promise<PublishResult>;
40
40
  export declare function runScriptsIfPresent(opts: RunLifecycleHookOptions, scriptNames: string[], manifest: ProjectManifest): Promise<void>;
@@ -11,7 +11,7 @@ import { pick } from 'ramda';
11
11
  import { realpathMissing } from 'realpath-missing';
12
12
  import { renderHelp } from 'render-help';
13
13
  import { temporaryDirectory } from 'tempy';
14
- import { extractManifestFromPacked, isTarballPath } from './extractManifestFromPacked.js';
14
+ import { extractPublishManifestFromPacked, isTarballPath } from './extractManifestFromPacked.js';
15
15
  import { optionsWithOtpEnv } from './otpEnv.js';
16
16
  import * as pack from './pack.js';
17
17
  import { publishPackedPkg } from './publishPackedPkg.js';
@@ -188,7 +188,7 @@ export async function publish(opts, params) {
188
188
  const dirInParams = (params.length > 0) ? params[0] : undefined;
189
189
  if (dirInParams != null && isTarballPath(dirInParams)) {
190
190
  const tarballPath = dirInParams;
191
- const publishedManifest = await extractManifestFromPacked(tarballPath);
191
+ const publishedManifest = await extractPublishManifestFromPacked(tarballPath);
192
192
  // Publishing a pre-built tarball bypasses `pack.api()`, so we don't have the file listing
193
193
  // or unpacked size — those summary fields are reported as empty/zero.
194
194
  const publishSummary = await publishPackedPkg({
@@ -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' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
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' | 'versioning'>> & Partial<Pick<ConfigContext, 'selectedProjectsGraph' | 'allProjectsGraph' | 'prodAllProjectsGraph' | 'prodOnlySelectedProjectDirs'>> & {
4
4
  access?: 'public' | 'restricted';
5
5
  argv: {
6
6
  original: string[];
package/lib/stage/list.js CHANGED
@@ -4,6 +4,9 @@ import { parseStagePackageSpec } from './parsing.js';
4
4
  import { renderStageItem } from './rendering.js';
5
5
  import { stageJsonRequest } from './request.js';
6
6
  const PER_PAGE = 100;
7
+ // Fail-safe bound on the pagination loop, so a registry that keeps answering
8
+ // full pages with an inflated `total` cannot drive it forever.
9
+ const MAX_PAGES = 1000;
7
10
  export async function stageList(opts, params) {
8
11
  const packageFilter = parsePackageFilter(params[0]);
9
12
  const context = createStageContext(opts, packageFilter);
@@ -22,6 +25,8 @@ export async function stageList(opts, params) {
22
25
  if (items.length >= res.total || res.items.length < PER_PAGE)
23
26
  break;
24
27
  page++;
28
+ if (page >= MAX_PAGES)
29
+ break;
25
30
  }
26
31
  if (opts.json)
27
32
  return JSON.stringify(items, null, 2);
@@ -1,12 +1,14 @@
1
1
  import { type Config } from '@pnpm/config.reader';
2
- import type { ProjectsGraph } from '@pnpm/types';
2
+ import type { Project, ProjectsGraph } from '@pnpm/types';
3
3
  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
7
  interface VersionHandlerOptions extends Config {
8
+ allProjects?: Project[];
8
9
  allowSameVersion?: boolean;
9
10
  commitHooks?: boolean;
11
+ dryRun?: boolean;
10
12
  gitChecks?: boolean;
11
13
  gitTagVersion?: boolean;
12
14
  json?: boolean;