UNPKG

41.7 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
416// folder, and tidy up the subfolders
417// THIS SHOULD BE REMOVED WHEN SUPPORT FOR CHANGESETS FROM V1 IS DROPPED
418
419const removeEmptyFolders = async folderPath => {
420 const dirContents = fs.readdirSync(folderPath);
421 return Promise.all(dirContents.map(async contentPath => {
422 const singleChangesetPath = path.resolve(folderPath, contentPath);
423
424 try {
425 if ((await fs.readdir(singleChangesetPath)).length < 1) {
426 await fs.rmdir(singleChangesetPath);
427 }
428 } catch (err) {
429 if (err.code !== "ENOTDIR") {
430 throw err;
431 }
432 }
433 }));
434};
435
436function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
437
438function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
439
440function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
441let importantSeparator = chalk.red("===============================IMPORTANT!===============================");
442let importantEnd = chalk.red("----------------------------------------------------------------------");
443async function version(cwd, options, config) {
444 const releaseConfig = _objectSpread(_objectSpread({}, config), {}, {
445 // Disable committing when in snapshot mode
446 commit: options.snapshot ? false : config.commit
447 });
448
449 const [changesets, preState] = await Promise.all([readChangesets(cwd), readPreState(cwd), removeEmptyFolders(path.resolve(cwd, ".changeset"))]);
450
451 if ((preState === null || preState === void 0 ? void 0 : preState.mode) === "pre") {
452 warn(importantSeparator);
453
454 if (options.snapshot !== undefined) {
455 error("Snapshot release is not allowed in pre mode");
456 log("To resolve this exit the pre mode by running `changeset pre exit`");
457 throw new ExitError(1);
458 } else {
459 warn("You are in prerelease mode");
460 warn("If you meant to do a normal release you should revert these changes and run `changeset pre exit`");
461 warn("You can then run `changeset version` again to do a normal release");
462 }
463
464 warn(importantEnd);
465 }
466
467 if (changesets.length === 0 && (preState === undefined || preState.mode !== "exit")) {
468 warn("No unreleased changesets found, exiting.");
469 return;
470 }
471
472 let packages = await getPackages(cwd);
473 let releasePlan = assembleReleasePlan(changesets, packages, releaseConfig, preState, options.snapshot);
474 await applyReleasePlan(releasePlan, packages, releaseConfig, options.snapshot);
475
476 if (releaseConfig.commit) {
477 log("All files have been updated and committed. You're ready to publish!");
478 } else {
479 log("All files have been updated. Review them and commit at your leisure");
480 }
481}
482
483// @ts-ignore
484var isCI = !!(isCI$1 || process.env.GITHUB_ACTIONS);
485
486const npmRequestLimit = pLimit(40);
487
488function jsonParse(input) {
489 try {
490 return JSON.parse(input);
491 } catch (err) {
492 if (err instanceof SyntaxError) {
493 console.error("error parsing json:", input);
494 }
495
496 throw err;
497 }
498}
499
500function getCorrectRegistry() {
501 let registry = process.env.npm_config_registry === "https://registry.yarnpkg.com" ? undefined : process.env.npm_config_registry;
502 return registry;
503}
504
505async function getPublishTool(cwd) {
506 const pm = await preferredPM(cwd);
507 if (!pm || pm.name !== "pnpm") return {
508 name: "npm"
509 };
510
511 try {
512 let result = await spawn("pnpm", ["--version"], {
513 cwd
514 });
515 let version = result.stdout.toString().trim();
516 let parsed = semver.parse(version);
517 return {
518 name: "pnpm",
519 shouldAddNoGitChecks: (parsed === null || parsed === void 0 ? void 0 : parsed.major) === undefined ? false : parsed.major >= 5
520 };
521 } catch (e) {
522 return {
523 name: "pnpm",
524 shouldAddNoGitChecks: false
525 };
526 }
527}
528
529async function getTokenIsRequired() {
530 // Due to a super annoying issue in yarn, we have to manually override this env variable
531 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
532 const envOverride = {
533 npm_config_registry: getCorrectRegistry()
534 };
535 let result = await spawn("npm", ["profile", "get", "--json"], {
536 env: Object.assign({}, process.env, envOverride)
537 });
538 let json = jsonParse(result.stdout.toString());
539
540 if (json.error || !json.tfa || !json.tfa.mode) {
541 return false;
542 }
543
544 return json.tfa.mode === "auth-and-writes";
545}
546function getPackageInfo(pkgName) {
547 return npmRequestLimit(async () => {
548 info(`npm info ${pkgName}`); // Due to a couple of issues with yarnpkg, we also want to override the npm registry when doing
549 // npm info.
550 // Issues: We sometimes get back cached responses, i.e old data about packages which causes
551 // `publish` to behave incorrectly. It can also cause issues when publishing private packages
552 // as they will always give a 404, which will tell `publish` to always try to publish.
553 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
554
555 const envOverride = {
556 npm_config_registry: getCorrectRegistry()
557 };
558 let result = await spawn("npm", ["info", pkgName, "--json"], {
559 env: Object.assign({}, process.env, envOverride)
560 });
561 return jsonParse(result.stdout.toString());
562 });
563}
564async function infoAllow404(pkgName) {
565 let pkgInfo = await getPackageInfo(pkgName);
566
567 if (pkgInfo.error && pkgInfo.error.code === "E404") {
568 warn(`Received 404 for npm info ${chalk.cyan(`"${pkgName}"`)}`);
569 return {
570 published: false,
571 pkgInfo: {}
572 };
573 }
574
575 if (pkgInfo.error) {
576 error(`Received an unknown error code: ${pkgInfo.error.code} for npm info ${chalk.cyan(`"${pkgName}"`)}`);
577 error(pkgInfo.error.summary);
578 if (pkgInfo.error.detail) error(pkgInfo.error.detail);
579 throw new ExitError(1);
580 }
581
582 return {
583 published: true,
584 pkgInfo
585 };
586}
587let otpAskLimit = pLimit(1);
588
589let askForOtpCode = twoFactorState => otpAskLimit(async () => {
590 if (twoFactorState.token !== null) return twoFactorState.token;
591 info("This operation requires a one-time password from your authenticator.");
592 let val = await askQuestion("Enter one-time password:");
593 twoFactorState.token = val;
594 return val;
595});
596
597let getOtpCode = async twoFactorState => {
598 if (twoFactorState.token !== null) {
599 return twoFactorState.token;
600 }
601
602 return askForOtpCode(twoFactorState);
603}; // we have this so that we can do try a publish again after a publish without
604// the call being wrapped in the npm request limit and causing the publishes to potentially never run
605
606async function internalPublish(pkgName, opts, twoFactorState) {
607 let publishTool = await getPublishTool(opts.cwd);
608 let publishFlags = opts.access ? ["--access", opts.access] : [];
609 publishFlags.push("--tag", opts.tag);
610
611 if ((await twoFactorState.isRequired) && !isCI) {
612 let otpCode = await getOtpCode(twoFactorState);
613 publishFlags.push("--otp", otpCode);
614 }
615
616 if (publishTool.name === "pnpm" && publishTool.shouldAddNoGitChecks) {
617 publishFlags.push("--no-git-checks");
618 } // Due to a super annoying issue in yarn, we have to manually override this env variable
619 // See: https://github.com/yarnpkg/yarn/issues/2935#issuecomment-355292633
620
621
622 const envOverride = {
623 npm_config_registry: getCorrectRegistry()
624 };
625 let {
626 stdout
627 } = await spawn(publishTool.name, ["publish", "--json", ...publishFlags], {
628 cwd: opts.cwd,
629 env: Object.assign({}, process.env, envOverride)
630 }); // New error handling. NPM's --json option is included alongside the `prepublish and
631 // `postpublish` contents in terminal. We want to handle this as best we can but it has
632 // some struggles
633 // Note that both pre and post publish hooks are printed before the json out, so this works.
634
635 let json = jsonParse(stdout.toString().replace(/[^{]*/, ""));
636
637 if (json.error) {
638 // The first case is no 2fa provided, the second is when the 2fa is wrong (timeout or wrong words)
639 if ((json.error.code === "EOTP" || json.error.code === "E401" && json.error.detail.includes("--otp=<code>")) && !isCI) {
640 if (twoFactorState.token !== null) {
641 // the current otp code must be invalid since it errored
642 twoFactorState.token = null;
643 } // just in case this isn't already true
644
645
646 twoFactorState.isRequired = Promise.resolve(true);
647 return internalPublish(pkgName, opts, twoFactorState);
648 }
649
650 error(`an error occurred while publishing ${pkgName}: ${json.error.code}`, json.error.summary, json.error.detail ? "\n" + json.error.detail : "");
651 return {
652 published: false
653 };
654 }
655
656 return {
657 published: true
658 };
659}
660
661function publish(pkgName, opts, twoFactorState) {
662 return npmRequestLimit(() => {
663 return internalPublish(pkgName, opts, twoFactorState);
664 });
665}
666
667function getReleaseTag(pkgInfo, preState, tag) {
668 if (tag) return tag;
669
670 if (preState !== undefined && pkgInfo.publishedState !== "only-pre") {
671 return preState.tag;
672 }
673
674 return "latest";
675}
676
677async function publishPackages({
678 packages,
679 access,
680 otp,
681 preState,
682 tag
683}) {
684 const packagesByName = new Map(packages.map(x => [x.packageJson.name, x]));
685 const publicPackages = packages.filter(pkg => !pkg.packageJson.private);
686 let twoFactorState = otp === undefined ? {
687 token: null,
688 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
689 getTokenIsRequired()
690 } : {
691 token: otp,
692 isRequired: Promise.resolve(true)
693 };
694 const unpublishedPackagesInfo = await getUnpublishedPackages(publicPackages, preState);
695
696 if (unpublishedPackagesInfo.length === 0) {
697 warn("No unpublished packages to publish");
698 }
699
700 let promises = [];
701
702 for (let pkgInfo of unpublishedPackagesInfo) {
703 let pkg = packagesByName.get(pkgInfo.name);
704 promises.push(publishAPackage(pkg, access, twoFactorState, getReleaseTag(pkgInfo, preState, tag)));
705 }
706
707 return Promise.all(promises);
708}
709
710async function publishAPackage(pkg, access, twoFactorState, tag) {
711 const {
712 name,
713 version,
714 publishConfig
715 } = pkg.packageJson;
716 const localAccess = publishConfig && publishConfig.access;
717 info(`Publishing ${chalk.cyan(`"${name}"`)} at ${chalk.green(`"${version}"`)}`);
718 const publishDir = publishConfig && publishConfig.directory ? join(pkg.dir, publishConfig.directory) : pkg.dir;
719 const publishConfirmation = await publish(name, {
720 cwd: publishDir,
721 access: localAccess || access,
722 tag
723 }, twoFactorState);
724 return {
725 name,
726 newVersion: version,
727 published: publishConfirmation.published
728 };
729}
730
731async function getUnpublishedPackages(packages, preState) {
732 const results = await Promise.all(packages.map(async pkg => {
733 const config = pkg.packageJson;
734 const response = await infoAllow404(config.name);
735 let publishedState = "never";
736
737 if (response.published) {
738 publishedState = "published";
739
740 if (preState !== undefined) {
741 if (response.pkgInfo.versions && response.pkgInfo.versions.every(version => semver.parse(version).prerelease[0] === preState.tag)) {
742 publishedState = "only-pre";
743 }
744 }
745 }
746
747 return {
748 name: config.name,
749 localVersion: config.version,
750 publishedState: publishedState,
751 publishedVersions: response.pkgInfo.versions || []
752 };
753 }));
754 const packagesToPublish = [];
755
756 for (const pkgInfo of results) {
757 const {
758 name,
759 publishedState,
760 localVersion,
761 publishedVersions
762 } = pkgInfo;
763
764 if (!publishedVersions.includes(localVersion)) {
765 packagesToPublish.push(pkgInfo);
766 info(`${name} is being published because our local version (${localVersion}) has not been published on npm`);
767
768 if (preState !== undefined && publishedState === "only-pre") {
769 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`);
770 }
771 } else {
772 // If the local version is behind npm, something is wrong, we warn here, and by not getting published later, it will fail
773 warn(`${name} is not being published because version ${localVersion} is already published on npm`);
774 }
775 }
776
777 return packagesToPublish;
778}
779
780function logReleases(pkgs) {
781 const mappedPkgs = pkgs.map(p => `${p.name}@${p.newVersion}`).join("\n");
782 log(mappedPkgs);
783}
784
785let importantSeparator$1 = chalk.red("===============================IMPORTANT!===============================");
786let importantEnd$1 = chalk.red("----------------------------------------------------------------------");
787
788function showNonLatestTagWarning(tag, preState) {
789 warn(importantSeparator$1);
790
791 if (preState) {
792 warn(`You are in prerelease mode so packages will be published to the ${chalk.cyan(preState.tag)}
793 dist tag except for packages that have not had normal releases which will be published to ${chalk.cyan("latest")}`);
794 } else if (tag !== "latest") {
795 warn(`Packages will be released under the ${tag} tag`);
796 }
797
798 warn(importantEnd$1);
799}
800
801async function run(cwd, {
802 otp,
803 tag: tag$1
804}, config) {
805 const releaseTag = tag$1 && tag$1.length > 0 ? tag$1 : undefined;
806 let preState = await readPreState(cwd);
807
808 if (releaseTag && preState && preState.mode === "pre") {
809 error("Releasing under custom tag is not allowed in pre mode");
810 log("To resolve this exit the pre mode by running `changeset pre exit`");
811 throw new ExitError(1);
812 }
813
814 if (releaseTag || preState) {
815 showNonLatestTagWarning(tag$1, preState);
816 }
817
818 const {
819 packages,
820 tool
821 } = await getPackages(cwd);
822 const response = await publishPackages({
823 packages,
824 // if not public, we wont pass the access, and it works as normal
825 access: config.access,
826 otp,
827 preState,
828 tag: releaseTag
829 });
830 const successful = response.filter(p => p.published);
831 const unsuccessful = response.filter(p => !p.published);
832
833 if (successful.length > 0) {
834 success("packages published successfully:");
835 logReleases(successful); // We create the tags after the push above so that we know that HEAD wont change and that pushing
836 // wont suffer from a race condition if another merge happens in the mean time (pushing tags wont
837 // fail if we are behind master).
838
839 log(`Creating git tag${successful.length > 1 ? "s" : ""}...`);
840
841 if (tool !== "root") {
842 for (const pkg of successful) {
843 const tag$1 = `${pkg.name}@${pkg.newVersion}`;
844 log("New tag: ", tag$1);
845 await tag(tag$1, cwd);
846 }
847 } else {
848 const tag$1 = `v${successful[0].newVersion}`;
849 log("New tag: ", tag$1);
850 await tag(tag$1, cwd);
851 }
852 }
853
854 if (unsuccessful.length > 0) {
855 error("packages failed to publish:");
856 logReleases(unsuccessful);
857 throw new ExitError(1);
858 }
859}
860
861async function getStatus(cwd, {
862 sinceMaster,
863 since,
864 verbose,
865 output
866}, config) {
867 if (sinceMaster) {
868 warn("--sinceMaster is deprecated and will be removed in a future major version");
869 warn("Use --since=master instead");
870 }
871
872 const releasePlan = await getReleasePlan(cwd, since === undefined ? sinceMaster ? "master" : undefined : since, config);
873 const {
874 changesets,
875 releases
876 } = releasePlan;
877
878 if (changesets.length < 1) {
879 error("No changesets present");
880 process.exit(1);
881 }
882
883 if (output) {
884 await fs.writeFile(path.join(cwd, output), JSON.stringify(releasePlan, undefined, 2));
885 return;
886 }
887
888 const print = verbose ? verbosePrint : SimplePrint;
889 print("patch", releases);
890 log("---");
891 print("minor", releases);
892 log("---");
893 print("major", releases);
894 return releasePlan;
895}
896
897function SimplePrint(type, releases) {
898 const packages = releases.filter(r => r.type === type);
899
900 if (packages.length) {
901 info(chalk`Packages to be bumped at {green ${type}}:\n`);
902 const pkgs = packages.map(({
903 name
904 }) => `- ${name}`).join("\n");
905 log(chalk.green(pkgs));
906 } else {
907 info(chalk`{red NO} packages to be bumped at {green ${type}}`);
908 }
909}
910
911function verbosePrint(type, releases) {
912 const packages = releases.filter(r => r.type === type);
913
914 if (packages.length) {
915 info(chalk`Packages to be bumped at {green ${type}}`);
916 const columns = packages.map(({
917 name,
918 newVersion: version,
919 changesets
920 }) => [chalk.green(name), version, changesets.map(c => chalk.blue(` .changeset/${c}/changes.md`)).join(" +")]);
921 const t1 = table([{
922 value: "Package Name",
923 width: 20
924 }, {
925 value: "New Version",
926 width: 20
927 }, {
928 value: "Related Changeset Summaries",
929 width: 70
930 }], columns, {
931 paddingLeft: 1,
932 paddingRight: 0,
933 headerAlign: "center",
934 align: "left"
935 });
936 log(t1.render() + "\n");
937 } else {
938 info(chalk`Running release would release {red NO} packages as a {green ${type}}`);
939 }
940}
941
942async function pre(cwd, options) {
943 if (options.command === "enter") {
944 try {
945 await enterPre(cwd, options.tag);
946 success(`Entered pre mode with tag ${chalk.cyan(options.tag)}`);
947 info("Run `changeset version` to version packages with prerelease versions");
948 } catch (err) {
949 if (err instanceof PreEnterButInPreModeError) {
950 error("`changeset pre enter` cannot be run when in pre mode");
951 info("If you're trying to exit pre mode, run `changeset pre exit`");
952 throw new ExitError(1);
953 }
954
955 throw err;
956 }
957 } else {
958 try {
959 await exitPre(cwd);
960 success(`Exited pre mode`);
961 info("Run `changeset version` to version packages with normal versions");
962 } catch (err) {
963 if (err instanceof PreExitButNotInPreModeError) {
964 error("`changeset pre exit` can only be run when in pre mode");
965 info("If you're trying to enter pre mode, run `changeset pre enter`");
966 throw new ExitError(1);
967 }
968
969 throw err;
970 }
971 }
972}
973
974async function run$1(input, flags, cwd) {
975 if (input[0] === "init") {
976 await init(cwd);
977 return;
978 }
979
980 if (!fs.existsSync(path.resolve(cwd, ".changeset"))) {
981 error("There is no .changeset folder. ");
982 error("If this is the first time `changesets` have been used in this project, run `yarn changeset init` to get set up.");
983 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.");
984 throw new ExitError(1);
985 }
986
987 const packages = await getPackages(cwd);
988 let config;
989
990 try {
991 config = await read(cwd, packages);
992 } catch (e) {
993 let oldConfigExists = await fs.pathExists(path.resolve(cwd, ".changeset/config.js"));
994
995 if (oldConfigExists) {
996 error("It looks like you're using the version 1 `.changeset/config.js` file");
997 error("You'll need to convert it to a `.changeset/config.json` file");
998 error("The format of the config object has significantly changed in v2 as well");
999 error(" - we thoroughly recommend looking at the changelog for this package for what has changed");
1000 throw new ExitError(1);
1001 } else {
1002 throw e;
1003 }
1004 }
1005
1006 if (input.length < 1) {
1007 const {
1008 empty
1009 } = flags; // @ts-ignore if this is undefined, we have already exited
1010
1011 await add(cwd, {
1012 empty
1013 }, config);
1014 } else if (input[0] !== "pre" && input.length > 1) {
1015 error("Too many arguments passed to changesets - we only accept the command name as an argument");
1016 } else {
1017 const {
1018 sinceMaster,
1019 since,
1020 verbose,
1021 output,
1022 otp,
1023 empty,
1024 ignore,
1025 snapshot,
1026 tag
1027 } = flags;
1028 const deadFlags = ["updateChangelog", "isPublic", "skipCI", "commit"];
1029 deadFlags.forEach(flag => {
1030 if (flags[flag]) {
1031 error(`the flag ${flag} has been removed from changesets for version 2`);
1032 error(`Please encode the desired value into your config`);
1033 error(`See our changelog for more details`);
1034 throw new ExitError(1);
1035 }
1036 }); // Command line options need to be undefined, otherwise their
1037 // default value overrides the user's provided config in their
1038 // config file. For this reason, we only assign them to this
1039 // object as and when they exist.
1040
1041 switch (input[0]) {
1042 case "add":
1043 {
1044 // @ts-ignore if this is undefined, we have already exited
1045 await add(cwd, {
1046 empty
1047 }, config);
1048 return;
1049 }
1050
1051 case "version":
1052 {
1053 let ignoreArrayFromCmd;
1054
1055 if (typeof ignore === "string") {
1056 ignoreArrayFromCmd = [ignore];
1057 } else {
1058 // undefined or an array
1059 ignoreArrayFromCmd = ignore;
1060 } // Validate that items in ignoreArrayFromCmd are valid project names
1061
1062
1063 let pkgNames = new Set(packages.packages.map(({
1064 packageJson
1065 }) => packageJson.name));
1066 const messages = [];
1067
1068 for (const pkgName of ignoreArrayFromCmd || []) {
1069 if (!pkgNames.has(pkgName)) {
1070 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.`);
1071 }
1072 }
1073
1074 if (config.ignore.length > 0 && ignoreArrayFromCmd) {
1075 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.`);
1076 } else if (ignoreArrayFromCmd) {
1077 // use the ignore flags from cli
1078 config.ignore = ignoreArrayFromCmd;
1079 } // Validate that all dependents of ignored packages are listed in the ignore list
1080
1081
1082 const dependentsGraph = getDependentsGraph(packages);
1083
1084 for (const ignoredPackage of config.ignore) {
1085 const dependents = dependentsGraph.get(ignoredPackage) || [];
1086
1087 for (const dependent of dependents) {
1088 if (!config.ignore.includes(dependent)) {
1089 messages.push(`The package "${dependent}" depends on the ignored package "${ignoredPackage}", but "${dependent}" is not being ignored. Please pass "${dependent}" to the \`--ignore\` flag.`);
1090 }
1091 }
1092 }
1093
1094 if (messages.length > 0) {
1095 error(messages.join("\n"));
1096 throw new ExitError(1);
1097 }
1098
1099 await version(cwd, {
1100 snapshot
1101 }, config);
1102 return;
1103 }
1104
1105 case "publish":
1106 {
1107 await run(cwd, {
1108 otp,
1109 tag
1110 }, config);
1111 return;
1112 }
1113
1114 case "status":
1115 {
1116 await getStatus(cwd, {
1117 sinceMaster,
1118 since,
1119 verbose,
1120 output
1121 }, config);
1122 return;
1123 }
1124
1125 case "pre":
1126 {
1127 let command = input[1];
1128
1129 if (command !== "enter" && command !== "exit") {
1130 error("`enter`, `exit` or `snapshot` must be passed after prerelease");
1131 throw new ExitError(1);
1132 }
1133
1134 let tag = input[2];
1135
1136 if (command === "enter" && typeof tag !== "string") {
1137 error(`A tag must be passed when using prerelese enter`);
1138 throw new ExitError(1);
1139 } // @ts-ignore
1140
1141
1142 await pre(cwd, {
1143 command,
1144 tag
1145 });
1146 return;
1147 }
1148
1149 case "bump":
1150 {
1151 error('In version 2 of changesets, "bump" has been renamed to "version" - see our changelog for an explanation');
1152 error("To fix this, use `changeset version` instead, and update any scripts that use changesets");
1153 throw new ExitError(1);
1154 }
1155
1156 case "release":
1157 {
1158 error('In version 2 of changesets, "release" has been renamed to "publish" - see our changelog for an explanation');
1159 error("To fix this, use `changeset publish` instead, and update any scripts that use changesets");
1160 throw new ExitError(1);
1161 }
1162
1163 default:
1164 {
1165 error(`Invalid command ${input[0]} was provided`);
1166 throw new ExitError(1);
1167 }
1168 }
1169 }
1170}
1171
1172const {
1173 input,
1174 flags
1175} = meow(`
1176 Usage
1177 $ changesets [command]
1178 Commands
1179 init
1180 add [--empty]
1181 version [--ignore]
1182 publish [--otp=code]
1183 status [--since-master --verbose --output=JSON_FILE.json]
1184 prerelease <tag>
1185 `, {
1186 flags: {
1187 sinceMaster: {
1188 type: "boolean"
1189 },
1190 verbose: {
1191 type: "boolean",
1192 alias: "v"
1193 },
1194 output: {
1195 type: "string",
1196 alias: "o"
1197 },
1198 otp: {
1199 type: "string",
1200 default: undefined
1201 },
1202 empty: {
1203 type: "boolean"
1204 },
1205 since: {
1206 type: "string",
1207 default: undefined
1208 },
1209 ignore: {
1210 type: "string",
1211 default: undefined,
1212 isMultiple: true
1213 },
1214 tag: {
1215 type: "string"
1216 }
1217 }
1218});
1219const cwd = process.cwd();
1220run$1(input, flags, cwd).catch(err => {
1221 if (err instanceof InternalError) {
1222 error("The following error is an internal unexpected error, these should never happen.");
1223 error("Please open an issue with the following link");
1224 error(`https://github.com/atlassian/changesets/issues/new?title=${encodeURIComponent(`Unexpected error during ${input[0] || "add"} command`)}&body=${encodeURIComponent(`## Error
1225
1226\`\`\`
1227${format("", err).replace(process.cwd(), "<cwd>")}
1228\`\`\`
1229
1230## Versions
1231
1232- @changesets/cli@${// eslint-disable-next-line import/no-extraneous-dependencies
1233 require("@changesets/cli/package.json").version}
1234- node@${process.version}
1235
1236## Extra details
1237
1238<!-- 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. 😁 -->
1239`)}`);
1240 }
1241
1242 if (err instanceof ExitError) {
1243 return process.exit(err.code);
1244 }
1245
1246 error(err);
1247 process.exit(1);
1248});