@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.
- package/CHANGELOG.md +996 -0
- package/lib/change/index.d.ts +41 -0
- package/lib/change/index.js +245 -0
- package/lib/deploy/createDeployFiles.d.ts +5 -2
- package/lib/deploy/createDeployFiles.js +42 -5
- package/lib/deploy/deploy.js +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/lane/index.d.ts +24 -0
- package/lib/lane/index.js +157 -0
- package/lib/publish/batchPublish.js +1 -1
- package/lib/publish/extractManifestFromPacked.d.ts +7 -0
- package/lib/publish/extractManifestFromPacked.js +55 -16
- package/lib/publish/otp.js +8 -5
- package/lib/publish/pack.d.ts +1 -1
- package/lib/publish/pack.js +61 -7
- package/lib/publish/previousChangelog.d.ts +19 -0
- package/lib/publish/previousChangelog.js +182 -0
- package/lib/publish/publish.d.ts +2 -2
- package/lib/publish/publish.js +2 -2
- package/lib/publish/recursivePublish.d.ts +1 -1
- package/lib/stage/list.js +5 -0
- package/lib/version/index.d.ts +3 -1
- package/lib/version/index.js +90 -1
- package/package.json +48 -43
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
resolve(text);
|
|
80
|
+
const text = Buffer.concat(chunks).toString();
|
|
81
|
+
if (isManifest) {
|
|
82
|
+
manifest = text;
|
|
46
83
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
101
|
+
return promise;
|
|
63
102
|
}
|
|
64
103
|
export class PublishArchiveMissingManifestError extends PnpmError {
|
|
65
104
|
tarballPath;
|
package/lib/publish/otp.js
CHANGED
|
@@ -28,11 +28,14 @@ export async function publishWithOtpHandling({ context = SHARED_CONTEXT, manifes
|
|
|
28
28
|
return withOtpHandling({
|
|
29
29
|
context,
|
|
30
30
|
fetchOptions,
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
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
|
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' | '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
|
};
|
package/lib/publish/pack.js
CHANGED
|
@@ -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
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
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
|
package/lib/publish/publish.d.ts
CHANGED
|
@@ -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>;
|
package/lib/publish/publish.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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);
|
package/lib/version/index.d.ts
CHANGED
|
@@ -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;
|