UNPKG

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