@pnpm/releasing.commands 1100.7.0 → 1100.8.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/lib/change/index.d.ts +42 -0
  3. package/lib/change/index.js +250 -0
  4. package/lib/deploy/createDeployFiles.d.ts +26 -0
  5. package/lib/deploy/createDeployFiles.js +255 -0
  6. package/lib/deploy/deploy.d.ts +12 -0
  7. package/lib/deploy/deploy.js +398 -0
  8. package/lib/deploy/deployHook.d.ts +2 -0
  9. package/lib/deploy/deployHook.js +15 -0
  10. package/lib/deploy/index.d.ts +2 -0
  11. package/lib/deploy/index.js +3 -0
  12. package/lib/index.d.ts +7 -0
  13. package/lib/lane/index.d.ts +24 -0
  14. package/lib/lane/index.js +157 -0
  15. package/lib/pack-app/index.d.ts +2 -0
  16. package/lib/pack-app/index.js +3 -0
  17. package/lib/pack-app/packApp.d.ts +22 -0
  18. package/lib/pack-app/packApp.js +558 -0
  19. package/lib/publish/FailedToPublishError.d.ts +22 -0
  20. package/lib/publish/FailedToPublishError.js +40 -0
  21. package/lib/publish/batchPublish.d.ts +20 -0
  22. package/lib/publish/batchPublish.js +201 -0
  23. package/lib/publish/displayError.d.ts +1 -0
  24. package/lib/publish/displayError.js +23 -0
  25. package/lib/publish/executeTokenHelper.d.ts +4 -0
  26. package/lib/publish/executeTokenHelper.js +17 -0
  27. package/lib/publish/extractManifestFromPacked.d.ts +19 -0
  28. package/lib/publish/extractManifestFromPacked.js +110 -0
  29. package/lib/publish/index.d.ts +3 -0
  30. package/lib/publish/index.js +4 -0
  31. package/lib/publish/oidc/authToken.d.ts +73 -0
  32. package/lib/publish/oidc/authToken.js +97 -0
  33. package/lib/publish/oidc/idToken.d.ts +76 -0
  34. package/lib/publish/oidc/idToken.js +90 -0
  35. package/lib/publish/oidc/provenance.d.ts +73 -0
  36. package/lib/publish/oidc/provenance.js +91 -0
  37. package/lib/publish/otp.d.ts +38 -0
  38. package/lib/publish/otp.js +41 -0
  39. package/lib/publish/otpEnv.d.ts +7 -0
  40. package/lib/publish/otpEnv.js +5 -0
  41. package/lib/publish/pack.d.ts +34 -0
  42. package/lib/publish/pack.js +425 -0
  43. package/lib/publish/previousChangelog.d.ts +25 -0
  44. package/lib/publish/previousChangelog.js +194 -0
  45. package/lib/publish/publish.d.ts +40 -0
  46. package/lib/publish/publish.js +256 -0
  47. package/lib/publish/publishPackedPkg.d.ts +79 -0
  48. package/lib/publish/publishPackedPkg.js +298 -0
  49. package/lib/publish/recursivePublish.d.ts +18 -0
  50. package/lib/publish/recursivePublish.js +135 -0
  51. package/lib/publish/registryConfigKeys.d.ts +30 -0
  52. package/lib/publish/registryConfigKeys.js +50 -0
  53. package/lib/publish/utils/shared-context.d.ts +7 -0
  54. package/lib/publish/utils/shared-context.js +20 -0
  55. package/lib/publishedNames.d.ts +18 -0
  56. package/lib/publishedNames.js +29 -0
  57. package/lib/resolveUnpublishedDirs.d.ts +18 -0
  58. package/lib/resolveUnpublishedDirs.js +20 -0
  59. package/lib/stage/approve.d.ts +2 -0
  60. package/lib/stage/approve.js +14 -0
  61. package/lib/stage/context.d.ts +14 -0
  62. package/lib/stage/context.js +25 -0
  63. package/lib/stage/download.d.ts +2 -0
  64. package/lib/stage/download.js +32 -0
  65. package/lib/stage/errors.d.ts +15 -0
  66. package/lib/stage/errors.js +17 -0
  67. package/lib/stage/help.d.ts +1 -0
  68. package/lib/stage/help.js +84 -0
  69. package/lib/stage/index.d.ts +13 -0
  70. package/lib/stage/index.js +57 -0
  71. package/lib/stage/list.d.ts +2 -0
  72. package/lib/stage/list.js +49 -0
  73. package/lib/stage/parsing.d.ts +6 -0
  74. package/lib/stage/parsing.js +27 -0
  75. package/lib/stage/publish.d.ts +7 -0
  76. package/lib/stage/publish.js +38 -0
  77. package/lib/stage/reject.d.ts +2 -0
  78. package/lib/stage/reject.js +16 -0
  79. package/lib/stage/rendering.d.ts +11 -0
  80. package/lib/stage/rendering.js +50 -0
  81. package/lib/stage/request.d.ts +29 -0
  82. package/lib/stage/request.js +110 -0
  83. package/lib/stage/types.d.ts +38 -0
  84. package/lib/stage/types.js +3 -0
  85. package/lib/stage/view.d.ts +2 -0
  86. package/lib/stage/view.js +14 -0
  87. package/lib/tarball/index.d.ts +2 -0
  88. package/lib/tarball/index.js +3 -0
  89. package/lib/tarball/publishSummary.d.ts +47 -0
  90. package/lib/tarball/publishSummary.js +36 -0
  91. package/lib/tarball/safeTarballFilename.d.ts +8 -0
  92. package/lib/tarball/safeTarballFilename.js +21 -0
  93. package/lib/tarball/summarizeTarball.d.ts +12 -0
  94. package/lib/tarball/summarizeTarball.js +84 -0
  95. package/lib/version/index.d.ts +35 -0
  96. package/lib/version/index.js +364 -0
  97. package/package.json +49 -49
