UNPKG

44.8 kBJavaScriptView Raw
1import meow from 'meow';
2import { ExitError, PreEnterButInPreModeError, PreExitButNotInPreModeError, InternalError } from '@changesets/errors';
3import { error, info, warn, log, prefix, success } from '@changesets/logger';
4import { format } from 'util';
5import fs from 'fs-extra';
6import path, { join } from 'path';
7import { getPackages } from '@manypkg/get-packages';
8import { getDependentsGraph } from '@changesets/get-dependents-graph';
9import { defaultWrittenConfig, read } from '@changesets/config';
10import chalk from 'chalk';
11import { spawn } from 'child_process';
12import termSize from 'term-size';
13import { prompt } from 'enquirer';
14import { edit, ExternalEditor } from 'external-editor';
15import { getChangedPackagesSinceRef, add as add$1, commit, tag, getAllTags } from '@changesets/git';
16import writeChangeset from '@changesets/write';
17import semver from 'semver';
18import boxen from 'boxen';
19import outdent from 'outdent';
20import applyReleasePlan from '@changesets/apply-release-plan';
21import readChangesets from '@changesets/read';
22import assembleReleasePlan from '@changesets/assemble-release-plan';
23import { readPreState, enterPre, exitPre } from '@changesets/pre';
24import pLimit from 'p-limit';
25import preferredPM from 'preferred-pm';
26import spawn$1 from 'spawndamnit';
27import isCI$1 from 'is-ci';
28import table from 'tty-table';
29import getReleasePlan from '@changesets/get-release-plan';
30
31const pkgPath = path.dirname(require.resolve("@changesets/cli/package.json"));
32async function init(cwd) {
33 const changesetBase = path.resolve(cwd, ".changeset");
34
35 if (fs.existsSync(changesetBase)) {
36 if (!fs.existsSync(path.join(changesetBase, "config.json"))) {
37 if (fs.existsSync(path.join(changesetBase, "config.js"))) {
38 error("It looks like you're using the version 1 `.changeset/config.js` file");
39 error("The format of the config object has significantly changed in v2 as well");
40 error(" - we thoroughly recommend looking at the changelog for this package for what has changed");
41 error("Changesets will write the defaults for the new config, remember to transfer your options into the new config at `.changeset/config.json`");
42 } else {
43 error("It looks like you don't have a config file");
44 info("The default config file will be written at `.changeset/config.json`");
45 }
46
47 await fs.writeFile(path.resolve(changesetBase, "config.json"), JSON.stringify(defaultWrittenConfig, null, 2));
48 } else {
49 warn("It looks like you already have changesets initialized. You should be able to run changeset commands no problems.");
50 }
51 } else {
52 await fs.copy(path.resolve(pkgPath, "./default-files"), changesetBase);
53 await fs.writeFile(path.resolve(changesetBase, "config.json"), JSON.stringify(defaultWrittenConfig, null, 2));
54 log(chalk`Thanks for choosing {green changesets} to help manage your versioning and publishing\n`);
55 log("You should be set up to start using changesets now!\n");
56 info("We have added a `.changeset` folder, and a couple of files to help you out:");
57 info(chalk`- {blue .changeset/README.md} contains information about using changesets`);
58 info(chalk`- {blue .changeset/config.json} is our default config`);
59 }
60}
61
62function _defineProperty(obj, key, value) {
63 if (key in obj) {
64 Object.defineProperty(obj, key, {
65 value: value,
66 enumerable: true,
67 configurable: true,
68 writable: true
69 });
70 } else {
71 obj[key] = value;
72 }
73
74 return obj;
75}
76
77function ownKeys(object, enumerableOnly) {
78 var keys = Object.keys(object);
79
80 if (Object.getOwnPropertySymbols) {
81 var symbols = Object.getOwnPropertySymbols(object);
82 if (enumerableOnly) symbols = symbols.filter(function (sym) {
83 return Object.getOwnPropertyDescriptor(object, sym).enumerable;
84 });
85 keys.push.apply(keys, symbols);
86 }
87
88 return keys;
89}
90
91function _objectSpread2(target) {
92 for (var i = 1; i < arguments.length; i++) {
93 var source = arguments[i] != null ? arguments[i] : {};
94
95 if (i % 2) {
96 ownKeys(Object(source), true).forEach(function (key) {
97 _defineProperty(target, key, source[key]);
98 });
99 } else if (Object.getOwnPropertyDescriptors) {
100 Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
101 } else {
102 ownKeys(Object(source)).forEach(function (key) {
103 Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
104 });
105 }
106 }
107
108 return target;
109}
110
111// @ts-ignore it's not worth writing a TS declaration file in this repo for a tiny module we use once like this
112// so we can make type assertions using them because `enquirer` types do no support `prefix` right now
113
114/* Notes on using inquirer:
115 * Each question needs a key, as inquirer is assembling an object behind-the-scenes.
116 * At each call, the entire responses object is returned, so we need a unique
117 * identifier for the name every time. This is why we are using serial IDs
118 */
119const serialId = function () {
120 let id = 0;
121 return () => id++;
122}();
123
124const limit = Math.max(termSize().rows - 5, 10);
125
126let cancelFlow = () => {
127 success("Cancelled... 👋 ");
128 process.exit();
129};
130
131async function askCheckboxPlus(message, choices, format) {
132 const name = `CheckboxPlus-${serialId()}`;
133 return prompt({
134 type: "autocomplete",
135 name,
136 message,
137 prefix,
138 multiple: true,
139 choices,
140 format,
141 limit,
142 onCancel: cancelFlow
143 }).then(responses => responses[name]).catch(err => {
144 error(err);
145 });
146}
147
148async function askQuestion(message) {
149 const name = `Question-${serialId()}`;
150 return prompt([{
151 type: "input",
152 message,
153 name,
154 prefix,
155 onCancel: cancelFlow
156 }]).then(responses => responses[name]).catch(err => {
157 error(err);
158 });
159}
160
161function askQuestionWithEditor(message) {
162 const response = edit(message, {
163 postfix: ".md"
164 });
165 return response.replace(/^#.*\n?/gm, "").replace(/\n+$/g, "").trim();
166}
167
168async function askConfirm(message) {
169 const name = `Confirm-${serialId()}`;
170 return prompt([{
171 message,
172 name,
173 prefix,
174 type: "confirm",
175 initial: true,
176 onCancel: cancelFlow
177 }]).then(responses => responses[name]).catch(err => {
178 error(err);
179 });
180}
181
182async function askList(message, choices) {
183 const name = `List-${serialId()}`;
184 return prompt([{
185 choices,
186 message,
187 name,
188 prefix,
189 type: "select",
190 onCancel: cancelFlow
191 }]).then(responses => responses[name]).catch(err => {
192 error(err);
193 });
194}
195
196const {
197 green,
198 yellow,
199 red,
200 bold,
201 blue,
202 cyan
203} = chalk;
204
205async function confirmMajorRelease(pkgJSON) {
206 if (semver.lt(pkgJSON.version, "1.0.0")) {
207 // prettier-ignore
208 log(yellow(`WARNING: Releasing a major version for ${green(pkgJSON.name)} will be its ${red('first major release')}.`));
209 log(yellow(`If you are unsure if this is correct, contact the package's maintainers ${red("before committing this changeset")}.`));
210 let shouldReleaseFirstMajor = await askConfirm(bold(`Are you sure you want still want to release the ${red("first major release")} of ${pkgJSON.name}?`));
211 return shouldReleaseFirstMajor;
212 }
213
214 return true;
215}
216
217async function getPackagesToRelease(changedPackages, allPackages) {
218 function askInitialReleaseQuestion(defaultChoiceList) {
219 return askCheckboxPlus( // TODO: Make this wording better
220 // TODO: take objects and be fancy with matching
221 `Which packages would you like to include?`, defaultChoiceList, x => {
222 // this removes changed packages and unchanged packages from the list
223 // of packages shown after selection
224 if (Array.isArray(x)) {
225 return x.filter(x => x !== "changed packages" && x !== "unchanged packages").map(x => cyan(x)).join(", ");
226 }
227
228 return x;
229 });
230 }
231
232 if (allPackages.length > 1) {
233 const unchangedPackagesNames = allPackages.map(({
234 packageJson
235 }) => packageJson.name).filter(name => !changedPackages.includes(name));
236 const defaultChoiceList = [{
237 name: "changed packages",
238 choices: changedPackages
239 }, {
240 name: "unchanged packages",
241 choices: unchangedPackagesNames
242 }].filter(({
243 choices
244 }) => choices.length !== 0);
245 let packagesToRelease = await askInitialReleaseQuestion(defaultChoiceList);
246
247 if (packagesToRelease.length === 0) {
248 do {
249 error("You must select at least one package to release");
250 error("(You most likely hit enter instead of space!)");
251 packagesToRelease = await askInitialReleaseQuestion(defaultChoiceList);
252 } while (packagesToRelease.length === 0);
253 }
254
255 return packagesToRelease.filter(pkgName => pkgName !== "changed packages" && pkgName !== "unchanged packages");
256 }
257
258 return [allPackages[0].packageJson.name];
259}
260
261function formatPkgNameAndVersion(pkgName, version) {
262 return `${bold(pkgName)}@${bold(version)}`;
263}
264
265async function createChangeset(changedPackages, allPackages) {
266 const releases = [];
267
268 if (allPackages.length > 1) {
269 const packagesToRelease = await getPackagesToRelease(changedPackages, allPackages);
270 let pkgJsonsByName = new Map(allPackages.map(({
271 packageJson
272 }) => [packageJson.name, packageJson]));
273 let pkgsLeftToGetBumpTypeFor = new Set(packagesToRelease);
274 let pkgsThatShouldBeMajorBumped = (await askCheckboxPlus(bold(`Which packages should have a ${red("major")} bump?`), [{
275 name: "all packages",
276 choices: packagesToRelease.map(pkgName => {
277 return {
278 name: pkgName,
279 message: formatPkgNameAndVersion(pkgName, pkgJsonsByName.get(pkgName).version)
280 };
281 })
282 }], x => {
283 // this removes changed packages and unchanged packages from the list
284 // of packages shown after selection
285 if (Array.isArray(x)) {
286 return x.filter(x => x !== "all packages").map(x => cyan(x)).join(", ");
287 }
288
289 return x;
290 })).filter(x => x !== "all packages");
291
292 for (const pkgName of pkgsThatShouldBeMajorBumped) {
293 // for packages that are under v1, we want to make sure major releases are intended,
294 // as some repo-wide sweeping changes have mistakenly release first majors
295 // of packages.
296 let pkgJson = pkgJsonsByName.get(pkgName);
297 let shouldReleaseFirstMajor = await confirmMajorRelease(pkgJson);
298
299 if (shouldReleaseFirstMajor) {
300 pkgsLeftToGetBumpTypeFor.delete(pkgName);
301 releases.push({
302 name: pkgName,
303 type: "major"
304 });
305 }
306 }
307
308 if (pkgsLeftToGetBumpTypeFor.size !== 0) {
309 let pkgsThatShouldBeMinorBumped = (await askCheckboxPlus(bold(`Which packages should have a ${green("minor")} bump?`), [{
310 name: "all packages",
311 choices: [...pkgsLeftToGetBumpTypeFor].map(pkgName => {
312 return {
313 name: pkgName,
314 message: formatPkgNameAndVersion(pkgName, pkgJsonsByName.get(pkgName).version)
315 };
316 })
317 }], x => {
318 // this removes changed packages and unchanged packages from the list
319 // of packages shown after selection
320 if (Array.isArray(x)) {
321 return x.filter(x => x !== "all packages").map(x => cyan(x)).join(", ");
322 }
323
324 return x;
325 })).filter(x => x !== "all packages");
326
327 for (const pkgName of pkgsThatShouldBeMinorBumped) {
328 pkgsLeftToGetBumpTypeFor.delete(pkgName);
329 releases.push({
330 name: pkgName,
331 type: "minor"
332 });
333 }
334 }
335
336 if (pkgsLeftToGetBumpTypeFor.size !== 0) {
337 log(`The following packages will be ${blue("patch")} bumped:`);
338 pkgsLeftToGetBumpTypeFor.forEach(pkgName => {
339 log(formatPkgNameAndVersion(pkgName, pkgJsonsByName.get(pkgName).version));
340 });
341
342 for (const pkgName of pkgsLeftToGetBumpTypeFor) {
343 releases.push({
344 name: pkgName,
345 type: "patch"
346 });
347 }
348 }
349 } else {
350 let pkg = allPackages[0];
351 let type = await askList(`What kind of change is this for ${green(pkg.packageJson.name)}? (current version is ${pkg.packageJson.version})`, ["patch", "minor", "major"]);
352
353 if (type === "major") {
354 let shouldReleaseAsMajor = await confirmMajorRelease(pkg.packageJson);
355
356 if (!shouldReleaseAsMajor) {
357 throw new ExitError(1);
358 }
359 }
360
361 releases.push({
362 name: pkg.packageJson.name,
363 type
364 });
365 }
366
367 log("Please enter a summary for this change (this will be in the changelogs). Submit empty line to open external editor");
368 let summary = await askQuestion("Summary");
369
370 if (summary.length === 0) {
371 try {
372 summary = askQuestionWithEditor("\n\n# Please enter a summary for your changes.\n# An empty message aborts the editor.");
373
374 if (summary.length > 0) {
375 return {
376 confirmed: true,
377 summary,
378 releases
379 };
380 }
381 } catch (err) {
382 log("An error happened using external editor. Please type your summary here:");
383 }
384
385 summary = await askQuestion("");
386
387 while (summary.length === 0) {
388 summary = await askQuestion("\n\n# A summary is required for the changelog! 😪");
389 }
390 }
391
392 return {
393 confirmed: false,
394 summary,
395 releases
396 };
397}
398
399function printConfirmationMessage(changeset, repoHasMultiplePackages) {
400 function getReleasesOfType(type) {
401 return changeset.releases.filter(release => release.type === type).map(release => release.name);
402 }
403
404 log("=== Releasing the following packages ===");
405 const majorReleases = getReleasesOfType("major");
406 const minorReleases = getReleasesOfType("minor");
407 const patchReleases = getReleasesOfType("patch");
408 if (majorReleases.length > 0) log(`${chalk.green("[Major]")}\n ${majorReleases.join(", ")}`);
409 if (minorReleases.length > 0) log(`${chalk.green("[Minor]")}\n ${minorReleases.join(", ")}`);
410 if (patchReleases.length > 0) log(`${chalk.green("[Patch]")}\n ${patchReleases.join(", ")}`);
411
412 if (repoHasMultiplePackages) {
413 const message = outdent`
414 ${chalk.red("========= NOTE ========")}
415 All dependents of these packages that will be incompatible with the new version will be ${chalk.red("patch bumped")} when this changeset is applied.`;
416 const prettyMessage = boxen(message, {
417 borderStyle: "double",
418 align: "center"
419 });
420 log(prettyMessage);
421 }
422}
423
424async function add(cwd, {
425 empty,
426 open
427}, config) {
428 const packages = await getPackages(cwd);
429 const changesetBase = path.resolve(cwd, ".changeset");
430 let newChangeset;
431
432 if (empty) {
433 newChangeset = {
434 confirmed: true,
435 releases: [],
436 summary: ``
437 };
438 } else {
439 const changedPackages = await getChangedPackagesSinceRef({
440 cwd,
441 ref: config.baseBranch
442 });
443 const changePackagesName = changedPackages.filter(a => a).map(pkg => pkg.packageJson.name);
444 newChangeset = await createChangeset(changePackagesName, packages.packages);
445 printConfirmationMessage(newChangeset, packages.packages.length > 1);
446
447 if (!newChangeset.confirmed) {
448 newChangeset = _objectSpread2(_objectSpread2({}, newChangeset), {}, {
449 confirmed: await askConfirm("Is this your desired changeset?")
450 });
451 }
452 }
453
454 if (newChangeset.confirmed) {
455 const changesetID = await writeChangeset(newChangeset, cwd);
456
457 if (config.commit) {
458 await add$1(path.resolve(changesetBase, `${changesetID}.md`), cwd);
459 await commit(`docs(changeset): ${newChangeset.summary}`, cwd);
460 log(chalk.green(`${empty ? "Empty " : ""}Changeset added and committed`));
461 } else {
462 log(chalk.green(`${empty ? "Empty " : ""}Changeset added! - you can now commit it\n`));
463 }
464
465 let hasMajorChange = [...newChangeset.releases].find(c => c.type === "major");
466
467 if (hasMajorChange) {
468 warn("This Changeset includes a major change and we STRONGLY recommend adding more information to the changeset:");
469 warn("WHAT the breaking change is");
470 warn("WHY the change was made");
471 warn("HOW a consumer should update their code");
472 } else {
473 log(chalk.green("If you want to modify or expand on the changeset summary, you can find it here"));
474 }
475
476 const changesetPath = path.resolve(changesetBase, `${changesetID}.md`);
477 info(chalk.blue(changesetPath));
478
479 if (open) {
480 // this is really a hack to reuse the logic embedded in `external-editor` related to determining the editor
481 const externalEditor = new ExternalEditor();
482 externalEditor.cleanup();
483 spawn(externalEditor.editor.bin, externalEditor.editor.args.concat([changesetPath]), {
484 detached: true,
485 stdio: "inherit"
486 });
487 }
488 }
489}
490
491// folder, and tidy up the subfolders
492// THIS SHOULD BE REMOVED WHEN SUPPORT FOR CHANGESETS FROM V1 IS DROPPED
493
494const removeEmptyFolders = async folderPath => {
495 const dirContents = fs.readdirSync(folderPath);
496 return Promise.all(dirContents.map(async contentPath => {
497 const singleChangesetPath = path.resolve(folderPath, contentPath);
498
499 try {
500 if ((await fs.readdir(singleChangesetPath)).length < 1) {
501 await fs.rmdir(singleChangesetPath);
502 }
503 } catch (err) {
504 if (err.code !== "ENOTDIR") {
505 throw err;
506 }
507 }
508 }));
509};
510
511let importantSeparator = chalk.red("===============================IMPORTANT!===============================");
512let importantEnd = chalk.red("----------------------------------------------------------------------");
513async function version(cwd, options, config) {
514 const releaseConfig = _objectSpread2(_objectSpread2({}, config), {}, {
515 // Disable committing when in snapshot mode
516 commit: options.snapshot ? false : config.commit
517 });
518
519 const [changesets, preState] = await Promise.all([readChangesets(cwd), readPreState(cwd), removeEmptyFolders(path.resolve(cwd, ".changeset"))]);
520
521 if ((preState === null || preState === void 0 ? void 0 : preState.mode) === "pre") {
522 warn(importantSeparator);
523
524 if (options.snapshot !== undefined) {
525 error("Snapshot release is not allowed in pre mode");
526 log("To resolve this exit the pre mode by running `changeset pre exit`");
527 throw new ExitError(1);
528 } else {
529 warn("You are in prerelease mode");
530 warn("If you meant to do a normal release you should revert these changes and run `changeset pre exit`");
531 warn("You can then run `changeset version` again to do a normal release");
532 }
533
534 warn(importantEnd);
535 }
536
537 if (changesets.length === 0 && (preState === undefined || preState.mode !== "exit")) {
538 warn("No unreleased changesets found, exiting.");
539 return;
540 }
541
542 let packages = await getPackages(cwd);
543 let releasePlan = assembleReleasePlan(changesets, packages, releaseConfig, preState, options.snapshot);
544 await applyReleasePlan(releasePlan, packages, releaseConfig, options.snapshot);
545
546 if (releaseConfig.commit) {
547 log("All files have been updated and committed. You're ready to publish!");
548 } else {
549 log("All files have been updated. Review them and commit at your leisure");
550 }
551}
552
553// @ts-ignore
554var isCI = !!(isCI$1 || process.env.GITHUB_ACTIONS);
555
556const npmRequestLimit = pLimit(40);
557const npmPublishLimit = pLimit(10);
558
559function jsonParse(input) {
560 try {
561 return JSON.parse(input);
562 } catch (err) {
563 if (err instanceof SyntaxError) {
564 console.error("error parsing json:", input);
565 }
566
567 throw err;
568 }
569}
570
571function getCorrectRegistry(packageJson) {
572 var _packageJson$publishC, _packageJson$publishC2;
573
574 const registry = (_packageJson$publishC = packageJson === null || packageJson === void 0 ? void 0 : (_packageJson$publishC2 = packageJson.publishConfig) === null || _packageJson$publishC2 === void 0 ? void 0 : _packageJson$publishC2.registry) !== null && _packageJson$publishC !== void 0 ? _packageJson$publishC : process.env.npm_config_registry;
575 return !registry || registry === "https://registry.yarnpkg.com" ? "https://registry.npmjs.org" : registry;
576}
577
578async function getPublishTool(cwd) {
579 const pm = await preferredPM(cwd);
580 if (!pm || pm.name !== "pnpm") return {
581 name: "npm"
582 };
583
584 try {
585 let result = await spawn$1("pnpm", ["--version"], {
586 cwd
587 });
588 let version = result.stdout.toString().trim();
589 let parsed = semver.parse(version);
590 return {
591 name: "pnpm",
592 shouldAddNoGitChecks: (parsed === null || parsed === void 0 ? void 0 : parsed.major) === undefined ? false : parsed.major >= 5
593 };
594 } catch (e) {
595 return {
596 name: "pnpm",
597 shouldAddNoGitChecks: false
598 };
599 }
600}
601
602async function getTokenIsRequired() {
603 // Due to a super annoying issue in yarn, we have to manually override this env variable
604 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
605 const envOverride = {
606 npm_config_registry: getCorrectRegistry()
607 };
608 let result = await spawn$1("npm", ["profile", "get", "--json"], {
609 env: Object.assign({}, process.env, envOverride)
610 });
611 let json = jsonParse(result.stdout.toString());
612
613 if (json.error || !json.tfa || !json.tfa.mode) {
614 return false;
615 }
616
617 return json.tfa.mode === "auth-and-writes";
618}
619function getPackageInfo(packageJson) {
620 return npmRequestLimit(async () => {
621 info(`npm info ${packageJson.name}`); // Due to a couple of issues with yarnpkg, we also want to override the npm registry when doing
622 // npm info.
623 // Issues: We sometimes get back cached responses, i.e old data about packages which causes
624 // `publish` to behave incorrectly. It can also cause issues when publishing private packages
625 // as they will always give a 404, which will tell `publish` to always try to publish.
626 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
627
628 let result = await spawn$1("npm", ["info", packageJson.name, "--registry", getCorrectRegistry(packageJson), "--json"]); // Github package registry returns empty string when calling npm info
629 // for a non-existant package instead of a E404
630
631 if (result.stdout.toString() === "") {
632 return {
633 error: {
634 code: "E404"
635 }
636 };
637 }
638
639 return jsonParse(result.stdout.toString());
640 });
641}
642async function infoAllow404(packageJson) {
643 var _pkgInfo$error;
644
645 let pkgInfo = await getPackageInfo(packageJson);
646
647 if (((_pkgInfo$error = pkgInfo.error) === null || _pkgInfo$error === void 0 ? void 0 : _pkgInfo$error.code) === "E404") {
648 warn(`Received 404 for npm info ${chalk.cyan(`"${packageJson.name}"`)}`);
649 return {
650 published: false,
651 pkgInfo: {}
652 };
653 }
654
655 if (pkgInfo.error) {
656 error(`Received an unknown error code: ${pkgInfo.error.code} for npm info ${chalk.cyan(`"${packageJson.name}"`)}`);
657 error(pkgInfo.error.summary);
658 if (pkgInfo.error.detail) error(pkgInfo.error.detail);
659 throw new ExitError(1);
660 }
661
662 return {
663 published: true,
664 pkgInfo
665 };
666}
667let otpAskLimit = pLimit(1);
668
669let askForOtpCode = twoFactorState => otpAskLimit(async () => {
670 if (twoFactorState.token !== null) return twoFactorState.token;
671 info("This operation requires a one-time password from your authenticator.");
672 let val = await askQuestion("Enter one-time password:");
673 twoFactorState.token = val;
674 return val;
675});
676
677let getOtpCode = async twoFactorState => {
678 if (twoFactorState.token !== null) {
679 return twoFactorState.token;
680 }
681
682 return askForOtpCode(twoFactorState);
683}; // we have this so that we can do try a publish again after a publish without
684// the call being wrapped in the npm request limit and causing the publishes to potentially never run
685
686async function internalPublish(pkgName, opts, twoFactorState) {
687 let publishTool = await getPublishTool(opts.cwd);
688 let publishFlags = opts.access ? ["--access", opts.access] : [];
689 publishFlags.push("--tag", opts.tag);
690
691 if ((await twoFactorState.isRequired) && !isCI) {
692 let otpCode = await getOtpCode(twoFactorState);
693 publishFlags.push("--otp", otpCode);
694 }
695
696 if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
697 publishFlags.push("--no-git-checks");
698 } // Due to a super annoying issue in yarn, we have to manually override this env variable
699 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
700
701
702 const envOverride = {
703 npm_config_registry: getCorrectRegistry()
704 };
705 let {
706 stdout
707 } = await spawn$1(publishTool.name, ["publish", opts.cwd, "--json", ...publishFlags], {
708 env: Object.assign({}, process.env, envOverride)
709 }); // New error handling. NPM's --json option is included alongside the `prepublish and
710 // `postpublish` contents in terminal. We want to handle this as best we can but it has
711 // some struggles
712 // Note that both pre and post publish hooks are printed before the json out, so this works.
713
714 let json = jsonParse(stdout.toString().replace(/[^{]*/, ""));
715
716 if (json.error) {
717 // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
718 if ((json.error.code === "EOTP" || json.error.code === "E401" && json.error.detail.includes("--otp=<code>")) && !isCI) {
719 if (twoFactorState.token !== null) {
720 // the current otp code must be invalid since it errored
721 twoFactorState.token = null;
722 } // just in case this isn't already true
723
724
725 twoFactorState.isRequired = Promise.resolve(true);
726 return internalPublish(pkgName, opts, twoFactorState);
727 }
728
729 error(`an error occurred while publishing ${pkgName}: ${json.error.code}`, json.error.summary, json.error.detail ? "\n" + json.error.detail : "");
730 return {
731 published: false
732 };
733 }
734
735 return {
736 published: true
737 };
738}
739
740function publish(pkgName, opts, twoFactorState) {
741 // If there are many packages to be published, it's better to limit the
742 // concurrency to avoid unwanted errors, for example from npm.
743 return npmRequestLimit(() => npmPublishLimit(() => internalPublish(pkgName, opts, twoFactorState)));
744}
745
746function getReleaseTag(pkgInfo, preState, tag) {
747 if (tag) return tag;
748
749 if (preState !== undefined && pkgInfo.publishedState !== "only-pre") {
750 return preState.tag;
751 }
752
753 return "latest";
754}
755
756const isCustomRegistry = registry => !!registry && registry !== "https://registry.npmjs.org" && registry !== "https://registry.yarnpkg.com";
757
758const getTwoFactorState = ({
759 otp,
760 publicPackages
761}) => {
762 if (otp) {
763 return {
764 token: otp,
765 isRequired: Promise.resolve(true)
766 };
767 }
768
769 if (isCI || publicPackages.some(pkg => {
770 var _pkg$packageJson$publ;
771
772 return isCustomRegistry((_pkg$packageJson$publ = pkg.packageJson.publishConfig) === null || _pkg$packageJson$publ === void 0 ? void 0 : _pkg$packageJson$publ.registry);
773 }) || isCustomRegistry(process.env.npm_config_registry)) {
774 return {
775 token: null,
776 isRequired: Promise.resolve(false)
777 };
778 }
779
780 return {
781 token: null,
782 // note: we're not awaiting this here, we want this request to happen in parallel with getUnpublishedPackages
783 isRequired: getTokenIsRequired()
784 };
785};
786
787async function publishPackages({
788 packages,
789 access,
790 otp,
791 preState,
792 tag
793}) {
794 const packagesByName = new Map(packages.map(x => [x.packageJson.name, x]));
795 const publicPackages = packages.filter(pkg => !pkg.packageJson.private);
796 const twoFactorState = getTwoFactorState({
797 otp,
798 publicPackages
799 });
800 const unpublishedPackagesInfo = await getUnpublishedPackages(publicPackages, preState);
801
802 if (unpublishedPackagesInfo.length === 0) {
803 warn("No unpublished packages to publish");
804 }
805
806 return Promise.all(unpublishedPackagesInfo.map(pkgInfo => {
807 let pkg = packagesByName.get(pkgInfo.name);
808 return publishAPackage(pkg, access, twoFactorState, getReleaseTag(pkgInfo, preState, tag));
809 }));
810}
811
812async function publishAPackage(pkg, access, twoFactorState, tag) {
813 const {
814 name,
815 version,
816 publishConfig
817 } = pkg.packageJson;
818 const localAccess = publishConfig === null || publishConfig === void 0 ? void 0 : publishConfig.access;
819 info(`Publishing ${chalk.cyan(`"${name}"`)} at ${chalk.green(`"${version}"`)}`);
820 const publishDir = (publishConfig === null || publishConfig === void 0 ? void 0 : publishConfig.directory) ? join(pkg.dir, publishConfig.directory) : pkg.dir;
821 const publishConfirmation = await publish(name, {
822 cwd: publishDir,
823 access: localAccess || access,
824 tag
825 }, twoFactorState);
826 return {
827 name,
828 newVersion: version,
829 published: publishConfirmation.published
830 };
831}
832
833async function getUnpublishedPackages(packages, preState) {
834 const results = await Promise.all(packages.map(async ({
835 packageJson
836 }) => {
837 const response = await infoAllow404(packageJson);
838 let publishedState = "never";
839
840 if (response.published) {
841 publishedState = "published";
842
843 if (preState !== undefined) {
844 if (response.pkgInfo.versions && response.pkgInfo.versions.every(version => semver.parse(version).prerelease[0] === preState.tag)) {
845 publishedState = "only-pre";
846 }
847 }
848 }
849
850 return {
851 name: packageJson.name,
852 localVersion: packageJson.version,
853 publishedState: publishedState,
854 publishedVersions: response.pkgInfo.versions || []
855 };
856 }));
857 const packagesToPublish = [];
858
859 for (const pkgInfo of results) {
860 const {
861 name,
862 publishedState,
863 localVersion,
864 publishedVersions
865 } = pkgInfo;
866
867 if (!publishedVersions.includes(localVersion)) {
868 packagesToPublish.push(pkgInfo);
869 info(`${name} is being published because our local version (${localVersion}) has not been published on npm`);
870
871 if (preState !== undefined && publishedState === "only-pre") {
872 info(`${name} is being published to ${chalk.cyan("latest")} rather than ${chalk.cyan(preState.tag)} because there has not been a regular release of it yet`);
873 }
874 } else {
875 // If the local version is behind npm, something is wrong, we warn here, and by not getting published later, it will fail
876 warn(`${name} is not being published because version ${localVersion} is already published on npm`);
877 }
878 }
879
880 return packagesToPublish;
881}
882
883function logReleases(pkgs) {
884 const mappedPkgs = pkgs.map(p => `${p.name}@${p.newVersion}`).join("\n");
885 log(mappedPkgs);
886}
887
888let importantSeparator$1 = chalk.red("===============================IMPORTANT!===============================");
889let importantEnd$1 = chalk.red("----------------------------------------------------------------------");
890
891function showNonLatestTagWarning(tag, preState) {
892 warn(importantSeparator$1);
893
894 if (preState) {
895 warn(`You are in prerelease mode so packages will be published to the ${chalk.cyan(preState.tag)}
896 dist tag except for packages that have not had normal releases which will be published to ${chalk.cyan("latest")}`);
897 } else if (tag !== "latest") {
898 warn(`Packages will be released under the ${tag} tag`);
899 }
900
901 warn(importantEnd$1);
902}
903
904async function run(cwd, {
905 otp,
906 tag: tag$1
907}, config) {
908 const releaseTag = tag$1 && tag$1.length > 0 ? tag$1 : undefined;
909 let preState = await readPreState(cwd);
910
911 if (releaseTag && preState && preState.mode === "pre") {
912 error("Releasing under custom tag is not allowed in pre mode");
913 log("To resolve this exit the pre mode by running `changeset pre exit`");
914 throw new ExitError(1);
915 }
916
917 if (releaseTag || preState) {
918 showNonLatestTagWarning(tag$1, preState);
919 }
920
921 const {
922 packages,
923 tool
924 } = await getPackages(cwd);
925 const response = await publishPackages({
926 packages,
927 // if not public, we wont pass the access, and it works as normal
928 access: config.access,
929 otp,
930 preState,
931 tag: releaseTag
932 });
933 const successful = response.filter(p => p.published);
934 const unsuccessful = response.filter(p => !p.published);
935
936 if (successful.length > 0) {
937 success("packages published successfully:");
938 logReleases(successful); // We create the tags after the push above so that we know that HEAD wont change and that pushing
939 // wont suffer from a race condition if another merge happens in the mean time (pushing tags wont
940 // fail if we are behind the base branch).
941
942 log(`Creating git tag${successful.length > 1 ? "s" : ""}...`);
943
944 if (tool !== "root") {
945 for (const pkg of successful) {
946 const tag$1 = `${pkg.name}@${pkg.newVersion}`;
947 log("New tag: ", tag$1);
948 await tag(tag$1, cwd);
949 }
950 } else {
951 const tag$1 = `v${successful[0].newVersion}`;
952 log("New tag: ", tag$1);
953 await tag(tag$1, cwd);
954 }
955 }
956
957 if (unsuccessful.length > 0) {
958 error("packages failed to publish:");
959 logReleases(unsuccessful);
960 throw new ExitError(1);
961 }
962}
963
964async function getStatus(cwd, {
965 sinceMaster,
966 since,
967 verbose,
968 output
969}, config) {
970 if (sinceMaster) {
971 warn("--sinceMaster is deprecated and will be removed in a future major version");
972 warn("Use --since=master instead");
973 }
974
975 const sinceBranch = since === undefined ? sinceMaster ? "master" : undefined : since;
976 const releasePlan = await getReleasePlan(cwd, sinceBranch, config);
977 const {
978 changesets,
979 releases
980 } = releasePlan;
981 const changedPackages = await getChangedPackagesSinceRef({
982 cwd,
983 ref: sinceBranch || config.baseBranch
984 });
985
986 if (changedPackages.length > 0 && changesets.length === 0) {
987 error("Some packages have been changed but no changesets were found. Run `changeset add` to resolve this error.");
988 error("If this change doesn't need a release, run `changeset add --empty`.");
989 process.exit(1);
990 }
991
992 if (output) {
993 await fs.writeFile(path.join(cwd, output), JSON.stringify(releasePlan, undefined, 2));
994 return;
995 }
996
997 const print = verbose ? verbosePrint : SimplePrint;
998 print("patch", releases);
999 log("---");
1000 print("minor", releases);
1001 log("---");
1002 print("major", releases);
1003 return releasePlan;
1004}
1005
1006function SimplePrint(type, releases) {
1007 const packages = releases.filter(r => r.type === type);
1008
1009 if (packages.length) {
1010 info(chalk`Packages to be bumped at {green ${type}}:\n`);
1011 const pkgs = packages.map(({
1012 name
1013 }) => `- ${name}`).join("\n");
1014 log(chalk.green(pkgs));
1015 } else {
1016 info(chalk`{red NO} packages to be bumped at {green ${type}}`);
1017 }
1018}
1019
1020function verbosePrint(type, releases) {
1021 const packages = releases.filter(r => r.type === type);
1022
1023 if (packages.length) {
1024 info(chalk`Packages to be bumped at {green ${type}}`);
1025 const columns = packages.map(({
1026 name,
1027 newVersion: version,
1028 changesets
1029 }) => [chalk.green(name), version, changesets.map(c => chalk.blue(` .changeset/${c}/changes.md`)).join(" +")]);
1030 const t1 = table([{
1031 value: "Package Name",
1032 width: 20
1033 }, {
1034 value: "New Version",
1035 width: 20
1036 }, {
1037 value: "Related Changeset Summaries",
1038 width: 70
1039 }], columns, {
1040 paddingLeft: 1,
1041 paddingRight: 0,
1042 headerAlign: "center",
1043 align: "left"
1044 });
1045 log(t1.render() + "\n");
1046 } else {
1047 info(chalk`Running release would release {red NO} packages as a {green ${type}}`);
1048 }
1049}
1050
1051async function pre(cwd, options) {
1052 if (options.command === "enter") {
1053 try {
1054 await enterPre(cwd, options.tag);
1055 success(`Entered pre mode with tag ${chalk.cyan(options.tag)}`);
1056 info("Run `changeset version` to version packages with prerelease versions");
1057 } catch (err) {
1058 if (err instanceof PreEnterButInPreModeError) {
1059 error("`changeset pre enter` cannot be run when in pre mode");
1060 info("If you're trying to exit pre mode, run `changeset pre exit`");
1061 throw new ExitError(1);
1062 }
1063
1064 throw err;
1065 }
1066 } else {
1067 try {
1068 await exitPre(cwd);
1069 success(`Exited pre mode`);
1070 info("Run `changeset version` to version packages with normal versions");
1071 } catch (err) {
1072 if (err instanceof PreExitButNotInPreModeError) {
1073 error("`changeset pre exit` can only be run when in pre mode");
1074 info("If you're trying to enter pre mode, run `changeset pre enter`");
1075 throw new ExitError(1);
1076 }
1077
1078 throw err;
1079 }
1080 }
1081}
1082
1083async function run$1(cwd) {
1084 const {
1085 packages,
1086 tool
1087 } = await getPackages(cwd);
1088 const allExistingTags = await getAllTags(cwd);
1089
1090 for (const pkg of packages) {
1091 const tag$1 = tool !== "root" ? `${pkg.packageJson.name}@${pkg.packageJson.version}` : `v${pkg.packageJson.version}`;
1092
1093 if (allExistingTags.has(tag$1)) {
1094 log("Skipping tag (already exists): ", tag$1);
1095 } else {
1096 log("New tag: ", tag$1);
1097 await tag(tag$1, cwd);
1098 }
1099 }
1100}
1101
1102async function run$2(input, flags, cwd) {
1103 if (input[0] === "init") {
1104 await init(cwd);
1105 return;
1106 }
1107
1108 if (!fs.existsSync(path.resolve(cwd, ".changeset"))) {
1109 error("There is no .changeset folder. ");
1110 error("If this is the first time `changesets` have been used in this project, run `yarn changeset init` to get set up.");
1111 error("If you expected there to be changesets, you should check git history for when the folder was removed to ensure you do not lose any configuration.");
1112 throw new ExitError(1);
1113 }
1114
1115 const packages = await getPackages(cwd);
1116 let config;
1117
1118 try {
1119 config = await read(cwd, packages);
1120 } catch (e) {
1121 let oldConfigExists = await fs.pathExists(path.resolve(cwd, ".changeset/config.js"));
1122
1123 if (oldConfigExists) {
1124 error("It looks like you're using the version 1 `.changeset/config.js` file");
1125 error("You'll need to convert it to a `.changeset/config.json` file");
1126 error("The format of the config object has significantly changed in v2 as well");
1127 error(" - we thoroughly recommend looking at the changelog for this package for what has changed");
1128 throw new ExitError(1);
1129 } else {
1130 throw e;
1131 }
1132 }
1133
1134 if (input.length < 1) {
1135 const {
1136 empty,
1137 open
1138 } = flags; // @ts-ignore if this is undefined, we have already exited
1139
1140 await add(cwd, {
1141 empty,
1142 open
1143 }, config);
1144 } else if (input[0] !== "pre" && input.length > 1) {
1145 error("Too many arguments passed to changesets - we only accept the command name as an argument");
1146 } else {
1147 const {
1148 sinceMaster,
1149 since,
1150 verbose,
1151 output,
1152 otp,
1153 empty,
1154 ignore,
1155 snapshot,
1156 tag,
1157 open
1158 } = flags;
1159 const deadFlags = ["updateChangelog", "isPublic", "skipCI", "commit"];
1160 deadFlags.forEach(flag => {
1161 if (flags[flag]) {
1162 error(`the flag ${flag} has been removed from changesets for version 2`);
1163 error(`Please encode the desired value into your config`);
1164 error(`See our changelog for more details`);
1165 throw new ExitError(1);
1166 }
1167 }); // Command line options need to be undefined, otherwise their
1168 // default value overrides the user's provided config in their
1169 // config file. For this reason, we only assign them to this
1170 // object as and when they exist.
1171
1172 switch (input[0]) {
1173 case "add":
1174 {
1175 // @ts-ignore if this is undefined, we have already exited
1176 await add(cwd, {
1177 empty,
1178 open
1179 }, config);
1180 return;
1181 }
1182
1183 case "version":
1184 {
1185 let ignoreArrayFromCmd;
1186
1187 if (typeof ignore === "string") {
1188 ignoreArrayFromCmd = [ignore];
1189 } else {
1190 // undefined or an array
1191 ignoreArrayFromCmd = ignore;
1192 } // Validate that items in ignoreArrayFromCmd are valid project names
1193
1194
1195 let pkgNames = new Set(packages.packages.map(({
1196 packageJson
1197 }) => packageJson.name));
1198 const messages = [];
1199
1200 for (const pkgName of ignoreArrayFromCmd || []) {
1201 if (!pkgNames.has(pkgName)) {
1202 messages.push(`The package "${pkgName}" is passed to the \`--ignore\` option but it is not found in the project. You may have misspelled the package name.`);
1203 }
1204 }
1205
1206 if (config.ignore.length > 0 && ignoreArrayFromCmd) {
1207 messages.push(`It looks like you are trying to use the \`--ignore\` option while ignore is defined in the config file. This is currently not allowed, you can only use one of them at a time.`);
1208 } else if (ignoreArrayFromCmd) {
1209 // use the ignore flags from cli
1210 config.ignore = ignoreArrayFromCmd;
1211 } // Validate that all dependents of ignored packages are listed in the ignore list
1212
1213
1214 const dependentsGraph = getDependentsGraph(packages, {
1215 bumpVersionsWithWorkspaceProtocolOnly: config.bumpVersionsWithWorkspaceProtocolOnly
1216 });
1217
1218 for (const ignoredPackage of config.ignore) {
1219 const dependents = dependentsGraph.get(ignoredPackage) || [];
1220
1221 for (const dependent of dependents) {
1222 if (!config.ignore.includes(dependent)) {
1223 messages.push(`The package "${dependent}" depends on the ignored package "${ignoredPackage}", but "${dependent}" is not being ignored. Please pass "${dependent}" to the \`--ignore\` flag.`);
1224 }
1225 }
1226 }
1227
1228 if (messages.length > 0) {
1229 error(messages.join("\n"));
1230 throw new ExitError(1);
1231 }
1232
1233 await version(cwd, {
1234 snapshot
1235 }, config);
1236 return;
1237 }
1238
1239 case "publish":
1240 {
1241 await run(cwd, {
1242 otp,
1243 tag
1244 }, config);
1245 return;
1246 }
1247
1248 case "status":
1249 {
1250 await getStatus(cwd, {
1251 sinceMaster,
1252 since,
1253 verbose,
1254 output
1255 }, config);
1256 return;
1257 }
1258
1259 case "tag":
1260 {
1261 await run$1(cwd);
1262 return;
1263 }
1264
1265 case "pre":
1266 {
1267 let command = input[1];
1268
1269 if (command !== "enter" && command !== "exit") {
1270 error("`enter`, `exit` or `snapshot` must be passed after prerelease");
1271 throw new ExitError(1);
1272 }
1273
1274 let tag = input[2];
1275
1276 if (command === "enter" && typeof tag !== "string") {
1277 error(`A tag must be passed when using prerelese enter`);
1278 throw new ExitError(1);
1279 } // @ts-ignore
1280
1281
1282 await pre(cwd, {
1283 command,
1284 tag
1285 });
1286 return;
1287 }
1288
1289 case "bump":
1290 {
1291 error('In version 2 of changesets, "bump" has been renamed to "version" - see our changelog for an explanation');
1292 error("To fix this, use `changeset version` instead, and update any scripts that use changesets");
1293 throw new ExitError(1);
1294 }
1295
1296 case "release":
1297 {
1298 error('In version 2 of changesets, "release" has been renamed to "publish" - see our changelog for an explanation');
1299 error("To fix this, use `changeset publish` instead, and update any scripts that use changesets");
1300 throw new ExitError(1);
1301 }
1302
1303 default:
1304 {
1305 error(`Invalid command ${input[0]} was provided`);
1306 throw new ExitError(1);
1307 }
1308 }
1309 }
1310}
1311
1312const {
1313 input,
1314 flags
1315} = meow(`
1316 Usage
1317 $ changesets [command]
1318 Commands
1319 init
1320 add [--empty] [--open]
1321 version [--ignore]
1322 publish [--otp=code]
1323 status [--since <branch>] [--verbose] [--output=JSON_FILE.json]
1324 pre <enter|exit> <tag>
1325 tag
1326 `, {
1327 flags: {
1328 sinceMaster: {
1329 type: "boolean"
1330 },
1331 verbose: {
1332 type: "boolean",
1333 alias: "v"
1334 },
1335 output: {
1336 type: "string",
1337 alias: "o"
1338 },
1339 otp: {
1340 type: "string"
1341 },
1342 empty: {
1343 type: "boolean"
1344 },
1345 since: {
1346 type: "string"
1347 },
1348 ignore: {
1349 type: "string",
1350 isMultiple: true
1351 },
1352 tag: {
1353 type: "string"
1354 },
1355 open: {
1356 type: "boolean"
1357 }
1358 }
1359});
1360const cwd = process.cwd();
1361run$2(input, flags, cwd).catch(err => {
1362 if (err instanceof InternalError) {
1363 error("The following error is an internal unexpected error, these should never happen.");
1364 error("Please open an issue with the following link");
1365 error(`https://github.com/atlassian/changesets/issues/new?title=${encodeURIComponent(`Unexpected error during ${input[0] || "add"} command`)}&body=${encodeURIComponent(`## Error
1366
1367\`\`\`
1368${format("", err).replace(process.cwd(), "<cwd>")}
1369\`\`\`
1370
1371## Versions
1372
1373- @changesets/cli@${// eslint-disable-next-line import/no-extraneous-dependencies
1374 require("@changesets/cli/package.json").version}
1375- node@${process.version}
1376
1377## Extra details
1378
1379<!-- Add any extra details of what you were doing, ideas you have about what might have caused the error and reproduction steps if possible. If you have a repository we can look at that would be great. 😁 -->
1380`)}`);
1381 }
1382
1383 if (err instanceof ExitError) {
1384 return process.exit(err.code);
1385 }
1386
1387 error(err);
1388 process.exit(1);
1389});