/
npm-utils.ts
247 lines (225 loc) 路 8.12 KB
/
npm-utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import { ExitError } from "@changesets/errors";
import { error, info, warn } from "@changesets/logger";
import { AccessType, PackageJSON } from "@changesets/types";
import pLimit from "p-limit";
import preferredPM from "preferred-pm";
import chalk from "chalk";
import spawn from "spawndamnit";
import semverParse from "semver/functions/parse";
import { askQuestion } from "../../utils/cli-utilities";
import { isCI } from "ci-info";
import { TwoFactorState } from "../../utils/types";
import { getLastJsonObjectFromString } from "../../utils/getLastJsonObjectFromString";
interface PublishOptions {
cwd: string;
publishDir: string;
access: AccessType;
tag: string;
}
const npmRequestLimit = pLimit(40);
const npmPublishLimit = pLimit(10);
function jsonParse(input: string) {
try {
return JSON.parse(input);
} catch (err) {
if (err instanceof SyntaxError) {
console.error("error parsing json:", input);
}
throw err;
}
}
function getCorrectRegistry(packageJson?: PackageJSON): string {
const registry =
packageJson?.publishConfig?.registry ?? process.env.npm_config_registry;
return !registry || registry === "https://registry.yarnpkg.com"
? "https://registry.npmjs.org"
: registry;
}
async function getPublishTool(
cwd: string
): Promise<{ name: "npm" } | { name: "pnpm"; shouldAddNoGitChecks: boolean }> {
const pm = await preferredPM(cwd);
if (!pm || pm.name !== "pnpm") return { name: "npm" };
try {
let result = await spawn("pnpm", ["--version"], { cwd });
let version = result.stdout.toString().trim();
let parsed = semverParse(version);
return {
name: "pnpm",
shouldAddNoGitChecks:
parsed?.major === undefined ? false : parsed.major >= 5,
};
} catch (e) {
return {
name: "pnpm",
shouldAddNoGitChecks: false,
};
}
}
export async function getTokenIsRequired() {
// Due to a super annoying issue in yarn, we have to manually override this env variable
// See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
const envOverride = {
npm_config_registry: getCorrectRegistry(),
};
let result = await spawn("npm", ["profile", "get", "--json"], {
env: Object.assign({}, process.env, envOverride),
});
if (result.code !== 0) {
error(
"error while checking if token is required",
result.stderr.toString().trim() || result.stdout.toString().trim()
);
return false;
}
let json = jsonParse(result.stdout.toString());
if (json.error || !json.tfa || !json.tfa.mode) {
return false;
}
return json.tfa.mode === "auth-and-writes";
}
export function getPackageInfo(packageJson: PackageJSON) {
return npmRequestLimit(async () => {
info(`npm info ${packageJson.name}`);
// Due to a couple of issues with yarnpkg, we also want to override the npm registry when doing
// npm info.
// Issues: We sometimes get back cached responses, i.e old data about packages which causes
// `publish` to behave incorrectly. It can also cause issues when publishing private packages
// as they will always give a 404, which will tell `publish` to always try to publish.
// See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
let result = await spawn("npm", [
"info",
packageJson.name,
"--registry",
getCorrectRegistry(packageJson),
"--json",
]);
// Github package registry returns empty string when calling npm info
// for a non-existent package instead of a E404
if (result.stdout.toString() === "") {
return {
error: {
code: "E404",
},
};
}
return jsonParse(result.stdout.toString());
});
}
export async function infoAllow404(packageJson: PackageJSON) {
let pkgInfo = await getPackageInfo(packageJson);
if (pkgInfo.error?.code === "E404") {
warn(`Received 404 for npm info ${chalk.cyan(`"${packageJson.name}"`)}`);
return { published: false, pkgInfo: {} };
}
if (pkgInfo.error) {
error(
`Received an unknown error code: ${
pkgInfo.error.code
} for npm info ${chalk.cyan(`"${packageJson.name}"`)}`
);
error(pkgInfo.error.summary);
if (pkgInfo.error.detail) error(pkgInfo.error.detail);
throw new ExitError(1);
}
return { published: true, pkgInfo };
}
let otpAskLimit = pLimit(1);
let askForOtpCode = (twoFactorState: TwoFactorState) =>
otpAskLimit(async () => {
if (twoFactorState.token !== null) return twoFactorState.token;
info(
"This operation requires a one-time password from your authenticator."
);
let val = await askQuestion("Enter one-time password:");
twoFactorState.token = val;
return val;
});
export let getOtpCode = async (twoFactorState: TwoFactorState) => {
if (twoFactorState.token !== null) {
return twoFactorState.token;
}
return askForOtpCode(twoFactorState);
};
// we have this so that we can do try a publish again after a publish without
// the call being wrapped in the npm request limit and causing the publishes to potentially never run
async function internalPublish(
pkgName: string,
opts: PublishOptions,
twoFactorState: TwoFactorState
): Promise<{ published: boolean }> {
let publishTool = await getPublishTool(opts.cwd);
let publishFlags = opts.access ? ["--access", opts.access] : [];
publishFlags.push("--tag", opts.tag);
if ((await twoFactorState.isRequired) && !isCI) {
let otpCode = await getOtpCode(twoFactorState);
publishFlags.push("--otp", otpCode);
}
if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
publishFlags.push("--no-git-checks");
}
// Due to a super annoying issue in yarn, we have to manually override this env variable
// See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
const envOverride = {
npm_config_registry: getCorrectRegistry(),
};
let { code, stdout, stderr } =
publishTool.name === "pnpm"
? await spawn("pnpm", ["publish", "--json", ...publishFlags], {
env: Object.assign({}, process.env, envOverride),
cwd: opts.cwd,
})
: await spawn(
publishTool.name,
["publish", opts.publishDir, "--json", ...publishFlags],
{
env: Object.assign({}, process.env, envOverride),
}
);
if (code !== 0) {
// NPM's --json output is included alongside the `prepublish` and `postpublish` output in terminal
// We want to handle this as best we can but it has some struggles:
// - output of those lifecycle scripts can contain JSON
// - npm7 has switched to printing `--json` errors to stderr (https://github.com/npm/cli/commit/1dbf0f9bb26ba70f4c6d0a807701d7652c31d7d4)
// Note that the `--json` output is always printed at the end so this should work
let json =
getLastJsonObjectFromString(stderr.toString()) ||
getLastJsonObjectFromString(stdout.toString());
if (json?.error) {
// The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
if (
(json.error.code === "EOTP" ||
(json.error.code === "E401" &&
json.error.detail.includes("--otp=<code>"))) &&
!isCI
) {
if (twoFactorState.token !== null) {
// the current otp code must be invalid since it errored
twoFactorState.token = null;
}
// just in case this isn't already true
twoFactorState.isRequired = Promise.resolve(true);
return internalPublish(pkgName, opts, twoFactorState);
}
error(
`an error occurred while publishing ${pkgName}: ${json.error.code}`,
json.error.summary,
json.error.detail ? "\n" + json.error.detail : ""
);
}
error(stderr.toString() || stdout.toString());
return { published: false };
}
return { published: true };
}
export function publish(
pkgName: string,
opts: PublishOptions,
twoFactorState: TwoFactorState
): Promise<{ published: boolean }> {
// If there are many packages to be published, it's better to limit the
// concurrency to avoid unwanted errors, for example from npm.
return npmRequestLimit(() =>
npmPublishLimit(() => internalPublish(pkgName, opts, twoFactorState))
);
}