@pnpm/releasing.commands 1100.4.4 → 1100.5.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.
@@ -20,4 +20,4 @@ export interface DeployFiles {
20
20
  manifest: ProjectManifest;
21
21
  workspaceManifest?: DeployWorkspaceManifest;
22
22
  }
23
- export declare function createDeployFiles({ allProjects, deployDir, lockfile, lockfileDir, patchedDependencies, selectedProjectManifest, projectId, rootProjectManifestDir, allowBuilds }: CreateDeployFilesOptions): DeployFiles;
23
+ export declare function createDeployFiles({ allProjects, deployDir, lockfile, lockfileDir, patchedDependencies, selectedProjectManifest, projectId, rootProjectManifestDir, allowBuilds, }: CreateDeployFilesOptions): DeployFiles;
@@ -0,0 +1,20 @@
1
+ import type { Config, ConfigContext } from '@pnpm/config.reader';
2
+ import type { Project } from '@pnpm/types';
3
+ import { type PublishSummary } from '../tarball/publishSummary.js';
4
+ import type { PublishRecursiveOpts } from './recursivePublish.js';
5
+ export declare const BATCH_PUBLISH_ENDPOINT = "/-/pnpm/v1/publish";
6
+ export type BatchPublishOptions = PublishRecursiveOpts & Pick<Config, 'embedReadme' | 'ignoreScripts' | 'nodeLinker' | 'packGzipLevel' | 'skipManifestObfuscation'> & Partial<Pick<ConfigContext, 'hooks'>>;
7
+ /**
8
+ * Publish all {@link pkgs} by sending a single `PUT /-/pnpm/v1/publish` request per target
9
+ * registry, instead of one request per package. The endpoint is not part of the standard npm
10
+ * registry API — the registry has to implement it explicitly (pnpr does), so this whole code
11
+ * path is opt-in via the `--batch` flag.
12
+ *
13
+ * Every package is packed (and its `prepublishOnly`/`prepublish` scripts run) before anything
14
+ * is sent, so a package that fails to pack aborts the publish before any package reaches the
15
+ * registry. The `publish`/`postpublish` scripts run only after the requests succeeded.
16
+ *
17
+ * @param pkgs packages to publish, already filtered (no private packages, no already-published
18
+ * versions) and in dependency order.
19
+ */
20
+ export declare function batchPublishPackages(pkgs: Project[], opts: BatchPublishOptions): Promise<PublishSummary[]>;
@@ -0,0 +1,201 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { PnpmError } from '@pnpm/error';
5
+ import { globalInfo, globalWarn } from '@pnpm/logger';
6
+ import { withOtpHandling } from '@pnpm/network.web-auth';
7
+ import { rimraf } from '@zkochan/rimraf';
8
+ import npmFetch from 'npm-registry-fetch';
9
+ import { realpathMissing } from 'realpath-missing';
10
+ import semver from 'semver';
11
+ import { temporaryDirectory } from 'tempy';
12
+ import { createPublishSummary } from '../tarball/publishSummary.js';
13
+ import * as pack from './pack.js';
14
+ import { runScriptsIfPresent } from './publish.js';
15
+ import { createPublishContext, createPublishOptions, findRegistryInfo, isPublishAccess, } from './publishPackedPkg.js';
16
+ export const BATCH_PUBLISH_ENDPOINT = '/-/pnpm/v1/publish';
17
+ /**
18
+ * Publish all {@link pkgs} by sending a single `PUT /-/pnpm/v1/publish` request per target
19
+ * registry, instead of one request per package. The endpoint is not part of the standard npm
20
+ * registry API — the registry has to implement it explicitly (pnpr does), so this whole code
21
+ * path is opt-in via the `--batch` flag.
22
+ *
23
+ * Every package is packed (and its `prepublishOnly`/`prepublish` scripts run) before anything
24
+ * is sent, so a package that fails to pack aborts the publish before any package reaches the
25
+ * registry. The `publish`/`postpublish` scripts run only after the requests succeeded.
26
+ *
27
+ * @param pkgs packages to publish, already filtered (no private packages, no already-published
28
+ * versions) and in dependency order.
29
+ */
30
+ export async function batchPublishPackages(pkgs, opts) {
31
+ if (opts.stage) {
32
+ throw new PnpmError('BATCH_PUBLISH_NO_STAGE', 'Staged publishing cannot be combined with --batch');
33
+ }
34
+ if (opts.provenance) {
35
+ throw new PnpmError('BATCH_PUBLISH_NO_PROVENANCE', 'Provenance statements cannot be generated when publishing with --batch', {
36
+ hint: 'Provenance is bound to a single package, but --batch sends many packages in one request. Publish without --batch to attach provenance.',
37
+ });
38
+ }
39
+ const packedByRegistry = new Map();
40
+ const packedPkgs = [];
41
+ for (const project of pkgs) {
42
+ // eslint-disable-next-line no-await-in-loop
43
+ const packedPkg = await packPkgForBatch(project, opts);
44
+ const publishConfigRegistry = typeof packedPkg.publishedManifest.publishConfig?.registry === 'string'
45
+ ? packedPkg.publishedManifest.publishConfig.registry
46
+ : undefined;
47
+ const { registry } = findRegistryInfo(packedPkg.publishedManifest, opts, publishConfigRegistry);
48
+ let group = packedByRegistry.get(registry);
49
+ if (!group) {
50
+ group = [];
51
+ packedByRegistry.set(registry, group);
52
+ }
53
+ group.push(packedPkg);
54
+ packedPkgs.push(packedPkg);
55
+ }
56
+ for (const [registry, group] of packedByRegistry.entries()) {
57
+ for (const { summary } of group) {
58
+ globalInfo(`📦 ${summary.id} → ${registry}`);
59
+ }
60
+ if (opts.dryRun) {
61
+ globalWarn(`Skip publishing ${group.length} package(s) to ${registry} (dry run)`);
62
+ continue;
63
+ }
64
+ // eslint-disable-next-line no-await-in-loop
65
+ await multiPublishToRegistry(registry, group, opts);
66
+ globalInfo(`✅ Published ${group.length} package(s) to ${registry} in a single request`);
67
+ }
68
+ if (!opts.ignoreScripts) {
69
+ for (const { project } of packedPkgs) {
70
+ // eslint-disable-next-line no-await-in-loop
71
+ await runScriptsIfPresent(await lifecycleOpts(project.rootDir, opts), ['publish', 'postpublish'], project.manifest);
72
+ }
73
+ }
74
+ return packedPkgs.map(({ summary }) => summary);
75
+ }
76
+ async function packPkgForBatch(project, opts) {
77
+ if (!opts.ignoreScripts) {
78
+ await runScriptsIfPresent(await lifecycleOpts(project.rootDir, opts), ['prepublishOnly', 'prepublish'], project.manifest);
79
+ }
80
+ // The tarball is packed into a temporary directory and read into memory right away — the
81
+ // request body carries it base64-encoded, so nothing needs to stay on disk.
82
+ const packDestination = temporaryDirectory();
83
+ try {
84
+ const packResult = await pack.api({
85
+ ...opts,
86
+ dir: project.rootDir,
87
+ packDestination,
88
+ dryRun: false,
89
+ });
90
+ const tarballData = await fs.readFile(packResult.tarballPath);
91
+ return {
92
+ project,
93
+ publishedManifest: packResult.publishedManifest,
94
+ tarballData,
95
+ summary: createPublishSummary(packResult, tarballData),
96
+ };
97
+ }
98
+ finally {
99
+ await rimraf(packDestination);
100
+ }
101
+ }
102
+ async function lifecycleOpts(pkgRoot, opts) {
103
+ return {
104
+ depPath: pkgRoot,
105
+ extraBinPaths: opts.extraBinPaths,
106
+ extraEnv: opts.extraEnv,
107
+ pkgRoot,
108
+ rootModulesDir: await realpathMissing(path.join(pkgRoot, 'node_modules')),
109
+ stdio: 'inherit',
110
+ unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
111
+ userAgent: opts.userAgent,
112
+ };
113
+ }
114
+ async function multiPublishToRegistry(registry, group, opts) {
115
+ // A package-scoped OIDC token cannot authorize a request that publishes many packages, so the
116
+ // OIDC exchange is skipped and only statically configured credentials are used.
117
+ const publishOptions = await createPublishOptions(group[0].publishedManifest, opts, { oidc: false });
118
+ const body = {
119
+ packages: group.map((packedPkg) => createPublishDocument(packedPkg, registry, opts)),
120
+ };
121
+ const fetchOptions = {
122
+ method: 'GET',
123
+ retry: {
124
+ factor: opts.fetchRetryFactor,
125
+ maxTimeout: opts.fetchRetryMaxtimeout,
126
+ minTimeout: opts.fetchRetryMintimeout,
127
+ retries: opts.fetchRetries,
128
+ },
129
+ timeout: opts.fetchTimeout,
130
+ };
131
+ try {
132
+ await withOtpHandling({
133
+ context: createPublishContext(opts),
134
+ fetchOptions,
135
+ operation: async (otp) => npmFetch(BATCH_PUBLISH_ENDPOINT, {
136
+ ...publishOptions,
137
+ access: publishOptions.access ?? undefined,
138
+ otp,
139
+ method: 'PUT',
140
+ body,
141
+ ignoreBody: true,
142
+ }),
143
+ });
144
+ }
145
+ catch (err) {
146
+ const statusCode = err.statusCode;
147
+ if (statusCode === 404 || statusCode === 405) {
148
+ throw new PnpmError('BATCH_PUBLISH_UNSUPPORTED', `The registry at ${registry} does not support publishing multiple packages in a single request`, {
149
+ hint: `Retry without the --batch flag, or publish to a registry that implements "PUT ${BATCH_PUBLISH_ENDPOINT}" (for example, pnpr).`,
150
+ });
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+ /**
156
+ * Build the npm publish document for one package — the same JSON body `libnpmpublish` would
157
+ * send as the whole `PUT /:pkg` request. The batch publish request body is an array of these,
158
+ * which lets the registry reuse its single-package publish pipeline per entry.
159
+ */
160
+ function createPublishDocument({ publishedManifest, tarballData }, registry, opts) {
161
+ const name = publishedManifest.name;
162
+ const version = semver.clean(publishedManifest.version);
163
+ if (!version) {
164
+ throw new PnpmError('BAD_SEMVER', `Invalid semver: ${publishedManifest.version}`);
165
+ }
166
+ const publishConfigAccess = publishedManifest.publishConfig?.access;
167
+ const access = opts.access ?? (isPublishAccess(publishConfigAccess) ? publishConfigAccess : null);
168
+ if (!name.startsWith('@') && access === 'restricted') {
169
+ throw new PnpmError('UNSCOPED_RESTRICTED', `Can't restrict access to the unscoped package ${name}`);
170
+ }
171
+ const tarballName = `${name}-${version}.tgz`;
172
+ const manifest = {
173
+ ...publishedManifest,
174
+ _id: `${name}@${version}`,
175
+ version,
176
+ _nodeVersion: process.versions.node,
177
+ dist: {
178
+ integrity: `sha512-${createHash('sha512').update(tarballData).digest('base64')}`,
179
+ shasum: createHash('sha1').update(tarballData).digest('hex'),
180
+ // libnpmpublish stores an http:// URL on purpose (clients fetch via HTTPS regardless when
181
+ // the registry is HTTPS); keep the wire format identical to a single-package publish.
182
+ tarball: new URL(`${name}/-/${tarballName}`, registry).href.replace(/^https:\/\//, 'http://'),
183
+ },
184
+ };
185
+ return {
186
+ _id: name,
187
+ name,
188
+ description: publishedManifest.description,
189
+ 'dist-tags': { [opts.tag ?? 'latest']: version },
190
+ versions: { [version]: manifest },
191
+ access,
192
+ _attachments: {
193
+ [tarballName]: {
194
+ content_type: 'application/octet-stream',
195
+ data: tarballData.toString('base64'),
196
+ length: tarballData.length,
197
+ },
198
+ },
199
+ };
200
+ }
201
+ //# sourceMappingURL=batchPublish.js.map
@@ -1,7 +1,7 @@
1
1
  import { PnpmError } from '@pnpm/error';
2
2
  import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest';
3
3
  declare const TARBALL_SUFFIXES: readonly ['.tar.gz', '.tgz'];
4
- export type TarballSuffix = (typeof TARBALL_SUFFIXES)[number];
4
+ export type TarballSuffix = typeof TARBALL_SUFFIXES[number];
5
5
  export type TarballPath = `${string}${TarballSuffix}`;
6
6
  export declare const isTarballPath: (path: string) => path is TarballPath;
7
7
  export declare function extractManifestFromPacked<Output = ExportedManifest>(tarballPath: TarballPath): Promise<Output>;
@@ -43,7 +43,7 @@ export interface AuthTokenParams {
43
43
  * @see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js#L112-L142 for npm's implementation.
44
44
  * @see https://github.com/yarnpkg/berry/blob/bafbef55/packages/plugin-npm/sources/npmHttpUtils.ts#L626-L641 for yarn's implementation.
45
45
  */
46
- export declare function fetchAuthToken({ context: { fetch }, options, idToken, packageName, registry }: AuthTokenParams): Promise<string>;
46
+ export declare function fetchAuthToken({ context: { fetch, }, options, idToken, packageName, registry, }: AuthTokenParams): Promise<string>;
47
47
  export declare abstract class AuthTokenError extends PnpmError {
48
48
  }
49
49
  export declare class AuthTokenFetchError extends AuthTokenError {
@@ -57,7 +57,7 @@ export interface IdTokenParams {
57
57
  * @see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js#L37-L110 for npm's implementation
58
58
  * @see https://github.com/yarnpkg/berry/blob/bafbef55/packages/plugin-npm/sources/npmHttpUtils.ts#L594-L624 for yarn's implementation
59
59
  */
60
- export declare function getIdToken({ context: { Date, ciInfo: { GITHUB_ACTIONS }, fetch, globalInfo, process: { env } }, options, registry }: IdTokenParams): Promise<string | undefined>;
60
+ export declare function getIdToken({ context: { Date, ciInfo: { GITHUB_ACTIONS }, fetch, globalInfo, process: { env }, }, options, registry, }: IdTokenParams): Promise<string | undefined>;
61
61
  export declare abstract class IdTokenError extends PnpmError {
62
62
  }
63
63
  export declare class IdTokenGitHubWorkflowIncorrectPermissionsError extends IdTokenError {
@@ -50,7 +50,7 @@ export interface ProvenanceParams {
50
50
  *
51
51
  * @see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js#L145-L164 for npm's implementation.
52
52
  */
53
- export declare function determineProvenance({ authToken, idToken, options, packageName, registry, context: { ciInfo: { GITHUB_ACTIONS, GITLAB }, fetch, process: { env } } }: ProvenanceParams): Promise<boolean | undefined>;
53
+ export declare function determineProvenance({ authToken, idToken, options, packageName, registry, context: { ciInfo: { GITHUB_ACTIONS, GITLAB }, fetch, process: { env }, }, }: ProvenanceParams): Promise<boolean | undefined>;
54
54
  export declare abstract class ProvenanceError extends PnpmError {
55
55
  }
56
56
  export declare class ProvenanceMalformedIdTokenError extends ProvenanceError {
@@ -35,4 +35,4 @@ export interface OtpParams {
35
35
  * @see https://github.com/npm/cli/blob/7d900c46/lib/utils/otplease.js for npm's implementation.
36
36
  * @see https://github.com/npm/npm-profile/blob/main/lib/index.js for the webauth polling flow.
37
37
  */
38
- export declare function publishWithOtpHandling({ context, manifest, publishOptions, tarballData }: OtpParams): Promise<OtpPublishResponse>;
38
+ export declare function publishWithOtpHandling({ context, manifest, publishOptions, tarballData, }: OtpParams): Promise<OtpPublishResponse>;
@@ -35,6 +35,7 @@ export function rcOptionsTypes() {
35
35
  export function cliOptionsTypes() {
36
36
  return {
37
37
  ...rcOptionsTypes(),
38
+ batch: Boolean,
38
39
  'dry-run': Boolean,
39
40
  force: Boolean,
40
41
  json: Boolean,
@@ -100,6 +101,10 @@ export function help() {
100
101
  name: '--recursive',
101
102
  shortAlias: '-r',
102
103
  },
104
+ {
105
+ description: 'Send all packages to the registry in a single request instead of one request per package. Requires --recursive and a registry that implements the "/-/pnpm/v1/publish" endpoint (for example, pnpr)',
106
+ name: '--batch',
107
+ },
103
108
  ],
104
109
  },
105
110
  FILTERING,
@@ -126,6 +131,11 @@ export async function handler(opts, params) {
126
131
  return result;
127
132
  }
128
133
  export async function publish(opts, params) {
134
+ if (opts.batch && !opts.recursive) {
135
+ throw new PnpmError('BATCH_PUBLISH_REQUIRES_RECURSIVE', '--batch can only be used together with --recursive', {
136
+ hint: 'Run "pnpm publish -r --batch" to publish all workspace packages in a single request.',
137
+ });
138
+ }
129
139
  if (opts.gitChecks !== false && await isGitRepo()) {
130
140
  if (!(await isWorkingTreeClean())) {
131
141
  throw new PnpmError('GIT_UNCLEAN', 'Unclean working tree. Commit or stash changes first.', {
@@ -1,9 +1,11 @@
1
1
  import type { Config } from '@pnpm/config.reader';
2
2
  import { PnpmError } from '@pnpm/error';
3
3
  import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest';
4
+ import { type RegistryConfig } from '@pnpm/types';
4
5
  import { type PublishSummary } from '../tarball/publishSummary.js';
5
6
  import { type OtpContext, type PublishOptionsWithDefaultAccess } from './otp.js';
6
7
  import type { PackResult } from './pack.js';
8
+ import { type NormalizedRegistryUrl } from './registryConfigKeys.js';
7
9
  export type { PublishSummary };
8
10
  export type PublishPackedPkgOptions = Pick<Config, 'configByUri' | 'dryRun' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchTimeout' | 'registries' | 'tag' | 'userAgent'> & Partial<Pick<Config, 'ca' | 'cert' | 'httpProxy' | 'httpsProxy' | 'key' | 'localAddress' | 'noProxy' | 'strictSsl'>> & {
9
11
  access?: 'public' | 'restricted';
@@ -27,10 +29,31 @@ type StagePublishOptions = PublishOptionsWithDefaultAccess & {
27
29
  stage?: boolean;
28
30
  };
29
31
  /**
30
- * @internal Exported for unit testing of the access / registry / auth fallback rules. Not part of the package's
31
- * public API.
32
+ * Build the libnpmpublish / npm-registry-fetch options (registry, auth, headers) for publishing
33
+ * {@link manifest}. When `oidc` is `false`, the per-package OIDC token exchange is skipped and only
34
+ * statically configured credentials are used — batch publish sends many packages in one request,
35
+ * which a package-scoped OIDC token cannot authorize.
36
+ *
37
+ * @internal Exported for unit testing of the access / registry / auth fallback rules and for batch
38
+ * publish. Not part of the package's public API.
39
+ */
40
+ export declare function createPublishOptions(manifest: ExportedManifest, options: PublishPackedPkgOptions, { oidc }?: {
41
+ oidc?: boolean;
42
+ }): Promise<StagePublishOptions>;
43
+ export declare function isPublishAccess(access: unknown): access is 'public' | 'restricted';
44
+ interface RegistryInfo {
45
+ registry: NormalizedRegistryUrl;
46
+ config: RegistryConfig;
47
+ }
48
+ /**
49
+ * Find credentials and SSL info for a package's registry.
50
+ * Follows {@link https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration}.
51
+ *
52
+ * The manifest's `publishConfig.registry`, when set, takes precedence over `registries`.
53
+ *
54
+ * @internal Exported for batch publish, which groups packages by their target registry.
32
55
  */
33
- export declare function createPublishOptions(manifest: ExportedManifest, options: PublishPackedPkgOptions): Promise<StagePublishOptions>;
56
+ export declare function findRegistryInfo({ name }: ExportedManifest, { configByUri, registries }: Pick<Config, 'configByUri' | 'registries'>, publishConfigRegistry?: string): Partial<RegistryInfo>;
34
57
  export declare class PublishUnsupportedRegistryProtocolError extends PnpmError {
35
58
  readonly registryUrl: string;
36
59
  constructor(registryUrl: string);
@@ -1,10 +1,9 @@
1
- import { createHash } from 'node:crypto';
2
1
  import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
2
  import { PnpmError } from '@pnpm/error';
5
3
  import { globalInfo, globalWarn } from '@pnpm/logger';
6
4
  import { createDispatchedFetch } from '@pnpm/network.fetch';
7
- import { extractBundledDependencies } from '../tarball/publishSummary.js';
5
+ import { DEFAULT_REGISTRY_SCOPE } from '@pnpm/types';
6
+ import { createPublishSummary } from '../tarball/publishSummary.js';
8
7
  import { displayError } from './displayError.js';
9
8
  import { executeTokenHelper } from './executeTokenHelper.js';
10
9
  import { createFailedToPublishError } from './FailedToPublishError.js';
@@ -22,21 +21,7 @@ export async function publishPackedPkg(packResult, opts) {
22
21
  const { registry } = publishOptions;
23
22
  const isStage = opts.stage === true;
24
23
  globalInfo(`📦 ${name}@${version} → ${registry ?? 'the default registry'}`);
25
- const summary = {
26
- id: `${name}@${version}`,
27
- name: name,
28
- version: version,
29
- size: tarballData.byteLength,
30
- unpackedSize,
31
- // SHA-1 is what `npm publish --json` reports as `shasum` for back-compat with the registry's
32
- // legacy dist.shasum field; `integrity` below is the modern SRI hash.
33
- shasum: createHash('sha1').update(tarballData).digest('hex'),
34
- integrity: `sha512-${createHash('sha512').update(tarballData).digest('base64')}`,
35
- filename: path.basename(tarballPath),
36
- files: contents.map((file) => ({ path: file })),
37
- entryCount: contents.length,
38
- bundled: extractBundledDependencies(publishedManifest),
39
- };
24
+ const summary = createPublishSummary({ publishedManifest, tarballPath, contents, unpackedSize }, tarballData);
40
25
  if (opts.dryRun) {
41
26
  globalWarn(`Skip ${isStage ? 'staging' : 'publishing'} ${name}@${version} (dry run)`);
42
27
  return summary;
@@ -71,15 +56,21 @@ export function createPublishContext(opts) {
71
56
  };
72
57
  }
73
58
  /**
74
- * @internal Exported for unit testing of the access / registry / auth fallback rules. Not part of the package's
75
- * public API.
59
+ * Build the libnpmpublish / npm-registry-fetch options (registry, auth, headers) for publishing
60
+ * {@link manifest}. When `oidc` is `false`, the per-package OIDC token exchange is skipped and only
61
+ * statically configured credentials are used — batch publish sends many packages in one request,
62
+ * which a package-scoped OIDC token cannot authorize.
63
+ *
64
+ * @internal Exported for unit testing of the access / registry / auth fallback rules and for batch
65
+ * publish. Not part of the package's public API.
76
66
  */
77
- export async function createPublishOptions(manifest, options) {
67
+ export async function createPublishOptions(manifest, options, { oidc = true } = {}) {
78
68
  const publishConfigRegistry = typeof manifest.publishConfig?.registry === 'string'
79
69
  ? manifest.publishConfig.registry
80
70
  : undefined;
81
71
  const { registry, config } = findRegistryInfo(manifest, options, publishConfigRegistry);
82
- const { creds, tls } = config ?? {};
72
+ const tls = config?.tls;
73
+ const creds = config?.[DEFAULT_REGISTRY_SCOPE];
83
74
  const publishConfigAccess = manifest.publishConfig?.access;
84
75
  const access = options.access ?? (isPublishAccess(publishConfigAccess) ? publishConfigAccess : null);
85
76
  const { ci: isFromCI, fetchRetries, fetchRetryFactor, fetchRetryMaxtimeout, fetchRetryMintimeout, fetchTimeout: timeout, otp, provenance, provenanceFile, tag: defaultTag, userAgent, } = options;
@@ -102,6 +93,7 @@ export async function createPublishOptions(manifest, options) {
102
93
  provenance,
103
94
  provenanceFile,
104
95
  registry,
96
+ strictSSL: options.strictSsl, // npm-registry-fetch defaults to true; must be set explicitly to honour strictSsl: false
105
97
  userAgent,
106
98
  // Signal to the registry that the client supports web-based authentication.
107
99
  // Without this, the registry would never offer the web auth flow and would
@@ -121,21 +113,23 @@ export async function createPublishOptions(manifest, options) {
121
113
  publishOptions.stage = true;
122
114
  }
123
115
  if (registry) {
124
- // OIDC takes precedence over a configured static `_authToken`, mirroring the npm CLI's
125
- // behavior (see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js). Trusted
126
- // publishing wins whenever the registry has it configured for the package; the static
127
- // token is used only as a fallback when OIDC is not applicable.
128
- const oidcTokenProvenance = await fetchTokenAndProvenanceByOidc(manifest.name, registry, options);
129
- if (oidcTokenProvenance?.authToken) {
130
- publishOptions.token = oidcTokenProvenance.authToken;
116
+ if (oidc) {
117
+ // OIDC takes precedence over a configured static `_authToken`, mirroring the npm CLI's
118
+ // behavior (see https://github.com/npm/cli/blob/7d900c46/lib/utils/oidc.js). Trusted
119
+ // publishing wins whenever the registry has it configured for the package; the static
120
+ // token is used only as a fallback when OIDC is not applicable.
121
+ const oidcTokenProvenance = await fetchTokenAndProvenanceByOidc(manifest.name, registry, options);
122
+ if (oidcTokenProvenance?.authToken) {
123
+ publishOptions.token = oidcTokenProvenance.authToken;
124
+ }
125
+ publishOptions.provenance ??= oidcTokenProvenance?.provenance;
131
126
  }
132
- publishOptions.provenance ??= oidcTokenProvenance?.provenance;
133
127
  appendAuthOptionsForRegistry(publishOptions, registry);
134
128
  }
135
129
  pruneUndefined(publishOptions);
136
130
  return publishOptions;
137
131
  }
138
- function isPublishAccess(access) {
132
+ export function isPublishAccess(access) {
139
133
  return access === 'public' || access === 'restricted';
140
134
  }
141
135
  /**
@@ -143,8 +137,10 @@ function isPublishAccess(access) {
143
137
  * Follows {@link https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration}.
144
138
  *
145
139
  * The manifest's `publishConfig.registry`, when set, takes precedence over `registries`.
140
+ *
141
+ * @internal Exported for batch publish, which groups packages by their target registry.
146
142
  */
147
- function findRegistryInfo({ name }, { configByUri, registries }, publishConfigRegistry) {
143
+ export function findRegistryInfo({ name }, { configByUri, registries }, publishConfigRegistry) {
148
144
  // eslint-disable-next-line regexp/no-unused-capturing-group
149
145
  const scopedMatches = /@(?<scope>[^/]+)\/(?<slug>[^/]+)/.exec(name);
150
146
  const registryName = scopedMatches?.groups ? `@${scopedMatches.groups.scope}` : 'default';
@@ -154,6 +150,7 @@ function findRegistryInfo({ name }, { configByUri, registries }, publishConfigRe
154
150
  throw new PublishUnsupportedRegistryProtocolError(nonNormalizedRegistry);
155
151
  }
156
152
  const { normalizedUrl: registry, longestConfigKey: initialRegistryConfigKey, } = supportedRegistryInfo;
153
+ const credsScope = registryName === 'default' ? DEFAULT_REGISTRY_SCOPE : registryName;
157
154
  let creds;
158
155
  let tls = {};
159
156
  for (const registryConfigKey of allRegistryConfigKeys(initialRegistryConfigKey)) {
@@ -161,13 +158,17 @@ function findRegistryInfo({ name }, { configByUri, registries }, publishConfigRe
161
158
  if (!entry)
162
159
  continue;
163
160
  // Auth from longer path collectively overrides shorter path
164
- creds ??= entry.creds;
161
+ creds ??= entry[credsScope] ?? entry[DEFAULT_REGISTRY_SCOPE];
165
162
  // TLS from longer path individually overrides shorter path
166
163
  tls = { ...entry.tls, ...tls };
167
164
  }
165
+ const config = { tls };
166
+ if (creds) {
167
+ config[DEFAULT_REGISTRY_SCOPE] = creds;
168
+ }
168
169
  return {
169
170
  registry,
170
- config: { creds, tls },
171
+ config,
171
172
  };
172
173
  }
173
174
  function extractToken({ authToken, tokenHelper, }) {
@@ -5,6 +5,7 @@ export type PublishRecursiveOpts = Required<Pick<Config, 'bin' | 'cacheDir' | 'd
5
5
  argv: {
6
6
  original: string[];
7
7
  };
8
+ batch?: boolean;
8
9
  reportSummary?: boolean;
9
10
  } & PublishPackedPkgOptions;
10
11
  export type RecursivePublishedPackage = PublishSummary | {
@@ -6,6 +6,7 @@ import { sortProjects } from '@pnpm/workspace.projects-sorter';
6
6
  import pFilter from 'p-filter';
7
7
  import { pick } from 'ramda';
8
8
  import { writeJsonFile } from 'write-json-file';
9
+ import { batchPublishPackages } from './batchPublish.js';
9
10
  import { publish } from './publish.js';
10
11
  export async function recursivePublish(opts) {
11
12
  const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package);
@@ -56,46 +57,55 @@ export async function recursivePublish(opts) {
56
57
  }
57
58
  const chunks = sortProjects(opts.selectedProjectsGraph);
58
59
  const tag = opts.tag ?? 'latest';
59
- const commandArgs = opts.stage ? ['stage', 'publish'] : ['publish'];
60
- for (const chunk of chunks) {
61
- // We can't run publish concurrently due to the npm CLI asking for OTP.
62
- // NOTE: If we solve the OTP issue, we still need to limit packages concurrency.
63
- // Otherwise, publishing will consume too much resources.
64
- // See related issue: https://github.com/pnpm/pnpm/issues/6968
65
- for (const pkgDir of chunk) {
66
- if (!publishedPkgDirs.has(pkgDir))
67
- continue;
68
- const pkg = opts.selectedProjectsGraph[pkgDir].package;
69
- const registry = pkg.manifest.publishConfig?.registry ?? pickRegistryForPackage(opts.registries, pkg.manifest.name);
70
- // eslint-disable-next-line no-await-in-loop
71
- const publishResult = await publish({
72
- ...opts,
73
- dir: pkg.rootDir,
74
- argv: {
75
- original: [
76
- ...commandArgs,
77
- '--tag',
78
- tag,
79
- '--registry',
80
- registry,
81
- ...appendedArgs,
82
- ],
83
- },
84
- gitChecks: false,
85
- recursive: false,
86
- }, [pkg.rootDir]);
87
- if (publishResult?.publishSummary != null) {
88
- publishedPackages.push(publishResult.publishSummary);
89
- }
90
- else {
91
- // Fallback for paths that don't produce a full PublishSummary (e.g. dry run via the
92
- // legacy npm-CLI bridge, or future call sites that bypass publishPackedPkg).
93
- const publishedManifest = publishResult?.publishedManifest ?? publishResult?.manifest;
94
- if (publishedManifest != null) {
95
- publishedPackages.push(pick(['name', 'version'], publishedManifest));
60
+ if (opts.batch) {
61
+ const sortedPkgs = chunks
62
+ .flat()
63
+ .filter((pkgDir) => publishedPkgDirs.has(pkgDir))
64
+ .map((pkgDir) => opts.selectedProjectsGraph[pkgDir].package);
65
+ publishedPackages.push(...await batchPublishPackages(sortedPkgs, { ...opts, tag }));
66
+ }
67
+ else {
68
+ const commandArgs = opts.stage ? ['stage', 'publish'] : ['publish'];
69
+ for (const chunk of chunks) {
70
+ // We can't run publish concurrently due to the npm CLI asking for OTP.
71
+ // NOTE: If we solve the OTP issue, we still need to limit packages concurrency.
72
+ // Otherwise, publishing will consume too much resources.
73
+ // See related issue: https://github.com/pnpm/pnpm/issues/6968
74
+ for (const pkgDir of chunk) {
75
+ if (!publishedPkgDirs.has(pkgDir))
76
+ continue;
77
+ const pkg = opts.selectedProjectsGraph[pkgDir].package;
78
+ const registry = pkg.manifest.publishConfig?.registry ?? pickRegistryForPackage(opts.registries, pkg.manifest.name);
79
+ // eslint-disable-next-line no-await-in-loop
80
+ const publishResult = await publish({
81
+ ...opts,
82
+ dir: pkg.rootDir,
83
+ argv: {
84
+ original: [
85
+ ...commandArgs,
86
+ '--tag',
87
+ tag,
88
+ '--registry',
89
+ registry,
90
+ ...appendedArgs,
91
+ ],
92
+ },
93
+ gitChecks: false,
94
+ recursive: false,
95
+ }, [pkg.rootDir]);
96
+ if (publishResult?.publishSummary != null) {
97
+ publishedPackages.push(publishResult.publishSummary);
96
98
  }
97
- else if (publishResult?.exitCode) {
98
- return { exitCode: publishResult.exitCode, publishedPackages };
99
+ else {
100
+ // Fallback for paths that don't produce a full PublishSummary (e.g. dry run via the
101
+ // legacy npm-CLI bridge, or future call sites that bypass publishPackedPkg).
102
+ const publishedManifest = publishResult?.publishedManifest ?? publishResult?.manifest;
103
+ if (publishedManifest != null) {
104
+ publishedPackages.push(pick(['name', 'version'], publishedManifest));
105
+ }
106
+ else if (publishResult?.exitCode) {
107
+ return { exitCode: publishResult.exitCode, publishedPackages };
108
+ }
99
109
  }
100
110
  }
101
111
  }
@@ -8,7 +8,7 @@ export function createStageContext(opts, packageName) {
8
8
  return {
9
9
  opts,
10
10
  registry,
11
- authHeaderValue: getAuthHeaderByUri(registry),
11
+ authHeaderValue: packageName ? getAuthHeaderByUri(registry, { pkgName: packageName }) : getAuthHeaderByUri(registry),
12
12
  fetchFromRegistry: createFetchFromRegistry(opts),
13
13
  };
14
14
  }
@@ -1,6 +1,6 @@
1
1
  import * as publishCommand from '../publish/publish.js';
2
2
  export declare const STAGE_SUBCOMMANDS: readonly ['publish', 'list', 'view', 'approve', 'reject', 'download'];
3
- export type StageSubcommand = (typeof STAGE_SUBCOMMANDS)[number];
3
+ export type StageSubcommand = typeof STAGE_SUBCOMMANDS[number];
4
4
  /**
5
5
  * Options accepted by every `pnpm stage` subcommand.
6
6
  *
@@ -1,3 +1,4 @@
1
+ import type { ExportedManifest } from '@pnpm/releasing.exportable-manifest';
1
2
  /**
2
3
  * Per-package summary describing a successful publish, modeled after `npm publish --json`.
3
4
  * Returned to callers and serialized to stdout when `pnpm publish --json` is used.
@@ -28,6 +29,13 @@ export interface PublishSummary {
28
29
  /** Staged publish identifier returned by the registry. Only present for staged publishes. */
29
30
  stageId?: string;
30
31
  }
32
+ export interface PackedPkgInfo {
33
+ publishedManifest: ExportedManifest;
34
+ tarballPath: string;
35
+ contents: string[];
36
+ unpackedSize: number;
37
+ }
38
+ export declare function createPublishSummary({ publishedManifest, tarballPath, contents, unpackedSize }: PackedPkgInfo, tarballData: Buffer): PublishSummary;
31
39
  /**
32
40
  * Normalize the two equivalent manifest keys (`bundledDependencies` and `bundleDependencies`)
33
41
  * into a flat list of dependency names, matching npm's interpretation.
@@ -1,3 +1,23 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ export function createPublishSummary({ publishedManifest, tarballPath, contents, unpackedSize }, tarballData) {
4
+ const { name, version } = publishedManifest;
5
+ return {
6
+ id: `${name}@${version}`,
7
+ name: name,
8
+ version: version,
9
+ size: tarballData.byteLength,
10
+ unpackedSize,
11
+ // SHA-1 is what `npm publish --json` reports as `shasum` for back-compat with the registry's
12
+ // legacy dist.shasum field; `integrity` below is the modern SRI hash.
13
+ shasum: createHash('sha1').update(tarballData).digest('hex'),
14
+ integrity: `sha512-${createHash('sha512').update(tarballData).digest('base64')}`,
15
+ filename: path.basename(tarballPath),
16
+ files: contents.map((file) => ({ path: file })),
17
+ entryCount: contents.length,
18
+ bundled: extractBundledDependencies(publishedManifest),
19
+ };
20
+ }
1
21
  /**
2
22
  * Normalize the two equivalent manifest keys (`bundledDependencies` and `bundleDependencies`)
3
23
  * into a flat list of dependency names, matching npm's interpretation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/releasing.commands",
3
- "version": "1100.4.4",
3
+ "version": "1100.5.0",
4
4
  "description": "Commands for deploy, pack, and publish",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -38,61 +38,62 @@
38
38
  "libnpmpublish": "^11.2.0",
39
39
  "normalize-path": "^3.0.0",
40
40
  "normalize-registry-url": "2.0.1",
41
+ "npm-registry-fetch": "^19.0.0",
41
42
  "p-filter": "^4.1.0",
42
43
  "p-limit": "^7.3.0",
43
44
  "ramda": "npm:@pnpm/ramda@0.28.1",
44
45
  "realpath-missing": "^2.0.0",
45
46
  "render-help": "^2.0.0",
46
- "semver": "^7.8.1",
47
+ "semver": "^7.8.4",
47
48
  "tar-stream": "^3.2.0",
48
49
  "tempy": "3.0.0",
49
50
  "tinyglobby": "^0.2.16",
50
51
  "validate-npm-package-name": "7.0.2",
51
52
  "write-json-file": "^7.0.0",
52
53
  "write-yaml-file": "^6.0.0",
53
- "@pnpm/bins.resolver": "1100.0.7",
54
+ "@pnpm/bins.resolver": "1100.0.8",
54
55
  "@pnpm/catalogs.types": "1100.0.0",
55
- "@pnpm/cli.common-cli-options-help": "1100.0.2",
56
- "@pnpm/cli.utils": "1101.0.11",
57
- "@pnpm/config.pick-registry-for-package": "1100.0.8",
58
- "@pnpm/config.reader": "1101.8.0",
56
+ "@pnpm/config.pick-registry-for-package": "1100.0.9",
57
+ "@pnpm/config.reader": "1101.9.0",
58
+ "@pnpm/engine.runtime.commands": "1100.1.5",
59
+ "@pnpm/deps.path": "1100.0.8",
59
60
  "@pnpm/constants": "1100.0.0",
60
- "@pnpm/deps.path": "1100.0.7",
61
- "@pnpm/engine.runtime.commands": "1100.1.4",
62
- "@pnpm/engine.runtime.node-resolver": "1101.1.6",
61
+ "@pnpm/engine.runtime.node-resolver": "1101.1.7",
63
62
  "@pnpm/error": "1100.0.0",
64
- "@pnpm/exec.lifecycle": "1100.0.17",
65
- "@pnpm/fetching.directory-fetcher": "1100.0.16",
66
- "@pnpm/exec.pnpm-cli-runner": "1100.0.1",
67
- "@pnpm/fs.indexed-pkg-importer": "1100.0.13",
63
+ "@pnpm/cli.utils": "1101.0.12",
64
+ "@pnpm/fs.indexed-pkg-importer": "1100.0.14",
65
+ "@pnpm/cli.common-cli-options-help": "1100.0.2",
68
66
  "@pnpm/fs.is-empty-dir-or-nothing": "1100.0.0",
69
- "@pnpm/fs.packlist": "1100.0.1",
70
- "@pnpm/installing.client": "1100.2.7",
71
- "@pnpm/lockfile.fs": "1100.1.4",
72
- "@pnpm/lockfile.types": "1100.0.10",
73
- "@pnpm/installing.commands": "1100.8.0",
74
- "@pnpm/network.auth-header": "1101.1.1",
75
- "@pnpm/network.fetch": "1100.1.2",
67
+ "@pnpm/installing.commands": "1100.9.0",
68
+ "@pnpm/exec.lifecycle": "1100.0.18",
69
+ "@pnpm/exec.pnpm-cli-runner": "1100.0.1",
70
+ "@pnpm/fetching.directory-fetcher": "1100.0.17",
71
+ "@pnpm/installing.client": "1100.2.8",
72
+ "@pnpm/lockfile.types": "1100.0.11",
73
+ "@pnpm/lockfile.fs": "1100.1.5",
74
+ "@pnpm/network.web-auth": "1101.1.1",
75
+ "@pnpm/releasing.exportable-manifest": "1100.1.6",
76
+ "@pnpm/resolving.resolver-base": "1100.4.2",
77
+ "@pnpm/network.fetch": "1100.1.3",
78
+ "@pnpm/workspace.projects-filter": "1100.0.21",
79
+ "@pnpm/network.auth-header": "1101.1.2",
80
+ "@pnpm/types": "1101.3.2",
76
81
  "@pnpm/network.git-utils": "1100.0.1",
77
- "@pnpm/network.web-auth": "1101.1.0",
78
- "@pnpm/releasing.exportable-manifest": "1100.1.5",
79
- "@pnpm/resolving.resolver-base": "1100.4.1",
80
- "@pnpm/types": "1101.3.1",
81
- "@pnpm/workspace.projects-filter": "1100.0.20",
82
- "@pnpm/workspace.projects-sorter": "1100.0.6"
82
+ "@pnpm/fs.packlist": "1100.0.1",
83
+ "@pnpm/workspace.projects-sorter": "1100.0.7"
83
84
  },
84
85
  "peerDependencies": {
85
- "@pnpm/logger": "^1001.0.1"
86
+ "@pnpm/logger": "^1100.0.0"
86
87
  },
87
88
  "devDependencies": {
88
- "@jest/globals": "30.3.0",
89
+ "@jest/globals": "30.4.1",
89
90
  "@types/cross-spawn": "^6.0.6",
90
91
  "@types/is-windows": "^1.0.2",
91
- "@types/libnpmpublish": "^9.0.1",
92
+ "@types/libnpmpublish": "^11.2.0",
93
+ "@types/npm-registry-fetch": "^8.0.9",
92
94
  "@types/proxyquire": "^1.3.31",
93
95
  "@types/ramda": "0.31.1",
94
96
  "@types/semver": "7.7.1",
95
- "@types/tar": "^7.0.87",
96
97
  "@types/tar-stream": "^3.1.4",
97
98
  "@types/validate-npm-package-name": "^4.0.2",
98
99
  "ci-info": "^4.4.0",
@@ -100,18 +101,18 @@
100
101
  "is-windows": "^1.0.2",
101
102
  "load-json-file": "^7.0.1",
102
103
  "tar": "^7.5.15",
103
- "undici": "^7.26.0",
104
+ "undici": "^7.27.2",
104
105
  "write-yaml-file": "^6.0.0",
105
- "@pnpm/assert-project": "1100.0.15",
106
- "@pnpm/catalogs.config": "1100.0.0",
107
- "@pnpm/hooks.pnpmfile": "1100.0.14",
108
106
  "@pnpm/logger": "1100.0.0",
109
- "@pnpm/prepare": "1100.0.15",
110
- "@pnpm/releasing.commands": "1100.4.4",
107
+ "@pnpm/hooks.pnpmfile": "1100.0.15",
108
+ "@pnpm/assert-project": "1100.0.16",
109
+ "@pnpm/prepare": "1100.0.16",
111
110
  "@pnpm/test-fixtures": "1100.0.0",
111
+ "@pnpm/releasing.commands": "1100.5.0",
112
112
  "@pnpm/test-ipc-server": "1100.0.0",
113
- "@pnpm/testing.command-defaults": "1100.0.5",
114
- "@pnpm/testing.registry-mock": "1100.0.5"
113
+ "@pnpm/testing.command-defaults": "1100.0.6",
114
+ "@pnpm/testing.registry-mock": "1100.0.6",
115
+ "@pnpm/catalogs.config": "1100.0.0"
115
116
  },
116
117
  "engines": {
117
118
  "node": ">=22.13"