From 633ab195c7dc7dcab73e6ec2a73ea47d87ad3613 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Sat, 16 Nov 2024 02:51:56 -0800 Subject: [PATCH 1/5] cleanup --- src/changefile/promptForChange.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/changefile/promptForChange.ts b/src/changefile/promptForChange.ts index 13815fd6..87340c66 100644 --- a/src/changefile/promptForChange.ts +++ b/src/changefile/promptForChange.ts @@ -26,12 +26,18 @@ export async function promptForChange(params: { // Get the questions for each package first, in case one package has a validation issue const packageQuestions: { [pkg: string]: prompts.PromptObject[] } = {}; + let hasError = false; for (const pkg of changedPackages) { const questions = getQuestionsForPackage({ pkg, ...params }); - if (!questions) { - return; // validation issue + if (questions) { + packageQuestions[pkg] = questions; + } else { + // show all the errors before returning + hasError = true; } - packageQuestions[pkg] = questions; + } + if (hasError) { + return; } // Now prompt for each package From 2bb973ebb84ece249695854a9688b2a0db57b67f Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Sat, 16 Nov 2024 03:44:00 -0800 Subject: [PATCH 2/5] Add v0-specific change descriptions --- .../changefile/getQuestionsForPackage.test.ts | 70 +++++++++++- ...ptForChange_promptForPackageChange.test.ts | 102 +++++++++--------- src/changefile/getQuestionsForPackage.ts | 59 +++++++--- src/types/ChangeFilePrompt.ts | 34 ++++++ 4 files changed, 197 insertions(+), 68 deletions(-) diff --git a/src/__tests__/changefile/getQuestionsForPackage.test.ts b/src/__tests__/changefile/getQuestionsForPackage.test.ts index 92ac8715..555dd7d5 100644 --- a/src/__tests__/changefile/getQuestionsForPackage.test.ts +++ b/src/__tests__/changefile/getQuestionsForPackage.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from '@jest/globals'; import prompts from 'prompts'; import { getQuestionsForPackage } from '../../changefile/getQuestionsForPackage'; -import { ChangeFilePromptOptions } from '../../types/ChangeFilePrompt'; +import { ChangeFilePromptOptions, ChangeTypeDescriptions } from '../../types/ChangeFilePrompt'; import { initMockLogs } from '../../__fixtures__/mockLogs'; import { makePackageInfos } from '../../__fixtures__/packageInfos'; @@ -28,10 +28,10 @@ describe('getQuestionsForPackage', () => { expect(questions).toEqual([ { choices: [ - { title: expect.stringContaining('Patch'), value: 'patch' }, - { title: expect.stringContaining('Minor'), value: 'minor' }, - { title: expect.stringContaining('None'), value: 'none' }, - { title: expect.stringContaining('Major'), value: 'major' }, + { title: ' Patch - bug fixes; no API changes', value: 'patch' }, + { title: ' Minor - new feature; backwards-compatible API changes', value: 'minor' }, + { title: ' None - this change does not affect the published package in any way', value: 'none' }, + { title: ' Major - breaking changes; major feature', value: 'major' }, ], message: 'Change type', name: 'type', @@ -48,6 +48,26 @@ describe('getQuestionsForPackage', () => { ]); }); + it('uses different descriptions for v0 package', () => { + const questions = getQuestionsForPackage({ + ...defaultQuestionsParams, + packageInfos: makePackageInfos({ [pkg]: { version: '0.1.0' } }), + }); + expect(questions![0].choices).toEqual([ + { + title: + ' Patch - bug fixes; new features; backwards-compatible API changes (ok in patches for v0.x packages)', + value: 'patch', + }, + { + title: ' Minor - breaking changes; major feature (ok in minor versions for v0.x packages)', + value: 'minor', + }, + { title: ' None - this change does not affect the published package in any way', value: 'none' }, + { title: ' Major - official release', value: 'major' }, + ]); + }); + // it's somewhat debatable if this is correct (maybe --type should be the override for disallowedChangeTypes?) it('errors if options.type is disallowed', () => { const questions = getQuestionsForPackage({ @@ -163,6 +183,46 @@ describe('getQuestionsForPackage', () => { ); }); + it('uses options.changeTypeDescriptions if set', () => { + const changeTypeDescriptions: ChangeTypeDescriptions = { + major: 'exciting', + minor: { v0: 'exciting v0!', general: 'boring' }, + premajor: 'almost exciting', + }; + const questions = getQuestionsForPackage({ + ...defaultQuestionsParams, + packageInfos: makePackageInfos({ + [pkg]: { version: '1.0.0', combinedOptions: { changeFilePrompt: { changeTypeDescriptions } } }, + }), + }); + + expect(questions![0].choices).toEqual([ + { title: ' Major - exciting', value: 'major' }, + { title: ' Minor - boring', value: 'minor' }, + { title: ' Premajor - almost exciting', value: 'premajor' }, + ]); + }); + + it('uses v0-specific options.changeTypeDescriptions if set', () => { + const changeTypeDescriptions: ChangeTypeDescriptions = { + major: 'exciting', + minor: { v0: 'exciting v0!', general: 'boring' }, + premajor: 'almost exciting', + }; + const questions = getQuestionsForPackage({ + ...defaultQuestionsParams, + packageInfos: makePackageInfos({ + [pkg]: { version: '0.1.0', combinedOptions: { changeFilePrompt: { changeTypeDescriptions } } }, + }), + }); + + expect(questions![0].choices).toEqual([ + { title: ' Major - exciting', value: 'major' }, + { title: ' Minor - exciting v0!', value: 'minor' }, + { title: ' Premajor - almost exciting', value: 'premajor' }, + ]); + }); + it('does case-insensitive filtering on description suggestions', async () => { const recentMessages = ['Foo', 'Bar', 'Baz']; const recentMessageChoices = [{ title: 'Foo' }, { title: 'Bar' }, { title: 'Baz' }]; diff --git a/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts b/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts index 202df6d9..c0664ce3 100644 --- a/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts +++ b/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts @@ -1,11 +1,11 @@ import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import prompts from 'prompts'; import { _promptForPackageChange } from '../../changefile/promptForChange'; +import { getQuestionsForPackage } from '../../changefile/getQuestionsForPackage'; import { initMockLogs } from '../../__fixtures__/mockLogs'; import { MockStdin } from '../../__fixtures__/mockStdin'; import { MockStdout } from '../../__fixtures__/mockStdout'; import { makePackageInfos } from '../../__fixtures__/packageInfos'; -import { getQuestionsForPackage } from '../../changefile/getQuestionsForPackage'; // prompts writes to stdout (not console) in a way that can't really be mocked with spies, // so instead we inject a custom mock stdout stream, as well as stdin for entering answers @@ -78,16 +78,16 @@ describe('promptForChange _promptForPackageChange', () => { expect(logs.getMockLines('log')).toMatchInlineSnapshot(`"Please describe the changes for: foo"`); expect(stdout.getOutput()).toMatchInlineSnapshot(` - "? Change type » - Use arrow-keys. Return to submit. - > Patch - bug fixes; no API changes. - Minor - small feature; backwards compatible API changes. - None - this change does not affect the published package in any way. - Major - major feature; breaking changes. - √ Change type » Patch - bug fixes; no API changes. - ? Describe changes (type or choose one) » - > message - √ Describe changes (type or choose one) » message" - `); + "? Change type » - Use arrow-keys. Return to submit. + > Patch - bug fixes; no API changes + Minor - new feature; backwards-compatible API changes + None - this change does not affect the published package in any way + Major - breaking changes; major feature + √ Change type » Patch - bug fixes; no API changes + ? Describe changes (type or choose one) » + > message + √ Describe changes (type or choose one) » message" + `); expect(answers).toEqual({ type: 'patch', comment: 'message' }); }); @@ -194,32 +194,32 @@ describe('promptForChange _promptForPackageChange', () => { await stdin.sendByChar('\n'); expect(stdout.getOutput()).toMatchInlineSnapshot(` - "? Change type » - Use arrow-keys. Return to submit. - > Patch - bug fixes; no API changes. - Minor - small feature; backwards compatible API changes. - None - this change does not affect the published package in any way. - Major - major feature; breaking changes. - ? Change type » - Use arrow-keys. Return to submit. - Patch - bug fixes; no API changes. - > Minor - small feature; backwards compatible API changes. - None - this change does not affect the published package in any way. - Major - major feature; breaking changes. - ? Change type » - Use arrow-keys. Return to submit. - Patch - bug fixes; no API changes. - Minor - small feature; backwards compatible API changes. - > None - this change does not affect the published package in any way. - Major - major feature; breaking changes. - √ Change type » None - this change does not affect the published package in any way. - ? Describe changes (type or choose one) » - > first - second - third - ? Describe changes (type or choose one) » - first - > second - third - √ Describe changes (type or choose one) » second" - `); + "? Change type » - Use arrow-keys. Return to submit. + > Patch - bug fixes; no API changes + Minor - new feature; backwards-compatible API changes + None - this change does not affect the published package in any way + Major - breaking changes; major feature + ? Change type » - Use arrow-keys. Return to submit. + Patch - bug fixes; no API changes + > Minor - new feature; backwards-compatible API changes + None - this change does not affect the published package in any way + Major - breaking changes; major feature + ? Change type » - Use arrow-keys. Return to submit. + Patch - bug fixes; no API changes + Minor - new feature; backwards-compatible API changes + > None - this change does not affect the published package in any way + Major - breaking changes; major feature + √ Change type » None - this change does not affect the published package in any way + ? Describe changes (type or choose one) » + > first + second + third + ? Describe changes (type or choose one) » + first + > second + third + √ Describe changes (type or choose one) » second" + `); const answers = await answerPromise; expect(answers).toEqual({ type: 'none', comment: 'second' }); @@ -305,22 +305,22 @@ describe('promptForChange _promptForPackageChange', () => { const answers = await answerPromise; expect(logs.getMockLines('log')).toMatchInlineSnapshot(` - "Please describe the changes for: foo - Cancelled, no change files are written" - `); + "Please describe the changes for: foo + Cancelled, no change files are written" + `); expect(stdout.getOutput()).toMatchInlineSnapshot(` - "? Change type » - Use arrow-keys. Return to submit. - > Patch - bug fixes; no API changes. - Minor - small feature; backwards compatible API changes. - None - this change does not affect the published package in any way. - Major - major feature; breaking changes. - √ Change type » Patch - bug fixes; no API changes. - ? Describe changes (type or choose one) » - > message - ? Describe changes (type or choose one) » a - × Describe changes (type or choose one) » a" - `); + "? Change type » - Use arrow-keys. Return to submit. + > Patch - bug fixes; no API changes + Minor - new feature; backwards-compatible API changes + None - this change does not affect the published package in any way + Major - breaking changes; major feature + √ Change type » Patch - bug fixes; no API changes + ? Describe changes (type or choose one) » + > message + ? Describe changes (type or choose one) » a + × Describe changes (type or choose one) » a" + `); expect(answers).toBeUndefined(); }); diff --git a/src/changefile/getQuestionsForPackage.ts b/src/changefile/getQuestionsForPackage.ts index 3afa2039..1aa54244 100644 --- a/src/changefile/getQuestionsForPackage.ts +++ b/src/changefile/getQuestionsForPackage.ts @@ -2,10 +2,28 @@ import prompts from 'prompts'; import semver from 'semver'; import { ChangeType } from '../types/ChangeInfo'; import { BeachballOptions } from '../types/BeachballOptions'; -import { DefaultPrompt } from '../types/ChangeFilePrompt'; +import { ChangeTypeDescriptions, DefaultPrompt } from '../types/ChangeFilePrompt'; import { getDisallowedChangeTypes } from './getDisallowedChangeTypes'; import { PackageGroups, PackageInfos } from '../types/PackageInfo'; +const defaultChangeTypeDescriptions: ChangeTypeDescriptions = { + prerelease: 'bump prerelease version', + patch: { + general: 'bug fixes; no API changes', + v0: 'bug fixes; new features; backwards-compatible API changes (ok in patches for version < 1)', + }, + minor: { + general: 'new feature; backwards-compatible API changes', + v0: 'breaking changes; major feature (ok in minors for version < 1)', + }, + none: 'this change does not affect the published package in any way', + major: { + general: 'breaking changes; major feature', + v0: 'official release', + }, + // TODO: add an option to show other pre* versions, and add their text +}; + /** * Build the list of questions to ask the user for this package. * Also validates the options and returns undefined if there's an issue. @@ -52,17 +70,29 @@ function getChangeTypePrompt(params: { return; } - const showPrereleaseOption = !!semver.prerelease(packageInfo.version); - const changeTypeChoices: prompts.Choice[] = [ - ...(showPrereleaseOption ? [{ value: 'prerelease', title: ' Prerelease - bump prerelease version' }] : []), - { value: 'patch', title: ' Patch - bug fixes; no API changes.' }, - { value: 'minor', title: ' Minor - small feature; backwards compatible API changes.' }, - { - value: 'none', - title: ' None - this change does not affect the published package in any way.', - }, - { value: 'major', title: ' Major - major feature; breaking changes.' }, - ].filter(choice => !disallowedChangeTypes?.includes(choice.value as ChangeType)); + const omittedChangeTypes = [...disallowedChangeTypes]; + // if the current version doesn't include a prerelease part, omit the prerelease option + if (!semver.prerelease(packageInfo.version)) { + omittedChangeTypes.push('prerelease'); + } + const isVersion0 = semver.major(packageInfo.version) === 0; + // this is used to determine padding length since it's the longest + const labelPadEnd = getChangeTypeLabel('prerelease').length; + + const changeTypeChoices: prompts.Choice[] = Object.entries( + packageInfo.combinedOptions.changeFilePrompt?.changeTypeDescriptions || defaultChangeTypeDescriptions + ) + .filter(([changeType]) => !omittedChangeTypes.includes(changeType as ChangeType)) + .map(([changeType, descriptions]): prompts.Choice => { + const label = getChangeTypeLabel(changeType); + // use the appropriate message for 0.x or >= 1.x (if different) + const description = + typeof descriptions === 'string' ? descriptions : isVersion0 ? descriptions.v0 : descriptions.general; + return { + value: changeType, + title: ` ${label.padEnd(labelPadEnd)} - ${description}`, + }; + }); if (!changeTypeChoices.length) { console.error(`No valid change types available for package "${pkg}"`); @@ -77,6 +107,11 @@ function getChangeTypePrompt(params: { }; } +function getChangeTypeLabel(changeType: string): string { + // bold formatting + return `${changeType[0].toUpperCase() + changeType.slice(1)}`; +} + function getDescriptionPrompt(recentMessages: string[]): prompts.PromptObject { // Do case-insensitive filtering of recent commit messages const recentMessageChoices: prompts.Choice[] = recentMessages.map(msg => ({ title: msg })); diff --git a/src/types/ChangeFilePrompt.ts b/src/types/ChangeFilePrompt.ts index 3805a895..5ab0b413 100644 --- a/src/types/ChangeFilePrompt.ts +++ b/src/types/ChangeFilePrompt.ts @@ -1,14 +1,48 @@ import prompts from 'prompts'; +import { ChangeType } from './ChangeInfo'; export interface DefaultPrompt { changeType: prompts.PromptObject | undefined; description: prompts.PromptObject | undefined; } +/** + * Mapping from change type to custom description. + * - A string is used for all versions. + * - An object can be used to provide different descriptions for 0.x versions and versions >= 1. + * - Omitting a change type means it won't be shown in the prompt. + */ +export type ChangeTypeDescriptions = { + [k in ChangeType]?: + | string // Description for all versions + | { + /** Description for versions >= 1 */ + general: string; + /** Description for 0.x versions */ + v0: string; + }; +}; + /** * Options for customizing change file prompt. * The package name is provided so that the prompt can be customized by package if desired. */ export interface ChangeFilePromptOptions { + /** + * Get custom change file prompt questions. + * The questions MUST result in an answers object `{ comment: string; type: ChangeType }`. + * If you just want to customize the descriptions of each change type, use `changeTypeDescriptions`. + * @param defaultPrompt Default prompt questions + * @param pkg Package name, so that changelog customizations can be specified at the package level + */ changePrompt?(defaultPrompt: DefaultPrompt, pkg: string): prompts.PromptObject[]; + + /** + * Custom descriptions for each change type. This is good for if you only want to customize the + * descriptions, not the whole prompt. + * + * Each description can either be a single string, or one string for 0.x versions (which follow + * different semver rules) and one string for general use with versions >= 1. + */ + changeTypeDescriptions?: ChangeTypeDescriptions; } From 8b01729fbb045d4e798ac221482d0ad034496430 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Mon, 18 Nov 2024 06:09:55 -0800 Subject: [PATCH 3/5] add version --- .../changefile/getQuestionsForPackage.test.ts | 4 +- .../changefile/promptForChange.test.ts | 12 +++--- ...ptForChange_promptForPackageChange.test.ts | 39 ++++++++++++------- src/changefile/promptForChange.ts | 10 ++--- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/__tests__/changefile/getQuestionsForPackage.test.ts b/src/__tests__/changefile/getQuestionsForPackage.test.ts index 555dd7d5..bbffa4dd 100644 --- a/src/__tests__/changefile/getQuestionsForPackage.test.ts +++ b/src/__tests__/changefile/getQuestionsForPackage.test.ts @@ -56,11 +56,11 @@ describe('getQuestionsForPackage', () => { expect(questions![0].choices).toEqual([ { title: - ' Patch - bug fixes; new features; backwards-compatible API changes (ok in patches for v0.x packages)', + ' Patch - bug fixes; new features; backwards-compatible API changes (ok in patches for version < 1)', value: 'patch', }, { - title: ' Minor - breaking changes; major feature (ok in minor versions for v0.x packages)', + title: ' Minor - breaking changes; major feature (ok in minors for version < 1)', value: 'minor', }, { title: ' None - this change does not affect the published package in any way', value: 'none' }, diff --git a/src/__tests__/changefile/promptForChange.test.ts b/src/__tests__/changefile/promptForChange.test.ts index a2e7b8a7..f250c35c 100644 --- a/src/__tests__/changefile/promptForChange.test.ts +++ b/src/__tests__/changefile/promptForChange.test.ts @@ -83,7 +83,7 @@ describe('promptForChange', () => { await waitForPrompt(); // verify asking for first package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); // choose custom options for this package stdin.emitKey({ name: 'down' }); await stdin.sendByChar('\n'); @@ -91,7 +91,7 @@ describe('promptForChange', () => { await stdin.sendByChar('\n'); // verify asking for second package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: bar'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: bar (currently v1.0.0)'); // choose defaults await stdin.sendByChar('\n\n'); @@ -120,11 +120,11 @@ describe('promptForChange', () => { await waitForPrompt(); // use defaults for first package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); await stdin.sendByChar('\n\n'); // cancel for second package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: bar'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: bar (currently v1.0.0)'); stdin.emitKey({ name: 'c', ctrl: true }); // nothing is returned @@ -148,12 +148,12 @@ describe('promptForChange', () => { await waitForPrompt(); // enter a valid type for foo - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); expect(stdout.getOutput()).toMatch(/enter any type/); await stdin.sendByChar('patch\n'); // enter an invalid type for bar - expect(logs.mocks.log).toHaveBeenCalledWith('Please describe the changes for: bar'); + expect(logs.mocks.log).toHaveBeenCalledWith('Please describe the changes for: bar (currently v1.0.0)'); await stdin.sendByChar('invalid\n'); expect(await changeFilesPromise).toBeUndefined(); diff --git a/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts b/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts index c0664ce3..08836e40 100644 --- a/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts +++ b/src/__tests__/changefile/promptForChange_promptForPackageChange.test.ts @@ -43,6 +43,9 @@ describe('promptForChange _promptForPackageChange', () => { expect.objectContaining({ name: 'comment', type: 'autocomplete' }), ]; + /** Info for the default package used in tests */ + const pkgInfo = defaultQuestionsParams.packageInfos[pkg]; + /** Wait for the prompt to finish rendering (simulates real user input) */ const waitForPrompt = () => new Promise(resolve => process.nextTick(resolve)); @@ -61,7 +64,7 @@ describe('promptForChange _promptForPackageChange', () => { }); it('returns an empty object and logs nothing if there are no questions', async () => { - const answers = await _promptForPackageChange([], pkg); + const answers = await _promptForPackageChange([], pkgInfo); expect(answers).toEqual({}); expect(logs.mocks.log).not.toHaveBeenCalled(); }); @@ -70,13 +73,15 @@ describe('promptForChange _promptForPackageChange', () => { const questions = getQuestionsForPackage(defaultQuestionsParams); expect(questions).toEqual(expectedQuestions); - const answersPromise = _promptForPackageChange(questions!, pkg); + const answersPromise = _promptForPackageChange(questions!, pkgInfo); // input: press enter twice to use defaults (with a pause in between to simulate real user input) await stdin.sendByChar('\n\n'); const answers = await answersPromise; - expect(logs.getMockLines('log')).toMatchInlineSnapshot(`"Please describe the changes for: foo"`); + expect(logs.getMockLines('log')).toMatchInlineSnapshot( + `"Please describe the changes for: foo (currently v1.0.0)"` + ); expect(stdout.getOutput()).toMatchInlineSnapshot(` "? Change type » - Use arrow-keys. Return to submit. > Patch - bug fixes; no API changes @@ -99,7 +104,7 @@ describe('promptForChange _promptForPackageChange', () => { }); expect(questions).toEqual(expectedQuestions.slice(1)); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); await waitForPrompt(); expect(stdout.lastOutput()).toMatchInlineSnapshot(` "? Describe changes (type or choose one) » @@ -112,7 +117,9 @@ describe('promptForChange _promptForPackageChange', () => { await stdin.sendByChar('abc\n'); const answers = await answerPromise; - expect(logs.getMockLines('log')).toMatchInlineSnapshot(`"Please describe the changes for: foo"`); + expect(logs.getMockLines('log')).toMatchInlineSnapshot( + `"Please describe the changes for: foo (currently v1.0.0)"` + ); expect(stdout.getOutput()).toMatchInlineSnapshot(` "? Describe changes (type or choose one) » a ? Describe changes (type or choose one) » ab @@ -130,7 +137,7 @@ describe('promptForChange _promptForPackageChange', () => { }); expect(questions).toEqual(expectedQuestions.slice(1)); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); await waitForPrompt(); expect(stdout.lastOutput()).toMatchInlineSnapshot(` "? Describe changes (type or choose one) » @@ -144,7 +151,9 @@ describe('promptForChange _promptForPackageChange', () => { await stdin.sendByChar('\n'); const answers = await answerPromise; - expect(logs.getMockLines('log')).toMatchInlineSnapshot(`"Please describe the changes for: foo"`); + expect(logs.getMockLines('log')).toMatchInlineSnapshot( + `"Please describe the changes for: foo (currently v1.0.0)"` + ); expect(stdout.getOutput()).toMatchInlineSnapshot(` "? Describe changes (type or choose one) » abc √ Describe changes (type or choose one) » abc" @@ -160,7 +169,7 @@ describe('promptForChange _promptForPackageChange', () => { }); expect(questions).toEqual(expectedQuestions.slice(1)); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); await waitForPrompt(); expect(stdout.lastOutput()).toMatchInlineSnapshot(` "? Describe changes (type or choose one) » @@ -173,7 +182,9 @@ describe('promptForChange _promptForPackageChange', () => { stdin.send('abc\n'); const answers = await answerPromise; - expect(logs.getMockLines('log')).toMatchInlineSnapshot(`"Please describe the changes for: foo"`); + expect(logs.getMockLines('log')).toMatchInlineSnapshot( + `"Please describe the changes for: foo (currently v1.0.0)"` + ); expect(stdout.getOutput()).toMatchInlineSnapshot(`""`); expect(answers).toEqual({ comment: 'abc' }); }); @@ -183,7 +194,7 @@ describe('promptForChange _promptForPackageChange', () => { const questions = getQuestionsForPackage({ ...defaultQuestionsParams, recentMessages }); expect(questions).toEqual(expectedQuestions); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); // arrow down to select the third type stdin.emitKey({ name: 'down' }); @@ -233,7 +244,7 @@ describe('promptForChange _promptForPackageChange', () => { }); expect(questions).toEqual(expectedQuestions.slice(1)); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); // type "ba" and press enter to select "bar" await stdin.sendByChar('ba\n'); @@ -264,7 +275,7 @@ describe('promptForChange _promptForPackageChange', () => { }); expect(questions).toEqual(expectedQuestions.slice(1)); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); // type "b", press backspace to delete it, press enter to select foo await stdin.sendByChar('b'); @@ -294,7 +305,7 @@ describe('promptForChange _promptForPackageChange', () => { const questions = getQuestionsForPackage(defaultQuestionsParams); expect(questions).toEqual(expectedQuestions); - const answerPromise = _promptForPackageChange(questions!, pkg); + const answerPromise = _promptForPackageChange(questions!, pkgInfo); // answer the first question await stdin.sendByChar('\n'); @@ -305,7 +316,7 @@ describe('promptForChange _promptForPackageChange', () => { const answers = await answerPromise; expect(logs.getMockLines('log')).toMatchInlineSnapshot(` - "Please describe the changes for: foo + "Please describe the changes for: foo (currently v1.0.0) Cancelled, no change files are written" `); diff --git a/src/changefile/promptForChange.ts b/src/changefile/promptForChange.ts index 87340c66..f62cf03c 100644 --- a/src/changefile/promptForChange.ts +++ b/src/changefile/promptForChange.ts @@ -2,7 +2,7 @@ import prompts from 'prompts'; import { ChangeFileInfo, ChangeType } from '../types/ChangeInfo'; import { BeachballOptions } from '../types/BeachballOptions'; import { isValidChangeType } from '../validation/isValidChangeType'; -import { PackageGroups, PackageInfos } from '../types/PackageInfo'; +import { PackageGroups, PackageInfo, PackageInfos } from '../types/PackageInfo'; import { getQuestionsForPackage } from './getQuestionsForPackage'; type ChangePromptResponse = { type?: ChangeType; comment?: string }; @@ -19,7 +19,7 @@ export async function promptForChange(params: { email: string | null; options: Pick; }): Promise { - const { changedPackages, email, options } = params; + const { changedPackages, packageInfos, email, options } = params; if (!changedPackages.length) { return; } @@ -43,7 +43,7 @@ export async function promptForChange(params: { // Now prompt for each package const packageChangeInfo: ChangeFileInfo[] = []; for (let pkg of changedPackages) { - const response = await _promptForPackageChange(packageQuestions[pkg], pkg); + const response = await _promptForPackageChange(packageQuestions[pkg], packageInfos[pkg]); if (!response) { return; // user cancelled } @@ -64,7 +64,7 @@ export async function promptForChange(params: { */ export async function _promptForPackageChange( questions: prompts.PromptObject[], - pkg: string + pkg: PackageInfo ): Promise { if (!questions.length) { // This MUST return an empty object rather than nothing, because returning nothing means the @@ -73,7 +73,7 @@ export async function _promptForPackageChange( } console.log(''); - console.log(`Please describe the changes for: ${pkg}`); + console.log(`Please describe the changes for: ${pkg.name} (currently v${pkg.version})`); let isCancelled = false; const onCancel = () => { From 55661a6d8422558494937307d356220e34557c4b Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Mon, 18 Nov 2024 06:11:17 -0800 Subject: [PATCH 4/5] Change files --- ...-f72ae41e-d073-44c8-913c-699450fe9a0d.json | 7 +++++++ src/__e2e__/change.test.ts | 20 +++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 change/beachball-f72ae41e-d073-44c8-913c-699450fe9a0d.json diff --git a/change/beachball-f72ae41e-d073-44c8-913c-699450fe9a0d.json b/change/beachball-f72ae41e-d073-44c8-913c-699450fe9a0d.json new file mode 100644 index 00000000..c5dfee83 --- /dev/null +++ b/change/beachball-f72ae41e-d073-44c8-913c-699450fe9a0d.json @@ -0,0 +1,7 @@ +{ + "comment": "Add v0-specific change descriptions", + "type": "minor", + "packageName": "beachball", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/src/__e2e__/change.test.ts b/src/__e2e__/change.test.ts index 4e4535af..9c63fd77 100644 --- a/src/__e2e__/change.test.ts +++ b/src/__e2e__/change.test.ts @@ -107,7 +107,7 @@ describe('change command', () => { await waitForPrompt(); // Use default change type and custom message - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); await stdin.sendByChar('\n'); // Also verify that the options shown are correct expect(stdout.lastOutput()).toMatchInlineSnapshot(` @@ -139,7 +139,7 @@ describe('change command', () => { const options = getOptions(); const changePromise = change(options); - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); await stdin.sendByChar('\n'); // default change type await stdin.sendByChar('commit me please\n'); // custom message await changePromise; @@ -170,7 +170,7 @@ describe('change command', () => { }); const changePromise = change(options); - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); await stdin.sendByChar('\n'); // default change type await stdin.sendByChar('commit me please\n'); // custom message await changePromise; @@ -198,7 +198,7 @@ describe('change command', () => { const changePromise = change(options); await waitForPrompt(); - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: foo (currently v1.0.0)'); await stdin.sendByChar('\n'); // default change type await stdin.sendByChar('stage me please\n'); // custom message await changePromise; @@ -218,7 +218,7 @@ describe('change command', () => { const changePromise = change(options); // use custom values for first package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1 (currently v1.0.0)'); stdin.emitKey({ name: 'down' }); await stdin.sendByChar('\n'); // also verify that the options shown are correct @@ -230,7 +230,7 @@ describe('change command', () => { await stdin.sendByChar('custom\n'); // use defaults for second package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2 (currently v1.0.0)'); await stdin.sendByChar('\n\n'); await changePromise; @@ -260,13 +260,13 @@ describe('change command', () => { const changePromise = change(options); // use custom values for first package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1 (currently v1.0.0)'); stdin.emitKey({ name: 'down' }); await stdin.sendByChar('\n'); await stdin.sendByChar('custom\n'); // use defaults for second package - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2 (currently v1.0.0)'); await stdin.sendByChar('\n\n'); await changePromise; @@ -305,13 +305,13 @@ describe('change command', () => { const changePromise = change(options); await waitForPrompt(); - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-1 (currently v1.0.0)'); expect(stdout.lastOutput()).toMatch(/Change type/); await stdin.sendByChar('\n'); expect(stdout.lastOutput()).toMatch(/Describe changes/); await stdin.sendByChar('\n'); - expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2'); + expect(logs.mocks.log).toHaveBeenLastCalledWith('Please describe the changes for: pkg-2 (currently v1.0.0)'); expect(stdout.lastOutput()).toMatch(/custom question/); await stdin.sendByChar('stuff\n'); expect(stdout.lastOutput()).toMatch(/Change type/); From 13d49d9b453949fc10303af107b90ace8d5791d2 Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Mon, 18 Nov 2024 07:34:28 -0800 Subject: [PATCH 5/5] comments, add other types --- src/changefile/changeTypes.ts | 12 +++++++++++- src/changefile/getQuestionsForPackage.ts | 19 ++++++++++++------- src/types/ChangeFilePrompt.ts | 11 ++++++----- src/types/ChangeInfo.ts | 3 ++- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/changefile/changeTypes.ts b/src/changefile/changeTypes.ts index faa88ac6..06226650 100644 --- a/src/changefile/changeTypes.ts +++ b/src/changefile/changeTypes.ts @@ -1,4 +1,4 @@ -import { ChangeSet, ChangeType } from '../types/ChangeInfo'; +import { ChangeSet, ChangeType, PrereleaseChangeType } from '../types/ChangeInfo'; /** * List of all change types from least to most significant. @@ -14,6 +14,16 @@ export const SortedChangeTypes: ChangeType[] = [ 'major', ]; +/** + * All the prerelease change types. + */ +export const PrereleaseChangeTypes: ChangeType[] = [ + 'prerelease', + 'prepatch', + 'preminor', + 'premajor', +] satisfies PrereleaseChangeType[]; + /** * Change type with the smallest weight. */ diff --git a/src/changefile/getQuestionsForPackage.ts b/src/changefile/getQuestionsForPackage.ts index 1aa54244..9337260e 100644 --- a/src/changefile/getQuestionsForPackage.ts +++ b/src/changefile/getQuestionsForPackage.ts @@ -5,9 +5,14 @@ import { BeachballOptions } from '../types/BeachballOptions'; import { ChangeTypeDescriptions, DefaultPrompt } from '../types/ChangeFilePrompt'; import { getDisallowedChangeTypes } from './getDisallowedChangeTypes'; import { PackageGroups, PackageInfos } from '../types/PackageInfo'; +import { PrereleaseChangeTypes } from './changeTypes'; -const defaultChangeTypeDescriptions: ChangeTypeDescriptions = { +const defaultChangeTypeDescriptions: Required = { prerelease: 'bump prerelease version', + // TODO: these pre* types are included for completeness but currently won't be shown + prepatch: 'bump to prerelease of the next patch version', + preminor: 'bump to prerelease of the next minor version', + premajor: 'bump to prerelease of the next major version', patch: { general: 'bug fixes; no API changes', v0: 'bug fixes; new features; backwards-compatible API changes (ok in patches for version < 1)', @@ -21,7 +26,6 @@ const defaultChangeTypeDescriptions: ChangeTypeDescriptions = { general: 'breaking changes; major feature', v0: 'official release', }, - // TODO: add an option to show other pre* versions, and add their text }; /** @@ -70,10 +74,11 @@ function getChangeTypePrompt(params: { return; } - const omittedChangeTypes = [...disallowedChangeTypes]; - // if the current version doesn't include a prerelease part, omit the prerelease option - if (!semver.prerelease(packageInfo.version)) { - omittedChangeTypes.push('prerelease'); + // TODO: conditionally add other prerelease types later + const omittedChangeTypes = new Set([...disallowedChangeTypes, ...PrereleaseChangeTypes]); + // if the current version includes a prerelease part, show the prerelease option + if (semver.prerelease(packageInfo.version)) { + omittedChangeTypes.add('prerelease'); } const isVersion0 = semver.major(packageInfo.version) === 0; // this is used to determine padding length since it's the longest @@ -82,7 +87,7 @@ function getChangeTypePrompt(params: { const changeTypeChoices: prompts.Choice[] = Object.entries( packageInfo.combinedOptions.changeFilePrompt?.changeTypeDescriptions || defaultChangeTypeDescriptions ) - .filter(([changeType]) => !omittedChangeTypes.includes(changeType as ChangeType)) + .filter(([changeType]) => !omittedChangeTypes.has(changeType as ChangeType)) .map(([changeType, descriptions]): prompts.Choice => { const label = getChangeTypeLabel(changeType); // use the appropriate message for 0.x or >= 1.x (if different) diff --git a/src/types/ChangeFilePrompt.ts b/src/types/ChangeFilePrompt.ts index 5ab0b413..3cd77605 100644 --- a/src/types/ChangeFilePrompt.ts +++ b/src/types/ChangeFilePrompt.ts @@ -25,13 +25,14 @@ export type ChangeTypeDescriptions = { /** * Options for customizing change file prompt. - * The package name is provided so that the prompt can be customized by package if desired. */ export interface ChangeFilePromptOptions { /** - * Get custom change file prompt questions. - * The questions MUST result in an answers object `{ comment: string; type: ChangeType }`. - * If you just want to customize the descriptions of each change type, use `changeTypeDescriptions`. + * Get custom change file prompt questions. The questions MUST result in an answers object + * `{ comment: string; type: ChangeType }`, though any extra properties returned will be preserved. + * + * (If you just want to customize the descriptions of each change type, use `changeTypeDescriptions`.) + * * @param defaultPrompt Default prompt questions * @param pkg Package name, so that changelog customizations can be specified at the package level */ @@ -39,7 +40,7 @@ export interface ChangeFilePromptOptions { /** * Custom descriptions for each change type. This is good for if you only want to customize the - * descriptions, not the whole prompt. + * descriptions, not the whole prompt. (Any types not included here will use the defaults.) * * Each description can either be a single string, or one string for 0.x versions (which follow * different semver rules) and one string for general use with versions >= 1. diff --git a/src/types/ChangeInfo.ts b/src/types/ChangeInfo.ts index b0e007d9..aa94029a 100644 --- a/src/types/ChangeInfo.ts +++ b/src/types/ChangeInfo.ts @@ -1,4 +1,5 @@ -export type ChangeType = 'prerelease' | 'prepatch' | 'patch' | 'preminor' | 'minor' | 'premajor' | 'major' | 'none'; +export type PrereleaseChangeType = 'prerelease' | 'prepatch' | 'preminor' | 'premajor'; +export type ChangeType = PrereleaseChangeType | 'patch' | 'minor' | 'major' | 'none'; /** * Info saved in each change file.