@pnpm/releasing.commands 1000.0.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/LICENSE +22 -0
- package/lib/deploy/createDeployFiles.d.ts +23 -0
- package/lib/deploy/createDeployFiles.js +218 -0
- package/lib/deploy/deploy.d.ts +11 -0
- package/lib/deploy/deploy.js +270 -0
- package/lib/deploy/deployHook.d.ts +2 -0
- package/lib/deploy/deployHook.js +15 -0
- package/lib/deploy/index.d.ts +2 -0
- package/lib/deploy/index.js +3 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/publish/FailedToPublishError.d.ts +22 -0
- package/lib/publish/FailedToPublishError.js +40 -0
- package/lib/publish/displayError.d.ts +1 -0
- package/lib/publish/displayError.js +23 -0
- package/lib/publish/executeTokenHelper.d.ts +4 -0
- package/lib/publish/executeTokenHelper.js +14 -0
- package/lib/publish/extractManifestFromPacked.d.ts +12 -0
- package/lib/publish/extractManifestFromPacked.js +71 -0
- package/lib/publish/index.d.ts +3 -0
- package/lib/publish/index.js +4 -0
- package/lib/publish/oidc/authToken.d.ts +73 -0
- package/lib/publish/oidc/authToken.js +95 -0
- package/lib/publish/oidc/idToken.d.ts +76 -0
- package/lib/publish/oidc/idToken.js +85 -0
- package/lib/publish/oidc/provenance.d.ts +73 -0
- package/lib/publish/oidc/provenance.js +90 -0
- package/lib/publish/otp.d.ts +89 -0
- package/lib/publish/otp.js +165 -0
- package/lib/publish/otpEnv.d.ts +7 -0
- package/lib/publish/otpEnv.js +5 -0
- package/lib/publish/pack.d.ts +32 -0
- package/lib/publish/pack.js +303 -0
- package/lib/publish/publish.d.ts +31 -0
- package/lib/publish/publish.js +216 -0
- package/lib/publish/publishPackedPkg.d.ts +19 -0
- package/lib/publish/publishPackedPkg.js +233 -0
- package/lib/publish/recursivePublish.d.ts +12 -0
- package/lib/publish/recursivePublish.js +114 -0
- package/lib/publish/registryConfigKeys.d.ts +30 -0
- package/lib/publish/registryConfigKeys.js +50 -0
- package/lib/publish/utils/shared-context.d.ts +7 -0
- package/lib/publish/utils/shared-context.js +17 -0
- package/package.json +118 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { FILTERING } from '@pnpm/cli.common-cli-options-help';
|
|
3
|
+
import { docsUrl, readProjectManifest } from '@pnpm/cli.utils';
|
|
4
|
+
import { types as allTypes } from '@pnpm/config.reader';
|
|
5
|
+
import { PnpmError } from '@pnpm/error';
|
|
6
|
+
import { runLifecycleHook } from '@pnpm/exec.lifecycle';
|
|
7
|
+
import { getCurrentBranch, isGitRepo, isRemoteHistoryClean, isWorkingTreeClean } from '@pnpm/network.git-utils';
|
|
8
|
+
import { rimraf } from '@zkochan/rimraf';
|
|
9
|
+
import enquirer from 'enquirer';
|
|
10
|
+
import { pick } from 'ramda';
|
|
11
|
+
import { realpathMissing } from 'realpath-missing';
|
|
12
|
+
import { renderHelp } from 'render-help';
|
|
13
|
+
import { temporaryDirectory } from 'tempy';
|
|
14
|
+
import { extractManifestFromPacked, isTarballPath } from './extractManifestFromPacked.js';
|
|
15
|
+
import { optionsWithOtpEnv } from './otpEnv.js';
|
|
16
|
+
import * as pack from './pack.js';
|
|
17
|
+
import { publishPackedPkg } from './publishPackedPkg.js';
|
|
18
|
+
import { recursivePublish } from './recursivePublish.js';
|
|
19
|
+
export function rcOptionsTypes() {
|
|
20
|
+
return pick([
|
|
21
|
+
'access',
|
|
22
|
+
'git-checks',
|
|
23
|
+
'ignore-scripts',
|
|
24
|
+
'provenance',
|
|
25
|
+
'npm-path',
|
|
26
|
+
'otp',
|
|
27
|
+
'publish-branch',
|
|
28
|
+
'registry',
|
|
29
|
+
'tag',
|
|
30
|
+
'unsafe-perm',
|
|
31
|
+
'embed-readme',
|
|
32
|
+
], allTypes);
|
|
33
|
+
}
|
|
34
|
+
export function cliOptionsTypes() {
|
|
35
|
+
return {
|
|
36
|
+
...rcOptionsTypes(),
|
|
37
|
+
'dry-run': Boolean,
|
|
38
|
+
force: Boolean,
|
|
39
|
+
json: Boolean,
|
|
40
|
+
otp: String,
|
|
41
|
+
recursive: Boolean,
|
|
42
|
+
'report-summary': Boolean,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export const commandNames = ['publish'];
|
|
46
|
+
export function help() {
|
|
47
|
+
return renderHelp({
|
|
48
|
+
description: 'Publishes a package to the npm registry.',
|
|
49
|
+
descriptionLists: [
|
|
50
|
+
{
|
|
51
|
+
title: 'Options',
|
|
52
|
+
list: [
|
|
53
|
+
{
|
|
54
|
+
description: "Don't check if current branch is your publish branch, clean, and up to date",
|
|
55
|
+
name: '--no-git-checks',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
description: 'Sets branch name to publish. Default is master',
|
|
59
|
+
name: '--publish-branch',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: 'Does everything a publish would do except actually publishing to the registry',
|
|
63
|
+
name: '--dry-run',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
description: 'Show information in JSON format',
|
|
67
|
+
name: '--json',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
description: 'Registers the published package with the given tag. By default, the "latest" tag is used.',
|
|
71
|
+
name: '--tag <tag>',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
description: 'Tells the registry whether this package should be published as public or restricted',
|
|
75
|
+
name: '--access <public|restricted>',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
description: 'Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)',
|
|
79
|
+
name: '--ignore-scripts',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
description: 'Packages are proceeded to be published even if their current version is already in the registry. This is useful when a "prepublishOnly" script bumps the version of the package before it is published',
|
|
83
|
+
name: '--force',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
description: 'Save the list of the newly published packages to "pnpm-publish-summary.json". Useful when some other tooling is used to report the list of published packages.',
|
|
87
|
+
name: '--report-summary',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
description: 'When publishing packages that require two-factor authentication, this option can specify a one-time password',
|
|
91
|
+
name: '--otp',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
description: 'Publish all packages from the workspace',
|
|
95
|
+
name: '--recursive',
|
|
96
|
+
shortAlias: '-r',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
FILTERING,
|
|
101
|
+
],
|
|
102
|
+
url: docsUrl('publish'),
|
|
103
|
+
usages: ['pnpm publish [<tarball>|<dir>] [--tag <tag>] [--access <public|restricted>] [options]'],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const GIT_CHECKS_HINT = 'If you want to disable Git checks on publish, set the "git-checks" setting to "false", or run again with "--no-git-checks".';
|
|
107
|
+
export async function handler(opts, params) {
|
|
108
|
+
const result = await publish(opts, params);
|
|
109
|
+
if (result?.manifest)
|
|
110
|
+
return;
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
export async function publish(opts, params) {
|
|
114
|
+
if (opts.gitChecks !== false && await isGitRepo()) {
|
|
115
|
+
if (!(await isWorkingTreeClean())) {
|
|
116
|
+
throw new PnpmError('GIT_UNCLEAN', 'Unclean working tree. Commit or stash changes first.', {
|
|
117
|
+
hint: GIT_CHECKS_HINT,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const branches = opts.publishBranch ? [opts.publishBranch] : ['master', 'main'];
|
|
121
|
+
const currentBranch = await getCurrentBranch();
|
|
122
|
+
if (currentBranch === null) {
|
|
123
|
+
throw new PnpmError('GIT_UNKNOWN_BRANCH', `The Git HEAD may not attached to any branch, but your "publish-branch" is set to "${branches.join('|')}".`, {
|
|
124
|
+
hint: GIT_CHECKS_HINT,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!branches.includes(currentBranch)) {
|
|
128
|
+
const { confirm } = await enquirer.prompt({
|
|
129
|
+
message: `You're on branch "${currentBranch}" but your "publish-branch" is set to "${branches.join('|')}". \
|
|
130
|
+
Do you want to continue?`,
|
|
131
|
+
name: 'confirm',
|
|
132
|
+
type: 'confirm',
|
|
133
|
+
}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
134
|
+
if (!confirm) {
|
|
135
|
+
throw new PnpmError('GIT_NOT_CORRECT_BRANCH', `Branch is not on '${branches.join('|')}'.`, {
|
|
136
|
+
hint: GIT_CHECKS_HINT,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!(await isRemoteHistoryClean())) {
|
|
141
|
+
throw new PnpmError('GIT_NOT_LATEST', 'Remote history differs. Please pull changes.', {
|
|
142
|
+
hint: GIT_CHECKS_HINT,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (opts.recursive && (opts.selectedProjectsGraph != null)) {
|
|
147
|
+
const { exitCode } = await recursivePublish({
|
|
148
|
+
...opts,
|
|
149
|
+
selectedProjectsGraph: opts.selectedProjectsGraph,
|
|
150
|
+
workspaceDir: opts.workspaceDir ?? process.cwd(),
|
|
151
|
+
});
|
|
152
|
+
return { exitCode };
|
|
153
|
+
}
|
|
154
|
+
opts = optionsWithOtpEnv(opts, process.env);
|
|
155
|
+
const dirInParams = (params.length > 0) ? params[0] : undefined;
|
|
156
|
+
if (dirInParams != null && isTarballPath(dirInParams)) {
|
|
157
|
+
const tarballPath = dirInParams;
|
|
158
|
+
const publishedManifest = await extractManifestFromPacked(tarballPath);
|
|
159
|
+
await publishPackedPkg({
|
|
160
|
+
tarballPath,
|
|
161
|
+
publishedManifest,
|
|
162
|
+
}, opts);
|
|
163
|
+
return { exitCode: 0 };
|
|
164
|
+
}
|
|
165
|
+
const dir = dirInParams ?? opts.dir ?? process.cwd();
|
|
166
|
+
const _runScriptsIfPresent = runScriptsIfPresent.bind(null, {
|
|
167
|
+
depPath: dir,
|
|
168
|
+
extraBinPaths: opts.extraBinPaths,
|
|
169
|
+
extraEnv: opts.extraEnv,
|
|
170
|
+
pkgRoot: dir,
|
|
171
|
+
rawConfig: opts.rawConfig,
|
|
172
|
+
rootModulesDir: await realpathMissing(path.join(dir, 'node_modules')),
|
|
173
|
+
stdio: 'inherit',
|
|
174
|
+
unsafePerm: true, // when running scripts explicitly, assume that they're trusted.
|
|
175
|
+
});
|
|
176
|
+
const { manifest } = await readProjectManifest(dir, opts);
|
|
177
|
+
// Unfortunately, we cannot support postpack at the moment
|
|
178
|
+
if (!opts.ignoreScripts) {
|
|
179
|
+
await _runScriptsIfPresent([
|
|
180
|
+
'prepublishOnly',
|
|
181
|
+
'prepublish',
|
|
182
|
+
], manifest);
|
|
183
|
+
}
|
|
184
|
+
// We have to publish the tarball from another location.
|
|
185
|
+
// Otherwise, npm would publish the package with the package.json file
|
|
186
|
+
// from the current working directory, ignoring the package.json file
|
|
187
|
+
// that was generated and packed to the tarball.
|
|
188
|
+
const packDestination = temporaryDirectory();
|
|
189
|
+
try {
|
|
190
|
+
const packResult = await pack.api({
|
|
191
|
+
...opts,
|
|
192
|
+
dir,
|
|
193
|
+
packDestination,
|
|
194
|
+
dryRun: false,
|
|
195
|
+
});
|
|
196
|
+
await publishPackedPkg(packResult, opts);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await rimraf(packDestination);
|
|
200
|
+
}
|
|
201
|
+
if (!opts.ignoreScripts) {
|
|
202
|
+
await _runScriptsIfPresent([
|
|
203
|
+
'publish',
|
|
204
|
+
'postpublish',
|
|
205
|
+
], manifest);
|
|
206
|
+
}
|
|
207
|
+
return { manifest };
|
|
208
|
+
}
|
|
209
|
+
export async function runScriptsIfPresent(opts, scriptNames, manifest) {
|
|
210
|
+
for (const scriptName of scriptNames) {
|
|
211
|
+
if (!manifest.scripts?.[scriptName])
|
|
212
|
+
continue;
|
|
213
|
+
await runLifecycleHook(scriptName, manifest, opts); // eslint-disable-line no-await-in-loop
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config } from '@pnpm/config.reader';
|
|
2
|
+
import { PnpmError } from '@pnpm/error';
|
|
3
|
+
import type { PackResult } from './pack.js';
|
|
4
|
+
type AuthConfigKey = 'authToken' | 'authUserPass' | 'tokenHelper';
|
|
5
|
+
type SslConfigKey = 'ca' | 'cert' | 'key';
|
|
6
|
+
type AuthSslConfigKey = AuthConfigKey | SslConfigKey | 'authInfos' | 'sslConfigs';
|
|
7
|
+
export type PublishPackedPkgOptions = Pick<Config, AuthSslConfigKey | 'dryRun' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'fetchTimeout' | 'registries' | 'tag' | 'userAgent'> & {
|
|
8
|
+
access?: 'public' | 'restricted';
|
|
9
|
+
ci?: boolean;
|
|
10
|
+
otp?: string;
|
|
11
|
+
provenance?: boolean;
|
|
12
|
+
provenanceFile?: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function publishPackedPkg(packResult: Pick<PackResult, 'publishedManifest' | 'tarballPath'>, opts: PublishPackedPkgOptions): Promise<void>;
|
|
15
|
+
export declare class PublishUnsupportedRegistryProtocolError extends PnpmError {
|
|
16
|
+
readonly registryUrl: string;
|
|
17
|
+
constructor(registryUrl: string);
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { PnpmError } from '@pnpm/error';
|
|
3
|
+
import { globalInfo, globalWarn } from '@pnpm/logger';
|
|
4
|
+
import { displayError } from './displayError.js';
|
|
5
|
+
import { executeTokenHelper } from './executeTokenHelper.js';
|
|
6
|
+
import { createFailedToPublishError } from './FailedToPublishError.js';
|
|
7
|
+
import { AuthTokenError, fetchAuthToken } from './oidc/authToken.js';
|
|
8
|
+
import { getIdToken, IdTokenError } from './oidc/idToken.js';
|
|
9
|
+
import { determineProvenance, ProvenanceError } from './oidc/provenance.js';
|
|
10
|
+
import { publishWithOtpHandling } from './otp.js';
|
|
11
|
+
import { allRegistryConfigKeys, parseSupportedRegistryUrl } from './registryConfigKeys.js';
|
|
12
|
+
export async function publishPackedPkg(packResult, opts) {
|
|
13
|
+
const { publishedManifest, tarballPath } = packResult;
|
|
14
|
+
const tarballData = await fs.readFile(tarballPath);
|
|
15
|
+
const publishOptions = await createPublishOptions(publishedManifest, opts);
|
|
16
|
+
const { name, version } = publishedManifest;
|
|
17
|
+
const { registry } = publishOptions;
|
|
18
|
+
globalInfo(`📦 ${name}@${version} → ${registry ?? 'the default registry'}`);
|
|
19
|
+
if (opts.dryRun) {
|
|
20
|
+
globalWarn(`Skip publishing ${name}@${version} (dry run)`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const response = await publishWithOtpHandling({ manifest: publishedManifest, tarballData, publishOptions });
|
|
24
|
+
if (response.ok) {
|
|
25
|
+
globalInfo(`✅ Published package ${name}@${version}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw await createFailedToPublishError(packResult, response);
|
|
29
|
+
}
|
|
30
|
+
async function createPublishOptions(manifest, options) {
|
|
31
|
+
const { registry, auth, ssl } = findAuthSslInfo(manifest, options);
|
|
32
|
+
const { access, ci: isFromCI, fetchRetries, fetchRetryFactor, fetchRetryMaxtimeout, fetchRetryMintimeout, fetchTimeout: timeout, otp, provenance, provenanceFile, tag: defaultTag, userAgent, } = options;
|
|
33
|
+
const headers = {
|
|
34
|
+
'npm-auth-type': 'web',
|
|
35
|
+
'npm-command': 'publish',
|
|
36
|
+
};
|
|
37
|
+
const publishOptions = {
|
|
38
|
+
access,
|
|
39
|
+
defaultTag,
|
|
40
|
+
fetchRetries,
|
|
41
|
+
fetchRetryFactor,
|
|
42
|
+
fetchRetryMaxtimeout,
|
|
43
|
+
fetchRetryMintimeout,
|
|
44
|
+
headers,
|
|
45
|
+
isFromCI,
|
|
46
|
+
otp,
|
|
47
|
+
timeout,
|
|
48
|
+
provenance,
|
|
49
|
+
provenanceFile,
|
|
50
|
+
registry,
|
|
51
|
+
userAgent,
|
|
52
|
+
// Signal to the registry that the client supports web-based authentication.
|
|
53
|
+
// Without this, the registry would never offer the web auth flow and would
|
|
54
|
+
// always fall back to prompting the user for an OTP code, even when the user
|
|
55
|
+
// has no OTP set up.
|
|
56
|
+
authType: 'web',
|
|
57
|
+
ca: ssl?.ca,
|
|
58
|
+
cert: Array.isArray(ssl?.cert) ? ssl.cert.join('\n') : ssl?.cert,
|
|
59
|
+
key: ssl?.key,
|
|
60
|
+
npmCommand: 'publish',
|
|
61
|
+
token: auth && extractToken(auth),
|
|
62
|
+
username: auth?.authUserPass?.username,
|
|
63
|
+
password: auth?.authUserPass?.password,
|
|
64
|
+
};
|
|
65
|
+
// This is necessary because getNetworkConfigs initialized them as { cert: '', key: '' }
|
|
66
|
+
// which may be a problem.
|
|
67
|
+
// The real fix is to change the type `SslConfig` into that of partial properties, but that
|
|
68
|
+
// is out of scope for now.
|
|
69
|
+
removeEmptyStringProperty(publishOptions, 'cert');
|
|
70
|
+
removeEmptyStringProperty(publishOptions, 'key');
|
|
71
|
+
if (registry) {
|
|
72
|
+
const oidcTokenProvenance = await fetchTokenAndProvenanceByOidcIfApplicable(publishOptions, manifest.name, registry, options);
|
|
73
|
+
publishOptions.token ??= oidcTokenProvenance?.authToken;
|
|
74
|
+
publishOptions.provenance ??= oidcTokenProvenance?.provenance;
|
|
75
|
+
appendAuthOptionsForRegistry(publishOptions, registry);
|
|
76
|
+
}
|
|
77
|
+
pruneUndefined(publishOptions);
|
|
78
|
+
return publishOptions;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find auth and ssl information according to {@link https://docs.npmjs.com/cli/v10/configuring-npm/npmrc#auth-related-configuration}.
|
|
82
|
+
*
|
|
83
|
+
* The example `.npmrc` demonstrated inheritance.
|
|
84
|
+
*/
|
|
85
|
+
function findAuthSslInfo({ name }, { authInfos, sslConfigs, registries, ...defaultInfos }) {
|
|
86
|
+
// eslint-disable-next-line regexp/no-unused-capturing-group
|
|
87
|
+
const scopedMatches = /@(?<scope>[^/]+)\/(?<slug>[^/]+)/.exec(name);
|
|
88
|
+
const registryName = scopedMatches?.groups ? `@${scopedMatches.groups.scope}` : 'default';
|
|
89
|
+
const nonNormalizedRegistry = registries[registryName] ?? registries.default;
|
|
90
|
+
const supportedRegistryInfo = parseSupportedRegistryUrl(nonNormalizedRegistry);
|
|
91
|
+
if (!supportedRegistryInfo) {
|
|
92
|
+
throw new PublishUnsupportedRegistryProtocolError(nonNormalizedRegistry);
|
|
93
|
+
}
|
|
94
|
+
const { normalizedUrl: registry, longestConfigKey: initialRegistryConfigKey, } = supportedRegistryInfo;
|
|
95
|
+
const result = { registry };
|
|
96
|
+
for (const registryConfigKey of allRegistryConfigKeys(initialRegistryConfigKey)) {
|
|
97
|
+
const auth = authInfos[registryConfigKey];
|
|
98
|
+
const ssl = sslConfigs[registryConfigKey];
|
|
99
|
+
result.auth ??= auth; // old auth from longer path collectively overrides new auth from shorter path
|
|
100
|
+
result.ssl = {
|
|
101
|
+
...ssl,
|
|
102
|
+
...result.ssl, // old ssl from longer path individually overrides new ssl from shorter path
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (nonNormalizedRegistry !== registries.default &&
|
|
106
|
+
registry !== registries.default &&
|
|
107
|
+
registry !== parseSupportedRegistryUrl(registries.default)?.normalizedUrl) {
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
registry,
|
|
112
|
+
auth: result.auth ?? defaultInfos, // old auth from longer path collectively overrides default auth
|
|
113
|
+
ssl: {
|
|
114
|
+
...defaultInfos,
|
|
115
|
+
...result.ssl, // old ssl from longer path individually overrides default ssl
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function extractToken({ authToken, tokenHelper, }) {
|
|
120
|
+
if (authToken)
|
|
121
|
+
return authToken;
|
|
122
|
+
if (tokenHelper) {
|
|
123
|
+
return executeTokenHelper(tokenHelper, { globalWarn });
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
export class PublishUnsupportedRegistryProtocolError extends PnpmError {
|
|
128
|
+
registryUrl;
|
|
129
|
+
constructor(registryUrl) {
|
|
130
|
+
super('PUBLISH_UNSUPPORTED_REGISTRY_PROTOCOL', `Registry ${registryUrl} has an unsupported protocol`, {
|
|
131
|
+
hint: '`pnpm publish` only supports HTTP and HTTPS registries',
|
|
132
|
+
});
|
|
133
|
+
this.registryUrl = registryUrl;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* If authentication information doesn't already set in {@link targetPublishOptions},
|
|
138
|
+
* try fetching an authentication token and provenance by OpenID Connect and return it.
|
|
139
|
+
*/
|
|
140
|
+
async function fetchTokenAndProvenanceByOidcIfApplicable(targetPublishOptions, packageName, registry, options) {
|
|
141
|
+
if (targetPublishOptions.token != null ||
|
|
142
|
+
(targetPublishOptions.username && targetPublishOptions.password))
|
|
143
|
+
return undefined;
|
|
144
|
+
let idToken;
|
|
145
|
+
try {
|
|
146
|
+
idToken = await getIdToken({
|
|
147
|
+
options,
|
|
148
|
+
registry,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof IdTokenError) {
|
|
153
|
+
globalWarn(`Skipped OIDC: ${displayError(error)}`);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (!idToken) {
|
|
159
|
+
globalWarn('Skipped OIDC: idToken is not available');
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
let authToken;
|
|
163
|
+
try {
|
|
164
|
+
authToken = await fetchAuthToken({
|
|
165
|
+
idToken,
|
|
166
|
+
options,
|
|
167
|
+
packageName,
|
|
168
|
+
registry,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error instanceof AuthTokenError) {
|
|
173
|
+
globalWarn(`Skipped OIDC: ${displayError(error)}`);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
if (options.provenance != null) {
|
|
179
|
+
return {
|
|
180
|
+
authToken,
|
|
181
|
+
provenance: options.provenance,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
let provenance;
|
|
185
|
+
try {
|
|
186
|
+
provenance = await determineProvenance({
|
|
187
|
+
authToken,
|
|
188
|
+
idToken,
|
|
189
|
+
options,
|
|
190
|
+
packageName,
|
|
191
|
+
registry,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof ProvenanceError) {
|
|
196
|
+
globalWarn(`Skipped setting provenance: ${displayError(error)}`);
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
return { authToken, provenance };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Appends authentication information to {@link targetPublishOptions} to explicitly target {@link registry}.
|
|
205
|
+
*
|
|
206
|
+
* `libnpmpublish` has a quirk in which it only read the authentication information from `//<registry>:_authToken`
|
|
207
|
+
* instead of `token`.
|
|
208
|
+
* This function fixes that by making sure the registry specific authentication information exists.
|
|
209
|
+
*/
|
|
210
|
+
function appendAuthOptionsForRegistry(targetPublishOptions, registry) {
|
|
211
|
+
const registryInfo = parseSupportedRegistryUrl(registry);
|
|
212
|
+
if (!registryInfo) {
|
|
213
|
+
globalWarn(`The registry ${registry} cannot be converted into a config key. Supplement is skipped. Subsequent libnpmpublish call may fail.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const registryConfigKey = registryInfo.longestConfigKey;
|
|
217
|
+
targetPublishOptions[`${registryConfigKey}:_authToken`] ??= targetPublishOptions.token;
|
|
218
|
+
targetPublishOptions[`${registryConfigKey}:username`] ??= targetPublishOptions.username;
|
|
219
|
+
targetPublishOptions[`${registryConfigKey}:_password`] ??= targetPublishOptions.password && btoa(targetPublishOptions.password);
|
|
220
|
+
}
|
|
221
|
+
function removeEmptyStringProperty(object, key) {
|
|
222
|
+
if (!object[key]) {
|
|
223
|
+
delete object[key];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function pruneUndefined(object) {
|
|
227
|
+
for (const key in object) {
|
|
228
|
+
if (object[key] === undefined) {
|
|
229
|
+
delete object[key];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=publishPackedPkg.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config } from '@pnpm/config.reader';
|
|
2
|
+
import type { PublishPackedPkgOptions } from './publishPackedPkg.js';
|
|
3
|
+
export type PublishRecursiveOpts = Required<Pick<Config, 'bin' | 'cacheDir' | 'cliOptions' | 'dir' | 'pnpmHomeDir' | 'rawConfig' | 'registries' | 'workspaceDir'>> & Partial<Pick<Config, 'tag' | 'ca' | 'catalogs' | 'cert' | 'fetchTimeout' | 'force' | 'dryRun' | 'extraBinPaths' | 'extraEnv' | 'fetchRetries' | 'fetchRetryFactor' | 'fetchRetryMaxtimeout' | 'fetchRetryMintimeout' | 'key' | 'httpProxy' | 'httpsProxy' | 'localAddress' | 'lockfileDir' | 'noProxy' | 'npmPath' | 'offline' | 'selectedProjectsGraph' | 'strictSsl' | 'unsafePerm' | 'userAgent' | 'userConfig' | 'verifyStoreIntegrity'>> & {
|
|
4
|
+
access?: 'public' | 'restricted';
|
|
5
|
+
argv: {
|
|
6
|
+
original: string[];
|
|
7
|
+
};
|
|
8
|
+
reportSummary?: boolean;
|
|
9
|
+
} & PublishPackedPkgOptions;
|
|
10
|
+
export declare function recursivePublish(opts: PublishRecursiveOpts & Required<Pick<Config, 'selectedProjectsGraph'>>): Promise<{
|
|
11
|
+
exitCode: number;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pickRegistryForPackage } from '@pnpm/config.pick-registry-for-package';
|
|
3
|
+
import { createResolver } from '@pnpm/installing.client';
|
|
4
|
+
import { logger } from '@pnpm/logger';
|
|
5
|
+
import { sortProjects } from '@pnpm/workspace.projects-sorter';
|
|
6
|
+
import pFilter from 'p-filter';
|
|
7
|
+
import { pick } from 'ramda';
|
|
8
|
+
import { writeJsonFile } from 'write-json-file';
|
|
9
|
+
import { publish } from './publish.js';
|
|
10
|
+
export async function recursivePublish(opts) {
|
|
11
|
+
const pkgs = Object.values(opts.selectedProjectsGraph).map((wsPkg) => wsPkg.package);
|
|
12
|
+
const { resolve } = createResolver({
|
|
13
|
+
...opts,
|
|
14
|
+
authConfig: opts.rawConfig,
|
|
15
|
+
userConfig: opts.userConfig,
|
|
16
|
+
retry: {
|
|
17
|
+
factor: opts.fetchRetryFactor,
|
|
18
|
+
maxTimeout: opts.fetchRetryMaxtimeout,
|
|
19
|
+
minTimeout: opts.fetchRetryMintimeout,
|
|
20
|
+
retries: opts.fetchRetries,
|
|
21
|
+
},
|
|
22
|
+
timeout: opts.fetchTimeout,
|
|
23
|
+
});
|
|
24
|
+
const pkgsToPublish = await pFilter(pkgs, async (pkg) => {
|
|
25
|
+
if (!pkg.manifest.name || !pkg.manifest.version || pkg.manifest.private)
|
|
26
|
+
return false;
|
|
27
|
+
if (opts.force)
|
|
28
|
+
return true;
|
|
29
|
+
return !(await isAlreadyPublished({
|
|
30
|
+
dir: pkg.rootDir,
|
|
31
|
+
lockfileDir: opts.lockfileDir ?? pkg.rootDir,
|
|
32
|
+
registries: opts.registries,
|
|
33
|
+
resolve,
|
|
34
|
+
}, pkg.manifest.name, pkg.manifest.version));
|
|
35
|
+
});
|
|
36
|
+
const publishedPkgDirs = new Set(pkgsToPublish.map(({ rootDir }) => rootDir));
|
|
37
|
+
const publishedPackages = [];
|
|
38
|
+
if (publishedPkgDirs.size === 0) {
|
|
39
|
+
logger.info({
|
|
40
|
+
message: 'There are no new packages that should be published',
|
|
41
|
+
prefix: opts.dir,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const appendedArgs = [];
|
|
46
|
+
if (opts.cliOptions['access']) {
|
|
47
|
+
appendedArgs.push(`--access=${opts.cliOptions['access']}`);
|
|
48
|
+
}
|
|
49
|
+
if (opts.dryRun) {
|
|
50
|
+
appendedArgs.push('--dry-run');
|
|
51
|
+
}
|
|
52
|
+
if (opts.force) {
|
|
53
|
+
appendedArgs.push('--force');
|
|
54
|
+
}
|
|
55
|
+
if (opts.cliOptions['otp']) {
|
|
56
|
+
appendedArgs.push(`--otp=${opts.cliOptions['otp']}`);
|
|
57
|
+
}
|
|
58
|
+
const chunks = sortProjects(opts.selectedProjectsGraph);
|
|
59
|
+
const tag = opts.tag ?? 'latest';
|
|
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
|
+
'publish',
|
|
77
|
+
'--tag',
|
|
78
|
+
tag,
|
|
79
|
+
'--registry',
|
|
80
|
+
registry,
|
|
81
|
+
...appendedArgs,
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
gitChecks: false,
|
|
85
|
+
recursive: false,
|
|
86
|
+
}, [pkg.rootDir]);
|
|
87
|
+
if (publishResult?.manifest != null) {
|
|
88
|
+
publishedPackages.push(pick(['name', 'version'], publishResult.manifest));
|
|
89
|
+
}
|
|
90
|
+
else if (publishResult?.exitCode) {
|
|
91
|
+
return { exitCode: publishResult.exitCode };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (opts.reportSummary) {
|
|
97
|
+
await writeJsonFile(path.join(opts.lockfileDir ?? opts.dir, 'pnpm-publish-summary.json'), { publishedPackages });
|
|
98
|
+
}
|
|
99
|
+
return { exitCode: 0 };
|
|
100
|
+
}
|
|
101
|
+
async function isAlreadyPublished(opts, pkgName, pkgVersion) {
|
|
102
|
+
try {
|
|
103
|
+
await opts.resolve({ alias: pkgName, bareSpecifier: pkgVersion }, {
|
|
104
|
+
lockfileDir: opts.lockfileDir,
|
|
105
|
+
preferredVersions: {},
|
|
106
|
+
projectDir: opts.dir,
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (err) { // eslint-disable-line
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=recursivePublish.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocols currently supported.
|
|
3
|
+
*/
|
|
4
|
+
type SupportedRegistryScheme = 'http' | 'https';
|
|
5
|
+
/**
|
|
6
|
+
* A registry URL that has been normalized to match its corresponding {@link RegistryConfigKey}.
|
|
7
|
+
*/
|
|
8
|
+
export type NormalizedRegistryUrl = `${SupportedRegistryScheme}://${string}/`;
|
|
9
|
+
/**
|
|
10
|
+
* A config key of a registry url is a key on the `.npmrc` file. This key starts with
|
|
11
|
+
* a "//" prefix followed by a hostname and the rest of the URI and ends with a "/".
|
|
12
|
+
* They usually specify authentication information.
|
|
13
|
+
*/
|
|
14
|
+
export type RegistryConfigKey = `//${string}/`;
|
|
15
|
+
export interface SupportedRegistryUrlInfo {
|
|
16
|
+
normalizedUrl: NormalizedRegistryUrl;
|
|
17
|
+
longestConfigKey: RegistryConfigKey;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* If the {@link registryUrl} is an HTTP or an HTTPS registry url, return the longest
|
|
21
|
+
* {@link RegistryConfigKey} that corresponds to the registry url and a {@link NormalizedRegistryUrl}
|
|
22
|
+
* that matches it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseSupportedRegistryUrl(registryUrl: string): SupportedRegistryUrlInfo | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Generate all {@link RegistryConfigKey} of the same hostname from the longest to the shortest,
|
|
27
|
+
* including {@link longest} itself.
|
|
28
|
+
*/
|
|
29
|
+
export declare function allRegistryConfigKeys(longest: RegistryConfigKey): Generator<RegistryConfigKey, void, void>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import normalizeRegistryUrl from 'normalize-registry-url';
|
|
2
|
+
/**
|
|
3
|
+
* If {@link text} starts with {@link oldPrefix}, replace it with {@link newPrefix}.
|
|
4
|
+
* Otherwise, return `undefined`.
|
|
5
|
+
*/
|
|
6
|
+
const replacePrefix = (text, oldPrefix, newPrefix) => text.startsWith(oldPrefix)
|
|
7
|
+
? text.replace(oldPrefix, newPrefix)
|
|
8
|
+
: undefined;
|
|
9
|
+
/**
|
|
10
|
+
* If {@link text} already ends with {@link suffix}, return it.
|
|
11
|
+
* Otherwise, append {@link suffix} to {@link text} and return it.
|
|
12
|
+
*/
|
|
13
|
+
const ensureSuffix = (text, suffix) => text.endsWith(suffix) ? text : `${text}${suffix}`;
|
|
14
|
+
/**
|
|
15
|
+
* If the {@link registryUrl} is an HTTP or an HTTPS registry url, return the longest
|
|
16
|
+
* {@link RegistryConfigKey} that corresponds to the registry url and a {@link NormalizedRegistryUrl}
|
|
17
|
+
* that matches it.
|
|
18
|
+
*/
|
|
19
|
+
export function parseSupportedRegistryUrl(registryUrl) {
|
|
20
|
+
registryUrl = normalizeRegistryUrl(registryUrl);
|
|
21
|
+
const keyPrefix = replacePrefix(registryUrl, 'http://', '//') ?? replacePrefix(registryUrl, 'https://', '//');
|
|
22
|
+
if (!keyPrefix)
|
|
23
|
+
return undefined;
|
|
24
|
+
const normalizedUrl = ensureSuffix(registryUrl, '/');
|
|
25
|
+
const longestConfigKey = ensureSuffix(keyPrefix, '/');
|
|
26
|
+
return { normalizedUrl, longestConfigKey };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* This value is used for termination check in {@link allRegistryConfigKeys} only.
|
|
30
|
+
* It is not actually a valid {@link RegistryConfigKey}.
|
|
31
|
+
*/
|
|
32
|
+
const EMPTY_REGISTRY_CONFIG_KEY = '///';
|
|
33
|
+
/**
|
|
34
|
+
* Generate all {@link RegistryConfigKey} of the same hostname from the longest to the shortest,
|
|
35
|
+
* including {@link longest} itself.
|
|
36
|
+
*/
|
|
37
|
+
export function* allRegistryConfigKeys(longest) {
|
|
38
|
+
if (!longest.startsWith('//')) {
|
|
39
|
+
throw new RangeError(`The string ${JSON.stringify(longest)} is not a valid registry config key`);
|
|
40
|
+
}
|
|
41
|
+
if (longest === EMPTY_REGISTRY_CONFIG_KEY) {
|
|
42
|
+
throw new RangeError('Registry config key cannot be without hostname');
|
|
43
|
+
}
|
|
44
|
+
if (longest.length <= EMPTY_REGISTRY_CONFIG_KEY.length)
|
|
45
|
+
return;
|
|
46
|
+
yield longest;
|
|
47
|
+
const next = longest.replace(/[^/]*\/$/, '');
|
|
48
|
+
yield* allRegistryConfigKeys(next);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registryConfigKeys.js.map
|