1import { Command } from '@expo/commander'; 2import chalk from 'chalk'; 3import fs from 'fs-extra'; 4import inquirer, { QuestionCollection } from 'inquirer'; 5import path from 'path'; 6 7import * as Changelogs from '../Changelogs'; 8import * as Directories from '../Directories'; 9import { formatChangelogEntry } from '../Formatter'; 10import logger from '../Logger'; 11 12type ActionOptions = { 13 package: string; 14 // true means that the user didn't use --pull-request or --no-pull-request 15 // unfortunately, we can't change this value 16 pullRequest: number[] | true; 17 author: string[]; 18 entry: string; 19 type: string; 20 version: string; 21}; 22 23async function checkOrAskForOptions(options: ActionOptions): Promise<ActionOptions> { 24 const lengthValidator = (x: { length: number }) => x.length !== 0; 25 const stringValidator = { 26 filter: (s: string) => s.trim(), 27 validate: lengthValidator, 28 }; 29 30 const questions: QuestionCollection[] = []; 31 if (!options.package) { 32 questions.push({ 33 type: 'input', 34 name: 'package', 35 message: 'What is the package that you want to add?', 36 ...stringValidator, 37 }); 38 } 39 40 if (options.pullRequest === true) { 41 questions.push({ 42 type: 'input', 43 name: 'pullRequest', 44 message: 'What is the pull request number?', 45 filter: (pullRequests) => 46 pullRequests 47 .split(',') 48 .map((pr) => parseInt(pr, 10)) 49 .filter(Boolean), 50 validate: lengthValidator, 51 }); 52 } 53 54 if (!options.author.length) { 55 questions.push({ 56 type: 'input', 57 name: 'author', 58 message: 'Who is the author?', 59 filter: (authors) => 60 authors 61 .split(',') 62 .map((author) => author.trim()) 63 .filter(Boolean), 64 validate: lengthValidator, 65 }); 66 } 67 68 if (!options.entry) { 69 questions.push({ 70 type: 'input', 71 name: 'entry', 72 message: 'What is the changelog message?', 73 ...stringValidator, 74 }); 75 } 76 77 if (!options.type) { 78 questions.push({ 79 type: 'list', 80 name: 'type', 81 message: 'What is the type?', 82 choices: ['bug-fix', 'new-feature', 'breaking-change', 'library-upgrade', 'notice'], 83 }); 84 } 85 86 const promptAnswer = questions.length > 0 ? await inquirer.prompt<ActionOptions>(questions) : {}; 87 88 return { ...options, ...promptAnswer }; 89} 90 91function toChangeType(type: string): Changelogs.ChangeType | null { 92 switch (type) { 93 case 'bug-fix': 94 return Changelogs.ChangeType.BUG_FIXES; 95 case 'new-feature': 96 return Changelogs.ChangeType.NEW_FEATURES; 97 case 'breaking-change': 98 return Changelogs.ChangeType.BREAKING_CHANGES; 99 case 'library-upgrade': 100 return Changelogs.ChangeType.LIBRARY_UPGRADES; 101 case 'notice': 102 return Changelogs.ChangeType.NOTICES; 103 } 104 return null; 105} 106 107async function action(options: ActionOptions) { 108 if (!process.env.CI) { 109 options = await checkOrAskForOptions(options); 110 } 111 112 if ( 113 !options.author.length || 114 !options.entry || 115 !options.package || 116 !options.type || 117 options.pullRequest === true 118 ) { 119 throw new Error( 120 `Must run with --package <string> --entry <string> --author <string> --pull-request <number> --type <string>` 121 ); 122 } 123 124 const type = toChangeType(options.type); 125 if (!type) { 126 throw new Error(`Invalid type: ${chalk.cyan(options.type)}`); 127 } 128 129 const packagePath = path.join(Directories.getPackagesDir(), options.package, 'CHANGELOG.md'); 130 if (!(await fs.pathExists(packagePath))) { 131 throw new Error(`Package ${chalk.green(options.package)} doesn't have changelog file.`); 132 } 133 134 const changelog = Changelogs.loadFrom(packagePath); 135 136 const message = options.entry.slice(-1) === '.' ? options.entry : `${options.entry}.`; 137 const insertedEntries = await changelog.insertEntriesAsync(options.version, type, null, [ 138 { 139 message, 140 pullRequests: options.pullRequest, 141 authors: options.author, 142 }, 143 ]); 144 145 if (insertedEntries.length > 0) { 146 logger.info( 147 `\n➕ Inserted ${chalk.magenta(options.type)} entry to ${chalk.green(options.package)}:` 148 ); 149 insertedEntries.forEach((entry) => { 150 logger.log(' ', formatChangelogEntry(Changelogs.getChangeEntryLabel(entry))); 151 }); 152 153 logger.info('\n Saving changelog file...'); 154 155 await changelog.saveAsync(); 156 157 logger.success('\n✅ Successfully inserted new entry.'); 158 } else { 159 logger.success('\n✅ Specified entry is already there.'); 160 } 161} 162 163export default (program: Command) => { 164 program 165 .command('add-changelog') 166 .alias('ac') 167 .description('Adds changelog entry to the package.') 168 .option( 169 '-p, --package <string>', 170 'Package name. For example `expo-image-picker` or `unimodules-file-system-interface.' 171 ) 172 .option('-e, --entry <string>', 'Change note to put into the changelog.') 173 .option( 174 '-a, --author <string>', 175 "GitHub's user name of someone who made this change. Can be passed multiple times.", 176 (value, previous) => previous.concat(value), 177 [] 178 ) 179 .option( 180 '-r, --pull-request <number>', 181 'Pull request number. Can be passed multiple times.', 182 (value, previous) => { 183 if (typeof previous === 'boolean') { 184 return [parseInt(value, 10)]; 185 } 186 187 return previous.concat(parseInt(value, 10)); 188 } 189 ) 190 .option( 191 '--no-pull-request', 192 'If changes were pushed directly to the main.', 193 (value, previous) => { 194 // we need to change how no-flag works in commander to be able to pass an array 195 if (!value) { 196 return []; 197 } 198 return previous; 199 } 200 ) 201 .option( 202 '-t, --type <string>', 203 'Type of change that determines the section into which the entry should be added. Possible options: bug-fix | new-feature | breaking-change | library-upgrade | notice.' 204 ) 205 .option( 206 '-v, --version [string]', 207 'Version in which the change was made.', 208 Changelogs.UNPUBLISHED_VERSION_NAME 209 ) 210 211 .asyncAction(action); 212}; 213