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