@@ -0,0 +1,425 @@
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 { normalizePackageName } from '../tarball/safeTarballFilename.js';
23
+ import { fetchPreviousChangelog } from './previousChangelog.js';
24
+ import { runScriptsIfPresent } from './publish.js';
25
+ const LICENSE_GLOB = 'LICEN{S,C}E{,.*}'; // cspell:disable-line
26
+ export function rcOptionsTypes() {
27
+ return {
28
+ ...cliOptionsTypes(),
29
+ ...pick([
30
+ 'npm-path',
31
+ 'skip-manifest-obfuscation',
32
+ ], allTypes),
33
+ };
34
+ }
35
+ export function cliOptionsTypes() {
36
+ return {
37
+ out: String,
38
+ recursive: Boolean,
39
+ ...pick([
40
+ 'dry-run',
41
+ 'pack-destination',
42
+ 'pack-gzip-level',
43
+ 'json',
44
+ 'skip-manifest-obfuscation',
45
+ 'workspace-concurrency',
46
+ ], allTypes),
47
+ };
48
+ }
49
+ export const commandNames = ['pack'];
50
+ export function help() {
51
+ return renderHelp({
52
+ description: 'Create a tarball from a package',
53
+ usages: ['pnpm pack'],
54
+ descriptionLists: [
55
+ {
56
+ title: 'Options',
57
+ list: [
58
+ {
59
+ description: 'Does everything `pnpm pack` would do except actually writing the tarball to disk.',
60
+ name: '--dry-run',
61
+ },
62
+ {
63
+ description: 'Directory in which `pnpm pack` will save tarballs. The default is the current working directory.',
64
+ name: '--pack-destination <dir>',
65
+ },
66
+ {
67
+ description: 'Prints the packed tarball and contents in the json format.',
68
+ name: '--json',
69
+ },
70
+ {
71
+ 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`.',
72
+ name: '--out <path>',
73
+ },
74
+ {
75
+ description: 'Pack all packages from the workspace',
76
+ name: '--recursive',
77
+ shortAlias: '-r',
78
+ },
79
+ {
80
+ 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.',
81
+ name: '--skip-manifest-obfuscation',
82
+ },
83
+ {
84
+ description: `Set the maximum number of concurrency. Default is ${getDefaultWorkspaceConcurrency()}. For unlimited concurrency use Infinity.`,
85
+ name: '--workspace-concurrency <number>',
86
+ },
87
+ ],
88
+ },
89
+ FILTERING,
90
+ ],
91
+ });
92
+ }
93
+ export async function handler(opts) {
94
+ const packedPackages = [];
95
+ if (opts.recursive) {
96
+ const selectedProjectsGraph = opts.selectedProjectsGraph;
97
+ const pkgsToPack = [];
98
+ for (const { package: pkg } of Object.values(selectedProjectsGraph)) {
99
+ if (pkg.manifest.name && pkg.manifest.version) {
100
+ pkgsToPack.push(pkg);
101
+ }
102
+ }
103
+ const packedPkgDirs = new Set(pkgsToPack.map(({ rootDir }) => rootDir));
104
+ if (packedPkgDirs.size === 0) {
105
+ logger.info({
106
+ message: 'There are no packages that should be packed',
107
+ prefix: opts.dir,
108
+ });
109
+ }
110
+ const chunks = sortFilteredProjects({
111
+ selectedProjectsGraph,
112
+ allProjectsGraph: opts.allProjectsGraph,
113
+ prodAllProjectsGraph: opts.prodAllProjectsGraph,
114
+ prodOnlySelectedProjectDirs: opts.prodOnlySelectedProjectDirs,
115
+ });
116
+ const limitPack = pLimit(getWorkspaceConcurrency(opts.workspaceConcurrency));
117
+ const resolvedOpts = { ...opts };
118
+ if (opts.out) {
119
+ resolvedOpts.out = path.resolve(opts.dir, opts.out);
120
+ }
121
+ else if (opts.packDestination) {
122
+ resolvedOpts.packDestination = path.resolve(opts.dir, opts.packDestination);
123
+ }
124
+ else {
125
+ resolvedOpts.packDestination = path.resolve(opts.dir);
126
+ }
127
+ for (const chunk of chunks) {
128
+ // eslint-disable-next-line no-await-in-loop
129
+ await Promise.all(chunk.map(pkgDir => limitPack(async () => {
130
+ if (!packedPkgDirs.has(pkgDir))
131
+ return;
132
+ const pkg = selectedProjectsGraph[pkgDir].package;
133
+ const packResult = await api({
134
+ ...resolvedOpts,
135
+ dir: pkg.rootDir,
136
+ });
137
+ packedPackages.push(toPackResultJson(packResult));
138
+ })));
139
+ }
140
+ }
141
+ else {
142
+ const packResult = await api(opts);
143
+ packedPackages.push(toPackResultJson(packResult));
144
+ }
145
+ if (opts.json) {
146
+ return JSON.stringify(packedPackages.length > 1 ? packedPackages : packedPackages[0], null, 2);
147
+ }
148
+ return packedPackages.map(({ name, version, filename, files }) => `${opts.unicode ? '📦 ' : 'package:'} ${name}@${version}
149
+ ${chalk.blueBright('Tarball Contents')}
150
+ ${files.map(({ path }) => path).join('\n')}
151
+ ${chalk.blueBright('Tarball Details')}
152
+ ${filename}`).join('\n\n');
153
+ }
154
+ export async function api(opts) {
155
+ const { manifest: entryManifest, fileName: manifestFileName } = await readProjectManifest(opts.dir, opts);
156
+ preventBundledDependenciesWithoutHoistedNodeLinker(opts.nodeLinker, entryManifest);
157
+ const _runScriptsIfPresent = runScriptsIfPresent.bind(null, {
158
+ depPath: opts.dir,
159
+ extraBinPaths: opts.extraBinPaths,
160
+ extraEnv: opts.extraEnv,
161
+ pkgRoot: opts.dir,
162
+ rootModulesDir: await realpathMissing(path.join(opts.dir, 'node_modules')),
163
+ stdio: 'inherit',
164
+ unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
165
+ userAgent: opts.userAgent,
166
+ });
167
+ if (!opts.ignoreScripts) {
168
+ await _runScriptsIfPresent([
169
+ 'prepack',
170
+ 'prepare',
171
+ ], entryManifest);
172
+ }
173
+ const dir = entryManifest.publishConfig?.directory
174
+ ? path.join(opts.dir, entryManifest.publishConfig.directory)
175
+ : opts.dir;
176
+ // always read the latest manifest, as "prepack" or "prepare" script may modify package manifest.
177
+ const { manifest } = await readProjectManifest(dir, opts);
178
+ preventBundledDependenciesWithoutHoistedNodeLinker(opts.nodeLinker, manifest);
179
+ if (!manifest.name) {
180
+ throw new PnpmError('PACKAGE_NAME_NOT_FOUND', `Package name is not defined in the ${manifestFileName}.`);
181
+ }
182
+ if (!validateNpmPackageName(manifest.name).validForOldPackages) {
183
+ throw new PnpmError('INVALID_PACKAGE_NAME', `Invalid package name "${manifest.name}".`);
184
+ }
185
+ if (!manifest.version) {
186
+ throw new PnpmError('PACKAGE_VERSION_NOT_FOUND', `Package version is not defined in the ${manifestFileName}.`);
187
+ }
188
+ const publishManifest = await createPublishManifest({
189
+ projectDir: dir,
190
+ modulesDir: path.join(opts.dir, 'node_modules'),
191
+ manifest,
192
+ embedReadme: opts.embedReadme,
193
+ catalogs: opts.catalogs ?? {},
194
+ hooks: opts.hooks,
195
+ skipManifestObfuscation: opts.skipManifestObfuscation,
196
+ });
197
+ // Strip semver build metadata (the `+<build>` segment) from the published version so that
198
+ // the tarball, the manifest packed inside it, and the metadata sent to the registry all agree.
199
+ // libnpmpublish runs `semver.clean()` on `manifest.version` before computing the provenance
200
+ // subject, which removes build metadata. Leaving it in here would mismatch the version embedded
201
+ // in the tarball's package.json and cause the registry to reject the publish with a 422 when
202
+ // verifying the sigstore provenance bundle. See https://github.com/pnpm/pnpm/issues/11518.
203
+ publishManifest.version = stripBuildMetadata(publishManifest.version);
204
+ let tarballName;
205
+ let packDestination;
206
+ // Read back off the publish manifest so a `publishConfig.name` rename reaches
207
+ // the filename too — the tarball name, the packed manifest, and the registry
208
+ // metadata all name one artifact. The rename never went through the check on
209
+ // `manifest.name` above, so it is validated here: it lands in the tarball
210
+ // filename, where a separator would smuggle path components into the join and
211
+ // write outside the pack destination.
212
+ const publishedName = publishManifest.name || manifest.name;
213
+ if (!validateNpmPackageName(publishedName).validForOldPackages) {
214
+ throw new PnpmError('INVALID_PACKAGE_NAME', `Invalid package name "${publishedName}".`);
215
+ }
216
+ const normalizedName = normalizePackageName(publishedName);
217
+ if (opts.out) {
218
+ if (opts.packDestination) {
219
+ throw new PnpmError('INVALID_OPTION', 'Cannot use --pack-destination and --out together');
220
+ }
221
+ const preparedOut = opts.out.replaceAll('%s', normalizedName).replaceAll('%v', publishManifest.version);
222
+ const parsedOut = path.parse(preparedOut);
223
+ packDestination = parsedOut.dir ? parsedOut.dir : opts.packDestination;
224
+ tarballName = parsedOut.base;
225
+ }
226
+ else {
227
+ tarballName = `${normalizedName}-${publishManifest.version}.tgz`;
228
+ packDestination = opts.packDestination;
229
+ }
230
+ const files = await packlist(dir, {
231
+ manifest: publishManifest,
232
+ workspaceDir: opts.workspaceDir,
233
+ });
234
+ const filesMap = Object.fromEntries(files.map((file) => [`package/${file}`, path.join(dir, file)]));
235
+ // cspell:disable-next-line
236
+ if (opts.workspaceDir != null && dir !== opts.workspaceDir && !files.some((file) => /LICEN[CS]E(?:\..+)?/i.test(file))) {
237
+ const { workspaceDir } = opts;
238
+ const licenses = await glob([LICENSE_GLOB], { cwd: workspaceDir, expandDirectories: false });
239
+ await Promise.all(licenses.map(async (license) => {
240
+ const licensePath = path.join(workspaceDir, license);
241
+ // Only inject a regular file. A symlink could point outside the workspace and leak its
242
+ // target's bytes into the published tarball, so `lstat()` (which does not follow symlinks)
243
+ // rejects it — matching pacquet's inject_workspace_license.
244
+ const stats = await fs.promises.lstat(licensePath);
245
+ if (stats.isFile()) {
246
+ filesMap[`package/${license}`] = licensePath;
247
+ }
248
+ }));
249
+ }
250
+ // In `registry` changelog storage the package carries no committed
251
+ // CHANGELOG.md; its section was parked at `pnpm version -r` time and is
252
+ // composed here on top of the previously published version's changelog and
253
+ // packed in. A composed entry supersedes any stale committed CHANGELOG.md.
254
+ const injectedEntries = {};
255
+ const composedChangelog = await composeRegistryChangelog(opts, manifest.name, publishedName, manifest.version);
256
+ if (composedChangelog != null) {
257
+ delete filesMap['package/CHANGELOG.md'];
258
+ injectedEntries['package/CHANGELOG.md'] = composedChangelog;
259
+ }
260
+ const destDir = packDestination
261
+ ? (path.isAbsolute(packDestination) ? packDestination : path.join(dir, packDestination ?? '.'))
262
+ : dir;
263
+ if (!opts.dryRun) {
264
+ await fs.promises.mkdir(destDir, { recursive: true });
265
+ }
266
+ // Derive `contents` and `unpackedSize` from `filesMap` (the full set of tar entries) rather than
267
+ // from `files` (the packlist subset) so that:
268
+ // - workspace LICENSE files appended to `filesMap` after the packlist call are included; and
269
+ // - `package.yaml` / `package.json5` entries are reported under the name they actually have in
270
+ // the tar (`package.json`), since `packPkg()` rewrites them.
271
+ // The `stat()` pass must run before `postpack`, which may delete prepack-generated files that
272
+ // were packed. See https://github.com/pnpm/pnpm/issues/12775.
273
+ const sizes = await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
274
+ if (isManifestEntry(name)) {
275
+ return Buffer.byteLength(JSON.stringify(publishManifest, null, 2));
276
+ }
277
+ const stat = await fs.promises.stat(source);
278
+ return stat.size;
279
+ }));
280
+ const injectedSize = Object.values(injectedEntries).reduce((acc, content) => acc + Buffer.byteLength(content), 0);
281
+ const unpackedSize = sizes.reduce((acc, size) => acc + size, 0) + injectedSize;
282
+ const packedContents = Array.from(new Set([
283
+ ...Object.keys(filesMap).map((name) => isManifestEntry(name)
284
+ ? 'package.json'
285
+ : name.replace(/^package\//, '')),
286
+ ...Object.keys(injectedEntries).map((name) => name.replace(/^package\//, '')),
287
+ ])).sort((a, b) => a.localeCompare(b, 'en'));
288
+ if (!opts.dryRun) {
289
+ await packPkg({
290
+ destFile: path.join(destDir, tarballName),
291
+ filesMap,
292
+ injectedEntries,
293
+ modulesDir: path.join(opts.dir, 'node_modules'),
294
+ packGzipLevel: opts.packGzipLevel,
295
+ manifest: publishManifest,
296
+ bins: [
297
+ ...(await getBinsFromPackageManifest(publishManifest, dir)).map(({ path }) => path),
298
+ ...(manifest.publishConfig?.executableFiles ?? [])
299
+ .map((executableFile) => path.join(dir, executableFile)),
300
+ ],
301
+ });
302
+ if (!opts.ignoreScripts) {
303
+ await _runScriptsIfPresent(['postpack'], entryManifest);
304
+ }
305
+ }
306
+ let packedTarballPath;
307
+ if (opts.dir !== destDir) {
308
+ packedTarballPath = path.join(destDir, tarballName);
309
+ }
310
+ else {
311
+ packedTarballPath = path.relative(opts.dir, path.join(dir, tarballName));
312
+ }
313
+ return {
314
+ publishedManifest: await withRegistryReadme(publishManifest, dir),
315
+ contents: packedContents,
316
+ tarballPath: packedTarballPath,
317
+ unpackedSize,
318
+ };
319
+ }
320
+ /**
321
+ * The readme is always sent to the registry as package metadata, matching the npm CLI, so that
322
+ * registries can render it on the package page. The `embed-readme` setting only controls whether
323
+ * the readme is additionally written into the `package.json` inside the tarball (via
324
+ * `createExportableManifest`), which is why it is added to the returned manifest here rather than
325
+ * to the packed one.
326
+ */
327
+ async function withRegistryReadme(manifest, projectDir) {
328
+ if (manifest.readme != null)
329
+ return manifest;
330
+ const readme = await readReadmeFile(projectDir);
331
+ if (readme == null)
332
+ return manifest;
333
+ return { ...manifest, readme };
334
+ }
335
+ // True when a `package/<path>` tar key names the package manifest, which is
336
+ // packed as a single serialized `package/package.json` entry and reported as
337
+ // `package.json` in the contents listing regardless of the source file name.
338
+ function isManifestEntry(name) {
339
+ return name === 'package/package.json' || name === 'package/package.json5' || name === 'package/package.yaml';
340
+ }
341
+ /**
342
+ * The CHANGELOG.md to pack for a `registry`-storage release: its parked
343
+ * section (written at `pnpm version -r` time) rendered on top of the
344
+ * previously published version's changelog. `undefined` when storage is
345
+ * `repository`, there is no workspace, or the release has no parked section
346
+ * (an ordinary `pnpm pack` of a package that is not mid-release).
347
+ */
348
+ /**
349
+ * `pkgName` keys the parked section — the workspace, the ledger, and every
350
+ * intent address the project by its manifest name. `publishedName` is the only
351
+ * name the registry knows, so it selects the previous changelog to build on and
352
+ * titles the composed one.
353
+ */
354
+ async function composeRegistryChangelog(opts, pkgName, publishedName, version) {
355
+ if (changelogStorage(opts.versioning) !== 'registry' || opts.workspaceDir == null)
356
+ return undefined;
357
+ const section = await readPendingChangelog(opts.workspaceDir, pkgName, version);
358
+ if (section == null)
359
+ return undefined;
360
+ const previous = opts.registries != null
361
+ ? await fetchPreviousChangelog(opts, publishedName, version)
362
+ : undefined;
363
+ return renderChangelog(previous ?? null, publishedName, section);
364
+ }
365
+ function stripBuildMetadata(version) {
366
+ const plusIndex = version.indexOf('+');
367
+ return plusIndex === -1 ? version : version.slice(0, plusIndex);
368
+ }
369
+ function preventBundledDependenciesWithoutHoistedNodeLinker(nodeLinker, manifest) {
370
+ if (nodeLinker === 'hoisted')
371
+ return;
372
+ for (const key of ['bundledDependencies', 'bundleDependencies']) {
373
+ const bundledDependencies = manifest[key];
374
+ if (bundledDependencies) {
375
+ throw new PnpmError('BUNDLED_DEPENDENCIES_WITHOUT_HOISTED', `${key} does not work with "nodeLinker: ${nodeLinker}"`, {
376
+ hint: `Add "nodeLinker: hoisted" to pnpm-workspace.yaml or delete ${key} from the root package.json to resolve this error`,
377
+ });
378
+ }
379
+ }
380
+ }
381
+ async function packPkg(opts) {
382
+ const { destFile, filesMap, injectedEntries, bins, manifest, } = opts;
383
+ const mtime = new Date('1985-10-26T08:15:00.000Z');
384
+ const pack = tar.pack();
385
+ await Promise.all(Object.entries(filesMap).map(async ([name, source]) => {
386
+ const isExecutable = bins.some((bin) => path.relative(bin, source) === '');
387
+ const mode = isExecutable ? 0o755 : 0o644;
388
+ if (isManifestEntry(name)) {
389
+ pack.entry({ mode, mtime, name: 'package/package.json' }, JSON.stringify(manifest, null, 2));
390
+ return;
391
+ }
392
+ pack.entry({ mode, mtime, name }, fs.readFileSync(source));
393
+ }));
394
+ for (const [name, content] of Object.entries(injectedEntries ?? {})) {
395
+ pack.entry({ mode: 0o644, mtime, name }, content);
396
+ }
397
+ const tarball = fs.createWriteStream(destFile);
398
+ pack.pipe(createGzip({ level: opts.packGzipLevel })).pipe(tarball);
399
+ pack.finalize();
400
+ return new Promise((resolve, reject) => {
401
+ tarball.on('close', () => {
402
+ resolve();
403
+ }).on('error', reject);
404
+ });
405
+ }
406
+ async function createPublishManifest(opts) {
407
+ const { projectDir, embedReadme, modulesDir, manifest, catalogs, hooks, skipManifestObfuscation } = opts;
408
+ return createExportableManifest(projectDir, manifest, {
409
+ catalogs,
410
+ hooks,
411
+ embedReadme,
412
+ modulesDir,
413
+ skipManifestObfuscation,
414
+ });
415
+ }
416
+ function toPackResultJson(packResult) {
417
+ const { publishedManifest, contents, tarballPath } = packResult;
418
+ return {
419
+ name: publishedManifest.name,
420
+ version: publishedManifest.version,
421
+ filename: tarballPath,
422
+ files: contents.map((file) => ({ path: file })),
423
+ };
424
+ }
425
+ //# 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