From 9aa43f7f0aa92a189117b4221c8db2e91c833d93 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 16:14:48 -0400 Subject: [PATCH 01/61] Lists all new classes --- .../__test__/generating-changelog.spec.ts | 50 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 21 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/core/changelog/__test__/generating-changelog.spec.ts create mode 100644 src/core/changelog/generate-changelog.ts diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts new file mode 100644 index 00000000..1c668d4f --- /dev/null +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -0,0 +1,50 @@ +import { generateChangeLog } from '../generate-changelog'; +import { reflect, Type } from '@cparra/apex-reflection'; + +function typeFromRawString(raw: string): Type { + const result = reflect(raw); + if (result.error) { + throw new Error(result.error.message); + } + + return result.typeMirror!; +} + +describe('when generating a change log', () => { + it('results in an empty change log when both the old and new versions are empty', () => { + const oldVersion = { types: [] }; + const newVersion = { types: [] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog).toEqual({ + newClasses: [], + }); + }); + + it('results in an empty change log when both the old and new versions are the same', () => { + const anyClassBody = 'public class AnyClass {}'; + const anyClass = typeFromRawString(anyClassBody); + const oldVersion = { types: [anyClass] }; + const newVersion = { types: [anyClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog).toEqual({ + newClasses: [], + }); + }); + + it('lists all new classes', () => { + const oldVersion = { types: [] }; + const newClassBody = 'public class NewClass {}'; + const newClass = typeFromRawString(newClassBody); + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog).toEqual({ + newClasses: [newClass.name], + }); + }); +}); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts new file mode 100644 index 00000000..11758862 --- /dev/null +++ b/src/core/changelog/generate-changelog.ts @@ -0,0 +1,21 @@ +import { Type } from '@cparra/apex-reflection'; + +type VersionManifest = { + types: Type[]; +}; + +type ChangeLog = { + newClasses: string[]; +}; + +export function generateChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { + return { + newClasses: getNewClasses(oldVersion, newVersion), + }; +} + +function getNewClasses(oldVersion: VersionManifest, newVersion: VersionManifest): string[] { + return newVersion.types + .filter((newType) => !oldVersion.types.some((oldType) => oldType.name.toLowerCase() === newType.name.toLowerCase())) + .map((type) => type.name); +} From d09de623aa4b842a4983453a96f4a52f3728f5dc Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 16:18:03 -0400 Subject: [PATCH 02/61] lists all new types --- .../__test__/generating-changelog.spec.ts | 14 ++++++++------ src/core/changelog/generate-changelog.ts | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 1c668d4f..456641e5 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -18,7 +18,7 @@ describe('when generating a change log', () => { const changeLog = generateChangeLog(oldVersion, newVersion); expect(changeLog).toEqual({ - newClasses: [], + newTypes: [], }); }); @@ -31,20 +31,22 @@ describe('when generating a change log', () => { const changeLog = generateChangeLog(oldVersion, newVersion); expect(changeLog).toEqual({ - newClasses: [], + newTypes: [], }); }); - it('lists all new classes', () => { - const oldVersion = { types: [] }; + it('lists all new types', () => { + const existingInBoth = 'public class ExistingInBoth {}'; + const existingClass = typeFromRawString(existingInBoth); + const oldVersion = { types: [existingClass] }; const newClassBody = 'public class NewClass {}'; const newClass = typeFromRawString(newClassBody); - const newVersion = { types: [newClass] }; + const newVersion = { types: [existingClass, newClass] }; const changeLog = generateChangeLog(oldVersion, newVersion); expect(changeLog).toEqual({ - newClasses: [newClass.name], + newTypes: [newClass.name], }); }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 11758862..0a54e1c3 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -5,12 +5,12 @@ type VersionManifest = { }; type ChangeLog = { - newClasses: string[]; + newTypes: string[]; }; export function generateChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { return { - newClasses: getNewClasses(oldVersion, newVersion), + newTypes: getNewClasses(oldVersion, newVersion), }; } From 4af8d425784b4aa3a8ea3673ffd35a96e5083ea5 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 16:28:41 -0400 Subject: [PATCH 03/61] Refactorings --- .../__test__/generating-changelog.spec.ts | 25 +++++++++++-------- src/core/changelog/generate-changelog.ts | 16 +++++++++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 456641e5..4f6ca220 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -11,18 +11,25 @@ function typeFromRawString(raw: string): Type { } describe('when generating a change log', () => { - it('results in an empty change log when both the old and new versions are empty', () => { + it('has no new types when both the old and new versions are empty', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; const changeLog = generateChangeLog(oldVersion, newVersion); - expect(changeLog).toEqual({ - newTypes: [], - }); + expect(changeLog.newTypes).toEqual([]); }); - it('results in an empty change log when both the old and new versions are the same', () => { + it('has no removed types when the old and new versions are empty', () => { + const oldVersion = { types: [] }; + const newVersion = { types: [] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.removedTypes).toEqual([]); + }); + + it('has no new types when both the old and new versions are the same', () => { const anyClassBody = 'public class AnyClass {}'; const anyClass = typeFromRawString(anyClassBody); const oldVersion = { types: [anyClass] }; @@ -30,9 +37,7 @@ describe('when generating a change log', () => { const changeLog = generateChangeLog(oldVersion, newVersion); - expect(changeLog).toEqual({ - newTypes: [], - }); + expect(changeLog.newTypes).toEqual([]); }); it('lists all new types', () => { @@ -45,8 +50,6 @@ describe('when generating a change log', () => { const changeLog = generateChangeLog(oldVersion, newVersion); - expect(changeLog).toEqual({ - newTypes: [newClass.name], - }); + expect(changeLog.newTypes).toEqual([newClass.name]); }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 0a54e1c3..eef97e01 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -1,21 +1,29 @@ import { Type } from '@cparra/apex-reflection'; -type VersionManifest = { +export type VersionManifest = { types: Type[]; }; -type ChangeLog = { +export type ChangeLog = { newTypes: string[]; + removedTypes: string[]; }; export function generateChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { return { - newTypes: getNewClasses(oldVersion, newVersion), + newTypes: getNewTypes(oldVersion, newVersion), + removedTypes: getRemovedTypes(oldVersion, newVersion), }; } -function getNewClasses(oldVersion: VersionManifest, newVersion: VersionManifest): string[] { +function getNewTypes(oldVersion: VersionManifest, newVersion: VersionManifest): string[] { return newVersion.types .filter((newType) => !oldVersion.types.some((oldType) => oldType.name.toLowerCase() === newType.name.toLowerCase())) .map((type) => type.name); } + +function getRemovedTypes(oldVersion: VersionManifest, newVersion: VersionManifest): string[] { + return oldVersion.types + .filter((oldType) => !newVersion.types.some((newType) => newType.name.toLowerCase() === oldType.name.toLowerCase())) + .map((type) => type.name); +} From 9792a097d7ff65d82c8a6ad9506946191ba5ff73 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 16:30:25 -0400 Subject: [PATCH 04/61] Lists all removed types --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 4f6ca220..aa701532 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -40,6 +40,17 @@ describe('when generating a change log', () => { expect(changeLog.newTypes).toEqual([]); }); + it('has no removed types when both the old and new versions are the same', () => { + const anyClassBody = 'public class AnyClass {}'; + const anyClass = typeFromRawString(anyClassBody); + const oldVersion = { types: [anyClass] }; + const newVersion = { types: [anyClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.removedTypes).toEqual([]); + }); + it('lists all new types', () => { const existingInBoth = 'public class ExistingInBoth {}'; const existingClass = typeFromRawString(existingInBoth); @@ -52,4 +63,17 @@ describe('when generating a change log', () => { expect(changeLog.newTypes).toEqual([newClass.name]); }); + + it('lists all removed types', () => { + const existingInBoth = 'public class ExistingInBoth {}'; + const existingClass = typeFromRawString(existingInBoth); + const existingOnlyInOld = 'public class ExistingOnlyInOld {}'; + const existingOnlyInOldClass = typeFromRawString(existingOnlyInOld); + const oldVersion = { types: [existingClass, existingOnlyInOldClass] }; + const newVersion = { types: [existingClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.removedTypes).toEqual([existingOnlyInOldClass.name]); + }); }); From 7b8d62e1810bee464a9df948415d2fb4f7ece09d Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 16:53:55 -0400 Subject: [PATCH 05/61] Lists all new values of a modified enum --- .../__test__/generating-changelog.spec.ts | 24 ++++++++ src/core/changelog/generate-changelog.ts | 57 ++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index aa701532..2c872264 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -76,4 +76,28 @@ describe('when generating a change log', () => { expect(changeLog.removedTypes).toEqual([existingOnlyInOldClass.name]); }); + + it('lists all new values of a modified enum', () => { + const enumBefore = 'public enum MyEnum { VALUE1 }'; + const oldEnum = typeFromRawString(enumBefore); + const enumAfter = 'public enum MyEnum { VALUE1, VALUE2 }'; + const newEnum = typeFromRawString(enumAfter); + + const oldVersion = { types: [oldEnum] }; + const newVersion = { types: [newEnum] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyEnum', + modifications: [ + { + __typename: 'NewEnumValue', + value: 'VALUE2', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index eef97e01..98861269 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -1,18 +1,33 @@ -import { Type } from '@cparra/apex-reflection'; +import { EnumMirror, Type } from '@cparra/apex-reflection'; +import { pipe } from 'fp-ts/function'; export type VersionManifest = { types: Type[]; }; +type NewEnumValue = { + __typename: 'NewEnumValue'; + value: string; +}; + +type MemberModificationType = NewEnumValue; + +type NewOrModifiedMember = { + typeName: string; + modifications: MemberModificationType[]; +}; + export type ChangeLog = { newTypes: string[]; removedTypes: string[]; + newOrModifiedMembers: NewOrModifiedMember[]; }; export function generateChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { return { newTypes: getNewTypes(oldVersion, newVersion), removedTypes: getRemovedTypes(oldVersion, newVersion), + newOrModifiedMembers: getNewOrModifiedEnumValues(oldVersion, newVersion), }; } @@ -27,3 +42,43 @@ function getRemovedTypes(oldVersion: VersionManifest, newVersion: VersionManifes .filter((oldType) => !newVersion.types.some((newType) => newType.name.toLowerCase() === oldType.name.toLowerCase())) .map((type) => type.name); } + +function getNewOrModifiedEnumValues(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { + return pipe( + getTypesInBothVersions(oldVersion, newVersion), + (types) => getEnums(types), + (enums) => + enums.map((enumMirror) => { + const oldEnum = oldVersion.types.find( + (type) => type.name.toLowerCase() === enumMirror.name.toLowerCase(), + ) as EnumMirror; + const newEnum = newVersion.types.find( + (type) => type.name.toLowerCase() === enumMirror.name.toLowerCase(), + ) as EnumMirror; + + return { + typeName: enumMirror.name, + modifications: getNewEnumValues(oldEnum, newEnum), + }; + }), + (newOrModifiedMembers) => newOrModifiedMembers.filter((member) => member.modifications.length > 0), + ); +} + +function getTypesInBothVersions(oldVersion: VersionManifest, newVersion: VersionManifest): Type[] { + return newVersion.types.filter((newType) => + oldVersion.types.some((oldType) => oldType.name.toLowerCase() === newType.name.toLowerCase()), + ); +} + +function getEnums(types: Type[]): EnumMirror[] { + return types.filter((type) => type.type_name === 'enum') as EnumMirror[]; +} + +function getNewEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): NewEnumValue[] { + return newEnum.values + .filter( + (newValue) => !oldEnum.values.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewEnumValue', value: value.name })); +} From 00fb0874422f900435fb6d98def511ba5b50a5fd Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 18:19:45 -0400 Subject: [PATCH 06/61] lists all removed enum values --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 17 +++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 2c872264..737f314f 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -100,4 +100,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('list all removed values of a modified enum', () => { + const enumBefore = 'public enum MyEnum { VALUE1, VALUE2 }'; + const oldEnum = typeFromRawString(enumBefore); + const enumAfter = 'public enum MyEnum { VALUE1 }'; + const newEnum = typeFromRawString(enumAfter); + + const oldVersion = { types: [oldEnum] }; + const newVersion = { types: [newEnum] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyEnum', + modifications: [ + { + __typename: 'RemovedEnumValue', + value: 'VALUE2', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 98861269..54abe1e6 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -10,7 +10,12 @@ type NewEnumValue = { value: string; }; -type MemberModificationType = NewEnumValue; +type RemovedEnumValue = { + __typename: 'RemovedEnumValue'; + value: string; +}; + +type MemberModificationType = NewEnumValue | RemovedEnumValue; type NewOrModifiedMember = { typeName: string; @@ -58,7 +63,7 @@ function getNewOrModifiedEnumValues(oldVersion: VersionManifest, newVersion: Ver return { typeName: enumMirror.name, - modifications: getNewEnumValues(oldEnum, newEnum), + modifications: [...getNewEnumValues(oldEnum, newEnum), ...getRemovedEnumValues(oldEnum, newEnum)], }; }), (newOrModifiedMembers) => newOrModifiedMembers.filter((member) => member.modifications.length > 0), @@ -82,3 +87,11 @@ function getNewEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): NewEnumValu ) .map((value) => ({ __typename: 'NewEnumValue', value: value.name })); } + +function getRemovedEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): RemovedEnumValue[] { + return oldEnum.values + .filter( + (oldValue) => !newEnum.values.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedEnumValue', value: value.name })); +} From 8c4f15b4627971a624fdeeb9df1337f710d78dc1 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 18:43:16 -0400 Subject: [PATCH 07/61] refactorings --- src/core/changelog/generate-changelog.ts | 54 ++++++++++++++---------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 54abe1e6..974b15da 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -15,7 +15,12 @@ type RemovedEnumValue = { value: string; }; -type MemberModificationType = NewEnumValue | RemovedEnumValue; +type NewMethod = { + __typename: 'NewMethod'; + name: string; +}; + +type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod; type NewOrModifiedMember = { typeName: string; @@ -32,7 +37,7 @@ export function generateChangeLog(oldVersion: VersionManifest, newVersion: Versi return { newTypes: getNewTypes(oldVersion, newVersion), removedTypes: getRemovedTypes(oldVersion, newVersion), - newOrModifiedMembers: getNewOrModifiedEnumValues(oldVersion, newVersion), + newOrModifiedMembers: getNewOrModifiedMembers(oldVersion, newVersion), }; } @@ -48,36 +53,39 @@ function getRemovedTypes(oldVersion: VersionManifest, newVersion: VersionManifes .map((type) => type.name); } -function getNewOrModifiedEnumValues(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { - return pipe( - getTypesInBothVersions(oldVersion, newVersion), - (types) => getEnums(types), - (enums) => - enums.map((enumMirror) => { - const oldEnum = oldVersion.types.find( - (type) => type.name.toLowerCase() === enumMirror.name.toLowerCase(), - ) as EnumMirror; - const newEnum = newVersion.types.find( - (type) => type.name.toLowerCase() === enumMirror.name.toLowerCase(), - ) as EnumMirror; +function getNewOrModifiedMembers(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { + return pipe(getTypesInBothVersions(oldVersion, newVersion), getNewOrModifiedEnumValues, (newOrModifiedMembers) => + newOrModifiedMembers.filter((member) => member.modifications.length > 0), + ); +} +function getNewOrModifiedEnumValues(typesInBoth: TypeInBoth[]): NewOrModifiedMember[] { + return pipe( + typesInBoth.filter((typeInBoth) => typeInBoth.oldType.type_name === 'enum'), + (enumsInBoth) => + enumsInBoth.map(({ oldType, newType }) => { + const oldEnum = oldType as EnumMirror; + const newEnum = newType as EnumMirror; return { - typeName: enumMirror.name, + typeName: newType.name, modifications: [...getNewEnumValues(oldEnum, newEnum), ...getRemovedEnumValues(oldEnum, newEnum)], }; }), - (newOrModifiedMembers) => newOrModifiedMembers.filter((member) => member.modifications.length > 0), ); } -function getTypesInBothVersions(oldVersion: VersionManifest, newVersion: VersionManifest): Type[] { - return newVersion.types.filter((newType) => - oldVersion.types.some((oldType) => oldType.name.toLowerCase() === newType.name.toLowerCase()), - ); -} +type TypeInBoth = { + oldType: Type; + newType: Type; +}; -function getEnums(types: Type[]): EnumMirror[] { - return types.filter((type) => type.type_name === 'enum') as EnumMirror[]; +function getTypesInBothVersions(oldVersion: VersionManifest, newVersion: VersionManifest): TypeInBoth[] { + return oldVersion.types + .map((oldType) => ({ + oldType, + newType: newVersion.types.find((newType) => newType.name.toLowerCase() === oldType.name.toLowerCase()), + })) + .filter((type) => type.newType !== undefined) as TypeInBoth[]; } function getNewEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): NewEnumValue[] { From dd10dd715afe506b6baaa64d551738b127206595 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 21 Sep 2024 18:49:14 -0400 Subject: [PATCH 08/61] lists all new methods of a class and an interface --- .../__test__/generating-changelog.spec.ts | 48 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 39 +++++++++++++-- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 737f314f..915048c9 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -124,4 +124,52 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists all new methods of an interface', () => { + const interfaceBefore = 'public interface MyInterface {}'; + const oldInterface = typeFromRawString(interfaceBefore); + const interfaceAfter = 'public interface MyInterface { void newMethod(); }'; + const newInterface = typeFromRawString(interfaceAfter); + + const oldVersion = { types: [oldInterface] }; + const newVersion = { types: [newInterface] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyInterface', + modifications: [ + { + __typename: 'NewMethod', + name: 'newMethod', + }, + ], + }, + ]); + }); + + it('lists all new methods of a class', () => { + const classBefore = 'public class MyClass {}'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { void newMethod() {} }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewMethod', + name: 'newMethod', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 974b15da..df93c862 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -1,4 +1,4 @@ -import { EnumMirror, Type } from '@cparra/apex-reflection'; +import { EnumMirror, MethodMirror, Type } from '@cparra/apex-reflection'; import { pipe } from 'fp-ts/function'; export type VersionManifest = { @@ -54,8 +54,10 @@ function getRemovedTypes(oldVersion: VersionManifest, newVersion: VersionManifes } function getNewOrModifiedMembers(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { - return pipe(getTypesInBothVersions(oldVersion, newVersion), getNewOrModifiedEnumValues, (newOrModifiedMembers) => - newOrModifiedMembers.filter((member) => member.modifications.length > 0), + return pipe( + getTypesInBothVersions(oldVersion, newVersion), + (typesInBoth) => [...getNewOrModifiedEnumValues(typesInBoth), ...getNewOrModifiedMethods(typesInBoth)], + (newOrModifiedMembers) => newOrModifiedMembers.filter((member) => member.modifications.length > 0), ); } @@ -74,6 +76,24 @@ function getNewOrModifiedEnumValues(typesInBoth: TypeInBoth[]): NewOrModifiedMem ); } +function getNewOrModifiedMethods(typesInBoth: TypeInBoth[]): NewOrModifiedMember[] { + return pipe( + typesInBoth.filter( + (typeInBoth) => typeInBoth.oldType.type_name === 'class' || typeInBoth.oldType.type_name === 'interface', + ), + (typesInBoth) => + typesInBoth.map(({ oldType, newType }) => { + const oldMethodAware = oldType as MethodAware; + const newMethodAware = newType as MethodAware; + + return { + typeName: newType.name, + modifications: getNewMethods(oldMethodAware, newMethodAware), + }; + }), + ); +} + type TypeInBoth = { oldType: Type; newType: Type; @@ -103,3 +123,16 @@ function getRemovedEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): Removed ) .map((value) => ({ __typename: 'RemovedEnumValue', value: value.name })); } + +type MethodAware = { + methods: MethodMirror[]; +}; + +function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): NewMethod[] { + return newMethodAware.methods + .filter( + (newMethod) => + !oldMethodAware.methods.some((oldMethod) => oldMethod.name.toLowerCase() === newMethod.name.toLowerCase()), + ) + .map((method) => ({ __typename: 'NewMethod', name: method.name })); +} From 49e675726908fd4ddaf63e7ce0152426f2ba9589 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:35:01 -0400 Subject: [PATCH 09/61] checking for method equality based on their shape --- .../__test__/checking-method-equality.spec.ts | 61 +++++++++++++++++++ .../__test__/generating-changelog.spec.ts | 2 +- src/core/changelog/generate-changelog.ts | 6 +- src/core/changelog/method-changes-checker.ts | 26 ++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/core/changelog/__test__/checking-method-equality.spec.ts create mode 100644 src/core/changelog/method-changes-checker.ts diff --git a/src/core/changelog/__test__/checking-method-equality.spec.ts b/src/core/changelog/__test__/checking-method-equality.spec.ts new file mode 100644 index 00000000..09b2e269 --- /dev/null +++ b/src/core/changelog/__test__/checking-method-equality.spec.ts @@ -0,0 +1,61 @@ +import { MethodMirrorBuilder, ParameterBuilder } from '../../../test-helpers/MethodMirrorBuilder'; +import { areMethodsEqual } from '../method-changes-checker'; + +describe('when checking if 2 methods are equal', () => { + it('returns false when the methods do not have the same name', () => { + const method1 = new MethodMirrorBuilder().withName('method1').build(); + const method2 = new MethodMirrorBuilder().withName('method2').build(); + + const result = areMethodsEqual(method1, method2); + + expect(result).toBe(false); + }); + + it('returns false when the methods do not have the same return type', () => { + const method1 = new MethodMirrorBuilder().withTypeReference({ type: 'String', rawDeclaration: 'String' }).build(); + const method2 = new MethodMirrorBuilder().withTypeReference({ type: 'Integer', rawDeclaration: 'Integer' }).build(); + + const result = areMethodsEqual(method1, method2); + + expect(result).toBe(false); + }); + + it('returns false when the methods do not have the same amount of parameters', () => { + const method1 = new MethodMirrorBuilder().addParameter(new ParameterBuilder().build()).build(); + const method2 = new MethodMirrorBuilder().build(); + + const result = areMethodsEqual(method1, method2); + + expect(result).toBe(false); + }); + + it('returns false when the types of the parameters are different', () => { + const method1 = new MethodMirrorBuilder() + .addParameter(new ParameterBuilder().withTypeReference({ type: 'String', rawDeclaration: 'String' }).build()) + .build(); + const method2 = new MethodMirrorBuilder() + .addParameter(new ParameterBuilder().withTypeReference({ type: 'Integer', rawDeclaration: 'Integer' }).build()) + .build(); + + const result = areMethodsEqual(method1, method2); + + expect(result).toBe(false); + }); + + it('returns true when the methods are equal', () => { + const method1 = new MethodMirrorBuilder() + .withName('method1') + .withTypeReference({ type: 'String', rawDeclaration: 'String' }) + .addParameter(new ParameterBuilder().withTypeReference({ type: 'String', rawDeclaration: 'String' }).build()) + .build(); + const method2 = new MethodMirrorBuilder() + .withName('method1') + .withTypeReference({ type: 'String', rawDeclaration: 'String' }) + .addParameter(new ParameterBuilder().withTypeReference({ type: 'String', rawDeclaration: 'String' }).build()) + .build(); + + const result = areMethodsEqual(method1, method2); + + expect(result).toBe(true); + }); +}); diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 915048c9..901a34b3 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -150,7 +150,7 @@ describe('when generating a change log', () => { }); it('lists all new methods of a class', () => { - const classBefore = 'public class MyClass {}'; + const classBefore = 'public class MyClass { void oldMethod() {} }'; const oldClass = typeFromRawString(classBefore); const classAfter = 'public class MyClass { void newMethod() {} }'; const newClass = typeFromRawString(classAfter); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index df93c862..4dcd1631 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -1,5 +1,6 @@ import { EnumMirror, MethodMirror, Type } from '@cparra/apex-reflection'; import { pipe } from 'fp-ts/function'; +import { areMethodsEqual } from './method-changes-checker'; export type VersionManifest = { types: Type[]; @@ -130,9 +131,6 @@ type MethodAware = { function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): NewMethod[] { return newMethodAware.methods - .filter( - (newMethod) => - !oldMethodAware.methods.some((oldMethod) => oldMethod.name.toLowerCase() === newMethod.name.toLowerCase()), - ) + .filter((newMethod) => !oldMethodAware.methods.some((oldMethod) => areMethodsEqual(oldMethod, newMethod))) .map((method) => ({ __typename: 'NewMethod', name: method.name })); } diff --git a/src/core/changelog/method-changes-checker.ts b/src/core/changelog/method-changes-checker.ts new file mode 100644 index 00000000..bfa49a2b --- /dev/null +++ b/src/core/changelog/method-changes-checker.ts @@ -0,0 +1,26 @@ +import { MethodMirror } from '@cparra/apex-reflection'; + +export function areMethodsEqual(method1: MethodMirror, method2: MethodMirror): boolean { + if (method1.name !== method2.name) { + return false; + } + + if (method1.typeReference.rawDeclaration.toLowerCase() !== method2.typeReference.rawDeclaration.toLowerCase()) { + return false; + } + + if (method1.parameters.length !== method2.parameters.length) { + return false; + } + + for (let i = 0; i < method1.parameters.length; i++) { + if ( + method1.parameters[i].typeReference.rawDeclaration.toLowerCase() !== + method2.parameters[i].typeReference.rawDeclaration.toLowerCase() + ) { + return false; + } + } + + return true; +} From e038996bc6386c53bfe8b8e807247c08bbcf20a8 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:37:16 -0400 Subject: [PATCH 10/61] lists all removed methods --- .../__test__/generating-changelog.spec.ts | 26 ++++++++++++++++++- src/core/changelog/generate-changelog.ts | 18 +++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 901a34b3..8fed0196 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -150,7 +150,7 @@ describe('when generating a change log', () => { }); it('lists all new methods of a class', () => { - const classBefore = 'public class MyClass { void oldMethod() {} }'; + const classBefore = 'public class MyClass { }'; const oldClass = typeFromRawString(classBefore); const classAfter = 'public class MyClass { void newMethod() {} }'; const newClass = typeFromRawString(classAfter); @@ -172,4 +172,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists all removed methods of an interface', () => { + const interfaceBefore = 'public interface MyInterface { void oldMethod(); }'; + const oldInterface = typeFromRawString(interfaceBefore); + const interfaceAfter = 'public interface MyInterface {}'; + const newInterface = typeFromRawString(interfaceAfter); + + const oldVersion = { types: [oldInterface] }; + const newVersion = { types: [newInterface] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyInterface', + modifications: [ + { + __typename: 'RemovedMethod', + name: 'oldMethod', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 4dcd1631..39bf0c0d 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -21,7 +21,12 @@ type NewMethod = { name: string; }; -type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod; +type RemovedMethod = { + __typename: 'RemovedMethod'; + name: string; +}; + +type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod | RemovedMethod; type NewOrModifiedMember = { typeName: string; @@ -89,7 +94,10 @@ function getNewOrModifiedMethods(typesInBoth: TypeInBoth[]): NewOrModifiedMember return { typeName: newType.name, - modifications: getNewMethods(oldMethodAware, newMethodAware), + modifications: [ + ...getNewMethods(oldMethodAware, newMethodAware), + ...getRemovedMethods(oldMethodAware, newMethodAware), + ], }; }), ); @@ -134,3 +142,9 @@ function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware) .filter((newMethod) => !oldMethodAware.methods.some((oldMethod) => areMethodsEqual(oldMethod, newMethod))) .map((method) => ({ __typename: 'NewMethod', name: method.name })); } + +function getRemovedMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): RemovedMethod[] { + return oldMethodAware.methods + .filter((oldMethod) => !newMethodAware.methods.some((newMethod) => areMethodsEqual(oldMethod, newMethod))) + .map((method) => ({ __typename: 'RemovedMethod', name: method.name })); +} From 87819271b88b941b727aed62d1b3f6dff2c7b575 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:42:57 -0400 Subject: [PATCH 11/61] lists new properties --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++ src/core/changelog/generate-changelog.ts | 40 +++++++++++++++++-- src/core/changelog/method-changes-checker.ts | 2 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 8fed0196..a0d2d930 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -196,4 +196,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists all new properties of a class', () => { + const classBefore = 'public class MyClass { }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { String newProperty { get; set; } }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewProperty', + name: 'newProperty', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 39bf0c0d..4daa537c 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -1,4 +1,4 @@ -import { EnumMirror, MethodMirror, Type } from '@cparra/apex-reflection'; +import { ClassMirror, EnumMirror, MethodMirror, Type } from '@cparra/apex-reflection'; import { pipe } from 'fp-ts/function'; import { areMethodsEqual } from './method-changes-checker'; @@ -26,7 +26,12 @@ type RemovedMethod = { name: string; }; -type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod | RemovedMethod; +type NewProperty = { + __typename: 'NewProperty'; + name: string; +}; + +type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod | RemovedMethod | NewProperty; type NewOrModifiedMember = { typeName: string; @@ -62,7 +67,11 @@ function getRemovedTypes(oldVersion: VersionManifest, newVersion: VersionManifes function getNewOrModifiedMembers(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { return pipe( getTypesInBothVersions(oldVersion, newVersion), - (typesInBoth) => [...getNewOrModifiedEnumValues(typesInBoth), ...getNewOrModifiedMethods(typesInBoth)], + (typesInBoth) => [ + ...getNewOrModifiedEnumValues(typesInBoth), + ...getNewOrModifiedMethods(typesInBoth), + ...getNewOrModifiedClassMembers(typesInBoth), + ], (newOrModifiedMembers) => newOrModifiedMembers.filter((member) => member.modifications.length > 0), ); } @@ -103,6 +112,22 @@ function getNewOrModifiedMethods(typesInBoth: TypeInBoth[]): NewOrModifiedMember ); } +function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedMember[] { + return pipe( + typesInBoth.filter((typeInBoth) => typeInBoth.oldType.type_name === 'class'), + (classesInBoth) => + classesInBoth.map(({ oldType, newType }) => { + const oldClass = oldType as ClassMirror; + const newClass = newType as ClassMirror; + + return { + typeName: newType.name, + modifications: [...getNewProperties(oldClass, newClass)], + }; + }), + ); +} + type TypeInBoth = { oldType: Type; newType: Type; @@ -148,3 +173,12 @@ function getRemovedMethods(oldMethodAware: MethodAware, newMethodAware: MethodAw .filter((oldMethod) => !newMethodAware.methods.some((newMethod) => areMethodsEqual(oldMethod, newMethod))) .map((method) => ({ __typename: 'RemovedMethod', name: method.name })); } + +function getNewProperties(oldClass: ClassMirror, newClass: ClassMirror): NewProperty[] { + return newClass.properties + .filter( + (newValue) => + !oldClass.properties.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewProperty', name: value.name })); +} diff --git a/src/core/changelog/method-changes-checker.ts b/src/core/changelog/method-changes-checker.ts index bfa49a2b..bd916b8b 100644 --- a/src/core/changelog/method-changes-checker.ts +++ b/src/core/changelog/method-changes-checker.ts @@ -1,7 +1,7 @@ import { MethodMirror } from '@cparra/apex-reflection'; export function areMethodsEqual(method1: MethodMirror, method2: MethodMirror): boolean { - if (method1.name !== method2.name) { + if (method1.name.toLowerCase() !== method2.name.toLowerCase()) { return false; } From 234f6bffcfe121bf4c496fe91658a710e37c7de2 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:44:48 -0400 Subject: [PATCH 12/61] lists removed properties --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 24 +++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index a0d2d930..fa609e90 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -220,4 +220,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists all removed properties of a class', () => { + const classBefore = 'public class MyClass { String oldProperty { get; set; } }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedProperty', + name: 'oldProperty', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 4daa537c..857c1204 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -31,7 +31,18 @@ type NewProperty = { name: string; }; -type MemberModificationType = NewEnumValue | RemovedEnumValue | NewMethod | RemovedMethod | NewProperty; +type RemovedProperty = { + __typename: 'RemovedProperty'; + name: string; +}; + +type MemberModificationType = + | NewEnumValue + | RemovedEnumValue + | NewMethod + | RemovedMethod + | NewProperty + | RemovedProperty; type NewOrModifiedMember = { typeName: string; @@ -122,7 +133,7 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM return { typeName: newType.name, - modifications: [...getNewProperties(oldClass, newClass)], + modifications: [...getNewProperties(oldClass, newClass), ...getRemovedProperties(oldClass, newClass)], }; }), ); @@ -182,3 +193,12 @@ function getNewProperties(oldClass: ClassMirror, newClass: ClassMirror): NewProp ) .map((value) => ({ __typename: 'NewProperty', name: value.name })); } + +function getRemovedProperties(oldClass: ClassMirror, newClass: ClassMirror): RemovedProperty[] { + return oldClass.properties + .filter( + (oldValue) => + !newClass.properties.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedProperty', name: value.name })); +} From a0f513e9e6d73a4153421ba90e4b7e34a1a946be Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:47:36 -0400 Subject: [PATCH 13/61] lists new and removed fields --- .../__test__/generating-changelog.spec.ts | 48 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 37 +++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index fa609e90..f38acf85 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -244,4 +244,52 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists all new fields of a class', () => { + const classBefore = 'public class MyClass { }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { String newField; }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewField', + name: 'newField', + }, + ], + }, + ]); + }); + + it('lists all removed fields of a class', () => { + const classBefore = 'public class MyClass { String oldField; }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedField', + name: 'oldField', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 857c1204..08b25ae5 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -36,13 +36,25 @@ type RemovedProperty = { name: string; }; +type NewField = { + __typename: 'NewField'; + name: string; +}; + +type RemovedField = { + __typename: 'RemovedField'; + name: string; +}; + type MemberModificationType = | NewEnumValue | RemovedEnumValue | NewMethod | RemovedMethod | NewProperty - | RemovedProperty; + | RemovedProperty + | NewField + | RemovedField; type NewOrModifiedMember = { typeName: string; @@ -133,7 +145,12 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM return { typeName: newType.name, - modifications: [...getNewProperties(oldClass, newClass), ...getRemovedProperties(oldClass, newClass)], + modifications: [ + ...getNewProperties(oldClass, newClass), + ...getRemovedProperties(oldClass, newClass), + ...getNewFields(oldClass, newClass), + ...getRemovedFields(oldClass, newClass), + ], }; }), ); @@ -202,3 +219,19 @@ function getRemovedProperties(oldClass: ClassMirror, newClass: ClassMirror): Rem ) .map((value) => ({ __typename: 'RemovedProperty', name: value.name })); } + +function getNewFields(oldClass: ClassMirror, newClass: ClassMirror): NewField[] { + return newClass.fields + .filter( + (newValue) => !oldClass.fields.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewField', name: value.name })); +} + +function getRemovedFields(oldClass: ClassMirror, newClass: ClassMirror): RemovedField[] { + return oldClass.fields + .filter( + (oldValue) => !newClass.fields.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedField', name: value.name })); +} From 175421b6b624237b21f2bddb8aab5dc185258b31 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:51:53 -0400 Subject: [PATCH 14/61] lists new inner classes --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 17 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index f38acf85..97a0bc61 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -292,4 +292,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists new inner classes of a class', () => { + const classBefore = 'public class MyClass { }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { class NewInnerClass { } }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewType', + name: 'NewInnerClass', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 08b25ae5..781c2128 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -46,6 +46,11 @@ type RemovedField = { name: string; }; +type NewType = { + __typename: 'NewType'; + name: string; +}; + type MemberModificationType = | NewEnumValue | RemovedEnumValue @@ -54,7 +59,8 @@ type MemberModificationType = | NewProperty | RemovedProperty | NewField - | RemovedField; + | RemovedField + | NewType; type NewOrModifiedMember = { typeName: string; @@ -150,6 +156,7 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM ...getRemovedProperties(oldClass, newClass), ...getNewFields(oldClass, newClass), ...getRemovedFields(oldClass, newClass), + ...getNewInnerClasses(oldClass, newClass), ], }; }), @@ -235,3 +242,11 @@ function getRemovedFields(oldClass: ClassMirror, newClass: ClassMirror): Removed ) .map((value) => ({ __typename: 'RemovedField', name: value.name })); } + +function getNewInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { + return newClass.classes + .filter( + (newValue) => !oldClass.classes.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewType', name: value.name })); +} From 6efd934447e6c1bce4c356839d08b85c1aa91810 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:53:38 -0400 Subject: [PATCH 15/61] lists removed inner classes --- .../__test__/generating-changelog.spec.ts | 24 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 17 ++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 97a0bc61..232dd51d 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -316,4 +316,28 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists removed inner classes of a class', () => { + const classBefore = 'public class MyClass { class OldInnerClass { } }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldInnerClass', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 781c2128..36883530 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -51,6 +51,11 @@ type NewType = { name: string; }; +type RemovedType = { + __typename: 'RemovedType'; + name: string; +}; + type MemberModificationType = | NewEnumValue | RemovedEnumValue @@ -60,7 +65,8 @@ type MemberModificationType = | RemovedProperty | NewField | RemovedField - | NewType; + | NewType + | RemovedType; type NewOrModifiedMember = { typeName: string; @@ -157,6 +163,7 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM ...getNewFields(oldClass, newClass), ...getRemovedFields(oldClass, newClass), ...getNewInnerClasses(oldClass, newClass), + ...getRemovedInnerClasses(oldClass, newClass), ], }; }), @@ -250,3 +257,11 @@ function getNewInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): NewTy ) .map((value) => ({ __typename: 'NewType', name: value.name })); } + +function getRemovedInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { + return oldClass.classes + .filter( + (oldValue) => !newClass.classes.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedType', name: value.name })); +} From 58b524d2f283005752609fbe2927404e89912397 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:56:34 -0400 Subject: [PATCH 16/61] lists new and removed interfaces --- .../__test__/generating-changelog.spec.ts | 48 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 20 ++++++++ 2 files changed, 68 insertions(+) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index 232dd51d..b36adfc6 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -340,4 +340,52 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists new inner interfaces of a class', () => { + const classBefore = 'public class MyClass { }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { interface NewInterface { } }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewType', + name: 'NewInterface', + }, + ], + }, + ]); + }); + + it('lists removed inner interfaces of a class', () => { + const classBefore = 'public class MyClass { interface OldInterface { } }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldInterface', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 36883530..14c35658 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -164,6 +164,8 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM ...getRemovedFields(oldClass, newClass), ...getNewInnerClasses(oldClass, newClass), ...getRemovedInnerClasses(oldClass, newClass), + ...getNewInnerInterfaces(oldClass, newClass), + ...getRemovedInnerInterfaces(oldClass, newClass), ], }; }), @@ -265,3 +267,21 @@ function getRemovedInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): R ) .map((value) => ({ __typename: 'RemovedType', name: value.name })); } + +function getNewInnerInterfaces(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { + return newClass.interfaces + .filter( + (newValue) => + !oldClass.interfaces.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewType', name: value.name })); +} + +function getRemovedInnerInterfaces(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { + return oldClass.interfaces + .filter( + (oldValue) => + !newClass.interfaces.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedType', name: value.name })); +} From 77571d4e737862aabac9d9e6bc68b7a251871086 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 08:57:51 -0400 Subject: [PATCH 17/61] lists new and removed enums --- .../__test__/generating-changelog.spec.ts | 48 +++++++++++++++++++ src/core/changelog/generate-changelog.ts | 18 +++++++ 2 files changed, 66 insertions(+) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index b36adfc6..a1780d74 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -388,4 +388,52 @@ describe('when generating a change log', () => { }, ]); }); + + it('lists new inner enums of a class', () => { + const classBefore = 'public class MyClass { }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { enum NewEnum { } }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewType', + name: 'NewEnum', + }, + ], + }, + ]); + }); + + it('lists removed inner enums of a class', () => { + const classBefore = 'public class MyClass { interface OldEnum { } }'; + const oldClass = typeFromRawString(classBefore); + const classAfter = 'public class MyClass { }'; + const newClass = typeFromRawString(classAfter); + + const oldVersion = { types: [oldClass] }; + const newVersion = { types: [newClass] }; + + const changeLog = generateChangeLog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldEnum', + }, + ], + }, + ]); + }); }); diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 14c35658..dae0bb9e 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -166,6 +166,8 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM ...getRemovedInnerClasses(oldClass, newClass), ...getNewInnerInterfaces(oldClass, newClass), ...getRemovedInnerInterfaces(oldClass, newClass), + ...getNewInnerEnums(oldClass, newClass), + ...getRemovedInnerEnums(oldClass, newClass), ], }; }), @@ -285,3 +287,19 @@ function getRemovedInnerInterfaces(oldClass: ClassMirror, newClass: ClassMirror) ) .map((value) => ({ __typename: 'RemovedType', name: value.name })); } + +function getNewInnerEnums(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { + return newClass.enums + .filter( + (newValue) => !oldClass.enums.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'NewType', name: value.name })); +} + +function getRemovedInnerEnums(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { + return oldClass.enums + .filter( + (oldValue) => !newClass.enums.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + ) + .map((value) => ({ __typename: 'RemovedType', name: value.name })); +} From 8dfb5e873773a6978ce75c803df3ab953aea3bf0 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sun, 22 Sep 2024 09:49:32 -0400 Subject: [PATCH 18/61] refactoring --- .../__test__/generating-changelog.spec.ts | 4 +- src/core/changelog/generate-changelog.ts | 220 +++++------------- 2 files changed, 58 insertions(+), 166 deletions(-) diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/generating-changelog.spec.ts index a1780d74..af6cb29a 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/generating-changelog.spec.ts @@ -94,7 +94,7 @@ describe('when generating a change log', () => { modifications: [ { __typename: 'NewEnumValue', - value: 'VALUE2', + name: 'VALUE2', }, ], }, @@ -118,7 +118,7 @@ describe('when generating a change log', () => { modifications: [ { __typename: 'RemovedEnumValue', - value: 'VALUE2', + name: 'VALUE2', }, ], }, diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index dae0bb9e..714fc27d 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -6,68 +6,23 @@ export type VersionManifest = { types: Type[]; }; -type NewEnumValue = { - __typename: 'NewEnumValue'; - value: string; -}; - -type RemovedEnumValue = { - __typename: 'RemovedEnumValue'; - value: string; -}; - -type NewMethod = { - __typename: 'NewMethod'; - name: string; -}; - -type RemovedMethod = { - __typename: 'RemovedMethod'; - name: string; -}; - -type NewProperty = { - __typename: 'NewProperty'; +type ModificationTypes = + | 'NewEnumValue' + | 'RemovedEnumValue' + | 'NewMethod' + | 'RemovedMethod' + | 'NewProperty' + | 'RemovedProperty' + | 'NewField' + | 'RemovedField' + | 'NewType' + | 'RemovedType'; + +type MemberModificationType = { + __typename: ModificationTypes; name: string; }; -type RemovedProperty = { - __typename: 'RemovedProperty'; - name: string; -}; - -type NewField = { - __typename: 'NewField'; - name: string; -}; - -type RemovedField = { - __typename: 'RemovedField'; - name: string; -}; - -type NewType = { - __typename: 'NewType'; - name: string; -}; - -type RemovedType = { - __typename: 'RemovedType'; - name: string; -}; - -type MemberModificationType = - | NewEnumValue - | RemovedEnumValue - | NewMethod - | RemovedMethod - | NewProperty - | RemovedProperty - | NewField - | RemovedField - | NewType - | RemovedType; - type NewOrModifiedMember = { typeName: string; modifications: MemberModificationType[]; @@ -120,7 +75,10 @@ function getNewOrModifiedEnumValues(typesInBoth: TypeInBoth[]): NewOrModifiedMem const newEnum = newType as EnumMirror; return { typeName: newType.name, - modifications: [...getNewEnumValues(oldEnum, newEnum), ...getRemovedEnumValues(oldEnum, newEnum)], + modifications: [ + ...getNewValues(oldEnum, newEnum, 'values', 'NewEnumValue'), + ...getRemovedValues(oldEnum, newEnum, 'values', 'RemovedEnumValue'), + ], }; }), ); @@ -158,16 +116,16 @@ function getNewOrModifiedClassMembers(typesInBoth: TypeInBoth[]): NewOrModifiedM return { typeName: newType.name, modifications: [ - ...getNewProperties(oldClass, newClass), - ...getRemovedProperties(oldClass, newClass), - ...getNewFields(oldClass, newClass), - ...getRemovedFields(oldClass, newClass), - ...getNewInnerClasses(oldClass, newClass), - ...getRemovedInnerClasses(oldClass, newClass), - ...getNewInnerInterfaces(oldClass, newClass), - ...getRemovedInnerInterfaces(oldClass, newClass), - ...getNewInnerEnums(oldClass, newClass), - ...getRemovedInnerEnums(oldClass, newClass), + ...getNewValues(oldClass, newClass, 'properties', 'NewProperty'), + ...getRemovedValues(oldClass, newClass, 'properties', 'RemovedProperty'), + ...getNewValues(oldClass, newClass, 'fields', 'NewField'), + ...getRemovedValues(oldClass, newClass, 'fields', 'RemovedField'), + ...getNewValues(oldClass, newClass, 'classes', 'NewType'), + ...getRemovedValues(oldClass, newClass, 'classes', 'RemovedType'), + ...getNewValues(oldClass, newClass, 'interfaces', 'NewType'), + ...getRemovedValues(oldClass, newClass, 'interfaces', 'RemovedType'), + ...getNewValues(oldClass, newClass, 'enums', 'NewType'), + ...getRemovedValues(oldClass, newClass, 'enums', 'RemovedType'), ], }; }), @@ -188,118 +146,52 @@ function getTypesInBothVersions(oldVersion: VersionManifest, newVersion: Version .filter((type) => type.newType !== undefined) as TypeInBoth[]; } -function getNewEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): NewEnumValue[] { - return newEnum.values +type NameAware = { + name: string; +}; + +function getNewValues, K extends keyof T>( + oldPlaceToSearch: T, + newPlaceToSearch: T, + keyToSearch: K, + typeName: ModificationTypes, +): MemberModificationType[] { + return newPlaceToSearch[keyToSearch] .filter( - (newValue) => !oldEnum.values.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), + (newValue) => + !oldPlaceToSearch[keyToSearch].some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), ) - .map((value) => ({ __typename: 'NewEnumValue', value: value.name })); + .map((value) => value.name) + .map((name) => ({ __typename: typeName, name })); } -function getRemovedEnumValues(oldEnum: EnumMirror, newEnum: EnumMirror): RemovedEnumValue[] { - return oldEnum.values +function getRemovedValues, K extends keyof T>( + oldPlaceToSearch: T, + newPlaceToSearch: T, + keyToSearch: K, + typeName: ModificationTypes, +): MemberModificationType[] { + return oldPlaceToSearch[keyToSearch] .filter( - (oldValue) => !newEnum.values.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), + (oldValue) => + !newPlaceToSearch[keyToSearch].some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), ) - .map((value) => ({ __typename: 'RemovedEnumValue', value: value.name })); + .map((value) => value.name) + .map((name) => ({ __typename: typeName, name })); } type MethodAware = { methods: MethodMirror[]; }; -function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): NewMethod[] { +function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): MemberModificationType[] { return newMethodAware.methods .filter((newMethod) => !oldMethodAware.methods.some((oldMethod) => areMethodsEqual(oldMethod, newMethod))) .map((method) => ({ __typename: 'NewMethod', name: method.name })); } -function getRemovedMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): RemovedMethod[] { +function getRemovedMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): MemberModificationType[] { return oldMethodAware.methods .filter((oldMethod) => !newMethodAware.methods.some((newMethod) => areMethodsEqual(oldMethod, newMethod))) .map((method) => ({ __typename: 'RemovedMethod', name: method.name })); } - -function getNewProperties(oldClass: ClassMirror, newClass: ClassMirror): NewProperty[] { - return newClass.properties - .filter( - (newValue) => - !oldClass.properties.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'NewProperty', name: value.name })); -} - -function getRemovedProperties(oldClass: ClassMirror, newClass: ClassMirror): RemovedProperty[] { - return oldClass.properties - .filter( - (oldValue) => - !newClass.properties.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'RemovedProperty', name: value.name })); -} - -function getNewFields(oldClass: ClassMirror, newClass: ClassMirror): NewField[] { - return newClass.fields - .filter( - (newValue) => !oldClass.fields.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'NewField', name: value.name })); -} - -function getRemovedFields(oldClass: ClassMirror, newClass: ClassMirror): RemovedField[] { - return oldClass.fields - .filter( - (oldValue) => !newClass.fields.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'RemovedField', name: value.name })); -} - -function getNewInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { - return newClass.classes - .filter( - (newValue) => !oldClass.classes.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'NewType', name: value.name })); -} - -function getRemovedInnerClasses(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { - return oldClass.classes - .filter( - (oldValue) => !newClass.classes.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'RemovedType', name: value.name })); -} - -function getNewInnerInterfaces(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { - return newClass.interfaces - .filter( - (newValue) => - !oldClass.interfaces.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'NewType', name: value.name })); -} - -function getRemovedInnerInterfaces(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { - return oldClass.interfaces - .filter( - (oldValue) => - !newClass.interfaces.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'RemovedType', name: value.name })); -} - -function getNewInnerEnums(oldClass: ClassMirror, newClass: ClassMirror): NewType[] { - return newClass.enums - .filter( - (newValue) => !oldClass.enums.some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'NewType', name: value.name })); -} - -function getRemovedInnerEnums(oldClass: ClassMirror, newClass: ClassMirror): RemovedType[] { - return oldClass.enums - .filter( - (oldValue) => !newClass.enums.some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) - .map((value) => ({ __typename: 'RemovedType', name: value.name })); -} From 993e060a66085e45f291b3133a6fd38a5028245e Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 07:59:45 -0400 Subject: [PATCH 19/61] refactoring --- src/core/changelog/generate-changelog.ts | 49 ++++++++++++------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/generate-changelog.ts index 714fc27d..f41d8c35 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/generate-changelog.ts @@ -97,8 +97,20 @@ function getNewOrModifiedMethods(typesInBoth: TypeInBoth[]): NewOrModifiedMember return { typeName: newType.name, modifications: [ - ...getNewMethods(oldMethodAware, newMethodAware), - ...getRemovedMethods(oldMethodAware, newMethodAware), + ...getNewValues( + oldMethodAware, + newMethodAware, + 'methods', + 'NewMethod', + areMethodsEqual, + ), + ...getRemovedValues( + oldMethodAware, + newMethodAware, + 'methods', + 'RemovedMethod', + areMethodsEqual, + ), ], }; }), @@ -150,32 +162,33 @@ type NameAware = { name: string; }; -function getNewValues, K extends keyof T>( +type AreEqualFn = (oldValue: T, newValue: T) => boolean; +function areEqualByName(oldValue: T, newValue: T): boolean { + return oldValue.name.toLowerCase() === newValue.name.toLowerCase(); +} + +function getNewValues, K extends keyof T>( oldPlaceToSearch: T, newPlaceToSearch: T, keyToSearch: K, typeName: ModificationTypes, + areEqualFn: AreEqualFn = areEqualByName, ): MemberModificationType[] { return newPlaceToSearch[keyToSearch] - .filter( - (newValue) => - !oldPlaceToSearch[keyToSearch].some((oldValue) => oldValue.name.toLowerCase() === newValue.name.toLowerCase()), - ) + .filter((newValue) => !oldPlaceToSearch[keyToSearch].some((oldValue) => areEqualFn(oldValue, newValue))) .map((value) => value.name) .map((name) => ({ __typename: typeName, name })); } -function getRemovedValues, K extends keyof T>( +function getRemovedValues, K extends keyof T>( oldPlaceToSearch: T, newPlaceToSearch: T, keyToSearch: K, typeName: ModificationTypes, + areEqualFn: AreEqualFn = areEqualByName, ): MemberModificationType[] { return oldPlaceToSearch[keyToSearch] - .filter( - (oldValue) => - !newPlaceToSearch[keyToSearch].some((newValue) => newValue.name.toLowerCase() === oldValue.name.toLowerCase()), - ) + .filter((oldValue) => !newPlaceToSearch[keyToSearch].some((newValue) => areEqualFn(oldValue, newValue))) .map((value) => value.name) .map((name) => ({ __typename: typeName, name })); } @@ -183,15 +196,3 @@ function getRemovedValues, K extends keyof T>( type MethodAware = { methods: MethodMirror[]; }; - -function getNewMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): MemberModificationType[] { - return newMethodAware.methods - .filter((newMethod) => !oldMethodAware.methods.some((oldMethod) => areMethodsEqual(oldMethod, newMethod))) - .map((method) => ({ __typename: 'NewMethod', name: method.name })); -} - -function getRemovedMethods(oldMethodAware: MethodAware, newMethodAware: MethodAware): MemberModificationType[] { - return oldMethodAware.methods - .filter((oldMethod) => !newMethodAware.methods.some((newMethod) => areMethodsEqual(oldMethod, newMethod))) - .map((method) => ({ __typename: 'RemovedMethod', name: method.name })); -} From 56ebd4d0aae719b8bfc9c1454d76235c3bee1821 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 08:48:04 -0400 Subject: [PATCH 20/61] includes the new classes section if there are any --- ...onverting-to-renderable-change-log.spec.ts | 34 ++++++++++++++ ...g.spec.ts => processing-changelog.spec.ts} | 44 +++++++++---------- ...ate-changelog.ts => process-change-log.ts} | 2 +- src/core/changelog/renderable-change-log.ts | 35 +++++++++++++++ 4 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts rename src/core/changelog/__test__/{generating-changelog.spec.ts => processing-changelog.spec.ts} (89%) rename src/core/changelog/{generate-changelog.ts => process-change-log.ts} (98%) create mode 100644 src/core/changelog/renderable-change-log.ts diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts new file mode 100644 index 00000000..f7912e44 --- /dev/null +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -0,0 +1,34 @@ +import { ChangeLog } from '../process-change-log'; +import { convertToRenderableChangeLog } from '../renderable-change-log'; +import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; + +describe('when converting a change log to a renderable change log', () => { + it('does not include new classes if there are none', () => { + const changeLog: ChangeLog = { + newTypes: [], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, []); + + expect(renderable.newClasses).toBeNull(); + }); + + it('includes new classes if there are any', () => { + const newClasses = [new ClassMirrorBuilder().withName('MyClass').build()]; + const changeLog: ChangeLog = { + newTypes: ['MyClass'], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, newClasses); + + expect(renderable.newClasses).toEqual({ + __type: 'class', + heading: 'New Classes', + description: 'These classes are new.', + }); + }); +}); diff --git a/src/core/changelog/__test__/generating-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts similarity index 89% rename from src/core/changelog/__test__/generating-changelog.spec.ts rename to src/core/changelog/__test__/processing-changelog.spec.ts index af6cb29a..434e1874 100644 --- a/src/core/changelog/__test__/generating-changelog.spec.ts +++ b/src/core/changelog/__test__/processing-changelog.spec.ts @@ -1,4 +1,4 @@ -import { generateChangeLog } from '../generate-changelog'; +import { processChangeLog } from '../process-change-log'; import { reflect, Type } from '@cparra/apex-reflection'; function typeFromRawString(raw: string): Type { @@ -15,7 +15,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([]); }); @@ -24,7 +24,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([]); }); @@ -35,7 +35,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [anyClass] }; const newVersion = { types: [anyClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([]); }); @@ -46,7 +46,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [anyClass] }; const newVersion = { types: [anyClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([]); }); @@ -59,7 +59,7 @@ describe('when generating a change log', () => { const newClass = typeFromRawString(newClassBody); const newVersion = { types: [existingClass, newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([newClass.name]); }); @@ -72,7 +72,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [existingClass, existingOnlyInOldClass] }; const newVersion = { types: [existingClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([existingOnlyInOldClass.name]); }); @@ -86,7 +86,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldEnum] }; const newVersion = { types: [newEnum] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -110,7 +110,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldEnum] }; const newVersion = { types: [newEnum] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -134,7 +134,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldInterface] }; const newVersion = { types: [newInterface] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -158,7 +158,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -182,7 +182,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldInterface] }; const newVersion = { types: [newInterface] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -206,7 +206,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -230,7 +230,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -254,7 +254,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -278,7 +278,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -302,7 +302,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -326,7 +326,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -350,7 +350,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -374,7 +374,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -398,7 +398,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -422,7 +422,7 @@ describe('when generating a change log', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = generateChangeLog(oldVersion, newVersion); + const changeLog = processChangeLog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { diff --git a/src/core/changelog/generate-changelog.ts b/src/core/changelog/process-change-log.ts similarity index 98% rename from src/core/changelog/generate-changelog.ts rename to src/core/changelog/process-change-log.ts index f41d8c35..f4fde461 100644 --- a/src/core/changelog/generate-changelog.ts +++ b/src/core/changelog/process-change-log.ts @@ -34,7 +34,7 @@ export type ChangeLog = { newOrModifiedMembers: NewOrModifiedMember[]; }; -export function generateChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { +export function processChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { return { newTypes: getNewTypes(oldVersion, newVersion), removedTypes: getRemovedTypes(oldVersion, newVersion), diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts new file mode 100644 index 00000000..8e5bfca6 --- /dev/null +++ b/src/core/changelog/renderable-change-log.ts @@ -0,0 +1,35 @@ +import { ChangeLog } from './process-change-log'; +import { Type } from '@cparra/apex-reflection'; + +type NewTypeSection = { + __type: T; + heading: string; + description: string; +}; + +type RenderableChangeLog = { + newClasses: NewTypeSection<'class'> | null; +}; + +export function convertToRenderableChangeLog(changeLog: ChangeLog, newTypes: Type[]): RenderableChangeLog { + const hasNewClasses = + changeLog.newTypes.filter((newType) => newTypes.some((type) => type.name.toLowerCase() === newType.toLowerCase())) + .length > 0; + + return { + newClasses: hasNewClasses + ? { __type: 'class', heading: 'New Classes', description: 'These classes are new.' } + : null, + }; +} + +// TODO: +// New Classes +// New Enums +// New Interfaces + +// Removed Classes +// Removed Enums +// Removed Interfaces + +// Changes... From 016c401485a9603f31a64f74fd84839ffdbeaed3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 09:21:29 -0400 Subject: [PATCH 21/61] Walking skeleton for generating the change log output from a pair of bundles. --- ...onverting-to-renderable-change-log.spec.ts | 4 +- .../__test__/generating-change-log.spec.ts | 32 +++++++++++++ src/core/changelog/generate-change-log.ts | 45 +++++++++++++++++++ .../templates/change-log-template.ts | 9 ++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/core/changelog/__test__/generating-change-log.spec.ts create mode 100644 src/core/changelog/generate-change-log.ts create mode 100644 src/core/changelog/templates/change-log-template.ts diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts index f7912e44..b58c721e 100644 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -3,7 +3,7 @@ import { convertToRenderableChangeLog } from '../renderable-change-log'; import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; describe('when converting a change log to a renderable change log', () => { - it('does not include new classes if there are none', () => { + it('does not include the New Classes section if there are none', () => { const changeLog: ChangeLog = { newTypes: [], removedTypes: [], @@ -15,7 +15,7 @@ describe('when converting a change log to a renderable change log', () => { expect(renderable.newClasses).toBeNull(); }); - it('includes new classes if there are any', () => { + it('includes the New Classes section if there are any', () => { const newClasses = [new ClassMirrorBuilder().withName('MyClass').build()]; const changeLog: ChangeLog = { newTypes: ['MyClass'], diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts new file mode 100644 index 00000000..b5c10dfb --- /dev/null +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -0,0 +1,32 @@ +import { UnparsedSourceFile } from '../../shared/types'; +import { generateChangeLog } from '../generate-change-log'; +// TODO: Move the assert either function to a shared place so we are not importing it from markdown +import { assertEither } from '../../markdown/__test__/expect-extensions'; + +describe('when generating a change log', () => { + describe('that does not include new classes', () => { + it('should not have a section for new classes', async () => { + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = []; + + const result = await generateChangeLog(oldBundle, newBundle)(); + + assertEither(result, (data) => expect(data).not.toContain('## New Classes')); + }); + }); + + describe('that includes new classes', () => { + it('should include a section for new classes', async () => { + const newClassSource = 'class Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle)(); + + assertEither(result, (data) => expect(data).toContain('## New Classes')); + }); + }); +}); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts new file mode 100644 index 00000000..9b9c1745 --- /dev/null +++ b/src/core/changelog/generate-change-log.ts @@ -0,0 +1,45 @@ +import { ParsedFile, UnparsedSourceFile } from '../shared/types'; +import { pipe } from 'fp-ts/function'; +import * as TE from 'fp-ts/TaskEither'; +// TODO: Move the reflection code to outside of the markdown folder since now it is shared with this +import { reflectBundles } from '../markdown/reflection/reflect-source'; +import { processChangeLog, VersionManifest } from './process-change-log'; +import { convertToRenderableChangeLog } from './renderable-change-log'; +// TODO: Also move this file since this is now shared +import { CompilationRequest, Template } from '../markdown/templates/template'; +import { changeLogTemplate } from './templates/change-log-template'; + +// TODO: We should provide the ability to filter out of scope if we are going +// to be relying on source files and not on a previously generated manifest. +export function generateChangeLog(oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[]) { + return pipe( + reflectBundles(oldBundles), + TE.bindTo('oldVersion'), + TE.bind('newVersion', () => reflectBundles(newBundles)), + TE.map(({ oldVersion, newVersion }) => ({ + oldManifest: parsedFilesToManifest(oldVersion), + newManifest: parsedFilesToManifest(newVersion), + })), + TE.map(({ oldManifest, newManifest }) => ({ + changeLog: processChangeLog(oldManifest, newManifest), + newManifest, + })), + TE.map(({ changeLog, newManifest }) => convertToRenderableChangeLog(changeLog, newManifest.types)), + TE.map((renderable) => { + const compilationRequest: CompilationRequest = { + template: changeLogTemplate, + source: renderable, + }; + + // TODO: At some point just having a string won't be enough, since we want an object + // that contains the target file name and directory as well as the content. + return Template.getInstance().compile(compilationRequest); + }), + ); +} + +function parsedFilesToManifest(parsedFiles: ParsedFile[]): VersionManifest { + return { + types: parsedFiles.map((parsedFile) => parsedFile.type), + }; +} diff --git a/src/core/changelog/templates/change-log-template.ts b/src/core/changelog/templates/change-log-template.ts new file mode 100644 index 00000000..a423f3ac --- /dev/null +++ b/src/core/changelog/templates/change-log-template.ts @@ -0,0 +1,9 @@ +export const changeLogTemplate = ` +# Change Log + +{{#if newClasses}} +## {{newClasses.heading}} + +{{newClasses.description}} +{{/if}} +`.trim(); From 0005629a49d30d7187b04dabaea6ac3f91430ddc Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 17:46:21 -0400 Subject: [PATCH 22/61] Changelog now returns a file that can be written to disk --- .../__test__/generating-change-log.spec.ts | 9 +++---- src/core/changelog/generate-change-log.ts | 24 +++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index b5c10dfb..5e853f3e 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -9,9 +9,10 @@ describe('when generating a change log', () => { const oldBundle: UnparsedSourceFile[] = []; const newBundle: UnparsedSourceFile[] = []; - const result = await generateChangeLog(oldBundle, newBundle)(); + const result = await generateChangeLog(oldBundle, newBundle, { targetDir: '' })(); - assertEither(result, (data) => expect(data).not.toContain('## New Classes')); + assertEither(result, (data) => expect(data.filePath).toContain('changelog.md')); // TODO: This assertion should have its own test + assertEither(result, (data) => expect(data.content).not.toContain('## New Classes')); }); }); @@ -24,9 +25,9 @@ describe('when generating a change log', () => { { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, ]; - const result = await generateChangeLog(oldBundle, newBundle)(); + const result = await generateChangeLog(oldBundle, newBundle, { targetDir: '' })(); - assertEither(result, (data) => expect(data).toContain('## New Classes')); + assertEither(result, (data) => expect(data.content).toContain('## New Classes')); }); }); }); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 9b9c1745..9b43363d 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -2,16 +2,30 @@ import { ParsedFile, UnparsedSourceFile } from '../shared/types'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; // TODO: Move the reflection code to outside of the markdown folder since now it is shared with this -import { reflectBundles } from '../markdown/reflection/reflect-source'; +import { reflectBundles, ReflectionErrors } from '../markdown/reflection/reflect-source'; import { processChangeLog, VersionManifest } from './process-change-log'; import { convertToRenderableChangeLog } from './renderable-change-log'; // TODO: Also move this file since this is now shared import { CompilationRequest, Template } from '../markdown/templates/template'; import { changeLogTemplate } from './templates/change-log-template'; +type ChangeLogPageData = { + // TODO: This should also support frontmatter (and the hook to add it) + content: string; + filePath: string; +}; + +type ChangeLogConfig = { + targetDir: string; +}; + // TODO: We should provide the ability to filter out of scope if we are going // to be relying on source files and not on a previously generated manifest. -export function generateChangeLog(oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[]) { +export function generateChangeLog( + oldBundles: UnparsedSourceFile[], + newBundles: UnparsedSourceFile[], + config: ChangeLogConfig, +): TE.TaskEither { return pipe( reflectBundles(oldBundles), TE.bindTo('oldVersion'), @@ -35,6 +49,12 @@ export function generateChangeLog(oldBundles: UnparsedSourceFile[], newBundles: // that contains the target file name and directory as well as the content. return Template.getInstance().compile(compilationRequest); }), + TE.map((content) => ({ + content, + // TODO: There should be a way that someone can change the name of the output file + // TODO: Do not concantenate the strings, but instead use the path module + filePath: `${config.targetDir}/changelog.md`, + })), ); } From 9ef7d8cf8edad799e1fbea8cae33b36ccba29c03 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 18:39:09 -0400 Subject: [PATCH 23/61] Introducing the changelog command --- .../changelog/current/classes/NewClass.cls | 5 ++ examples/changelog/package.json | 19 +++++ examples/changelog/previous/.gitkeep | 0 src/application/Apexdocs.ts | 79 +++++++++++++++---- src/application/generators/changelog.ts | 36 +++++++++ src/application/generators/markdown.ts | 2 +- src/cli/args.ts | 16 +++- src/cli/commands/changelog.ts | 28 +++++++ src/cli/generate.ts | 2 +- src/core/changelog/generate-change-log.ts | 16 ++-- src/core/shared/types.d.ts | 15 +++- src/defaults.ts | 5 ++ src/index.ts | 2 + src/node/process.ts | 5 +- 14 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 examples/changelog/current/classes/NewClass.cls create mode 100644 examples/changelog/package.json create mode 100644 examples/changelog/previous/.gitkeep create mode 100644 src/application/generators/changelog.ts create mode 100644 src/cli/commands/changelog.ts diff --git a/examples/changelog/current/classes/NewClass.cls b/examples/changelog/current/classes/NewClass.cls new file mode 100644 index 00000000..9e18815f --- /dev/null +++ b/examples/changelog/current/classes/NewClass.cls @@ -0,0 +1,5 @@ +public class NewClass { + public void newMethod() { + System.debug('Hello workd!'); + } +} diff --git a/examples/changelog/package.json b/examples/changelog/package.json new file mode 100644 index 00000000..ed53a483 --- /dev/null +++ b/examples/changelog/package.json @@ -0,0 +1,19 @@ +{ + "name": "changelog-example", + "scripts": { + "docs:clean": "rimraf docs", + "docs:build": "ts-node ../../src/cli/generate.ts changelog", + "docs:help": "ts-node ../../src/cli/generate.ts changelog --help", + "docs:gen": "npm run docs:clean && npm run docs:build" + }, + "devDependencies": { + "ts-node": "^10.9.2" + }, + "dependencies": { + "rimraf": "^5.0.7" + }, + "apexdocs": { + "previousVersionDir": "previous", + "currentVersionDir": "current" + } +} diff --git a/examples/changelog/previous/.gitkeep b/examples/changelog/previous/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index 4af5e2cd..6902bc0c 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -1,14 +1,22 @@ -import markdown from './generators/markdown'; +import markdown, { FileWritingError } from './generators/markdown'; import openApi from './generators/openapi'; +import changelog from './generators/changelog'; import { ApexFileReader } from './apex-file-reader'; import { DefaultFileSystem } from './file-system'; import { Logger } from '#utils/logger'; -import { UnparsedSourceFile, UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; +import { + UnparsedSourceFile, + UserDefinedChangelogConfig, + UserDefinedConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../core/shared/types'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; -import { ReflectionError } from '../core/markdown/reflection/reflect-source'; +import { ReflectionError, ReflectionErrors } from '../core/markdown/reflection/reflect-source'; +import { HookError } from '../core/markdown/generate-docs'; /** * Application entry-point to generate documentation out of Apex source files. @@ -21,19 +29,14 @@ export class Apexdocs { logger.logSingle(`Generating ${config.targetGenerator} documentation...`); try { - const fileBodies = await ApexFileReader.processFiles( - new DefaultFileSystem(), - config.sourceDir, - config.targetGenerator === 'markdown' ? config.includeMetadata : false, - config.exclude, - ); - switch (config.targetGenerator) { case 'markdown': - return await generateMarkdownDocumentation(fileBodies, config)(); + return await processMarkdown(config); case 'openapi': - await openApi(logger, fileBodies, config); + await processOpenApi(config, logger); return E.right('✔️ Documentation generated successfully!'); + case 'changelog': + return await processChangeLog(config); } } catch (error) { return E.left([error]); @@ -41,12 +44,31 @@ export class Apexdocs { } } -function generateMarkdownDocumentation( - fileBodies: UnparsedSourceFile[], - config: UserDefinedMarkdownConfig, +async function processMarkdown(config: UserDefinedMarkdownConfig) { + const fileBodies = await ApexFileReader.processFiles( + new DefaultFileSystem(), + config.sourceDir, + config.includeMetadata, + config.exclude, + ); + return generateMarkdownDocumentation(fileBodies, config)(); +} + +async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) { + const fileBodies = await ApexFileReader.processFiles( + new DefaultFileSystem(), + config.sourceDir, + false, + config.exclude, + ); + return openApi(logger, fileBodies, config); +} + +function generateDocs( + fn: () => TE.TaskEither, ): TE.TaskEither { return pipe( - markdown(fileBodies, config), + fn(), TE.map(() => '✔️ Documentation generated successfully!'), TE.mapLeft((error) => { if (error._tag === 'HookError') { @@ -65,6 +87,31 @@ function generateMarkdownDocumentation( ); } +function generateMarkdownDocumentation( + fileBodies: UnparsedSourceFile[], + config: UserDefinedMarkdownConfig, +): TE.TaskEither { + return generateDocs(() => markdown(fileBodies, config)); +} + +async function processChangeLog(config: UserDefinedChangelogConfig) { + const previousVersionFiles = await ApexFileReader.processFiles( + new DefaultFileSystem(), + config.previousVersionDir, + false, + [], + ); + + const currentVersionFiles = await ApexFileReader.processFiles( + new DefaultFileSystem(), + config.currentVersionDir, + false, + [], + ); + + return generateDocs(() => changelog(previousVersionFiles, currentVersionFiles, config))(); +} + function formatReflectionError(error: ReflectionError) { return `Source file: ${error.file}\n${error.message}\n`; } diff --git a/src/application/generators/changelog.ts b/src/application/generators/changelog.ts new file mode 100644 index 00000000..193d4085 --- /dev/null +++ b/src/application/generators/changelog.ts @@ -0,0 +1,36 @@ +import { pipe } from 'fp-ts/function'; +import { PageData, UnparsedSourceFile, UserDefinedChangelogConfig } from '../../core/shared/types'; +import * as TE from 'fp-ts/TaskEither'; +import { writeFiles } from '../file-writer'; +import { ChangeLogPageData, generateChangeLog } from '../../core/changelog/generate-change-log'; + +// TODO: Share this error with the one that already exists in markdown.ts +class FileWritingError { + readonly _tag = 'FileWritingError'; + + constructor( + public message: string, + public error: unknown, + ) {} +} + +export default function generate( + oldBundles: UnparsedSourceFile[], + newBundles: UnparsedSourceFile[], + config: UserDefinedChangelogConfig, +) { + return pipe( + generateChangeLog(oldBundles, newBundles, config), + TE.flatMap((files) => writeFilesToSystem(files, config.targetDir)), + ); +} + +function writeFilesToSystem(pageData: ChangeLogPageData, outputDir: string) { + return pipe( + [pageData], + (files) => writeFiles(files as PageData[], outputDir), + TE.mapLeft((error) => { + return new FileWritingError('An error occurred while writing files to the system.', error); + }), + ); +} diff --git a/src/application/generators/markdown.ts b/src/application/generators/markdown.ts index b7c59667..12283474 100644 --- a/src/application/generators/markdown.ts +++ b/src/application/generators/markdown.ts @@ -11,7 +11,7 @@ import * as TE from 'fp-ts/TaskEither'; import { isSkip } from '../../core/shared/utils'; import { writeFiles } from '../file-writer'; -class FileWritingError { +export class FileWritingError { readonly _tag = 'FileWritingError'; constructor( diff --git a/src/cli/args.ts b/src/cli/args.ts index b055ab55..495ea763 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,9 +1,10 @@ import { cosmiconfig, CosmiconfigResult } from 'cosmiconfig'; import * as yargs from 'yargs'; -import { UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; +import { Generators, UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; import { TypeScriptLoader } from 'cosmiconfig-typescript-loader'; import { markdownOptions } from './commands/markdown'; import { openApiOptions } from './commands/openapi'; +import { changeLogOptions } from './commands/changelog'; const configOnlyMarkdownDefaults: Partial = { excludeTags: [], @@ -39,6 +40,9 @@ function _extractYargs(config?: CosmiconfigResult) { .command('openapi', 'Generate an OpenApi REST specification from Apex classes.', () => yargs.options(openApiOptions), ) + .command('changelog', 'Generate a changelog from 2 versions of the source code.', () => + yargs.options(changeLogOptions), + ) .demandCommand() .parseSync(); } @@ -51,10 +55,16 @@ export async function extractArgs(): Promise { const cliArgs = _extractYargs(config); const commandName = cliArgs._[0]; - const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as 'markdown' | 'openapi' }; + const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; + // TODO: Use switch statement instead of if-else if (mergedConfig.targetGenerator === 'markdown') { return { ...configOnlyMarkdownDefaults, ...mergedConfig }; - } else { + } else if (mergedConfig.targetGenerator === 'openapi') { return { ...configOnlyOpenApiDefaults, ...mergedConfig }; + } else if (mergedConfig.targetGenerator === 'changelog') { + return mergedConfig; + } else { + // TODO: Handle the changelog case + throw new Error(`Unknown command: ${commandName}`); } } diff --git a/src/cli/commands/changelog.ts b/src/cli/commands/changelog.ts new file mode 100644 index 00000000..61a7a555 --- /dev/null +++ b/src/cli/commands/changelog.ts @@ -0,0 +1,28 @@ +import { Options } from 'yargs'; +import { changeLogDefaults } from '../../defaults'; + +export const changeLogOptions: { [key: string]: Options } = { + previousVersionDir: { + type: 'string', + alias: 'p', + demandOption: true, + describe: 'The directory location of the previous version of the source code.', + }, + currentVersionDir: { + type: 'string', + alias: 'c', + demandOption: true, + describe: 'The directory location of the current version of the source code.', + }, + targetDir: { + type: 'string', + alias: 't', + default: changeLogDefaults.targetDir, + describe: 'The directory location where the changelog file will be generated.', + }, + fileName: { + type: 'string', + default: changeLogDefaults.fileName, + describe: 'The name of the changelog file to be generated.', + }, +}; diff --git a/src/cli/generate.ts b/src/cli/generate.ts index c13606e6..16083f5d 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -10,7 +10,7 @@ function main() { function parseResult(result: E.Either) { E.match( (error) => { - logger.error(`❌ An error occurred while generating the documentation: ${error}`); + logger.error(`❌ An error occurred while processing the request: ${error}`); process.exit(1); }, (successMessage: string) => { diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 9b43363d..929e72d2 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -1,4 +1,4 @@ -import { ParsedFile, UnparsedSourceFile } from '../shared/types'; +import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../shared/types'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; // TODO: Move the reflection code to outside of the markdown folder since now it is shared with this @@ -9,14 +9,10 @@ import { convertToRenderableChangeLog } from './renderable-change-log'; import { CompilationRequest, Template } from '../markdown/templates/template'; import { changeLogTemplate } from './templates/change-log-template'; -type ChangeLogPageData = { +export type ChangeLogPageData = { // TODO: This should also support frontmatter (and the hook to add it) content: string; - filePath: string; -}; - -type ChangeLogConfig = { - targetDir: string; + outputDocPath: string; }; // TODO: We should provide the ability to filter out of scope if we are going @@ -24,7 +20,7 @@ type ChangeLogConfig = { export function generateChangeLog( oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[], - config: ChangeLogConfig, + config: UserDefinedChangelogConfig, ): TE.TaskEither { return pipe( reflectBundles(oldBundles), @@ -51,9 +47,7 @@ export function generateChangeLog( }), TE.map((content) => ({ content, - // TODO: There should be a way that someone can change the name of the output file - // TODO: Do not concantenate the strings, but instead use the path module - filePath: `${config.targetDir}/changelog.md`, + outputDocPath: `${config.fileName}.md`, })), ); } diff --git a/src/core/shared/types.d.ts b/src/core/shared/types.d.ts index 3af57b9e..785284a1 100644 --- a/src/core/shared/types.d.ts +++ b/src/core/shared/types.d.ts @@ -1,4 +1,7 @@ import { Type } from '@cparra/apex-reflection'; +import { ChangeLogPageData } from '../changelog/generate-change-log'; + +export type Generators = 'markdown' | 'openapi' | 'changelog'; type LinkingStrategy = // Links will be generated using relative paths. @@ -37,7 +40,15 @@ export type UserDefinedOpenApiConfig = { exclude: string[]; }; -export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig; +export type UserDefinedChangelogConfig = { + targetGenerator: 'changelog'; + previousVersionDir: string; + currentVersionDir: string; + targetDir: string; + fileName: string; +}; + +export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig | UserDefinedChangelogConfig; export type UnparsedSourceFile = { filePath: string; @@ -88,7 +99,7 @@ export type DocPageData = { export type OpenApiPageData = Omit; -export type PageData = DocPageData | OpenApiPageData | ReferenceGuidePageData; +export type PageData = DocPageData | OpenApiPageData | ReferenceGuidePageData | ChangeLogPageData; export type DocumentationBundle = { referenceGuide: ReferenceGuidePageData; diff --git a/src/defaults.ts b/src/defaults.ts index 9ff35677..fa01bbfc 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -19,3 +19,8 @@ export const openApiDefaults = { title: 'Apex REST API', apiVersion: '1.0.0', }; + +export const changeLogDefaults = { + ...commonDefaults, + fileName: 'changelog', +}; diff --git a/src/index.ts b/src/index.ts index 5387d317..ebd3f688 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,8 @@ function skip(): Skip { }; } +// TODO: It should be possible to generate the changelog imperatively + export { defineMarkdownConfig, ConfigurableMarkdownConfig, diff --git a/src/node/process.ts b/src/node/process.ts index f345a612..1de45424 100644 --- a/src/node/process.ts +++ b/src/node/process.ts @@ -1,10 +1,10 @@ -import type { UserDefinedConfig } from '../core/shared/types'; +import type { Generators, UserDefinedConfig } from '../core/shared/types'; import { NoLogger } from '#utils/logger'; import { Apexdocs } from '../application/Apexdocs'; import * as E from 'fp-ts/Either'; import { markdownDefaults, openApiDefaults } from '../defaults'; -type CallableConfig = Partial & { sourceDir: string; targetGenerator: 'markdown' | 'openapi' }; +type CallableConfig = Partial & { sourceDir: string; targetGenerator: Generators }; /** * Processes the documentation generation, similar to the main function in the CLI. @@ -37,6 +37,7 @@ function getDefault(config: CallableConfig) { case 'openapi': return openApiDefaults; default: + // TODO: handle changelog throw new Error('Unknown target generator'); } } From b0f24f3a6932ba5a73c90807de2b13bcc8f05610 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Mon, 23 Sep 2024 18:39:43 -0400 Subject: [PATCH 24/61] Introducing the changelog command --- src/application/Apexdocs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index 6902bc0c..4430810b 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -69,7 +69,7 @@ function generateDocs( ): TE.TaskEither { return pipe( fn(), - TE.map(() => '✔️ Documentation generated successfully!'), + TE.map(() => '✔️ Documentation generated successfully!'), // TODO: Different success message for changelog TE.mapLeft((error) => { if (error._tag === 'HookError') { return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; From a1ee1afee43398d7c355a5c43da165b35e760728 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 08:25:41 -0400 Subject: [PATCH 25/61] Introducing the changelog command --- examples/changelog/docs/changelog.md | 5 + examples/changelog/package-lock.json | 724 ++++++++++++++++++ examples/changelog/sfdx-project.json | 12 + .../docs/.vitepress/cache/deps/_metadata.json | 14 +- .../__test__/generating-change-log.spec.ts | 13 +- src/core/changelog/generate-change-log.ts | 2 +- 6 files changed, 759 insertions(+), 11 deletions(-) create mode 100644 examples/changelog/docs/changelog.md create mode 100644 examples/changelog/package-lock.json create mode 100644 examples/changelog/sfdx-project.json diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md new file mode 100644 index 00000000..613e8b79 --- /dev/null +++ b/examples/changelog/docs/changelog.md @@ -0,0 +1,5 @@ +# Change Log + +## New Classes + +These classes are new. \ No newline at end of file diff --git a/examples/changelog/package-lock.json b/examples/changelog/package-lock.json new file mode 100644 index 00000000..9b36fb9c --- /dev/null +++ b/examples/changelog/package-lock.json @@ -0,0 +1,724 @@ +{ + "name": "changelog-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "changelog-example", + "dependencies": { + "rimraf": "^5.0.7" + }, + "devDependencies": { + "ts-node": "^10.9.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/examples/changelog/sfdx-project.json b/examples/changelog/sfdx-project.json new file mode 100644 index 00000000..98f63012 --- /dev/null +++ b/examples/changelog/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "changelog", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "61.0" +} diff --git a/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json b/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json index a27bf72b..7bee2621 100644 --- a/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json +++ b/examples/vitepress/docs/.vitepress/cache/deps/_metadata.json @@ -1,31 +1,31 @@ { - "hash": "4a05af45", + "hash": "98853dc2", "configHash": "7f7b0dad", - "lockfileHash": "8018888b", - "browserHash": "73d477e4", + "lockfileHash": "2250ed1c", + "browserHash": "0b73d2e4", "optimized": { "vue": { "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "d22fde12", + "fileHash": "dd9e77dd", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../../node_modules/@vue/devtools-api/dist/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "5478d716", + "fileHash": "522b9fac", "needsInterop": false }, "vitepress > @vueuse/core": { "src": "../../../../node_modules/@vueuse/core/index.mjs", "file": "vitepress___@vueuse_core.js", - "fileHash": "23bf9c76", + "fileHash": "5b6312b7", "needsInterop": false }, "@theme/index": { "src": "../../../../node_modules/vitepress/dist/client/theme-default/index.js", "file": "@theme_index.js", - "fileHash": "07dcd573", + "fileHash": "8adc2f2d", "needsInterop": false } }, diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 5e853f3e..0919f220 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -3,15 +3,22 @@ import { generateChangeLog } from '../generate-change-log'; // TODO: Move the assert either function to a shared place so we are not importing it from markdown import { assertEither } from '../../markdown/__test__/expect-extensions'; +const config = { + fileName: 'changelog', + targetDir: '', + currentVersionDir: '', + previousVersionDir: '', +}; + describe('when generating a change log', () => { describe('that does not include new classes', () => { it('should not have a section for new classes', async () => { const oldBundle: UnparsedSourceFile[] = []; const newBundle: UnparsedSourceFile[] = []; - const result = await generateChangeLog(oldBundle, newBundle, { targetDir: '' })(); + const result = await generateChangeLog(oldBundle, newBundle, config)(); - assertEither(result, (data) => expect(data.filePath).toContain('changelog.md')); // TODO: This assertion should have its own test + assertEither(result, (data) => expect(data.outputDocPath).toContain('changelog.md')); // TODO: This assertion should have its own test assertEither(result, (data) => expect(data.content).not.toContain('## New Classes')); }); }); @@ -25,7 +32,7 @@ describe('when generating a change log', () => { { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, ]; - const result = await generateChangeLog(oldBundle, newBundle, { targetDir: '' })(); + const result = await generateChangeLog(oldBundle, newBundle, config)(); assertEither(result, (data) => expect(data.content).toContain('## New Classes')); }); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 929e72d2..c2819e61 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -20,7 +20,7 @@ export type ChangeLogPageData = { export function generateChangeLog( oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[], - config: UserDefinedChangelogConfig, + config: Omit, ): TE.TaskEither { return pipe( reflectBundles(oldBundles), From 94fe5254e0872f918ae5c4e91af0e9dca1c22275 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 15:48:18 -0400 Subject: [PATCH 26/61] Introducing the changelog command --- .../current/classes/AccountService.cls | 10 +++++ .../changelog/current/classes/NewClass.cls | 5 --- examples/changelog/docs/changelog.md | 8 +++- ...onverting-to-renderable-change-log.spec.ts | 2 +- .../__test__/generating-change-log.spec.ts | 2 +- .../__test__/processing-changelog.spec.ts | 2 +- src/core/changelog/generate-change-log.ts | 5 ++- src/core/changelog/renderable-change-log.ts | 42 +++++++++++++++---- .../templates/change-log-template.ts | 9 +++- src/core/markdown/adapters/documentables.ts | 1 + 10 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 examples/changelog/current/classes/AccountService.cls delete mode 100644 examples/changelog/current/classes/NewClass.cls diff --git a/examples/changelog/current/classes/AccountService.cls b/examples/changelog/current/classes/AccountService.cls new file mode 100644 index 00000000..eda26bc2 --- /dev/null +++ b/examples/changelog/current/classes/AccountService.cls @@ -0,0 +1,10 @@ +// This class is new + +/** + * @description This is a new class that does foo and bar and references {@link Baz}. + */ +public class AccountService { + public void newMethod() { + System.debug('Hello workd!'); + } +} diff --git a/examples/changelog/current/classes/NewClass.cls b/examples/changelog/current/classes/NewClass.cls deleted file mode 100644 index 9e18815f..00000000 --- a/examples/changelog/current/classes/NewClass.cls +++ /dev/null @@ -1,5 +0,0 @@ -public class NewClass { - public void newMethod() { - System.debug('Hello workd!'); - } -} diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index 613e8b79..6e6c4b54 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -1,5 +1,9 @@ -# Change Log +# Changelog ## New Classes -These classes are new. \ No newline at end of file +These classes are new. + +### AccountService + +This is a new class that does foo and bar and references Baz . \ No newline at end of file diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts index b58c721e..2e51afd5 100644 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -2,7 +2,7 @@ import { ChangeLog } from '../process-change-log'; import { convertToRenderableChangeLog } from '../renderable-change-log'; import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; -describe('when converting a change log to a renderable change log', () => { +describe('when converting a changelog to a renderable changelog', () => { it('does not include the New Classes section if there are none', () => { const changeLog: ChangeLog = { newTypes: [], diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 0919f220..08914848 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -10,7 +10,7 @@ const config = { previousVersionDir: '', }; -describe('when generating a change log', () => { +describe('when generating a changelog', () => { describe('that does not include new classes', () => { it('should not have a section for new classes', async () => { const oldBundle: UnparsedSourceFile[] = []; diff --git a/src/core/changelog/__test__/processing-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts index 434e1874..0c4a1d04 100644 --- a/src/core/changelog/__test__/processing-changelog.spec.ts +++ b/src/core/changelog/__test__/processing-changelog.spec.ts @@ -10,7 +10,7 @@ function typeFromRawString(raw: string): Type { return result.typeMirror!; } -describe('when generating a change log', () => { +describe('when generating a changelog', () => { it('has no new types when both the old and new versions are empty', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index c2819e61..b09a971f 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -15,6 +15,9 @@ export type ChangeLogPageData = { outputDocPath: string; }; +// TODO: Also provide the ability to have a previously generated manifest as an input +// TODO: Also provide the ability to have 2 git refs as input + // TODO: We should provide the ability to filter out of scope if we are going // to be relying on source files and not on a previously generated manifest. export function generateChangeLog( @@ -41,8 +44,6 @@ export function generateChangeLog( source: renderable, }; - // TODO: At some point just having a string won't be enough, since we want an object - // that contains the target file name and directory as well as the content. return Template.getInstance().compile(compilationRequest); }), TE.map((content) => ({ diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index 8e5bfca6..05132f44 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -1,25 +1,53 @@ import { ChangeLog } from './process-change-log'; import { Type } from '@cparra/apex-reflection'; +import { RenderableContent } from '../markdown/adapters/types'; +import { adaptDescribable } from '../markdown/adapters/documentables'; + +type NewTypeRenderable = { + name: string; + description?: RenderableContent[]; +}; type NewTypeSection = { __type: T; heading: string; description: string; + types: NewTypeRenderable[]; }; type RenderableChangeLog = { newClasses: NewTypeSection<'class'> | null; }; -export function convertToRenderableChangeLog(changeLog: ChangeLog, newTypes: Type[]): RenderableChangeLog { - const hasNewClasses = - changeLog.newTypes.filter((newType) => newTypes.some((type) => type.name.toLowerCase() === newType.toLowerCase())) - .length > 0; +export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: Type[]): RenderableChangeLog { + const allNewTypes = changeLog.newTypes.map( + (newType) => newManifest.find((type) => type.name.toLowerCase() === newType.toLowerCase())!, + ); + + const newClasses = allNewTypes.filter((type) => type.type_name === 'class'); + + return { + newClasses: + newClasses.length > 0 + ? { + __type: 'class', + heading: 'New Classes', + description: 'These classes are new.', + types: newClasses.map(typeToRenderable), // TODO: Unit test this gets populated + } + : null, + }; +} + +function typeToRenderable(type: Type): NewTypeRenderable { + function adapt() { + const describable = adaptDescribable(type.docComment?.descriptionLines, (typeName) => typeName); + return describable.description; + } return { - newClasses: hasNewClasses - ? { __type: 'class', heading: 'New Classes', description: 'These classes are new.' } - : null, + name: type.name, + description: adapt(), // TODO: integration test that the content renders correctly }; } diff --git a/src/core/changelog/templates/change-log-template.ts b/src/core/changelog/templates/change-log-template.ts index a423f3ac..caaa37f8 100644 --- a/src/core/changelog/templates/change-log-template.ts +++ b/src/core/changelog/templates/change-log-template.ts @@ -1,9 +1,16 @@ export const changeLogTemplate = ` -# Change Log +# Changelog {{#if newClasses}} ## {{newClasses.heading}} {{newClasses.description}} + +{{#each newClasses.types}} +### {{this.name}} + +{{{renderContent this.description}}} +{{/each}} + {{/if}} `.trim(); diff --git a/src/core/markdown/adapters/documentables.ts b/src/core/markdown/adapters/documentables.ts index 6e03769f..c17bc79f 100644 --- a/src/core/markdown/adapters/documentables.ts +++ b/src/core/markdown/adapters/documentables.ts @@ -60,6 +60,7 @@ export function describableToRenderableContent( }, ]; } + return ( content // If the last element is an empty line, remove it From ed3872f571fc13e8315b7254d009f921b5cdbf90 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 16:15:28 -0400 Subject: [PATCH 27/61] refactorings --- src/application/Apexdocs.ts | 10 +++++----- ...converting-to-renderable-change-log.spec.ts | 6 +----- src/core/changelog/generate-change-log.ts | 3 ++- src/core/errors/errors.ts | 18 ++++++++++++++++++ src/core/markdown/generate-docs.ts | 7 +------ src/core/markdown/reflection/reflect-source.ts | 14 +------------- 6 files changed, 28 insertions(+), 30 deletions(-) create mode 100644 src/core/errors/errors.ts diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index 4430810b..c7cc8204 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -1,3 +1,7 @@ +import { pipe } from 'fp-ts/function'; +import * as TE from 'fp-ts/TaskEither'; +import * as E from 'fp-ts/Either'; + import markdown, { FileWritingError } from './generators/markdown'; import openApi from './generators/openapi'; import changelog from './generators/changelog'; @@ -12,11 +16,7 @@ import { UserDefinedMarkdownConfig, UserDefinedOpenApiConfig, } from '../core/shared/types'; -import { pipe } from 'fp-ts/function'; -import * as TE from 'fp-ts/TaskEither'; -import * as E from 'fp-ts/Either'; -import { ReflectionError, ReflectionErrors } from '../core/markdown/reflection/reflect-source'; -import { HookError } from '../core/markdown/generate-docs'; +import { ReflectionError, ReflectionErrors, HookError } from '../core/errors/errors'; /** * Application entry-point to generate documentation out of Apex source files. diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts index 2e51afd5..d28a4224 100644 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -25,10 +25,6 @@ describe('when converting a changelog to a renderable changelog', () => { const renderable = convertToRenderableChangeLog(changeLog, newClasses); - expect(renderable.newClasses).toEqual({ - __type: 'class', - heading: 'New Classes', - description: 'These classes are new.', - }); + expect(renderable.newClasses).not.toBeNull(); }); }); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index b09a971f..d9589965 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -2,12 +2,13 @@ import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../s import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; // TODO: Move the reflection code to outside of the markdown folder since now it is shared with this -import { reflectBundles, ReflectionErrors } from '../markdown/reflection/reflect-source'; +import { reflectBundles } from '../markdown/reflection/reflect-source'; import { processChangeLog, VersionManifest } from './process-change-log'; import { convertToRenderableChangeLog } from './renderable-change-log'; // TODO: Also move this file since this is now shared import { CompilationRequest, Template } from '../markdown/templates/template'; import { changeLogTemplate } from './templates/change-log-template'; +import { ReflectionErrors } from '../errors/errors'; export type ChangeLogPageData = { // TODO: This should also support frontmatter (and the hook to add it) diff --git a/src/core/errors/errors.ts b/src/core/errors/errors.ts new file mode 100644 index 00000000..d7ccb794 --- /dev/null +++ b/src/core/errors/errors.ts @@ -0,0 +1,18 @@ +export class ReflectionError { + constructor( + public file: string, + public message: string, + ) {} +} + +export class ReflectionErrors { + readonly _tag = 'ReflectionErrors'; + + constructor(public errors: ReflectionError[]) {} +} + +export class HookError { + readonly _tag = 'HookError'; + + constructor(public error: unknown) {} +} diff --git a/src/core/markdown/generate-docs.ts b/src/core/markdown/generate-docs.ts index d397c8d0..c872f744 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -30,6 +30,7 @@ import { sortTypesAndMembers } from './reflection/sort-types-and-members'; import { isSkip } from '../shared/utils'; import { parsedFilesToReferenceGuide } from './adapters/reference-guide'; import { removeExcludedTags } from './reflection/remove-excluded-tags'; +import { HookError } from '../errors/errors'; export type MarkdownGeneratorConfig = Omit< UserDefinedMarkdownConfig, @@ -38,12 +39,6 @@ export type MarkdownGeneratorConfig = Omit< referenceGuideTemplate: string; }; -export class HookError { - readonly _tag = 'HookError'; - - constructor(public error: unknown) {} -} - export function generateDocs(apexBundles: UnparsedSourceFile[], config: MarkdownGeneratorConfig) { const filterOutOfScope = apply(filterScope, config.scope); const convertToReferences = apply(parsedFilesToReferenceGuide, config); diff --git a/src/core/markdown/reflection/reflect-source.ts b/src/core/markdown/reflection/reflect-source.ts index e515c3b9..e1679283 100644 --- a/src/core/markdown/reflection/reflect-source.ts +++ b/src/core/markdown/reflection/reflect-source.ts @@ -10,19 +10,7 @@ import { parseApexMetadata } from '../../parse-apex-metadata'; import { ParsingError } from '@cparra/apex-reflection'; import { apply } from '#utils/fp'; import { Semigroup } from 'fp-ts/Semigroup'; - -export class ReflectionErrors { - readonly _tag = 'ReflectionErrors'; - - constructor(public errors: ReflectionError[]) {} -} - -export class ReflectionError { - constructor( - public file: string, - public message: string, - ) {} -} +import { ReflectionError, ReflectionErrors } from '../../errors/errors'; async function reflectAsync(rawSource: string): Promise { return new Promise((resolve, reject) => { From 10a4e1ee544fc5e3f2407eea2e725d144d46dce3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 16:28:38 -0400 Subject: [PATCH 28/61] refactorings --- src/application/Apexdocs.ts | 66 +++++++++++-------------- src/application/errors.ts | 8 +++ src/application/generators/changelog.ts | 11 +---- src/application/generators/markdown.ts | 10 +--- 4 files changed, 40 insertions(+), 55 deletions(-) create mode 100644 src/application/errors.ts diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index c7cc8204..23025cad 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -2,7 +2,7 @@ import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; -import markdown, { FileWritingError } from './generators/markdown'; +import markdown from './generators/markdown'; import openApi from './generators/openapi'; import changelog from './generators/changelog'; @@ -10,13 +10,13 @@ import { ApexFileReader } from './apex-file-reader'; import { DefaultFileSystem } from './file-system'; import { Logger } from '#utils/logger'; import { - UnparsedSourceFile, UserDefinedChangelogConfig, UserDefinedConfig, UserDefinedMarkdownConfig, UserDefinedOpenApiConfig, } from '../core/shared/types'; import { ReflectionError, ReflectionErrors, HookError } from '../core/errors/errors'; +import { FileWritingError } from './errors'; /** * Application entry-point to generate documentation out of Apex source files. @@ -31,12 +31,12 @@ export class Apexdocs { try { switch (config.targetGenerator) { case 'markdown': - return await processMarkdown(config); + return (await processMarkdown(config))(); case 'openapi': await processOpenApi(config, logger); return E.right('✔️ Documentation generated successfully!'); case 'changelog': - return await processChangeLog(config); + return (await processChangeLog(config))(); } } catch (error) { return E.left([error]); @@ -51,7 +51,12 @@ async function processMarkdown(config: UserDefinedMarkdownConfig) { config.includeMetadata, config.exclude, ); - return generateMarkdownDocumentation(fileBodies, config)(); + + return pipe( + markdown(fileBodies, config), + TE.map(() => '✔️ Documentation generated successfully!'), + TE.mapLeft(toErrors), + ); } async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) { @@ -64,36 +69,6 @@ async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) return openApi(logger, fileBodies, config); } -function generateDocs( - fn: () => TE.TaskEither, -): TE.TaskEither { - return pipe( - fn(), - TE.map(() => '✔️ Documentation generated successfully!'), // TODO: Different success message for changelog - TE.mapLeft((error) => { - if (error._tag === 'HookError') { - return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; - } - - if (error._tag === 'FileWritingError') { - return ['Error(s) occurred while writing files. Please review the following issues:', error.error]; - } - - return [ - 'Error(s) occurred while parsing files. Please review the following issues:', - ...error.errors.map(formatReflectionError), - ]; - }), - ); -} - -function generateMarkdownDocumentation( - fileBodies: UnparsedSourceFile[], - config: UserDefinedMarkdownConfig, -): TE.TaskEither { - return generateDocs(() => markdown(fileBodies, config)); -} - async function processChangeLog(config: UserDefinedChangelogConfig) { const previousVersionFiles = await ApexFileReader.processFiles( new DefaultFileSystem(), @@ -109,7 +84,26 @@ async function processChangeLog(config: UserDefinedChangelogConfig) { [], ); - return generateDocs(() => changelog(previousVersionFiles, currentVersionFiles, config))(); + return pipe( + changelog(previousVersionFiles, currentVersionFiles, config), + TE.map(() => '✔️ Changelog generated successfully!'), + TE.mapLeft(toErrors), + ); +} + +function toErrors(error: ReflectionErrors | HookError | FileWritingError): unknown[] { + if (error._tag === 'HookError') { + return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; + } + + if (error._tag === 'FileWritingError') { + return ['Error(s) occurred while writing files. Please review the following issues:', error.error]; + } + + return [ + 'Error(s) occurred while parsing files. Please review the following issues:', + ...error.errors.map(formatReflectionError), + ]; } function formatReflectionError(error: ReflectionError) { diff --git a/src/application/errors.ts b/src/application/errors.ts new file mode 100644 index 00000000..d94c46f6 --- /dev/null +++ b/src/application/errors.ts @@ -0,0 +1,8 @@ +export class FileWritingError { + readonly _tag = 'FileWritingError'; + + constructor( + public message: string, + public error: unknown, + ) {} +} diff --git a/src/application/generators/changelog.ts b/src/application/generators/changelog.ts index 193d4085..0a245407 100644 --- a/src/application/generators/changelog.ts +++ b/src/application/generators/changelog.ts @@ -3,16 +3,7 @@ import { PageData, UnparsedSourceFile, UserDefinedChangelogConfig } from '../../ import * as TE from 'fp-ts/TaskEither'; import { writeFiles } from '../file-writer'; import { ChangeLogPageData, generateChangeLog } from '../../core/changelog/generate-change-log'; - -// TODO: Share this error with the one that already exists in markdown.ts -class FileWritingError { - readonly _tag = 'FileWritingError'; - - constructor( - public message: string, - public error: unknown, - ) {} -} +import { FileWritingError } from '../errors'; export default function generate( oldBundles: UnparsedSourceFile[], diff --git a/src/application/generators/markdown.ts b/src/application/generators/markdown.ts index 12283474..50fa766a 100644 --- a/src/application/generators/markdown.ts +++ b/src/application/generators/markdown.ts @@ -10,15 +10,7 @@ import { referenceGuideTemplate } from '../../core/markdown/templates/reference- import * as TE from 'fp-ts/TaskEither'; import { isSkip } from '../../core/shared/utils'; import { writeFiles } from '../file-writer'; - -export class FileWritingError { - readonly _tag = 'FileWritingError'; - - constructor( - public message: string, - public error: unknown, - ) {} -} +import { FileWritingError } from '../errors'; export default function generate(bundles: UnparsedSourceFile[], config: UserDefinedMarkdownConfig) { return pipe( From 46bf95072f91ad45b1b84147aec2d569a98ca252 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 16:35:21 -0400 Subject: [PATCH 29/61] refactorings --- src/cli/args.ts | 20 +++++++++---------- .../__test__/generating-change-log.spec.ts | 10 +++++++--- .../markdown/__test__/expect-extensions.ts | 7 ------- .../__test__/generating-class-docs.spec.ts | 3 ++- .../markdown/__test__/generating-docs.spec.ts | 3 ++- .../__test__/generating-enum-docs.spec.ts | 3 ++- .../generating-interface-docs.spec.ts | 3 ++- .../generating-reference-guide.spec.ts | 3 ++- src/core/test-helpers/assert-either.ts | 8 ++++++++ 9 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 src/core/test-helpers/assert-either.ts diff --git a/src/cli/args.ts b/src/cli/args.ts index 495ea763..e620628e 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -56,15 +56,15 @@ export async function extractArgs(): Promise { const commandName = cliArgs._[0]; const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; - // TODO: Use switch statement instead of if-else - if (mergedConfig.targetGenerator === 'markdown') { - return { ...configOnlyMarkdownDefaults, ...mergedConfig }; - } else if (mergedConfig.targetGenerator === 'openapi') { - return { ...configOnlyOpenApiDefaults, ...mergedConfig }; - } else if (mergedConfig.targetGenerator === 'changelog') { - return mergedConfig; - } else { - // TODO: Handle the changelog case - throw new Error(`Unknown command: ${commandName}`); + + switch (mergedConfig.targetGenerator) { + case 'markdown': + return { ...configOnlyMarkdownDefaults, ...mergedConfig }; + case 'openapi': + return { ...configOnlyOpenApiDefaults, ...mergedConfig }; + case 'changelog': + return mergedConfig; + default: + throw new Error(`Unknown command: ${commandName}`); } } diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 08914848..f736a749 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -1,7 +1,6 @@ import { UnparsedSourceFile } from '../../shared/types'; import { generateChangeLog } from '../generate-change-log'; -// TODO: Move the assert either function to a shared place so we are not importing it from markdown -import { assertEither } from '../../markdown/__test__/expect-extensions'; +import { assertEither } from '../../test-helpers/assert-either'; const config = { fileName: 'changelog', @@ -11,6 +10,12 @@ const config = { }; describe('when generating a changelog', () => { + it('should return a file path', async () => { + const result = await generateChangeLog([], [], config)(); + + assertEither(result, (data) => expect(data.outputDocPath).toContain('changelog.md')); + }); + describe('that does not include new classes', () => { it('should not have a section for new classes', async () => { const oldBundle: UnparsedSourceFile[] = []; @@ -18,7 +23,6 @@ describe('when generating a changelog', () => { const result = await generateChangeLog(oldBundle, newBundle, config)(); - assertEither(result, (data) => expect(data.outputDocPath).toContain('changelog.md')); // TODO: This assertion should have its own test assertEither(result, (data) => expect(data.content).not.toContain('## New Classes')); }); }); diff --git a/src/core/markdown/__test__/expect-extensions.ts b/src/core/markdown/__test__/expect-extensions.ts index bc568c9a..90a2540b 100644 --- a/src/core/markdown/__test__/expect-extensions.ts +++ b/src/core/markdown/__test__/expect-extensions.ts @@ -23,10 +23,3 @@ export function extendExpect() { }, }); } - -export function assertEither(result: E.Either, assertion: (data: U) => void): void { - E.match( - (error) => fail(error), - (data) => assertion(data), - )(result); -} diff --git a/src/core/markdown/__test__/generating-class-docs.spec.ts b/src/core/markdown/__test__/generating-class-docs.spec.ts index 5958917f..76d8a023 100644 --- a/src/core/markdown/__test__/generating-class-docs.spec.ts +++ b/src/core/markdown/__test__/generating-class-docs.spec.ts @@ -1,5 +1,6 @@ -import { assertEither, extendExpect } from './expect-extensions'; +import { extendExpect } from './expect-extensions'; import { apexBundleFromRawString, generateDocs } from './test-helpers'; +import { assertEither } from '../../test-helpers/assert-either'; describe('When generating documentation for a class', () => { beforeAll(() => { diff --git a/src/core/markdown/__test__/generating-docs.spec.ts b/src/core/markdown/__test__/generating-docs.spec.ts index df4b3455..769300f4 100644 --- a/src/core/markdown/__test__/generating-docs.spec.ts +++ b/src/core/markdown/__test__/generating-docs.spec.ts @@ -1,6 +1,7 @@ import { DocPageData, PostHookDocumentationBundle } from '../../shared/types'; -import { assertEither, extendExpect } from './expect-extensions'; +import { extendExpect } from './expect-extensions'; import { apexBundleFromRawString, generateDocs } from './test-helpers'; +import { assertEither } from '../../test-helpers/assert-either'; function aSingleDoc(result: PostHookDocumentationBundle): DocPageData { expect(result.docs).toHaveLength(1); diff --git a/src/core/markdown/__test__/generating-enum-docs.spec.ts b/src/core/markdown/__test__/generating-enum-docs.spec.ts index 0bdc5c3b..f0f7aa67 100644 --- a/src/core/markdown/__test__/generating-enum-docs.spec.ts +++ b/src/core/markdown/__test__/generating-enum-docs.spec.ts @@ -1,5 +1,6 @@ -import { assertEither, extendExpect } from './expect-extensions'; +import { extendExpect } from './expect-extensions'; import { apexBundleFromRawString, generateDocs } from './test-helpers'; +import { assertEither } from '../../test-helpers/assert-either'; describe('Generates enum documentation', () => { beforeAll(() => { diff --git a/src/core/markdown/__test__/generating-interface-docs.spec.ts b/src/core/markdown/__test__/generating-interface-docs.spec.ts index 9949df9c..c6d7e964 100644 --- a/src/core/markdown/__test__/generating-interface-docs.spec.ts +++ b/src/core/markdown/__test__/generating-interface-docs.spec.ts @@ -1,5 +1,6 @@ -import { assertEither, extendExpect } from './expect-extensions'; +import { extendExpect } from './expect-extensions'; import { apexBundleFromRawString, generateDocs } from './test-helpers'; +import { assertEither } from '../../test-helpers/assert-either'; describe('Generates interface documentation', () => { beforeAll(() => { diff --git a/src/core/markdown/__test__/generating-reference-guide.spec.ts b/src/core/markdown/__test__/generating-reference-guide.spec.ts index e4d5d819..ad64980c 100644 --- a/src/core/markdown/__test__/generating-reference-guide.spec.ts +++ b/src/core/markdown/__test__/generating-reference-guide.spec.ts @@ -1,8 +1,9 @@ -import { assertEither, extendExpect } from './expect-extensions'; +import { extendExpect } from './expect-extensions'; import { pipe } from 'fp-ts/function'; import * as E from 'fp-ts/Either'; import { apexBundleFromRawString, generateDocs } from './test-helpers'; import { ReferenceGuidePageData } from '../../shared/types'; +import { assertEither } from '../../test-helpers/assert-either'; describe('When generating the Reference Guide', () => { beforeAll(() => { diff --git a/src/core/test-helpers/assert-either.ts b/src/core/test-helpers/assert-either.ts new file mode 100644 index 00000000..8e2bd0b6 --- /dev/null +++ b/src/core/test-helpers/assert-either.ts @@ -0,0 +1,8 @@ +import * as E from 'fp-ts/Either'; + +export function assertEither(result: E.Either, assertion: (data: U) => void): void { + E.match( + (error) => fail(error), + (data) => assertion(data), + )(result); +} From 5146229635668b6eb00577b812fe0de323ff70e7 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 16:39:02 -0400 Subject: [PATCH 30/61] refactorings --- src/index.ts | 2 -- src/node/process.ts | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index ebd3f688..5387d317 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,8 +53,6 @@ function skip(): Skip { }; } -// TODO: It should be possible to generate the changelog imperatively - export { defineMarkdownConfig, ConfigurableMarkdownConfig, diff --git a/src/node/process.ts b/src/node/process.ts index 1de45424..006fdf5a 100644 --- a/src/node/process.ts +++ b/src/node/process.ts @@ -2,7 +2,7 @@ import type { Generators, UserDefinedConfig } from '../core/shared/types'; import { NoLogger } from '#utils/logger'; import { Apexdocs } from '../application/Apexdocs'; import * as E from 'fp-ts/Either'; -import { markdownDefaults, openApiDefaults } from '../defaults'; +import { changeLogDefaults, markdownDefaults, openApiDefaults } from '../defaults'; type CallableConfig = Partial & { sourceDir: string; targetGenerator: Generators }; @@ -36,8 +36,9 @@ function getDefault(config: CallableConfig) { return markdownDefaults; case 'openapi': return openApiDefaults; + case 'changelog': + return changeLogDefaults; default: - // TODO: handle changelog throw new Error('Unknown target generator'); } } From 669842385f275e1d453721f7931700f1edb442a9 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 18:49:14 -0400 Subject: [PATCH 31/61] refactorings --- src/application/Apexdocs.ts | 26 ++------ src/application/apex-file-reader.ts | 93 ++++++++++++++--------------- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index 23025cad..fe899b66 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -6,7 +6,7 @@ import markdown from './generators/markdown'; import openApi from './generators/openapi'; import changelog from './generators/changelog'; -import { ApexFileReader } from './apex-file-reader'; +import { processFiles } from './apex-file-reader'; import { DefaultFileSystem } from './file-system'; import { Logger } from '#utils/logger'; import { @@ -45,7 +45,7 @@ export class Apexdocs { } async function processMarkdown(config: UserDefinedMarkdownConfig) { - const fileBodies = await ApexFileReader.processFiles( + const fileBodies = await processFiles( new DefaultFileSystem(), config.sourceDir, config.includeMetadata, @@ -60,29 +60,13 @@ async function processMarkdown(config: UserDefinedMarkdownConfig) { } async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) { - const fileBodies = await ApexFileReader.processFiles( - new DefaultFileSystem(), - config.sourceDir, - false, - config.exclude, - ); + const fileBodies = await processFiles(new DefaultFileSystem(), config.sourceDir, false, config.exclude); return openApi(logger, fileBodies, config); } async function processChangeLog(config: UserDefinedChangelogConfig) { - const previousVersionFiles = await ApexFileReader.processFiles( - new DefaultFileSystem(), - config.previousVersionDir, - false, - [], - ); - - const currentVersionFiles = await ApexFileReader.processFiles( - new DefaultFileSystem(), - config.currentVersionDir, - false, - [], - ); + const previousVersionFiles = await processFiles(new DefaultFileSystem(), config.previousVersionDir, false, []); + const currentVersionFiles = await processFiles(new DefaultFileSystem(), config.currentVersionDir, false, []); return pipe( changelog(previousVersionFiles, currentVersionFiles, config), diff --git a/src/application/apex-file-reader.ts b/src/application/apex-file-reader.ts index c5beb88a..4ddd3697 100644 --- a/src/application/apex-file-reader.ts +++ b/src/application/apex-file-reader.ts @@ -9,60 +9,55 @@ const APEX_FILE_EXTENSION = '.cls'; /** * Reads from .cls files and returns their raw body. */ -export class ApexFileReader { - /** - * Reads from .cls files and returns their raw body. - */ - static async processFiles( - fileSystem: FileSystem, - rootPath: string, - includeMetadata: boolean, - exclude: string[], - ): Promise { - const processSingleFile = apply(this.processFile, fileSystem, includeMetadata); - - return pipe( - await this.getFilePaths(fileSystem, rootPath), - (filePaths) => filePaths.filter((filePath) => !this.isExcluded(filePath, exclude)), - (filePaths) => filePaths.filter(this.isApexFile), - (filePaths) => Promise.all(filePaths.map(processSingleFile)), - ); - } +export async function processFiles( + fileSystem: FileSystem, + rootPath: string, + includeMetadata: boolean, + exclude: string[], +): Promise { + const processSingleFile = apply(processFile, fileSystem, includeMetadata); + + return pipe( + await getFilePaths(fileSystem, rootPath), + (filePaths) => filePaths.filter((filePath) => !isExcluded(filePath, exclude)), + (filePaths) => filePaths.filter(isApexFile), + (filePaths) => Promise.all(filePaths.map(processSingleFile)), + ); +} - private static async getFilePaths(fileSystem: FileSystem, rootPath: string): Promise { - const directoryContents = await fileSystem.readDirectory(rootPath); - const paths: string[] = []; - for (const filePath of directoryContents) { - const currentPath = fileSystem.joinPath(rootPath, filePath); - if (await fileSystem.isDirectory(currentPath)) { - paths.push(...(await this.getFilePaths(fileSystem, currentPath))); - } else { - paths.push(currentPath); - } +async function getFilePaths(fileSystem: FileSystem, rootPath: string): Promise { + const directoryContents = await fileSystem.readDirectory(rootPath); + const paths: string[] = []; + for (const filePath of directoryContents) { + const currentPath = fileSystem.joinPath(rootPath, filePath); + if (await fileSystem.isDirectory(currentPath)) { + paths.push(...(await getFilePaths(fileSystem, currentPath))); + } else { + paths.push(currentPath); } - return paths; - } - - private static isExcluded(filePath: string, exclude: string[]): boolean { - return exclude.some((pattern) => minimatch(filePath, pattern)); } + return paths; +} - private static async processFile( - fileSystem: FileSystem, - includeMetadata: boolean, - filePath: string, - ): Promise { - const rawTypeContent = await fileSystem.readFile(filePath); - const metadataPath = `${filePath}-meta.xml`; - let rawMetadataContent = null; - if (includeMetadata) { - rawMetadataContent = fileSystem.exists(metadataPath) ? await fileSystem.readFile(metadataPath) : null; - } +function isExcluded(filePath: string, exclude: string[]): boolean { + return exclude.some((pattern) => minimatch(filePath, pattern)); +} - return { filePath, content: rawTypeContent, metadataContent: rawMetadataContent }; +async function processFile( + fileSystem: FileSystem, + includeMetadata: boolean, + filePath: string, +): Promise { + const rawTypeContent = await fileSystem.readFile(filePath); + const metadataPath = `${filePath}-meta.xml`; + let rawMetadataContent = null; + if (includeMetadata) { + rawMetadataContent = fileSystem.exists(metadataPath) ? await fileSystem.readFile(metadataPath) : null; } - private static isApexFile(currentFile: string): boolean { - return currentFile.endsWith(APEX_FILE_EXTENSION); - } + return { filePath, content: rawTypeContent, metadataContent: rawMetadataContent }; +} + +function isApexFile(currentFile: string): boolean { + return currentFile.endsWith(APEX_FILE_EXTENSION); } From a459ad0417244832de2b082522509d3af441d038 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 18:50:40 -0400 Subject: [PATCH 32/61] refactorings --- src/application/Apexdocs.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index fe899b66..f89ff582 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -17,6 +17,7 @@ import { } from '../core/shared/types'; import { ReflectionError, ReflectionErrors, HookError } from '../core/errors/errors'; import { FileWritingError } from './errors'; +import { apply } from '#utils/fp'; /** * Application entry-point to generate documentation out of Apex source files. @@ -44,13 +45,10 @@ export class Apexdocs { } } +const readFiles = apply(processFiles, new DefaultFileSystem()); + async function processMarkdown(config: UserDefinedMarkdownConfig) { - const fileBodies = await processFiles( - new DefaultFileSystem(), - config.sourceDir, - config.includeMetadata, - config.exclude, - ); + const fileBodies = await readFiles(config.sourceDir, config.includeMetadata, config.exclude); return pipe( markdown(fileBodies, config), @@ -60,13 +58,13 @@ async function processMarkdown(config: UserDefinedMarkdownConfig) { } async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) { - const fileBodies = await processFiles(new DefaultFileSystem(), config.sourceDir, false, config.exclude); + const fileBodies = await readFiles(config.sourceDir, false, config.exclude); return openApi(logger, fileBodies, config); } async function processChangeLog(config: UserDefinedChangelogConfig) { - const previousVersionFiles = await processFiles(new DefaultFileSystem(), config.previousVersionDir, false, []); - const currentVersionFiles = await processFiles(new DefaultFileSystem(), config.currentVersionDir, false, []); + const previousVersionFiles = await readFiles(config.previousVersionDir, false, []); + const currentVersionFiles = await readFiles(config.currentVersionDir, false, []); return pipe( changelog(previousVersionFiles, currentVersionFiles, config), From 19e8c40f3c36ce2be90c815aec16b87282fa99cc Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 18:58:40 -0400 Subject: [PATCH 33/61] refactorings --- src/application/Apexdocs.ts | 47 ++++++++++++++++++++++--------------- src/application/errors.ts | 9 +++++++ 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index f89ff582..c64da88f 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -10,13 +10,14 @@ import { processFiles } from './apex-file-reader'; import { DefaultFileSystem } from './file-system'; import { Logger } from '#utils/logger'; import { + UnparsedSourceFile, UserDefinedChangelogConfig, UserDefinedConfig, UserDefinedMarkdownConfig, UserDefinedOpenApiConfig, } from '../core/shared/types'; import { ReflectionError, ReflectionErrors, HookError } from '../core/errors/errors'; -import { FileWritingError } from './errors'; +import { FileReadingError, FileWritingError } from './errors'; import { apply } from '#utils/fp'; /** @@ -48,10 +49,12 @@ export class Apexdocs { const readFiles = apply(processFiles, new DefaultFileSystem()); async function processMarkdown(config: UserDefinedMarkdownConfig) { - const fileBodies = await readFiles(config.sourceDir, config.includeMetadata, config.exclude); - return pipe( - markdown(fileBodies, config), + TE.tryCatch( + () => readFiles(config.sourceDir, config.includeMetadata, config.exclude), + (e) => new FileReadingError('An error occurred while reading files.', e), + ), + TE.flatMap((fileBodies) => markdown(fileBodies, config)), TE.map(() => '✔️ Documentation generated successfully!'), TE.mapLeft(toErrors), ); @@ -63,29 +66,35 @@ async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) } async function processChangeLog(config: UserDefinedChangelogConfig) { - const previousVersionFiles = await readFiles(config.previousVersionDir, false, []); - const currentVersionFiles = await readFiles(config.currentVersionDir, false, []); + async function loadFiles(): Promise<[UnparsedSourceFile[], UnparsedSourceFile[]]> { + return [ + await readFiles(config.previousVersionDir, false, []), + await readFiles(config.currentVersionDir, false, []), + ]; + } return pipe( - changelog(previousVersionFiles, currentVersionFiles, config), + TE.tryCatch(loadFiles, (e) => new FileReadingError('An error occurred while reading files.', e)), + TE.flatMap(([previous, current]) => changelog(previous, current, config)), TE.map(() => '✔️ Changelog generated successfully!'), TE.mapLeft(toErrors), ); } -function toErrors(error: ReflectionErrors | HookError | FileWritingError): unknown[] { - if (error._tag === 'HookError') { - return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; +function toErrors(error: ReflectionErrors | HookError | FileReadingError | FileWritingError): unknown[] { + switch (error._tag) { + case 'HookError': + return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; + case 'FileReadingError': + return ['Error(s) occurred while reading files. Please review the following issues:', error.error]; + case 'FileWritingError': + return ['Error(s) occurred while writing files. Please review the following issues:', error.error]; + default: + return [ + 'Error(s) occurred while parsing files. Please review the following issues:', + ...error.errors.map(formatReflectionError), + ]; } - - if (error._tag === 'FileWritingError') { - return ['Error(s) occurred while writing files. Please review the following issues:', error.error]; - } - - return [ - 'Error(s) occurred while parsing files. Please review the following issues:', - ...error.errors.map(formatReflectionError), - ]; } function formatReflectionError(error: ReflectionError) { diff --git a/src/application/errors.ts b/src/application/errors.ts index d94c46f6..57b2f680 100644 --- a/src/application/errors.ts +++ b/src/application/errors.ts @@ -1,3 +1,12 @@ +export class FileReadingError { + readonly _tag = 'FileReadingError'; + + constructor( + public message: string, + public error: unknown, + ) {} +} + export class FileWritingError { readonly _tag = 'FileWritingError'; From c4ed63c20b95093e272b4c04e7aaee782ec162c0 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 19:10:05 -0400 Subject: [PATCH 34/61] Unit testing that new class information is added --- .../__test__/generating-change-log.spec.ts | 31 +++++++++++++++++++ src/core/changelog/generate-change-log.ts | 1 + src/core/changelog/renderable-change-log.ts | 16 +++++----- .../__test__/generating-class-docs.spec.ts | 2 -- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index f736a749..833548aa 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -40,5 +40,36 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).toContain('## New Classes')); }); + + it('should include the new class name', async () => { + const newClassSource = 'class Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('### Test')); + }); + + it('should include the new class description', async () => { + const newClassSource = ` + /** + * This is a test class. + */ + class Test {} + `; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('This is a test class.')); + }); }); }); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index d9589965..78436c7b 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -21,6 +21,7 @@ export type ChangeLogPageData = { // TODO: We should provide the ability to filter out of scope if we are going // to be relying on source files and not on a previously generated manifest. +// TODO: And also the "exclude" property in the config, it should be fairly simple to add. export function generateChangeLog( oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[], diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index 05132f44..169966bf 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -33,7 +33,7 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: __type: 'class', heading: 'New Classes', description: 'These classes are new.', - types: newClasses.map(typeToRenderable), // TODO: Unit test this gets populated + types: newClasses.map(typeToRenderable), } : null, }; @@ -47,17 +47,15 @@ function typeToRenderable(type: Type): NewTypeRenderable { return { name: type.name, - description: adapt(), // TODO: integration test that the content renders correctly + description: adapt(), }; } -// TODO: -// New Classes -// New Enums -// New Interfaces +// TODO: New Enums +// TODO: New Interfaces -// Removed Classes -// Removed Enums -// Removed Interfaces +// TODO: Removed Classes +// TODO: Removed Enums +// TODO: Removed Interfaces // Changes... diff --git a/src/core/markdown/__test__/generating-class-docs.spec.ts b/src/core/markdown/__test__/generating-class-docs.spec.ts index 76d8a023..9c4ab2c5 100644 --- a/src/core/markdown/__test__/generating-class-docs.spec.ts +++ b/src/core/markdown/__test__/generating-class-docs.spec.ts @@ -359,5 +359,3 @@ describe('When generating documentation for a class', () => { }); }); }); - -// TODO: Skips tags at the member level From 13dd0c1a4a7fa7900a52ca9706a7df2acfa1133e Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 19:17:18 -0400 Subject: [PATCH 35/61] Displaying new interfaces --- .../current/classes/IAnotherExample.cls | 1 + .../current/classes/IExemplificable.cls | 3 ++ examples/changelog/docs/changelog.md | 10 +++- .../__tests__/apex-file-reader.spec.ts | 12 ++--- ...onverting-to-renderable-change-log.spec.ts | 26 +++++++++++ .../__test__/generating-change-log.spec.ts | 46 +++++++++++++++++++ src/core/changelog/renderable-change-log.ts | 11 +++++ .../templates/change-log-template.ts | 11 +++++ 8 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 examples/changelog/current/classes/IAnotherExample.cls create mode 100644 examples/changelog/current/classes/IExemplificable.cls diff --git a/examples/changelog/current/classes/IAnotherExample.cls b/examples/changelog/current/classes/IAnotherExample.cls new file mode 100644 index 00000000..cfcd5771 --- /dev/null +++ b/examples/changelog/current/classes/IAnotherExample.cls @@ -0,0 +1 @@ +public interface IAnotherExample {} diff --git a/examples/changelog/current/classes/IExemplificable.cls b/examples/changelog/current/classes/IExemplificable.cls new file mode 100644 index 00000000..76323342 --- /dev/null +++ b/examples/changelog/current/classes/IExemplificable.cls @@ -0,0 +1,3 @@ +public interface IExemplificable { + public void exampleMethod(); +} diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index 6e6c4b54..6fa81fa5 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -6,4 +6,12 @@ These classes are new. ### AccountService -This is a new class that does foo and bar and references Baz . \ No newline at end of file +This is a new class that does foo and bar and references Baz . + +## New Interfaces + +These interfaces are new. + +### IAnotherExample + +### IExemplificable \ No newline at end of file diff --git a/src/application/__tests__/apex-file-reader.spec.ts b/src/application/__tests__/apex-file-reader.spec.ts index 6286079a..9b33fc5c 100644 --- a/src/application/__tests__/apex-file-reader.spec.ts +++ b/src/application/__tests__/apex-file-reader.spec.ts @@ -1,5 +1,5 @@ -import { ApexFileReader } from '../apex-file-reader'; import { FileSystem } from '../file-system'; +import { processFiles } from '../apex-file-reader'; type File = { type: 'file'; @@ -70,7 +70,7 @@ describe('File Reader', () => { }, ]); - const result = await ApexFileReader.processFiles(fileSystem, '', false, []); + const result = await processFiles(fileSystem, '', false, []); expect(result.length).toBe(0); }); @@ -90,7 +90,7 @@ describe('File Reader', () => { }, ]); - const result = await ApexFileReader.processFiles(fileSystem, '', false, []); + const result = await processFiles(fileSystem, '', false, []); expect(result.length).toBe(0); }); @@ -120,7 +120,7 @@ describe('File Reader', () => { }, ]); - const result = await ApexFileReader.processFiles(fileSystem, '', false, []); + const result = await processFiles(fileSystem, '', false, []); expect(result.length).toBe(2); expect(result[0].content).toBe('public class MyClass{}'); expect(result[1].content).toBe('public class AnotherClass{}'); @@ -152,7 +152,7 @@ describe('File Reader', () => { }, ]); - const result = await ApexFileReader.processFiles(fileSystem, '', false, ['**/AnotherFile.cls']); + const result = await processFiles(fileSystem, '', false, ['**/AnotherFile.cls']); expect(result.length).toBe(1); expect(result[0].content).toBe('public class MyClass{}'); }); @@ -205,7 +205,7 @@ describe('File Reader', () => { }, ]); - const result = await ApexFileReader.processFiles(fileSystem, '', false, []); + const result = await processFiles(fileSystem, '', false, []); expect(result.length).toBe(4); }); }); diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts index d28a4224..2f18a9ff 100644 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -1,6 +1,7 @@ import { ChangeLog } from '../process-change-log'; import { convertToRenderableChangeLog } from '../renderable-change-log'; import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; +import { InterfaceMirrorBuilder } from '../../../test-helpers/InterfaceMirrorBuilder'; describe('when converting a changelog to a renderable changelog', () => { it('does not include the New Classes section if there are none', () => { @@ -27,4 +28,29 @@ describe('when converting a changelog to a renderable changelog', () => { expect(renderable.newClasses).not.toBeNull(); }); + + it('does not include the New Interfaces section if there are none', () => { + const changeLog: ChangeLog = { + newTypes: [], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, []); + + expect(renderable.newInterfaces).toBeNull(); + }); + + it('includes the New Interfaces section if there are any', () => { + const newInterfaces = [new InterfaceMirrorBuilder().withName('MyInterface').build()]; + const changeLog: ChangeLog = { + newTypes: ['MyInterface'], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, newInterfaces); + + expect(renderable.newInterfaces).not.toBeNull(); + }); }); diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 833548aa..100badf6 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -72,4 +72,50 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).toContain('This is a test class.')); }); }); + + describe('that include new interfaces', () => { + it('should include a section for new interfaces', async () => { + const newInterfaceSource = 'interface Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newInterfaceSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('## New Interfaces')); + }); + + it('should include the new interface name', async () => { + const newInterfaceSource = 'interface Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newInterfaceSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('### Test')); + }); + + it('should include the new interface description', async () => { + const newInterfaceSource = ` + /** + * This is a test interface. + */ + interface Test {} + `; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newInterfaceSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('This is a test interface.')); + }); + }); }); diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index 169966bf..2041a308 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -17,6 +17,7 @@ type NewTypeSection = { type RenderableChangeLog = { newClasses: NewTypeSection<'class'> | null; + newInterfaces: NewTypeSection<'interface'> | null; }; export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: Type[]): RenderableChangeLog { @@ -25,6 +26,7 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: ); const newClasses = allNewTypes.filter((type) => type.type_name === 'class'); + const newInterfaces = allNewTypes.filter((type) => type.type_name === 'interface'); return { newClasses: @@ -36,6 +38,15 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: types: newClasses.map(typeToRenderable), } : null, + newInterfaces: + newInterfaces.length > 0 + ? { + __type: 'interface', + heading: 'New Interfaces', + description: 'These interfaces are new.', + types: newInterfaces.map(typeToRenderable), + } + : null, }; } diff --git a/src/core/changelog/templates/change-log-template.ts b/src/core/changelog/templates/change-log-template.ts index caaa37f8..3726ef8a 100644 --- a/src/core/changelog/templates/change-log-template.ts +++ b/src/core/changelog/templates/change-log-template.ts @@ -11,6 +11,17 @@ export const changeLogTemplate = ` {{{renderContent this.description}}} {{/each}} +{{/if}} + +{{#if newInterfaces}} +## {{newInterfaces.heading}} +{{newInterfaces.description}} + +{{#each newInterfaces.types}} +### {{this.name}} + +{{{renderContent this.description}}} +{{/each}} {{/if}} `.trim(); From f1be07fb677a0096f286c9cbc098287935c1f1b3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Tue, 24 Sep 2024 19:26:32 -0400 Subject: [PATCH 36/61] Incorporating the changelog into the vitepress example --- examples/vitepress/docs/.vitepress/config.mts | 17 +++++----- examples/vitepress/docs/changelog.md | 31 +++++++++++++++++++ examples/vitepress/package.json | 1 + examples/vitepress/previous/.gitkeep | 0 src/cli/args.ts | 5 +++ 5 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 examples/vitepress/docs/changelog.md create mode 100644 examples/vitepress/previous/.gitkeep diff --git a/examples/vitepress/docs/.vitepress/config.mts b/examples/vitepress/docs/.vitepress/config.mts index e877381c..1f8f3cbc 100644 --- a/examples/vitepress/docs/.vitepress/config.mts +++ b/examples/vitepress/docs/.vitepress/config.mts @@ -1,21 +1,20 @@ -import { defineConfig } from 'vitepress' +import { defineConfig } from 'vitepress'; import * as sidebar from './sidebar.json'; // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "Apexdocs Vitepress Example", - description: "Apexdocs Vitepress Example", + title: 'Apexdocs Vitepress Example', + description: 'Apexdocs Vitepress Example', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' } + { text: 'Changelog', link: '/changelog' }, + { text: 'Examples', link: '/markdown-examples' }, ], sidebar: sidebar.default, - socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } - ] - } -}) + socialLinks: [{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }], + }, +}); diff --git a/examples/vitepress/docs/changelog.md b/examples/vitepress/docs/changelog.md new file mode 100644 index 00000000..1ff38fe0 --- /dev/null +++ b/examples/vitepress/docs/changelog.md @@ -0,0 +1,31 @@ +# Changelog + +## New Classes + +These classes are new. + +### BaseClass + +### MultiInheritanceClass + +### Url + +Represents a uniform resource locator (URL) and provides access to parts of the URL. +Enables access to the base URL used to access your Salesforce org. +### SampleClass + +aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex +**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. +### SampleException + +This is a sample exception. + +## New Interfaces + +These interfaces are new. + +### ParentInterface + +### SampleInterface + +This is a sample interface \ No newline at end of file diff --git a/examples/vitepress/package.json b/examples/vitepress/package.json index 51bad928..40fda193 100644 --- a/examples/vitepress/package.json +++ b/examples/vitepress/package.json @@ -3,6 +3,7 @@ "scripts": { "docs:clean": "rimraf --glob 'docs/!(.vitepress|index-frontmatter.md|api-examples.md|markdown-examples.md)'", "apexdocs:build": "npm run docs:clean && ts-node ../../src/cli/generate.ts markdown", + "changelog:build": "ts-node ../../src/cli/generate.ts changelog --previousVersionDir previous --currentVersionDir force-app", "docs:build": "vitepress build docs", "docs:gen": "npm run docs:clean && npm run docs:build", "docs:dev": "vitepress dev docs", diff --git a/examples/vitepress/previous/.gitkeep b/examples/vitepress/previous/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/cli/args.ts b/src/cli/args.ts index e620628e..e40760bd 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -57,6 +57,11 @@ export async function extractArgs(): Promise { const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; + // TODO: It should be possible to have a version of the config that supports + // generating all commands at the same time. So users should be able to provide the key for the main + // command, and then it should be possible to provide the specific configuration for each command. + // this is invaluable if we want to allow for the combination of changelog + markdown generation, for example + switch (mergedConfig.targetGenerator) { case 'markdown': return { ...configOnlyMarkdownDefaults, ...mergedConfig }; From 19f96999ddf916cd4ca117c4a8613a0d04b3a21b Mon Sep 17 00:00:00 2001 From: cesarParra Date: Wed, 25 Sep 2024 08:45:31 -0400 Subject: [PATCH 37/61] New enums appear on the changelog. --- .../current/classes/AccountService.cls | 2 - .../current/classes/PossibleValues.cls | 5 +++ examples/changelog/docs/changelog.md | 8 +++- ...onverting-to-renderable-change-log.spec.ts | 26 ++++++++++++ .../__test__/generating-change-log.spec.ts | 40 +++++++++++++++++++ src/core/changelog/renderable-change-log.ts | 12 ++++++ .../templates/change-log-template.ts | 12 ++++++ src/test-helpers/EnumMirrorBuilder.ts | 32 +++++++++++++++ 8 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 examples/changelog/current/classes/PossibleValues.cls create mode 100644 src/test-helpers/EnumMirrorBuilder.ts diff --git a/examples/changelog/current/classes/AccountService.cls b/examples/changelog/current/classes/AccountService.cls index eda26bc2..16eeef9c 100644 --- a/examples/changelog/current/classes/AccountService.cls +++ b/examples/changelog/current/classes/AccountService.cls @@ -1,5 +1,3 @@ -// This class is new - /** * @description This is a new class that does foo and bar and references {@link Baz}. */ diff --git a/examples/changelog/current/classes/PossibleValues.cls b/examples/changelog/current/classes/PossibleValues.cls new file mode 100644 index 00000000..4189e02d --- /dev/null +++ b/examples/changelog/current/classes/PossibleValues.cls @@ -0,0 +1,5 @@ +public enum PossibleValues { + VALUE1, + VALUE2, + VALUE3 +} diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index 6fa81fa5..f576225b 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -14,4 +14,10 @@ These interfaces are new. ### IAnotherExample -### IExemplificable \ No newline at end of file +### IExemplificable + +## New Enums + +These enums are new. + +### PossibleValues \ No newline at end of file diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts index 2f18a9ff..31a2223c 100644 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts @@ -2,6 +2,7 @@ import { ChangeLog } from '../process-change-log'; import { convertToRenderableChangeLog } from '../renderable-change-log'; import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; import { InterfaceMirrorBuilder } from '../../../test-helpers/InterfaceMirrorBuilder'; +import { EnumMirrorBuilder } from '../../../test-helpers/EnumMirrorBuilder'; describe('when converting a changelog to a renderable changelog', () => { it('does not include the New Classes section if there are none', () => { @@ -53,4 +54,29 @@ describe('when converting a changelog to a renderable changelog', () => { expect(renderable.newInterfaces).not.toBeNull(); }); + + it('does not include the New Enums section if there are none', () => { + const changeLog: ChangeLog = { + newTypes: [], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, []); + + expect(renderable.newEnums).toBeNull(); + }); + + it('includes the New Enums section if there are any', () => { + const newEnums = [new EnumMirrorBuilder().withName('MyEnum').build()]; + const changeLog: ChangeLog = { + newTypes: ['MyEnum'], + removedTypes: [], + newOrModifiedMembers: [], + }; + + const renderable = convertToRenderableChangeLog(changeLog, newEnums); + + expect(renderable.newEnums).not.toBeNull(); + }); }); diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 100badf6..e0f55d26 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -118,4 +118,44 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).toContain('This is a test interface.')); }); }); + + describe('that include new enums', () => { + it('should include a section for new enums', async () => { + const newEnumSource = 'enum Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [{ content: newEnumSource, filePath: 'Test.cls', metadataContent: null }]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('## New Enums')); + }); + + it('should include the new enum name', async () => { + const newEnumSource = 'enum Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [{ content: newEnumSource, filePath: 'Test.cls', metadataContent: null }]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('### Test')); + }); + + it('should include the new enum description', async () => { + const newEnumSource = ` + /** + * This is a test enum. + */ + enum Test {} + `; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [{ content: newEnumSource, filePath: 'Test.cls', metadataContent: null }]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('This is a test enum.')); + }); + }); }); diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index 2041a308..4aa172f5 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -1,5 +1,6 @@ import { ChangeLog } from './process-change-log'; import { Type } from '@cparra/apex-reflection'; +// TODO: For these 2 imports, we should move the adapters and documentables to a shared place to not have to get into the markdown folder import { RenderableContent } from '../markdown/adapters/types'; import { adaptDescribable } from '../markdown/adapters/documentables'; @@ -18,6 +19,7 @@ type NewTypeSection = { type RenderableChangeLog = { newClasses: NewTypeSection<'class'> | null; newInterfaces: NewTypeSection<'interface'> | null; + newEnums: NewTypeSection<'enum'> | null; }; export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: Type[]): RenderableChangeLog { @@ -27,6 +29,7 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: const newClasses = allNewTypes.filter((type) => type.type_name === 'class'); const newInterfaces = allNewTypes.filter((type) => type.type_name === 'interface'); + const newEnums = allNewTypes.filter((type) => type.type_name === 'enum'); return { newClasses: @@ -47,6 +50,15 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: types: newInterfaces.map(typeToRenderable), } : null, + newEnums: + newEnums.length > 0 + ? { + __type: 'enum', + heading: 'New Enums', + description: 'These enums are new.', + types: newEnums.map(typeToRenderable), + } + : null, }; } diff --git a/src/core/changelog/templates/change-log-template.ts b/src/core/changelog/templates/change-log-template.ts index 3726ef8a..c1d93094 100644 --- a/src/core/changelog/templates/change-log-template.ts +++ b/src/core/changelog/templates/change-log-template.ts @@ -21,6 +21,18 @@ export const changeLogTemplate = ` {{#each newInterfaces.types}} ### {{this.name}} +{{{renderContent this.description}}} +{{/each}} +{{/if}} + +{{#if newEnums}} +## {{newEnums.heading}} + +{{newEnums.description}} + +{{#each newEnums.types}} +### {{this.name}} + {{{renderContent this.description}}} {{/each}} {{/if}} diff --git a/src/test-helpers/EnumMirrorBuilder.ts b/src/test-helpers/EnumMirrorBuilder.ts new file mode 100644 index 00000000..b8d266ce --- /dev/null +++ b/src/test-helpers/EnumMirrorBuilder.ts @@ -0,0 +1,32 @@ +import { Annotation, DocComment, EnumMirror } from '@cparra/apex-reflection'; + +/** + * Builder class to create Enum objects. + * For testing purposes only. + */ +export class EnumMirrorBuilder { + private name = 'SampleEnum'; + private annotations: Annotation[] = []; + private docComment?: DocComment; + + withName(name: string): EnumMirrorBuilder { + this.name = name; + return this; + } + + addAnnotation(annotation: Annotation): EnumMirrorBuilder { + this.annotations.push(annotation); + return this; + } + + build(): EnumMirror { + return { + annotations: this.annotations, + name: this.name, + type_name: 'enum', + access_modifier: 'public', + docComment: this.docComment, + values: [], + }; + } +} From c589723a82ea24f2f368ad9ab8648cfcd6493834 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Thu, 26 Sep 2024 07:53:27 -0400 Subject: [PATCH 38/61] Refactoring the argument extraction logic to allow for a single config file to hold multiple commands --- examples/vitepress/apexdocs.config.ts | 128 +++---- examples/vitepress/docs/changelog.md | 31 -- examples/vitepress/docs/index.md | 56 ---- .../vitepress/docs/miscellaneous/BaseClass.md | 20 -- .../miscellaneous/MultiInheritanceClass.md | 76 ----- .../docs/miscellaneous/ParentInterface.md | 19 -- .../docs/miscellaneous/ReferencedEnum.md | 15 - .../docs/miscellaneous/SampleException.md | 28 -- .../docs/miscellaneous/SampleInterface.md | 116 ------- examples/vitepress/docs/miscellaneous/Url.md | 317 ------------------ .../vitepress/docs/sample-enums/SampleEnum.md | 40 --- .../vitepress/docs/samplegroup/SampleClass.md | 174 ---------- examples/vitepress/package.json | 2 +- src/cli/args.ts | 103 ++++-- src/cli/generate.ts | 6 +- src/core/changelog/process-change-log.ts | 6 +- 16 files changed, 148 insertions(+), 989 deletions(-) delete mode 100644 examples/vitepress/docs/changelog.md delete mode 100644 examples/vitepress/docs/index.md delete mode 100644 examples/vitepress/docs/miscellaneous/BaseClass.md delete mode 100644 examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md delete mode 100644 examples/vitepress/docs/miscellaneous/ParentInterface.md delete mode 100644 examples/vitepress/docs/miscellaneous/ReferencedEnum.md delete mode 100644 examples/vitepress/docs/miscellaneous/SampleException.md delete mode 100644 examples/vitepress/docs/miscellaneous/SampleInterface.md delete mode 100644 examples/vitepress/docs/miscellaneous/Url.md delete mode 100644 examples/vitepress/docs/sample-enums/SampleEnum.md delete mode 100644 examples/vitepress/docs/samplegroup/SampleClass.md diff --git a/examples/vitepress/apexdocs.config.ts b/examples/vitepress/apexdocs.config.ts index 20184640..b7764cf8 100644 --- a/examples/vitepress/apexdocs.config.ts +++ b/examples/vitepress/apexdocs.config.ts @@ -25,70 +25,72 @@ function writeFileAsync(filePath: string, data: string): Promise { }); } -export default defineMarkdownConfig({ - sourceDir: 'force-app', - scope: ['global', 'public', 'protected', 'private', 'namespaceaccessible'], - sortAlphabetically: true, - namespace: 'apexdocs', - transformReference: (reference) => { - return { - // remove the trailing .md - referencePath: reference.referencePath.replace(/\.md$/, ''), - }; - }, - transformReferenceGuide: async () => { - const frontMatter = await loadFileAsync('./docs/index-frontmatter.md'); - return { - frontmatter: frontMatter, - }; - }, - excludeTags: ['internal'], - transformDocs: async (docs) => { - // Update sidebar - const sidebar = [ - { - text: 'API Reference', - items: [ - { - text: 'Grouped By Type', - items: [ - { - text: 'Classes', - items: docs.filter((doc) => doc.source.type === 'class').map(toSidebarLink), - }, - { - text: 'Interfaces', - items: docs.filter((doc) => doc.source.type === 'interface').map(toSidebarLink), - }, - { - text: 'Enums', - items: docs.filter((doc) => doc.source.type === 'enum').map(toSidebarLink), - }, - ], - }, - { - text: 'Grouped by Group', - items: Array.from(extractGroups(docs)).map(([groupName, groupDocs]) => ({ - text: groupName, - items: groupDocs.map(toSidebarLink), - })), - }, - ], - }, - ]; - await writeFileAsync('./docs/.vitepress/sidebar.json', JSON.stringify(sidebar, null, 2)); +export default { + markdown: defineMarkdownConfig({ + sourceDir: 'force-app', + scope: ['global', 'public', 'protected', 'private', 'namespaceaccessible'], + sortAlphabetically: true, + namespace: 'apexdocs', + transformReference: (reference) => { + return { + // remove the trailing .md + referencePath: reference.referencePath.replace(/\.md$/, ''), + }; + }, + transformReferenceGuide: async () => { + const frontMatter = await loadFileAsync('./docs/index-frontmatter.md'); + return { + frontmatter: frontMatter, + }; + }, + excludeTags: ['internal'], + transformDocs: async (docs) => { + // Update sidebar + const sidebar = [ + { + text: 'API Reference', + items: [ + { + text: 'Grouped By Type', + items: [ + { + text: 'Classes', + items: docs.filter((doc) => doc.source.type === 'class').map(toSidebarLink), + }, + { + text: 'Interfaces', + items: docs.filter((doc) => doc.source.type === 'interface').map(toSidebarLink), + }, + { + text: 'Enums', + items: docs.filter((doc) => doc.source.type === 'enum').map(toSidebarLink), + }, + ], + }, + { + text: 'Grouped by Group', + items: Array.from(extractGroups(docs)).map(([groupName, groupDocs]) => ({ + text: groupName, + items: groupDocs.map(toSidebarLink), + })), + }, + ], + }, + ]; + await writeFileAsync('./docs/.vitepress/sidebar.json', JSON.stringify(sidebar, null, 2)); - return docs; - }, - transformDocPage: async (docPage) => { - return { - ...docPage, - frontmatter: { - title: docPage.source.name, - }, - }; - }, -}); + return docs; + }, + transformDocPage: async (docPage) => { + return { + ...docPage, + frontmatter: { + title: docPage.source.name, + }, + }; + }, + }), +}; function toSidebarLink(doc: DocPageData) { return { diff --git a/examples/vitepress/docs/changelog.md b/examples/vitepress/docs/changelog.md deleted file mode 100644 index 1ff38fe0..00000000 --- a/examples/vitepress/docs/changelog.md +++ /dev/null @@ -1,31 +0,0 @@ -# Changelog - -## New Classes - -These classes are new. - -### BaseClass - -### MultiInheritanceClass - -### Url - -Represents a uniform resource locator (URL) and provides access to parts of the URL. -Enables access to the base URL used to access your Salesforce org. -### SampleClass - -aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex -**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. -### SampleException - -This is a sample exception. - -## New Interfaces - -These interfaces are new. - -### ParentInterface - -### SampleInterface - -This is a sample interface \ No newline at end of file diff --git a/examples/vitepress/docs/index.md b/examples/vitepress/docs/index.md deleted file mode 100644 index cdb614cd..00000000 --- a/examples/vitepress/docs/index.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -# https://vitepress.dev/reference/default-theme-home-page -layout: home - -hero: - name: "Apexdocs Vitepress Example" - text: "API Documentation" - tagline: My great project tagline - actions: - - theme: brand - text: Markdown Examples - link: /markdown-examples - - theme: alt - text: API Examples - link: /api-examples ---- - -# Apex Reference Guide - -## Miscellaneous - -### [BaseClass](miscellaneous/BaseClass) - -### [MultiInheritanceClass](miscellaneous/MultiInheritanceClass) - -### [ParentInterface](miscellaneous/ParentInterface) - -### [ReferencedEnum](miscellaneous/ReferencedEnum) - -### [SampleException](miscellaneous/SampleException) - -This is a sample exception. - -### [SampleInterface](miscellaneous/SampleInterface) - -This is a sample interface - -### [Url](miscellaneous/Url) - -Represents a uniform resource locator (URL) and provides access to parts of the URL. -Enables access to the base URL used to access your Salesforce org. - -## Sample Enums - -### [SampleEnum](sample-enums/SampleEnum) - -This is a sample enum. This references [ReferencedEnum](miscellaneous/ReferencedEnum) . - -This description has several lines - -## SampleGroup - -### [SampleClass](samplegroup/SampleClass) - -aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex -**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/BaseClass.md b/examples/vitepress/docs/miscellaneous/BaseClass.md deleted file mode 100644 index 62d3cf76..00000000 --- a/examples/vitepress/docs/miscellaneous/BaseClass.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: BaseClass ---- - -# BaseClass Class -`abstract` - -## Namespace -apexdocs - -## Fields -### `sampleEnumFromBase` - -#### Signature -```apex -public sampleEnumFromBase -``` - -#### Type -[SampleEnum](../sample-enums/SampleEnum) \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md b/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md deleted file mode 100644 index f0470f9b..00000000 --- a/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: MultiInheritanceClass ---- - -# MultiInheritanceClass Class - -## Namespace -apexdocs - -**Inheritance** - -[SampleClass](../samplegroup/SampleClass) < [BaseClass](BaseClass) - -## Fields -### `sampleEnumFromBase` - -*Inherited* - -#### Signature -```apex -public sampleEnumFromBase -``` - -#### Type -[SampleEnum](../sample-enums/SampleEnum) - -## Properties -### Group Name -#### `someProperty` - -*Inherited* - -##### Signature -```apex -public someProperty -``` - -##### Type -String - -## Methods -### Available Methods -#### `doSomething()` - -*Inherited* - -##### Signature -```apex -public void doSomething() -``` - -##### Return Type -**void** - -### Deprecated Methods -#### `sayHello()` - -*Inherited* - -`DEPRECATED` - -This is a sample method. - -##### Signature -```apex -public virtual String sayHello() -``` - -##### Return Type -**String** - -A string value. - -##### Example -SampleClass sample = new SampleClass(); -sample.doSomething(); \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/ParentInterface.md b/examples/vitepress/docs/miscellaneous/ParentInterface.md deleted file mode 100644 index 6bbe2741..00000000 --- a/examples/vitepress/docs/miscellaneous/ParentInterface.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: ParentInterface ---- - -# ParentInterface Interface - -## Namespace -apexdocs - -## Methods -### `sampleParentMethod()` - -#### Signature -```apex -public void sampleParentMethod() -``` - -#### Return Type -**void** \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/ReferencedEnum.md b/examples/vitepress/docs/miscellaneous/ReferencedEnum.md deleted file mode 100644 index 69e97a43..00000000 --- a/examples/vitepress/docs/miscellaneous/ReferencedEnum.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: ReferencedEnum ---- - -# ReferencedEnum Enum - -## Namespace -apexdocs - -## Values -| Value | Description | -|-------|-------------| -| VALUE1 | | -| VALUE2 | | -| VALUE3 | | \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/SampleException.md b/examples/vitepress/docs/miscellaneous/SampleException.md deleted file mode 100644 index cc919c74..00000000 --- a/examples/vitepress/docs/miscellaneous/SampleException.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: SampleException ---- - -# SampleException Class - -This is a sample exception. - -**Usage** - -You can use the exception the following way. -You can also take a look at [SampleClass](../samplegroup/SampleClass) to see how it is used. -This is a dangerous HTML tag: <script>alert('Hello');</script> - -```apex -try { - throw new SampleException(); -} catch (SampleException e) { - System.debug('Caught exception'); -} -``` - -## Namespace -apexdocs - -**Inheritance** - -Exception \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/SampleInterface.md b/examples/vitepress/docs/miscellaneous/SampleInterface.md deleted file mode 100644 index 2a719b54..00000000 --- a/examples/vitepress/docs/miscellaneous/SampleInterface.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -title: SampleInterface ---- - -# SampleInterface Interface - -`NAMESPACEACCESSIBLE` - -This is a sample interface - -**Mermaid** - -graph TD -A[SampleInterface] -->|extends| B[ParentInterface] -B -->|extends| C[GrandParentInterface] -C -->|extends| D[GreatGrandParentInterface] - -**Author** John Doe - -**Date** 2020-01-01 - -**See** [SampleEnum](../sample-enums/SampleEnum) - -**See** [ReferencedEnum](ReferencedEnum) - -## Namespace -apexdocs - -## Example -SampleInterface sampleInterface = new SampleInterface(); -sampleInterface.sampleMethod(); - -**Extends** -[ParentInterface](ParentInterface) - -## Methods -### `sampleMethod()` - -`NAMESPACEACCESSIBLE` - -This is a sample method - -**Custom Tag** - -This is a custom tag - -**Another Custom Tag** - -This is another custom tag - -**Mermaid** - -graph TD -A[SampleInterface] -->|extends| B[ParentInterface] -B -->|extends| C[GrandParentInterface] -C -->|extends| D[GreatGrandParentInterface] - -#### Signature -```apex -public String sampleMethod() -``` - -#### Return Type -**String** - -Some return value - -#### Throws -[SampleException](SampleException): This is a sample exception - -AnotherSampleException: This is another sample exception - -#### Example -SampleInterface sampleInterface = new SampleInterface(); -sampleInterface.sampleMethod(); - ---- - -### `sampleMethodWithParams(param1, param2, theEnum)` - -`NAMESPACEACCESSIBLE` -`DEPRECATED` - -This is a sample method with parameters -Sometimes it won't be possible to find a NonExistent link. - -#### Signature -```apex -public SampleEnum sampleMethodWithParams(String param1, Integer param2, SampleEnum theEnum) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| param1 | String | This is the first parameter | -| param2 | Integer | This is the second parameter | -| theEnum | [SampleEnum](../sample-enums/SampleEnum) | This is an enum parameter | - -#### Return Type -**[SampleEnum](../sample-enums/SampleEnum)** - -Some return value - ---- - -### `sampleParentMethod()` - -*Inherited* - -#### Signature -```apex -public void sampleParentMethod() -``` - -#### Return Type -**void** \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/Url.md b/examples/vitepress/docs/miscellaneous/Url.md deleted file mode 100644 index ff2038da..00000000 --- a/examples/vitepress/docs/miscellaneous/Url.md +++ /dev/null @@ -1,317 +0,0 @@ ---- -title: Url ---- - -# Url Class - -Represents a uniform resource locator (URL) and provides access to parts of the URL. -Enables access to the base URL used to access your Salesforce org. - -**Usage** - -Use the methods of the `System.URL` class to create links to objects in your organization. Such objects can be files, images, -logos, or records that you want to include in external emails, in activities, or in Chatter posts. For example, you can create -a link to a file uploaded as an attachment to a Chatter post by concatenating the Salesforce base URL with the file ID: - -```apex -// Get a file uploaded through Chatter. -ContentDocument doc = [SELECT Id FROM ContentDocument - WHERE Title = 'myfile']; -// Create a link to the file. -String fullFileURL = URL.getOrgDomainURL().toExternalForm() + - '/' + doc.id; -System.debug(fullFileURL); -``` - - -The following example creates a link to a Salesforce record. The full URL is created by concatenating the Salesforce base -URL with the record ID. - -```apex -Account acct = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1]; -String fullRecordURL = URL.getOrgDomainURL().toExternalForm() + '/' + acct.Id; -``` - -**Version Behavior Changes** - -In API version 41.0 and later, Apex URL objects are represented by the java.net.URI type, not the java.net.URL type. - -The API version in which the URL object was instantiated determines the behavior of subsequent method calls to the -specific instance. Salesforce strongly encourages you to use API 41.0 and later versions for fully RFC-compliant URL -parsing that includes proper handling of edge cases of complex URL structures. API 41.0 and later versions also enforce -that inputs are valid, RFC-compliant URL or URI strings. - -* [URL Constructors](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_url.htm#apex_System_URL_constructors) -* [URL Methods](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_url.htm#apex_System_URL_methods) - -**See Also** -* [URL Class](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_url.htm) - -## Namespace -apexdocs - -## Example -In this example, the base URL and the full request URL of the current Salesforce server instance are retrieved. Next, a URL -pointing to a specific account object is created. Finally, components of the base and full URL are obtained. This example -prints out all the results to the debug log output. - -```apex -// Create a new account called Acme that we will create a link for later. -Account myAccount = new Account(Name='Acme'); -insert myAccount; - -// Get the base URL. -String sfdcBaseURL = URL.getOrgDomainURL().toExternalForm(); -System.debug('Base URL: ' + sfdcBaseURL ); - -// Get the URL for the current request. -String currentRequestURL = URL.getCurrentRequestUrl().toExternalForm(); -System.debug('Current request URL: ' + currentRequestURL); - -// Create the account URL from the base URL. -String accountURL = URL.getOrgDomainURL().toExternalForm() + - '/' + myAccount.Id; -System.debug('URL of a particular account: ' + accountURL); - -// Get some parts of the base URL. -System.debug('Host: ' + URL.getOrgDomainURL().getHost()); -System.debug('Protocol: ' + URL.getOrgDomainURL().getProtocol()); - -// Get the query string of the current request. -System.debug('Query: ' + URL.getCurrentRequestUrl().getQuery()); -``` - -## Constructors -### `Url(spec)` - -Creates a new instance of the URL class using the specified string representation of the URL. - -#### Signature -```apex -global Url(String spec) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| spec | String | The string to parse as a URL. | - ---- - -### `Url(context, spec)` - -Creates a new instance of the URL class by parsing the specified spec within the specified context. - -**Usage** - -The new URL is created from the given context URL and the spec argument as described in RFC2396 "Uniform Resource Identifiers : Generic * Syntax" : -```xml -://?# -``` - - -For more information about the arguments of this constructor, see the corresponding URL(java.net.URL, java.lang.String) constructor for Java. - -#### Signature -```apex -global Url(Url context, String spec) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| context | [Url](Url) | The context in which to parse the specification. | -| spec | String | The string to parse as a URL. | - ---- - -### `Url(protocol, host, file)` - -Creates a new instance of the URL class using the specified protocol, host, and file on the host. The default port for the specified protocol is used. - -#### Signature -```apex -global Url(String protocol, String host, String file) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| protocol | String | The protocol name for this URL. | -| host | String | The host name for this URL. | -| file | String | The file name for this URL. | - ---- - -### `Url(protocol, host, port, file)` - -Creates a new instance of the URL class using the specified protocol, host, port number, and file on the host. - -#### Signature -```apex -global Url(String protocol, String host, Integer port, String file) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| protocol | String | The protocol name for this URL. | -| host | String | The host name for this URL. | -| port | Integer | The port number for this URL. | -| file | String | The file name for this URL. | - -## Methods -### `getAuthority()` - -Returns the authority portion of the current URL. - -#### Signature -```apex -global String getAuthority() -``` - -#### Return Type -**String** - -The authority portion of the current URL. - ---- - -### `getCurrentRequestUrl()` - -Returns the URL of an entire request on a Salesforce instance. - -**Usage** - -An example of a URL for an entire request is https://yourInstance.salesforce.com/apex/myVfPage.apexp. - -#### Signature -```apex -global static Url getCurrentRequestUrl() -``` - -#### Return Type -**[Url](Url)** - -The URL of the entire request. - ---- - -### `getDefPort()` - -Returns the default port number of the protocol associated with the current URL. - -**Usage** - -Returns -1 if the URL scheme or the stream protocol handler for the URL doesn't define a default port number. - -#### Signature -```apex -global Integer getDefPort() -``` - -#### Return Type -**Integer** - -The default port number of the protocol associated with the current URL. - ---- - -### `getFile()` - -Returns the file name of the current URL. - -#### Signature -```apex -global String getFile() -``` - -#### Return Type -**String** - -The file name of the current URL. - ---- - -### `getFileFieldURL(entityId, fieldName)` - -Returns the download URL for a file attachment. - -#### Signature -```apex -global static String getFileFieldURL(String entityId, String fieldName) -``` - -#### Parameters -| Name | Type | Description | -|------|------|-------------| -| entityId | String | Specifies the ID of the entity that holds the file data. | -| fieldName | String | Specifies the API name of a file field component, such as `AttachmentBody` . | - -#### Return Type -**String** - -The download URL for the file attachment. - -#### Example -String fileURL = -URL.getFileFieldURL( -'087000000000123' , -'AttachmentBody'); - ---- - -### `getHost()` - -Returns the host name of the current URL. - -#### Signature -```apex -global String getHost() -``` - -#### Return Type -**String** - -The host name of the current URL. - ---- - -### `getOrgDomainUrl()` - -Returns the canonical URL for your org. For example, https://MyDomainName.my.salesforce.com. - -**Usage** - -Use getOrgDomainUrl() to interact with Salesforce REST and SOAP APIs in Apex code. Get endpoints for User Interface API calls, for creating and customizing picklist value sets and custom fields, and more. - - `getOrgDomainUrl()` can access the domain URL only for the org in which the Apex code is running. - -You don't need a RemoteSiteSetting for your org to interact with the Salesforce APIs using domain URLs retrieved with this method. - -**See Also** - -* [Lightning Aura Components Developer Guide: Making API Calls from Apex](https://developer.salesforce.com/docs/atlas.en-us.250.0.lightning.meta/lightning/apex_api_calls.htm) - -#### Signature -```apex -global static Url getOrgDomainUrl() -``` - -#### Return Type -**[Url](Url)** - -getOrgDomainUrl() always returns the login URL for your org, regardless of context. Use that URL when making API calls to your org. - -#### Example -This example uses the Salesforce REST API to get organization limit values. For information on limits, see Limits in the REST API Developer Guide. - -```apex -Http h = new Http(); -HttpRequest req = new HttpRequest(); -req.setEndpoint(Url.getOrgDomainUrl().toExternalForm() - + '/services/data/v44.0/limits'); -req.setMethod('GET'); -req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId()); -HttpResponse res = h.send(req); -``` \ No newline at end of file diff --git a/examples/vitepress/docs/sample-enums/SampleEnum.md b/examples/vitepress/docs/sample-enums/SampleEnum.md deleted file mode 100644 index 539f01e0..00000000 --- a/examples/vitepress/docs/sample-enums/SampleEnum.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: SampleEnum ---- - -# SampleEnum Enum - -`NAMESPACEACCESSIBLE` - -This is a sample enum. This references [ReferencedEnum](../miscellaneous/ReferencedEnum) . - -This description has several lines - -**Some Custom** - -Test. I can also have a [ReferencedEnum](../miscellaneous/ReferencedEnum) here. -And it can be multiline. - -**Mermaid** - -graph TD -A[SampleEnum] -->|references| B[ReferencedEnum] -B -->|referenced by| A - -**Group** Sample Enums - -**Author** John Doe - -**Date** 2022-01-01 - -**See** [ReferencedEnum](../miscellaneous/ReferencedEnum) - -## Namespace -apexdocs - -## Values -| Value | Description | -|-------|-------------| -| VALUE1 | This is value 1 | -| VALUE2 | This is value 2 | -| VALUE3 | This is value 3 | \ No newline at end of file diff --git a/examples/vitepress/docs/samplegroup/SampleClass.md b/examples/vitepress/docs/samplegroup/SampleClass.md deleted file mode 100644 index 38de2d1f..00000000 --- a/examples/vitepress/docs/samplegroup/SampleClass.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: SampleClass ---- - -# SampleClass Class -`virtual` - -aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex -**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. - -**Group** SampleGroup - -## Namespace -apexdocs - -## Example -SampleClass sample = new SampleClass(); -sample.doSomething(); - -**Inheritance** - -[BaseClass](../miscellaneous/BaseClass) - -**Implements** - -[SampleInterface](../miscellaneous/SampleInterface), -[ParentInterface](../miscellaneous/ParentInterface) - -## Fields -### Group Name -#### `name` - -This is a sample field. - -##### Signature -```apex -private final name -``` - -##### Type -String - -### Other -#### `sampleEnumFromBase` - -*Inherited* - -##### Signature -```apex -public sampleEnumFromBase -``` - -##### Type -[SampleEnum](../sample-enums/SampleEnum) - -## Properties -### Group Name -#### `someProperty` - -##### Signature -```apex -public someProperty -``` - -##### Type -String - -## Constructors -### Other -#### `SampleClass()` - -This is a sample constructor. - -##### Signature -```apex -public SampleClass() -``` - -### Other Constructors -#### `SampleClass(name)` - -##### Signature -```apex -public SampleClass(String name) -``` - -##### Parameters -| Name | Type | Description | -|------|------|-------------| -| name | String | | - -## Methods -### Available Methods -#### `doSomething()` - -##### Signature -```apex -public void doSomething() -``` - -##### Return Type -**void** - -### Deprecated Methods -#### `sayHello()` - -`DEPRECATED` - -This is a sample method. - -##### Signature -```apex -public virtual String sayHello() -``` - -##### Return Type -**String** - -A string value. - -##### Example -SampleClass sample = new SampleClass(); -sample.doSomething(); - -## Classes -### SomeInnerClass Class - -#### Fields -##### `someInnerField` - -###### Signature -```apex -public someInnerField -``` - -###### Type -String - -#### Methods -##### `doSomething()` - -###### Signature -```apex -public void doSomething() -``` - -###### Return Type -**void** - -## Enums -### SomeEnum Enum - -This enum is used for foo and bar. - -#### Values -| Value | Description | -|-------|-------------| -| TEST_1 | This is a test. | -| TEST_2 | | -| TEST_3 | | - -## Interfaces -### SomeInterface Interface - -#### Methods -##### `doSomething()` - -###### Signature -```apex -public void doSomething() -``` - -###### Return Type -**void** \ No newline at end of file diff --git a/examples/vitepress/package.json b/examples/vitepress/package.json index 40fda193..0a735999 100644 --- a/examples/vitepress/package.json +++ b/examples/vitepress/package.json @@ -2,7 +2,7 @@ "name": "vitepress-example", "scripts": { "docs:clean": "rimraf --glob 'docs/!(.vitepress|index-frontmatter.md|api-examples.md|markdown-examples.md)'", - "apexdocs:build": "npm run docs:clean && ts-node ../../src/cli/generate.ts markdown", + "apexdocs:build": "npm run docs:clean && ts-node ../../src/cli/generate.ts", "changelog:build": "ts-node ../../src/cli/generate.ts changelog --previousVersionDir previous --currentVersionDir force-app", "docs:build": "vitepress build docs", "docs:gen": "npm run docs:clean && npm run docs:build", diff --git a/src/cli/args.ts b/src/cli/args.ts index e40760bd..08649c41 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -15,6 +15,22 @@ const configOnlyOpenApiDefaults = { exclude: [], }; +/** + * Combines the extracted configuration and arguments. + */ +export async function extractArgs(): Promise<[UserDefinedConfig]> { + const config = await _extractConfig(); + const configType = getConfigType(config); + + switch (configType._type) { + case 'no-config': + case 'single-command-config': + return [await extractArgsForCommandProvidedThroughCli(config)]; + case 'multi-command-config': + throw new Error('Not implemented yet'); + } +} + /** * Extracts configuration from a configuration file or the package.json * through cosmiconfig. @@ -27,11 +43,67 @@ function _extractConfig(): Promise { }).search(); } +export async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): Promise { + const cliArgs = _extractYargsDemandingCommand(config); + const commandName = cliArgs._[0]; + + const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; + + switch (mergedConfig.targetGenerator) { + case 'markdown': + return { ...configOnlyMarkdownDefaults, ...mergedConfig }; + case 'openapi': + return { ...configOnlyOpenApiDefaults, ...mergedConfig }; + case 'changelog': + return mergedConfig; + default: + throw new Error(`Unknown command: ${commandName}`); + } +} + +type NoConfig = { + _type: 'no-config'; +}; +type SingleCommandConfig = { + _type: 'single-command-config'; +}; +type MultiCommandConfig = { + _type: 'multi-command-config'; + commands: Generators[]; +}; + +function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfig | MultiCommandConfig { + if (!config) { + return { _type: 'no-config' }; + } + + // When the config has a shape that looks as follows: + // { + // COMMAND_NAME: ... + // } + // That means that the config is providing the name of the command, and the user is not + // expected to provide it through the CLI. + // We call this a "multi-command-config", as it allows for the config file to provide + // configuration for multiple commands at the same time. + const rootKeys = Object.keys(config.config); + const validRootKeys = ['markdown', 'openapi', 'changelog']; + const containsAnyValidRootKey = rootKeys.some((key) => validRootKeys.includes(key)); + if (containsAnyValidRootKey) { + return { + _type: 'multi-command-config', + // TODO: Throw if the same root key is provided more than once + commands: rootKeys.filter((key) => validRootKeys.includes(key)) as Generators[], + }; + } + + return { _type: 'single-command-config' }; +} + /** - * Extracts arguments from the command line. + * Extracts arguments from the command line, expecting a command to be provided. * @param config The configuration object from the configuration file, if any. */ -function _extractYargs(config?: CosmiconfigResult) { +function _extractYargsDemandingCommand(config?: CosmiconfigResult) { return yargs .config(config?.config) .command('markdown', 'Generate documentation from Apex classes as a Markdown site.', (yargs) => @@ -46,30 +118,3 @@ function _extractYargs(config?: CosmiconfigResult) { .demandCommand() .parseSync(); } - -/** - * Combines the extracted configuration and arguments. - */ -export async function extractArgs(): Promise { - const config = await _extractConfig(); - const cliArgs = _extractYargs(config); - const commandName = cliArgs._[0]; - - const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; - - // TODO: It should be possible to have a version of the config that supports - // generating all commands at the same time. So users should be able to provide the key for the main - // command, and then it should be possible to provide the specific configuration for each command. - // this is invaluable if we want to allow for the combination of changelog + markdown generation, for example - - switch (mergedConfig.targetGenerator) { - case 'markdown': - return { ...configOnlyMarkdownDefaults, ...mergedConfig }; - case 'openapi': - return { ...configOnlyOpenApiDefaults, ...mergedConfig }; - case 'changelog': - return mergedConfig; - default: - throw new Error(`Unknown command: ${commandName}`); - } -} diff --git a/src/cli/generate.ts b/src/cli/generate.ts index 16083f5d..5a0b3d2d 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -25,7 +25,11 @@ function main() { } extractArgs() - .then((config) => Apexdocs.generate(config, logger).then(parseResult)) + .then((configs) => { + for (const config of configs) { + Apexdocs.generate(config, logger).then(parseResult); + } + }) .catch(catchUnexpectedError); } diff --git a/src/core/changelog/process-change-log.ts b/src/core/changelog/process-change-log.ts index f4fde461..26c69caa 100644 --- a/src/core/changelog/process-change-log.ts +++ b/src/core/changelog/process-change-log.ts @@ -7,6 +7,8 @@ export type VersionManifest = { }; type ModificationTypes = + | 'NewType' + | 'RemovedType' | 'NewEnumValue' | 'RemovedEnumValue' | 'NewMethod' @@ -14,9 +16,7 @@ type ModificationTypes = | 'NewProperty' | 'RemovedProperty' | 'NewField' - | 'RemovedField' - | 'NewType' - | 'RemovedType'; + | 'RemovedField'; type MemberModificationType = { __typename: ModificationTypes; From 56191826ea92c8ed3f0ed2c57f233ff6e798d7ce Mon Sep 17 00:00:00 2001 From: cesarParra Date: Thu, 26 Sep 2024 08:06:27 -0400 Subject: [PATCH 39/61] Refactoring the argument extraction logic to allow for a single config file to hold multiple commands --- examples/vitepress/docs/index.md | 56 ++++ .../vitepress/docs/miscellaneous/BaseClass.md | 20 ++ .../miscellaneous/MultiInheritanceClass.md | 76 +++++ .../docs/miscellaneous/ParentInterface.md | 19 ++ .../docs/miscellaneous/ReferencedEnum.md | 15 + .../docs/miscellaneous/SampleException.md | 28 ++ .../docs/miscellaneous/SampleInterface.md | 116 +++++++ examples/vitepress/docs/miscellaneous/Url.md | 317 ++++++++++++++++++ .../vitepress/docs/sample-enums/SampleEnum.md | 40 +++ .../vitepress/docs/samplegroup/SampleClass.md | 174 ++++++++++ src/cli/args.ts | 39 ++- 11 files changed, 894 insertions(+), 6 deletions(-) create mode 100644 examples/vitepress/docs/index.md create mode 100644 examples/vitepress/docs/miscellaneous/BaseClass.md create mode 100644 examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md create mode 100644 examples/vitepress/docs/miscellaneous/ParentInterface.md create mode 100644 examples/vitepress/docs/miscellaneous/ReferencedEnum.md create mode 100644 examples/vitepress/docs/miscellaneous/SampleException.md create mode 100644 examples/vitepress/docs/miscellaneous/SampleInterface.md create mode 100644 examples/vitepress/docs/miscellaneous/Url.md create mode 100644 examples/vitepress/docs/sample-enums/SampleEnum.md create mode 100644 examples/vitepress/docs/samplegroup/SampleClass.md diff --git a/examples/vitepress/docs/index.md b/examples/vitepress/docs/index.md new file mode 100644 index 00000000..cdb614cd --- /dev/null +++ b/examples/vitepress/docs/index.md @@ -0,0 +1,56 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Apexdocs Vitepress Example" + text: "API Documentation" + tagline: My great project tagline + actions: + - theme: brand + text: Markdown Examples + link: /markdown-examples + - theme: alt + text: API Examples + link: /api-examples +--- + +# Apex Reference Guide + +## Miscellaneous + +### [BaseClass](miscellaneous/BaseClass) + +### [MultiInheritanceClass](miscellaneous/MultiInheritanceClass) + +### [ParentInterface](miscellaneous/ParentInterface) + +### [ReferencedEnum](miscellaneous/ReferencedEnum) + +### [SampleException](miscellaneous/SampleException) + +This is a sample exception. + +### [SampleInterface](miscellaneous/SampleInterface) + +This is a sample interface + +### [Url](miscellaneous/Url) + +Represents a uniform resource locator (URL) and provides access to parts of the URL. +Enables access to the base URL used to access your Salesforce org. + +## Sample Enums + +### [SampleEnum](sample-enums/SampleEnum) + +This is a sample enum. This references [ReferencedEnum](miscellaneous/ReferencedEnum) . + +This description has several lines + +## SampleGroup + +### [SampleClass](samplegroup/SampleClass) + +aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex +**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/BaseClass.md b/examples/vitepress/docs/miscellaneous/BaseClass.md new file mode 100644 index 00000000..62d3cf76 --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/BaseClass.md @@ -0,0 +1,20 @@ +--- +title: BaseClass +--- + +# BaseClass Class +`abstract` + +## Namespace +apexdocs + +## Fields +### `sampleEnumFromBase` + +#### Signature +```apex +public sampleEnumFromBase +``` + +#### Type +[SampleEnum](../sample-enums/SampleEnum) \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md b/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md new file mode 100644 index 00000000..f0470f9b --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/MultiInheritanceClass.md @@ -0,0 +1,76 @@ +--- +title: MultiInheritanceClass +--- + +# MultiInheritanceClass Class + +## Namespace +apexdocs + +**Inheritance** + +[SampleClass](../samplegroup/SampleClass) < [BaseClass](BaseClass) + +## Fields +### `sampleEnumFromBase` + +*Inherited* + +#### Signature +```apex +public sampleEnumFromBase +``` + +#### Type +[SampleEnum](../sample-enums/SampleEnum) + +## Properties +### Group Name +#### `someProperty` + +*Inherited* + +##### Signature +```apex +public someProperty +``` + +##### Type +String + +## Methods +### Available Methods +#### `doSomething()` + +*Inherited* + +##### Signature +```apex +public void doSomething() +``` + +##### Return Type +**void** + +### Deprecated Methods +#### `sayHello()` + +*Inherited* + +`DEPRECATED` + +This is a sample method. + +##### Signature +```apex +public virtual String sayHello() +``` + +##### Return Type +**String** + +A string value. + +##### Example +SampleClass sample = new SampleClass(); +sample.doSomething(); \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/ParentInterface.md b/examples/vitepress/docs/miscellaneous/ParentInterface.md new file mode 100644 index 00000000..6bbe2741 --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/ParentInterface.md @@ -0,0 +1,19 @@ +--- +title: ParentInterface +--- + +# ParentInterface Interface + +## Namespace +apexdocs + +## Methods +### `sampleParentMethod()` + +#### Signature +```apex +public void sampleParentMethod() +``` + +#### Return Type +**void** \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/ReferencedEnum.md b/examples/vitepress/docs/miscellaneous/ReferencedEnum.md new file mode 100644 index 00000000..69e97a43 --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/ReferencedEnum.md @@ -0,0 +1,15 @@ +--- +title: ReferencedEnum +--- + +# ReferencedEnum Enum + +## Namespace +apexdocs + +## Values +| Value | Description | +|-------|-------------| +| VALUE1 | | +| VALUE2 | | +| VALUE3 | | \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/SampleException.md b/examples/vitepress/docs/miscellaneous/SampleException.md new file mode 100644 index 00000000..cc919c74 --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/SampleException.md @@ -0,0 +1,28 @@ +--- +title: SampleException +--- + +# SampleException Class + +This is a sample exception. + +**Usage** + +You can use the exception the following way. +You can also take a look at [SampleClass](../samplegroup/SampleClass) to see how it is used. +This is a dangerous HTML tag: <script>alert('Hello');</script> + +```apex +try { + throw new SampleException(); +} catch (SampleException e) { + System.debug('Caught exception'); +} +``` + +## Namespace +apexdocs + +**Inheritance** + +Exception \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/SampleInterface.md b/examples/vitepress/docs/miscellaneous/SampleInterface.md new file mode 100644 index 00000000..2a719b54 --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/SampleInterface.md @@ -0,0 +1,116 @@ +--- +title: SampleInterface +--- + +# SampleInterface Interface + +`NAMESPACEACCESSIBLE` + +This is a sample interface + +**Mermaid** + +graph TD +A[SampleInterface] -->|extends| B[ParentInterface] +B -->|extends| C[GrandParentInterface] +C -->|extends| D[GreatGrandParentInterface] + +**Author** John Doe + +**Date** 2020-01-01 + +**See** [SampleEnum](../sample-enums/SampleEnum) + +**See** [ReferencedEnum](ReferencedEnum) + +## Namespace +apexdocs + +## Example +SampleInterface sampleInterface = new SampleInterface(); +sampleInterface.sampleMethod(); + +**Extends** +[ParentInterface](ParentInterface) + +## Methods +### `sampleMethod()` + +`NAMESPACEACCESSIBLE` + +This is a sample method + +**Custom Tag** + +This is a custom tag + +**Another Custom Tag** + +This is another custom tag + +**Mermaid** + +graph TD +A[SampleInterface] -->|extends| B[ParentInterface] +B -->|extends| C[GrandParentInterface] +C -->|extends| D[GreatGrandParentInterface] + +#### Signature +```apex +public String sampleMethod() +``` + +#### Return Type +**String** + +Some return value + +#### Throws +[SampleException](SampleException): This is a sample exception + +AnotherSampleException: This is another sample exception + +#### Example +SampleInterface sampleInterface = new SampleInterface(); +sampleInterface.sampleMethod(); + +--- + +### `sampleMethodWithParams(param1, param2, theEnum)` + +`NAMESPACEACCESSIBLE` +`DEPRECATED` + +This is a sample method with parameters +Sometimes it won't be possible to find a NonExistent link. + +#### Signature +```apex +public SampleEnum sampleMethodWithParams(String param1, Integer param2, SampleEnum theEnum) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| param1 | String | This is the first parameter | +| param2 | Integer | This is the second parameter | +| theEnum | [SampleEnum](../sample-enums/SampleEnum) | This is an enum parameter | + +#### Return Type +**[SampleEnum](../sample-enums/SampleEnum)** + +Some return value + +--- + +### `sampleParentMethod()` + +*Inherited* + +#### Signature +```apex +public void sampleParentMethod() +``` + +#### Return Type +**void** \ No newline at end of file diff --git a/examples/vitepress/docs/miscellaneous/Url.md b/examples/vitepress/docs/miscellaneous/Url.md new file mode 100644 index 00000000..ff2038da --- /dev/null +++ b/examples/vitepress/docs/miscellaneous/Url.md @@ -0,0 +1,317 @@ +--- +title: Url +--- + +# Url Class + +Represents a uniform resource locator (URL) and provides access to parts of the URL. +Enables access to the base URL used to access your Salesforce org. + +**Usage** + +Use the methods of the `System.URL` class to create links to objects in your organization. Such objects can be files, images, +logos, or records that you want to include in external emails, in activities, or in Chatter posts. For example, you can create +a link to a file uploaded as an attachment to a Chatter post by concatenating the Salesforce base URL with the file ID: + +```apex +// Get a file uploaded through Chatter. +ContentDocument doc = [SELECT Id FROM ContentDocument + WHERE Title = 'myfile']; +// Create a link to the file. +String fullFileURL = URL.getOrgDomainURL().toExternalForm() + + '/' + doc.id; +System.debug(fullFileURL); +``` + + +The following example creates a link to a Salesforce record. The full URL is created by concatenating the Salesforce base +URL with the record ID. + +```apex +Account acct = [SELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1]; +String fullRecordURL = URL.getOrgDomainURL().toExternalForm() + '/' + acct.Id; +``` + +**Version Behavior Changes** + +In API version 41.0 and later, Apex URL objects are represented by the java.net.URI type, not the java.net.URL type. + +The API version in which the URL object was instantiated determines the behavior of subsequent method calls to the +specific instance. Salesforce strongly encourages you to use API 41.0 and later versions for fully RFC-compliant URL +parsing that includes proper handling of edge cases of complex URL structures. API 41.0 and later versions also enforce +that inputs are valid, RFC-compliant URL or URI strings. + +* [URL Constructors](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_url.htm#apex_System_URL_constructors) +* [URL Methods](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_url.htm#apex_System_URL_methods) + +**See Also** +* [URL Class](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_url.htm) + +## Namespace +apexdocs + +## Example +In this example, the base URL and the full request URL of the current Salesforce server instance are retrieved. Next, a URL +pointing to a specific account object is created. Finally, components of the base and full URL are obtained. This example +prints out all the results to the debug log output. + +```apex +// Create a new account called Acme that we will create a link for later. +Account myAccount = new Account(Name='Acme'); +insert myAccount; + +// Get the base URL. +String sfdcBaseURL = URL.getOrgDomainURL().toExternalForm(); +System.debug('Base URL: ' + sfdcBaseURL ); + +// Get the URL for the current request. +String currentRequestURL = URL.getCurrentRequestUrl().toExternalForm(); +System.debug('Current request URL: ' + currentRequestURL); + +// Create the account URL from the base URL. +String accountURL = URL.getOrgDomainURL().toExternalForm() + + '/' + myAccount.Id; +System.debug('URL of a particular account: ' + accountURL); + +// Get some parts of the base URL. +System.debug('Host: ' + URL.getOrgDomainURL().getHost()); +System.debug('Protocol: ' + URL.getOrgDomainURL().getProtocol()); + +// Get the query string of the current request. +System.debug('Query: ' + URL.getCurrentRequestUrl().getQuery()); +``` + +## Constructors +### `Url(spec)` + +Creates a new instance of the URL class using the specified string representation of the URL. + +#### Signature +```apex +global Url(String spec) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| spec | String | The string to parse as a URL. | + +--- + +### `Url(context, spec)` + +Creates a new instance of the URL class by parsing the specified spec within the specified context. + +**Usage** + +The new URL is created from the given context URL and the spec argument as described in RFC2396 "Uniform Resource Identifiers : Generic * Syntax" : +```xml +://?# +``` + + +For more information about the arguments of this constructor, see the corresponding URL(java.net.URL, java.lang.String) constructor for Java. + +#### Signature +```apex +global Url(Url context, String spec) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| context | [Url](Url) | The context in which to parse the specification. | +| spec | String | The string to parse as a URL. | + +--- + +### `Url(protocol, host, file)` + +Creates a new instance of the URL class using the specified protocol, host, and file on the host. The default port for the specified protocol is used. + +#### Signature +```apex +global Url(String protocol, String host, String file) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| protocol | String | The protocol name for this URL. | +| host | String | The host name for this URL. | +| file | String | The file name for this URL. | + +--- + +### `Url(protocol, host, port, file)` + +Creates a new instance of the URL class using the specified protocol, host, port number, and file on the host. + +#### Signature +```apex +global Url(String protocol, String host, Integer port, String file) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| protocol | String | The protocol name for this URL. | +| host | String | The host name for this URL. | +| port | Integer | The port number for this URL. | +| file | String | The file name for this URL. | + +## Methods +### `getAuthority()` + +Returns the authority portion of the current URL. + +#### Signature +```apex +global String getAuthority() +``` + +#### Return Type +**String** + +The authority portion of the current URL. + +--- + +### `getCurrentRequestUrl()` + +Returns the URL of an entire request on a Salesforce instance. + +**Usage** + +An example of a URL for an entire request is https://yourInstance.salesforce.com/apex/myVfPage.apexp. + +#### Signature +```apex +global static Url getCurrentRequestUrl() +``` + +#### Return Type +**[Url](Url)** + +The URL of the entire request. + +--- + +### `getDefPort()` + +Returns the default port number of the protocol associated with the current URL. + +**Usage** + +Returns -1 if the URL scheme or the stream protocol handler for the URL doesn't define a default port number. + +#### Signature +```apex +global Integer getDefPort() +``` + +#### Return Type +**Integer** + +The default port number of the protocol associated with the current URL. + +--- + +### `getFile()` + +Returns the file name of the current URL. + +#### Signature +```apex +global String getFile() +``` + +#### Return Type +**String** + +The file name of the current URL. + +--- + +### `getFileFieldURL(entityId, fieldName)` + +Returns the download URL for a file attachment. + +#### Signature +```apex +global static String getFileFieldURL(String entityId, String fieldName) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| entityId | String | Specifies the ID of the entity that holds the file data. | +| fieldName | String | Specifies the API name of a file field component, such as `AttachmentBody` . | + +#### Return Type +**String** + +The download URL for the file attachment. + +#### Example +String fileURL = +URL.getFileFieldURL( +'087000000000123' , +'AttachmentBody'); + +--- + +### `getHost()` + +Returns the host name of the current URL. + +#### Signature +```apex +global String getHost() +``` + +#### Return Type +**String** + +The host name of the current URL. + +--- + +### `getOrgDomainUrl()` + +Returns the canonical URL for your org. For example, https://MyDomainName.my.salesforce.com. + +**Usage** + +Use getOrgDomainUrl() to interact with Salesforce REST and SOAP APIs in Apex code. Get endpoints for User Interface API calls, for creating and customizing picklist value sets and custom fields, and more. + + `getOrgDomainUrl()` can access the domain URL only for the org in which the Apex code is running. + +You don't need a RemoteSiteSetting for your org to interact with the Salesforce APIs using domain URLs retrieved with this method. + +**See Also** + +* [Lightning Aura Components Developer Guide: Making API Calls from Apex](https://developer.salesforce.com/docs/atlas.en-us.250.0.lightning.meta/lightning/apex_api_calls.htm) + +#### Signature +```apex +global static Url getOrgDomainUrl() +``` + +#### Return Type +**[Url](Url)** + +getOrgDomainUrl() always returns the login URL for your org, regardless of context. Use that URL when making API calls to your org. + +#### Example +This example uses the Salesforce REST API to get organization limit values. For information on limits, see Limits in the REST API Developer Guide. + +```apex +Http h = new Http(); +HttpRequest req = new HttpRequest(); +req.setEndpoint(Url.getOrgDomainUrl().toExternalForm() + + '/services/data/v44.0/limits'); +req.setMethod('GET'); +req.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId()); +HttpResponse res = h.send(req); +``` \ No newline at end of file diff --git a/examples/vitepress/docs/sample-enums/SampleEnum.md b/examples/vitepress/docs/sample-enums/SampleEnum.md new file mode 100644 index 00000000..539f01e0 --- /dev/null +++ b/examples/vitepress/docs/sample-enums/SampleEnum.md @@ -0,0 +1,40 @@ +--- +title: SampleEnum +--- + +# SampleEnum Enum + +`NAMESPACEACCESSIBLE` + +This is a sample enum. This references [ReferencedEnum](../miscellaneous/ReferencedEnum) . + +This description has several lines + +**Some Custom** + +Test. I can also have a [ReferencedEnum](../miscellaneous/ReferencedEnum) here. +And it can be multiline. + +**Mermaid** + +graph TD +A[SampleEnum] -->|references| B[ReferencedEnum] +B -->|referenced by| A + +**Group** Sample Enums + +**Author** John Doe + +**Date** 2022-01-01 + +**See** [ReferencedEnum](../miscellaneous/ReferencedEnum) + +## Namespace +apexdocs + +## Values +| Value | Description | +|-------|-------------| +| VALUE1 | This is value 1 | +| VALUE2 | This is value 2 | +| VALUE3 | This is value 3 | \ No newline at end of file diff --git a/examples/vitepress/docs/samplegroup/SampleClass.md b/examples/vitepress/docs/samplegroup/SampleClass.md new file mode 100644 index 00000000..38de2d1f --- /dev/null +++ b/examples/vitepress/docs/samplegroup/SampleClass.md @@ -0,0 +1,174 @@ +--- +title: SampleClass +--- + +# SampleClass Class +`virtual` + +aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex +**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. + +**Group** SampleGroup + +## Namespace +apexdocs + +## Example +SampleClass sample = new SampleClass(); +sample.doSomething(); + +**Inheritance** + +[BaseClass](../miscellaneous/BaseClass) + +**Implements** + +[SampleInterface](../miscellaneous/SampleInterface), +[ParentInterface](../miscellaneous/ParentInterface) + +## Fields +### Group Name +#### `name` + +This is a sample field. + +##### Signature +```apex +private final name +``` + +##### Type +String + +### Other +#### `sampleEnumFromBase` + +*Inherited* + +##### Signature +```apex +public sampleEnumFromBase +``` + +##### Type +[SampleEnum](../sample-enums/SampleEnum) + +## Properties +### Group Name +#### `someProperty` + +##### Signature +```apex +public someProperty +``` + +##### Type +String + +## Constructors +### Other +#### `SampleClass()` + +This is a sample constructor. + +##### Signature +```apex +public SampleClass() +``` + +### Other Constructors +#### `SampleClass(name)` + +##### Signature +```apex +public SampleClass(String name) +``` + +##### Parameters +| Name | Type | Description | +|------|------|-------------| +| name | String | | + +## Methods +### Available Methods +#### `doSomething()` + +##### Signature +```apex +public void doSomething() +``` + +##### Return Type +**void** + +### Deprecated Methods +#### `sayHello()` + +`DEPRECATED` + +This is a sample method. + +##### Signature +```apex +public virtual String sayHello() +``` + +##### Return Type +**String** + +A string value. + +##### Example +SampleClass sample = new SampleClass(); +sample.doSomething(); + +## Classes +### SomeInnerClass Class + +#### Fields +##### `someInnerField` + +###### Signature +```apex +public someInnerField +``` + +###### Type +String + +#### Methods +##### `doSomething()` + +###### Signature +```apex +public void doSomething() +``` + +###### Return Type +**void** + +## Enums +### SomeEnum Enum + +This enum is used for foo and bar. + +#### Values +| Value | Description | +|-------|-------------| +| TEST_1 | This is a test. | +| TEST_2 | | +| TEST_3 | | + +## Interfaces +### SomeInterface Interface + +#### Methods +##### `doSomething()` + +###### Signature +```apex +public void doSomething() +``` + +###### Return Type +**void** \ No newline at end of file diff --git a/src/cli/args.ts b/src/cli/args.ts index 08649c41..2b23544c 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -18,8 +18,8 @@ const configOnlyOpenApiDefaults = { /** * Combines the extracted configuration and arguments. */ -export async function extractArgs(): Promise<[UserDefinedConfig]> { - const config = await _extractConfig(); +export async function extractArgs(): Promise { + const config = await extractConfig(); const configType = getConfigType(config); switch (configType._type) { @@ -27,7 +27,7 @@ export async function extractArgs(): Promise<[UserDefinedConfig]> { case 'single-command-config': return [await extractArgsForCommandProvidedThroughCli(config)]; case 'multi-command-config': - throw new Error('Not implemented yet'); + return extractArgsForCommandsProvidedInConfig(config!.config); } } @@ -35,7 +35,7 @@ export async function extractArgs(): Promise<[UserDefinedConfig]> { * Extracts configuration from a configuration file or the package.json * through cosmiconfig. */ -function _extractConfig(): Promise { +function extractConfig(): Promise { return cosmiconfig('apexdocs', { loaders: { '.ts': TypeScriptLoader(), @@ -43,7 +43,7 @@ function _extractConfig(): Promise { }).search(); } -export async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): Promise { +async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): Promise { const cliArgs = _extractYargsDemandingCommand(config); const commandName = cliArgs._[0]; @@ -61,6 +61,27 @@ export async function extractArgsForCommandProvidedThroughCli(config: Cosmiconfi } } +type ConfigByGenerator = { + [key in Generators]: UserDefinedConfig; +}; + +async function extractArgsForCommandsProvidedInConfig(config: ConfigByGenerator): Promise { + // TODO: Use yargs for checking that things are ok per command + // TODO: Throw if a command was still passed through the CLI + return Object.entries(config).map(([generator, generatorConfig]) => { + switch (generator) { + case 'markdown': + return { ...configOnlyMarkdownDefaults, ...generatorConfig }; + case 'openapi': + return { ...configOnlyOpenApiDefaults, ...generatorConfig }; + case 'changelog': + return generatorConfig; + default: + throw new Error(`Unknown command: ${generator}`); + } + }); +} + type NoConfig = { _type: 'no-config'; }; @@ -89,10 +110,16 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi const validRootKeys = ['markdown', 'openapi', 'changelog']; const containsAnyValidRootKey = rootKeys.some((key) => validRootKeys.includes(key)); if (containsAnyValidRootKey) { + const commands = rootKeys.filter((key) => validRootKeys.includes(key)); + const hasInvalidCommands = rootKeys.some((key) => !validRootKeys.includes(key)); + if (hasInvalidCommands) { + throw new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`); + } + return { _type: 'multi-command-config', // TODO: Throw if the same root key is provided more than once - commands: rootKeys.filter((key) => validRootKeys.includes(key)) as Generators[], + commands: commands as Generators[], }; } From a77ee8eca41a2f333528ea9bfb5eb8b8285df687 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Thu, 26 Sep 2024 09:39:45 -0400 Subject: [PATCH 40/61] Validating that the correct options are passed when using a config file with multiple configs --- examples/vitepress/apexdocs.config.ts | 6 +++- examples/vitepress/docs/changelog.md | 43 +++++++++++++++++++++++++++ src/cli/args.ts | 35 ++++++++++++++++++++-- src/cli/generate.ts | 4 +-- src/index.ts | 19 +++++++++++- 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 examples/vitepress/docs/changelog.md diff --git a/examples/vitepress/apexdocs.config.ts b/examples/vitepress/apexdocs.config.ts index b7764cf8..2fad539c 100644 --- a/examples/vitepress/apexdocs.config.ts +++ b/examples/vitepress/apexdocs.config.ts @@ -1,4 +1,4 @@ -import { defineMarkdownConfig, DocPageData } from '../../src'; +import { defineChangelogConfig, defineMarkdownConfig, DocPageData } from '../../src'; import * as fs from 'node:fs'; function loadFileAsync(filePath: string): Promise { @@ -26,6 +26,10 @@ function writeFileAsync(filePath: string, data: string): Promise { } export default { + changelog: defineChangelogConfig({ + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + }), markdown: defineMarkdownConfig({ sourceDir: 'force-app', scope: ['global', 'public', 'protected', 'private', 'namespaceaccessible'], diff --git a/examples/vitepress/docs/changelog.md b/examples/vitepress/docs/changelog.md new file mode 100644 index 00000000..59bf6f89 --- /dev/null +++ b/examples/vitepress/docs/changelog.md @@ -0,0 +1,43 @@ +# Changelog + +## New Classes + +These classes are new. + +### BaseClass + +### MultiInheritanceClass + +### Url + +Represents a uniform resource locator (URL) and provides access to parts of the URL. +Enables access to the base URL used to access your Salesforce org. +### SampleClass + +aliquip ex sunt officia ullamco anim deserunt magna aliquip nisi eiusmod in sit officia veniam ex +**deserunt** ea officia exercitation laboris enim in duis quis enim eiusmod eu amet cupidatat. +### SampleException + +This is a sample exception. + +## New Interfaces + +These interfaces are new. + +### ParentInterface + +### SampleInterface + +This is a sample interface + +## New Enums + +These enums are new. + +### ReferencedEnum + +### SampleEnum + +This is a sample enum. This references ReferencedEnum . + +This description has several lines \ No newline at end of file diff --git a/src/cli/args.ts b/src/cli/args.ts index 2b23544c..18a67746 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -6,6 +6,11 @@ import { markdownOptions } from './commands/markdown'; import { openApiOptions } from './commands/openapi'; import { changeLogOptions } from './commands/changelog'; +// TODO: Never throw in here, because if we throw then we will +// get the message saying that "an unexpected error occurred", +// but in reality we know what the failure was, so we should +// accurately reflect that + const configOnlyMarkdownDefaults: Partial = { excludeTags: [], exclude: [], @@ -44,7 +49,7 @@ function extractConfig(): Promise { } async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): Promise { - const cliArgs = _extractYargsDemandingCommand(config); + const cliArgs = extractYargsDemandingCommand(config); const commandName = cliArgs._[0]; const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; @@ -66,15 +71,17 @@ type ConfigByGenerator = { }; async function extractArgsForCommandsProvidedInConfig(config: ConfigByGenerator): Promise { - // TODO: Use yargs for checking that things are ok per command // TODO: Throw if a command was still passed through the CLI return Object.entries(config).map(([generator, generatorConfig]) => { switch (generator) { case 'markdown': + validateConfig('markdown', generatorConfig); return { ...configOnlyMarkdownDefaults, ...generatorConfig }; case 'openapi': + validateConfig('openapi', generatorConfig); return { ...configOnlyOpenApiDefaults, ...generatorConfig }; case 'changelog': + validateConfig('changelog', generatorConfig); return generatorConfig; default: throw new Error(`Unknown command: ${generator}`); @@ -130,7 +137,7 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi * Extracts arguments from the command line, expecting a command to be provided. * @param config The configuration object from the configuration file, if any. */ -function _extractYargsDemandingCommand(config?: CosmiconfigResult) { +function extractYargsDemandingCommand(config?: CosmiconfigResult) { return yargs .config(config?.config) .command('markdown', 'Generate documentation from Apex classes as a Markdown site.', (yargs) => @@ -145,3 +152,25 @@ function _extractYargsDemandingCommand(config?: CosmiconfigResult) { .demandCommand() .parseSync(); } + +function validateConfig(command: Generators, config: UserDefinedConfig) { + function getOptions(generator: Generators) { + switch (generator) { + case 'markdown': + return markdownOptions; + case 'openapi': + return openApiOptions; + case 'changelog': + return changeLogOptions; + } + } + + const options = getOptions(command); + yargs + .config(config) + .options(options) + .fail((msg) => { + throw new Error(`Invalid configuration for command "${command}": ${msg}`); + }) + .parse(); +} diff --git a/src/cli/generate.ts b/src/cli/generate.ts index 5a0b3d2d..3e590587 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -25,9 +25,9 @@ function main() { } extractArgs() - .then((configs) => { + .then(async (configs) => { for (const config of configs) { - Apexdocs.generate(config, logger).then(parseResult); + await Apexdocs.generate(config, logger).then(parseResult); } }) .catch(catchUnexpectedError); diff --git a/src/index.ts b/src/index.ts index 5387d317..37b5ae82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,9 @@ import type { TransformReference, ConfigurableDocPageReference, UserDefinedOpenApiConfig, + UserDefinedChangelogConfig, } from './core/shared/types'; -import { markdownDefaults, openApiDefaults } from './defaults'; +import { changeLogDefaults, markdownDefaults, openApiDefaults } from './defaults'; import { process } from './node/process'; type ConfigurableMarkdownConfig = Omit, 'targetGenerator'>; @@ -44,6 +45,20 @@ function defineOpenApiConfig(config: ConfigurableOpenApiConfig): Partial, 'targetGenerator'>; + +/** + * Helper function to define a configuration to generate a changelog. + * @param config The configuration to use. + */ +function defineChangelogConfig(config: ConfigurableChangelogConfig): Partial { + return { + ...changeLogDefaults, + ...config, + targetGenerator: 'changelog' as const, + }; +} + /** * Represents a file to be skipped. */ @@ -58,6 +73,8 @@ export { ConfigurableMarkdownConfig, defineOpenApiConfig, ConfigurableOpenApiConfig, + defineChangelogConfig, + ConfigurableChangelogConfig, skip, TransformReferenceGuide, TransformDocs, From 76e11c00992b186ba36eabb8b41c80ca3399303e Mon Sep 17 00:00:00 2001 From: cesarParra Date: Thu, 26 Sep 2024 09:41:53 -0400 Subject: [PATCH 41/61] Validating that the correct options are passed when using a config file with multiple configs --- src/util/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/logger.ts b/src/util/logger.ts index 50dd2c7e..37ff41d1 100644 --- a/src/util/logger.ts +++ b/src/util/logger.ts @@ -9,7 +9,7 @@ export interface Logger { /** * Logs messages to the console. */ -export class StdOutLogger { +export class StdOutLogger implements Logger { /** * Logs a message with optional arguments. * @param message The message to log. From 775a26d02cf3eedd38780eb782acecc12ffed8b8 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Thu, 26 Sep 2024 19:28:53 -0400 Subject: [PATCH 42/61] Functional error handling when processing args. --- examples/vitepress/package.json | 1 - src/cli/args.ts | 82 +++++++++++++++++++-------------- src/cli/generate.ts | 27 +++++------ 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/examples/vitepress/package.json b/examples/vitepress/package.json index 0a735999..fdf2595c 100644 --- a/examples/vitepress/package.json +++ b/examples/vitepress/package.json @@ -3,7 +3,6 @@ "scripts": { "docs:clean": "rimraf --glob 'docs/!(.vitepress|index-frontmatter.md|api-examples.md|markdown-examples.md)'", "apexdocs:build": "npm run docs:clean && ts-node ../../src/cli/generate.ts", - "changelog:build": "ts-node ../../src/cli/generate.ts changelog --previousVersionDir previous --currentVersionDir force-app", "docs:build": "vitepress build docs", "docs:gen": "npm run docs:clean && npm run docs:build", "docs:dev": "vitepress dev docs", diff --git a/src/cli/args.ts b/src/cli/args.ts index 18a67746..b1c85fed 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,15 +1,12 @@ import { cosmiconfig, CosmiconfigResult } from 'cosmiconfig'; import * as yargs from 'yargs'; +import * as E from 'fp-ts/Either'; import { Generators, UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; import { TypeScriptLoader } from 'cosmiconfig-typescript-loader'; import { markdownOptions } from './commands/markdown'; import { openApiOptions } from './commands/openapi'; import { changeLogOptions } from './commands/changelog'; - -// TODO: Never throw in here, because if we throw then we will -// get the message saying that "an unexpected error occurred", -// but in reality we know what the failure was, so we should -// accurately reflect that +import { pipe } from 'fp-ts/function'; const configOnlyMarkdownDefaults: Partial = { excludeTags: [], @@ -23,14 +20,14 @@ const configOnlyOpenApiDefaults = { /** * Combines the extracted configuration and arguments. */ -export async function extractArgs(): Promise { +export async function extractArgs(): Promise> { const config = await extractConfig(); const configType = getConfigType(config); switch (configType._type) { case 'no-config': case 'single-command-config': - return [await extractArgsForCommandProvidedThroughCli(config)]; + return handleSingleCommand(config); case 'multi-command-config': return extractArgsForCommandsProvidedInConfig(config!.config); } @@ -48,7 +45,15 @@ function extractConfig(): Promise { }).search(); } -async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): Promise { +function handleSingleCommand(config: CosmiconfigResult) { + return pipe( + E.right(config), + E.flatMap(extractArgsForCommandProvidedThroughCli), + E.map((config) => [config]), + ); +} + +function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): E.Either { const cliArgs = extractYargsDemandingCommand(config); const commandName = cliArgs._[0]; @@ -56,13 +61,13 @@ async function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult switch (mergedConfig.targetGenerator) { case 'markdown': - return { ...configOnlyMarkdownDefaults, ...mergedConfig }; + return E.right({ ...configOnlyMarkdownDefaults, ...mergedConfig }); case 'openapi': - return { ...configOnlyOpenApiDefaults, ...mergedConfig }; + return E.right({ ...configOnlyOpenApiDefaults, ...mergedConfig }); case 'changelog': - return mergedConfig; + return E.right(mergedConfig); default: - throw new Error(`Unknown command: ${commandName}`); + throw E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); } } @@ -70,23 +75,29 @@ type ConfigByGenerator = { [key in Generators]: UserDefinedConfig; }; -async function extractArgsForCommandsProvidedInConfig(config: ConfigByGenerator): Promise { - // TODO: Throw if a command was still passed through the CLI - return Object.entries(config).map(([generator, generatorConfig]) => { - switch (generator) { +function extractArgsForCommandsProvidedInConfig(config: ConfigByGenerator) { + // TODO: Error if a command was still passed through the CLI + const configs = Object.entries(config).map(([generator, generatorConfig]) => { + switch (generator as Generators) { case 'markdown': - validateConfig('markdown', generatorConfig); - return { ...configOnlyMarkdownDefaults, ...generatorConfig }; + return pipe( + validateConfig('markdown', generatorConfig), + E.map(() => ({ ...configOnlyMarkdownDefaults, ...generatorConfig })), + ); case 'openapi': - validateConfig('openapi', generatorConfig); - return { ...configOnlyOpenApiDefaults, ...generatorConfig }; + return pipe( + validateConfig('openapi', generatorConfig), + E.map(() => ({ ...configOnlyOpenApiDefaults, ...generatorConfig })), + ); case 'changelog': - validateConfig('changelog', generatorConfig); - return generatorConfig; - default: - throw new Error(`Unknown command: ${generator}`); + return pipe( + validateConfig('changelog', generatorConfig), + E.map(() => generatorConfig), + ); } }); + + return E.sequenceArray(configs); } type NoConfig = { @@ -106,9 +117,10 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi } // When the config has a shape that looks as follows: - // { - // COMMAND_NAME: ... - // } + // Partial<{ + // COMMAND_NAME: ..., + // ANOTHER_COMMAND_NAME: ..., + // }> // That means that the config is providing the name of the command, and the user is not // expected to provide it through the CLI. // We call this a "multi-command-config", as it allows for the config file to provide @@ -166,11 +178,13 @@ function validateConfig(command: Generators, config: UserDefinedConfig) { } const options = getOptions(command); - yargs - .config(config) - .options(options) - .fail((msg) => { - throw new Error(`Invalid configuration for command "${command}": ${msg}`); - }) - .parse(); + return E.tryCatch(() => { + yargs + .config(config) + .options(options) + .fail((msg) => { + throw new Error(`Invalid configuration for command "${command}": ${msg}`); + }) + .parse(); + }, E.toError); } diff --git a/src/cli/generate.ts b/src/cli/generate.ts index 3e590587..b0eee5ae 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -3,32 +3,29 @@ import { Apexdocs } from '../application/Apexdocs'; import { extractArgs } from './args'; import { StdOutLogger } from '#utils/logger'; import * as E from 'fp-ts/Either'; +import { UserDefinedConfig } from '../core/shared/types'; const logger = new StdOutLogger(); function main() { function parseResult(result: E.Either) { - E.match( - (error) => { - logger.error(`❌ An error occurred while processing the request: ${error}`); - process.exit(1); - }, - (successMessage: string) => { - logger.logSingle(successMessage); - }, - )(result); + E.match(catchUnexpectedError, (successMessage: string) => { + logger.logSingle(successMessage); + })(result); } - function catchUnexpectedError(error: Error) { - logger.error(`❌ An unexpected error occurred: ${error.message}`); + function catchUnexpectedError(error: Error | unknown) { + logger.error(`❌ An error occurred while processing the request: ${error}`); process.exit(1); } extractArgs() - .then(async (configs) => { - for (const config of configs) { - await Apexdocs.generate(config, logger).then(parseResult); - } + .then(async (maybeConfigs) => { + E.match(catchUnexpectedError, async (configs: readonly UserDefinedConfig[]) => { + for (const config of configs) { + await Apexdocs.generate(config, logger).then(parseResult); + } + })(maybeConfigs); }) .catch(catchUnexpectedError); } From 0505f0257790faecdef993651df0de29fcaedb8f Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 27 Sep 2024 08:04:55 -0400 Subject: [PATCH 43/61] Test - extracts arguments for markdown --- src/cli/__tests__/args.spec.ts | 23 +++++++++++++++++++++++ src/cli/args.ts | 28 ++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 src/cli/__tests__/args.spec.ts diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args.spec.ts new file mode 100644 index 00000000..00cb05af --- /dev/null +++ b/src/cli/__tests__/args.spec.ts @@ -0,0 +1,23 @@ +import { extractArgs } from '../args'; +import { assertEither } from '../../core/test-helpers/assert-either'; +import { UserDefinedMarkdownConfig } from '../../core/shared/types'; + +describe('when extracting arguments', () => { + describe('and no configuration is provided', () => { + it('extracts the arguments from the process for the markdown command', async () => { + function getFromProcess() { + return ['markdown', '--sourceDir', 'force-app']; + } + + const result = await extractArgs(getFromProcess); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('markdown'); + + const markdownConfig = configs[0] as UserDefinedMarkdownConfig; + expect(markdownConfig.sourceDir).toEqual('force-app'); + }); + }); + }); +}); diff --git a/src/cli/args.ts b/src/cli/args.ts index b1c85fed..310c8707 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -17,17 +17,24 @@ const configOnlyOpenApiDefaults = { exclude: [], }; +type ExtractArgsFromProcess = () => string[]; +function getArgumentsFromProcess() { + return process.argv.slice(2); +} + /** * Combines the extracted configuration and arguments. */ -export async function extractArgs(): Promise> { +export async function extractArgs( + extractFromProcessFn: ExtractArgsFromProcess = getArgumentsFromProcess, +): Promise> { const config = await extractConfig(); const configType = getConfigType(config); switch (configType._type) { case 'no-config': case 'single-command-config': - return handleSingleCommand(config); + return handleSingleCommand(extractFromProcessFn, config); case 'multi-command-config': return extractArgsForCommandsProvidedInConfig(config!.config); } @@ -45,16 +52,19 @@ function extractConfig(): Promise { }).search(); } -function handleSingleCommand(config: CosmiconfigResult) { +function handleSingleCommand(extractFromProcessFn: ExtractArgsFromProcess, config: CosmiconfigResult) { return pipe( E.right(config), - E.flatMap(extractArgsForCommandProvidedThroughCli), + E.flatMap((config) => extractArgsForCommandProvidedThroughCli(extractFromProcessFn, config)), E.map((config) => [config]), ); } -function extractArgsForCommandProvidedThroughCli(config: CosmiconfigResult): E.Either { - const cliArgs = extractYargsDemandingCommand(config); +function extractArgsForCommandProvidedThroughCli( + extractFromProcessFn: ExtractArgsFromProcess, + config: CosmiconfigResult, +): E.Either { + const cliArgs = extractYargsDemandingCommand(extractFromProcessFn, config); const commandName = cliArgs._[0]; const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; @@ -132,6 +142,7 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi const commands = rootKeys.filter((key) => validRootKeys.includes(key)); const hasInvalidCommands = rootKeys.some((key) => !validRootKeys.includes(key)); if (hasInvalidCommands) { + // TODO: Do not throw throw new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`); } @@ -147,9 +158,10 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi /** * Extracts arguments from the command line, expecting a command to be provided. + * @param extractFromProcessFn The function that extracts the arguments from the process. * @param config The configuration object from the configuration file, if any. */ -function extractYargsDemandingCommand(config?: CosmiconfigResult) { +function extractYargsDemandingCommand(extractFromProcessFn: ExtractArgsFromProcess, config?: CosmiconfigResult) { return yargs .config(config?.config) .command('markdown', 'Generate documentation from Apex classes as a Markdown site.', (yargs) => @@ -162,7 +174,7 @@ function extractYargsDemandingCommand(config?: CosmiconfigResult) { yargs.options(changeLogOptions), ) .demandCommand() - .parseSync(); + .parseSync(extractFromProcessFn()); } function validateConfig(command: Generators, config: UserDefinedConfig) { From 255e500c05d3f4ed75679512c66d53685dfea5fa Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 27 Sep 2024 08:06:40 -0400 Subject: [PATCH 44/61] Test - extracts arguments for openapi --- src/cli/__tests__/args.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args.spec.ts index 00cb05af..9f416eb3 100644 --- a/src/cli/__tests__/args.spec.ts +++ b/src/cli/__tests__/args.spec.ts @@ -1,6 +1,6 @@ import { extractArgs } from '../args'; import { assertEither } from '../../core/test-helpers/assert-either'; -import { UserDefinedMarkdownConfig } from '../../core/shared/types'; +import { UserDefinedMarkdownConfig, UserDefinedOpenApiConfig } from '../../core/shared/types'; describe('when extracting arguments', () => { describe('and no configuration is provided', () => { @@ -19,5 +19,21 @@ describe('when extracting arguments', () => { expect(markdownConfig.sourceDir).toEqual('force-app'); }); }); + + it('extracts the arguments from the process for the openapi command', async () => { + function getFromProcess() { + return ['openapi', '--sourceDir', 'force-app']; + } + + const result = await extractArgs(getFromProcess); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('openapi'); + + const openApiConfig = configs[0] as UserDefinedOpenApiConfig; + expect(openApiConfig.sourceDir).toEqual('force-app'); + }); + }); }); }); From 3904133c495c233d44ca074ddf3a7b3d5c8d34f5 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 27 Sep 2024 08:07:41 -0400 Subject: [PATCH 45/61] Test - extracts arguments for changelog --- src/cli/__tests__/args.spec.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args.spec.ts index 9f416eb3..9af4ee7b 100644 --- a/src/cli/__tests__/args.spec.ts +++ b/src/cli/__tests__/args.spec.ts @@ -1,6 +1,10 @@ import { extractArgs } from '../args'; import { assertEither } from '../../core/test-helpers/assert-either'; -import { UserDefinedMarkdownConfig, UserDefinedOpenApiConfig } from '../../core/shared/types'; +import { + UserDefinedChangelogConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../../core/shared/types'; describe('when extracting arguments', () => { describe('and no configuration is provided', () => { @@ -35,5 +39,25 @@ describe('when extracting arguments', () => { expect(openApiConfig.sourceDir).toEqual('force-app'); }); }); + + it('extracts the arguments from the process for the changelog command', async () => { + function getFromProcess() { + return ['changelog', '--previousVersionDir', 'previous', '--currentVersionDir', 'force-app']; + } + + const result = await extractArgs(getFromProcess); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('changelog'); + + const changelogConfig = configs[0] as UserDefinedChangelogConfig; + + expect(changelogConfig.previousVersionDir).toEqual('previous'); + expect(changelogConfig.currentVersionDir).toEqual('force-app'); + }); + }); }); }); + +// TODO: Console errors when the command is not provided From 0edefb09d75ea4eaa13032463cb90702e45c0ca3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 27 Sep 2024 08:28:22 -0400 Subject: [PATCH 46/61] Test - prints errors to the console --- src/cli/__tests__/args.spec.ts | 62 ++++++++++++++++++++++++++++++++-- src/cli/args.ts | 2 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args.spec.ts index 9af4ee7b..bace83af 100644 --- a/src/cli/__tests__/args.spec.ts +++ b/src/cli/__tests__/args.spec.ts @@ -1,4 +1,5 @@ import { extractArgs } from '../args'; +import * as E from 'fp-ts/Either'; import { assertEither } from '../../core/test-helpers/assert-either'; import { UserDefinedChangelogConfig, @@ -7,6 +8,17 @@ import { } from '../../core/shared/types'; describe('when extracting arguments', () => { + beforeEach(() => { + // Remove all cached modules. The cache needs to be cleared before running + // each command, otherwise you will see the same results from the command + // run in your first test in subsequent tests. + jest.resetModules(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + describe('and no configuration is provided', () => { it('extracts the arguments from the process for the markdown command', async () => { function getFromProcess() { @@ -57,7 +69,53 @@ describe('when extracting arguments', () => { expect(changelogConfig.currentVersionDir).toEqual('force-app'); }); }); + + it('fails when a not-supported command is provided', async () => { + function getFromProcess() { + return ['not-supported']; + } + + const result = await extractArgs(getFromProcess); + + expect(E.isLeft(result)).toBeTruthy(); + }); + + it('prints an error to the console when no command is provided', async () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + // @ts-ignore + const mockExit = jest.spyOn(process, 'exit').mockImplementation((_code) => { + // Do nothing + return null; + }); + function getFromProcess() { + return []; + } + + await extractArgs(getFromProcess); + + expect(consoleSpy).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + mockExit.mockRestore(); + }); + + it('prints an error to the console when a required argument is not provided', async () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + // @ts-ignore + const mockExit = jest.spyOn(process, 'exit').mockImplementation((_code) => { + // Do nothing + return null; + }); + function getFromProcess() { + return ['markdown']; + } + + await extractArgs(getFromProcess); + + expect(consoleSpy).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + mockExit.mockRestore(); + }); }); }); - -// TODO: Console errors when the command is not provided diff --git a/src/cli/args.ts b/src/cli/args.ts index 310c8707..e569b56b 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -77,7 +77,7 @@ function extractArgsForCommandProvidedThroughCli( case 'changelog': return E.right(mergedConfig); default: - throw E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); + return E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); } } From d8f2cb0c460387df4cffb5d23064e5917568bcae Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 08:59:13 -0400 Subject: [PATCH 47/61] Refactoring cosmicconfig dependency --- src/cli/__tests__/args.spec.ts | 78 ++++++++++++++++++++++++++++------ src/cli/args.ts | 62 ++++++++++++++++----------- 2 files changed, 103 insertions(+), 37 deletions(-) diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args.spec.ts index bace83af..b04b930c 100644 --- a/src/cli/__tests__/args.spec.ts +++ b/src/cli/__tests__/args.spec.ts @@ -7,6 +7,10 @@ import { UserDefinedOpenApiConfig, } from '../../core/shared/types'; +function exitFake(_code?: string | number | null | undefined): never { + throw new Error('process.exit() was called'); +} + describe('when extracting arguments', () => { beforeEach(() => { // Remove all cached modules. The cache needs to be cleared before running @@ -82,16 +86,16 @@ describe('when extracting arguments', () => { it('prints an error to the console when no command is provided', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - // @ts-ignore - const mockExit = jest.spyOn(process, 'exit').mockImplementation((_code) => { - // Do nothing - return null; - }); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(exitFake); function getFromProcess() { return []; } - await extractArgs(getFromProcess); + try { + await extractArgs(getFromProcess); + } catch (error) { + // Do nothing + } expect(consoleSpy).toHaveBeenCalled(); @@ -101,16 +105,16 @@ describe('when extracting arguments', () => { it('prints an error to the console when a required argument is not provided', async () => { const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - // @ts-ignore - const mockExit = jest.spyOn(process, 'exit').mockImplementation((_code) => { - // Do nothing - return null; - }); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(exitFake); function getFromProcess() { return ['markdown']; } - await extractArgs(getFromProcess); + try { + await extractArgs(getFromProcess); + } catch (error) { + // Do nothing + } expect(consoleSpy).toHaveBeenCalled(); @@ -118,4 +122,54 @@ describe('when extracting arguments', () => { mockExit.mockRestore(); }); }); + + describe('and a configuration is provided for a single command', () => { + it('extracts the arguments from the process for the markdown command from the configuration', async () => { + function getFromProcess() { + return ['markdown']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + sourceDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('markdown'); + + const markdownConfig = configs[0] as UserDefinedMarkdownConfig; + expect(markdownConfig.sourceDir).toEqual('force-app'); + }); + }); + + it('extracts the arguments from the process for the openapi command from the configuration', async () => { + function getFromProcess() { + return ['openapi']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + sourceDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('openapi'); + + const openApiConfig = configs[0] as UserDefinedOpenApiConfig; + expect(openApiConfig.sourceDir).toEqual('force-app'); + }); + }); + }); }); diff --git a/src/cli/args.ts b/src/cli/args.ts index e569b56b..d4992c7f 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,7 +1,13 @@ -import { cosmiconfig, CosmiconfigResult } from 'cosmiconfig'; +import { cosmiconfig } from 'cosmiconfig'; import * as yargs from 'yargs'; import * as E from 'fp-ts/Either'; -import { Generators, UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; +import { + Generators, + UserDefinedChangelogConfig, + UserDefinedConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../core/shared/types'; import { TypeScriptLoader } from 'cosmiconfig-typescript-loader'; import { markdownOptions } from './commands/markdown'; import { openApiOptions } from './commands/openapi'; @@ -22,13 +28,31 @@ function getArgumentsFromProcess() { return process.argv.slice(2); } +type ConfigResult = { + config: Record; +}; +type ExtractConfig = () => Promise; +/** + * Extracts configuration from a configuration file or the package.json + * through cosmiconfig. + */ +async function extractConfig(): Promise { + const result = await cosmiconfig('apexdocs', { + loaders: { + '.ts': TypeScriptLoader(), + }, + }).search(); + return { config: result?.config ?? {} }; +} + /** * Combines the extracted configuration and arguments. */ export async function extractArgs( extractFromProcessFn: ExtractArgsFromProcess = getArgumentsFromProcess, + extractConfigFn: ExtractConfig = extractConfig, ): Promise> { - const config = await extractConfig(); + const config = await extractConfigFn(); const configType = getConfigType(config); switch (configType._type) { @@ -36,23 +60,11 @@ export async function extractArgs( case 'single-command-config': return handleSingleCommand(extractFromProcessFn, config); case 'multi-command-config': - return extractArgsForCommandsProvidedInConfig(config!.config); + return extractArgsForCommandsProvidedInConfig(config.config as ConfigByGenerator); } } -/** - * Extracts configuration from a configuration file or the package.json - * through cosmiconfig. - */ -function extractConfig(): Promise { - return cosmiconfig('apexdocs', { - loaders: { - '.ts': TypeScriptLoader(), - }, - }).search(); -} - -function handleSingleCommand(extractFromProcessFn: ExtractArgsFromProcess, config: CosmiconfigResult) { +function handleSingleCommand(extractFromProcessFn: ExtractArgsFromProcess, config: ConfigResult) { return pipe( E.right(config), E.flatMap((config) => extractArgsForCommandProvidedThroughCli(extractFromProcessFn, config)), @@ -62,20 +74,20 @@ function handleSingleCommand(extractFromProcessFn: ExtractArgsFromProcess, confi function extractArgsForCommandProvidedThroughCli( extractFromProcessFn: ExtractArgsFromProcess, - config: CosmiconfigResult, + config: ConfigResult, ): E.Either { const cliArgs = extractYargsDemandingCommand(extractFromProcessFn, config); const commandName = cliArgs._[0]; - const mergedConfig = { ...config?.config, ...cliArgs, targetGenerator: commandName as Generators }; + const mergedConfig = { ...config.config, ...cliArgs, targetGenerator: commandName as Generators }; switch (mergedConfig.targetGenerator) { case 'markdown': - return E.right({ ...configOnlyMarkdownDefaults, ...mergedConfig }); + return E.right({ ...configOnlyMarkdownDefaults, ...mergedConfig } as UserDefinedMarkdownConfig); case 'openapi': - return E.right({ ...configOnlyOpenApiDefaults, ...mergedConfig }); + return E.right({ ...configOnlyOpenApiDefaults, ...mergedConfig } as unknown as UserDefinedOpenApiConfig); case 'changelog': - return E.right(mergedConfig); + return E.right(mergedConfig as unknown as UserDefinedChangelogConfig); default: return E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); } @@ -121,7 +133,7 @@ type MultiCommandConfig = { commands: Generators[]; }; -function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfig | MultiCommandConfig { +function getConfigType(config: ConfigResult): NoConfig | SingleCommandConfig | MultiCommandConfig { if (!config) { return { _type: 'no-config' }; } @@ -161,9 +173,9 @@ function getConfigType(config: CosmiconfigResult): NoConfig | SingleCommandConfi * @param extractFromProcessFn The function that extracts the arguments from the process. * @param config The configuration object from the configuration file, if any. */ -function extractYargsDemandingCommand(extractFromProcessFn: ExtractArgsFromProcess, config?: CosmiconfigResult) { +function extractYargsDemandingCommand(extractFromProcessFn: ExtractArgsFromProcess, config: ConfigResult) { return yargs - .config(config?.config) + .config(config.config as Record) .command('markdown', 'Generate documentation from Apex classes as a Markdown site.', (yargs) => yargs.options(markdownOptions), ) From 22046bb4dbf6622554867795f4f3cd215cb1faec Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 09:36:14 -0400 Subject: [PATCH 48/61] simple config ut --- .../{args.spec.ts => args/no-config.spec.ts} | 60 +------ src/cli/__tests__/args/simple-config.spec.ts | 165 ++++++++++++++++++ 2 files changed, 170 insertions(+), 55 deletions(-) rename src/cli/__tests__/{args.spec.ts => args/no-config.spec.ts} (68%) create mode 100644 src/cli/__tests__/args/simple-config.spec.ts diff --git a/src/cli/__tests__/args.spec.ts b/src/cli/__tests__/args/no-config.spec.ts similarity index 68% rename from src/cli/__tests__/args.spec.ts rename to src/cli/__tests__/args/no-config.spec.ts index b04b930c..0bc0d21d 100644 --- a/src/cli/__tests__/args.spec.ts +++ b/src/cli/__tests__/args/no-config.spec.ts @@ -1,13 +1,13 @@ -import { extractArgs } from '../args'; -import * as E from 'fp-ts/Either'; -import { assertEither } from '../../core/test-helpers/assert-either'; +import { extractArgs } from '../../args'; +import { assertEither } from '../../../core/test-helpers/assert-either'; import { UserDefinedChangelogConfig, UserDefinedMarkdownConfig, UserDefinedOpenApiConfig, -} from '../../core/shared/types'; +} from '../../../core/shared/types'; +import * as E from 'fp-ts/Either'; -function exitFake(_code?: string | number | null | undefined): never { +function exitFake(): never { throw new Error('process.exit() was called'); } @@ -122,54 +122,4 @@ describe('when extracting arguments', () => { mockExit.mockRestore(); }); }); - - describe('and a configuration is provided for a single command', () => { - it('extracts the arguments from the process for the markdown command from the configuration', async () => { - function getFromProcess() { - return ['markdown']; - } - - function extractConfig() { - return Promise.resolve({ - config: { - sourceDir: 'force-app', - }, - }); - } - - const result = await extractArgs(getFromProcess, extractConfig); - - assertEither(result, (configs) => { - expect(configs).toHaveLength(1); - expect(configs[0].targetGenerator).toEqual('markdown'); - - const markdownConfig = configs[0] as UserDefinedMarkdownConfig; - expect(markdownConfig.sourceDir).toEqual('force-app'); - }); - }); - - it('extracts the arguments from the process for the openapi command from the configuration', async () => { - function getFromProcess() { - return ['openapi']; - } - - function extractConfig() { - return Promise.resolve({ - config: { - sourceDir: 'force-app', - }, - }); - } - - const result = await extractArgs(getFromProcess, extractConfig); - - assertEither(result, (configs) => { - expect(configs).toHaveLength(1); - expect(configs[0].targetGenerator).toEqual('openapi'); - - const openApiConfig = configs[0] as UserDefinedOpenApiConfig; - expect(openApiConfig.sourceDir).toEqual('force-app'); - }); - }); - }); }); diff --git a/src/cli/__tests__/args/simple-config.spec.ts b/src/cli/__tests__/args/simple-config.spec.ts new file mode 100644 index 00000000..bb0480d8 --- /dev/null +++ b/src/cli/__tests__/args/simple-config.spec.ts @@ -0,0 +1,165 @@ +import { extractArgs } from '../../args'; +import * as E from 'fp-ts/Either'; +import { assertEither } from '../../../core/test-helpers/assert-either'; +import { + UserDefinedChangelogConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../../../core/shared/types'; + +function exitFake(): never { + throw new Error('process.exit() was called'); +} + +describe('when extracting arguments', () => { + beforeEach(() => { + // Remove all cached modules. The cache needs to be cleared before running + // each command, otherwise you will see the same results from the command + // run in your first test in subsequent tests. + jest.resetModules(); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + describe('and a configuration is provided for a single command', () => { + it('extracts the arguments from the process for the markdown command from the configuration', async () => { + function getFromProcess() { + return ['markdown']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + sourceDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('markdown'); + + const markdownConfig = configs[0] as UserDefinedMarkdownConfig; + expect(markdownConfig.sourceDir).toEqual('force-app'); + }); + }); + + it('extracts the arguments from the process for the openapi command from the configuration', async () => { + function getFromProcess() { + return ['openapi']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + sourceDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('openapi'); + + const openApiConfig = configs[0] as UserDefinedOpenApiConfig; + expect(openApiConfig.sourceDir).toEqual('force-app'); + }); + }); + + it('extracts the arguments from the process for the changelog command from the configuration', async () => { + function getFromProcess() { + return ['changelog']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(1); + expect(configs[0].targetGenerator).toEqual('changelog'); + + const changelogConfig = configs[0] as UserDefinedChangelogConfig; + + expect(changelogConfig.previousVersionDir).toEqual('previous'); + expect(changelogConfig.currentVersionDir).toEqual('force-app'); + }); + }); + + it('fails when a not-supported command is provided', async () => { + function getFromProcess() { + return ['not-supported']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + expect(E.isLeft(result)).toBeTruthy(); + }); + + it('errors when no command is provided', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(process, 'exit').mockImplementation(exitFake); + function getFromProcess() { + return []; + } + + function extractConfig() { + return Promise.resolve({ + config: {}, + }); + } + + try { + await extractArgs(getFromProcess, extractConfig); + expect(false).toBeTruthy(); + } catch (error) { + expect(error).toBeDefined(); + } + }); + + it('errors when a required argument is not provided', async () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(process, 'exit').mockImplementation(exitFake); + + function getFromProcess() { + return ['markdown']; + } + + function extractConfig() { + return Promise.resolve({ + config: {}, + }); + } + + try { + await extractArgs(getFromProcess, extractConfig); + expect(false).toBeTruthy(); + } catch (error) { + expect(error).toBeDefined(); + } + }); + }); +}); From af7c3d6afc772371e8012762ef0c122f3fba08c2 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:13:50 -0400 Subject: [PATCH 49/61] config uts --- .../args/multiple-command-config.spec.ts | 104 ++++++++++++++++++ src/cli/args.ts | 90 +++++++++------ src/core/test-helpers/assert-either.ts | 4 + 3 files changed, 162 insertions(+), 36 deletions(-) create mode 100644 src/cli/__tests__/args/multiple-command-config.spec.ts diff --git a/src/cli/__tests__/args/multiple-command-config.spec.ts b/src/cli/__tests__/args/multiple-command-config.spec.ts new file mode 100644 index 00000000..db8efad6 --- /dev/null +++ b/src/cli/__tests__/args/multiple-command-config.spec.ts @@ -0,0 +1,104 @@ +import { extractArgs } from '../../args'; +import * as E from 'fp-ts/Either'; +import { assertEither } from '../../../core/test-helpers/assert-either'; +import { + UserDefinedChangelogConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../../../core/shared/types'; + +describe('when extracting arguments', () => { + describe('and a configuration is provided for multiple commands', () => { + it('errors when a command was still passed through the cli', async () => { + function getFromProcess() { + return ['markdown']; + } + + function extractConfig() { + return Promise.resolve({ + config: { + markdown: { + sourceDir: 'force-app', + }, + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + expect(E.isLeft(result)).toBeTruthy(); + }); + + it('extracts multiple configurations', async () => { + function getFromProcess() { + return []; + } + + function extractConfig() { + return Promise.resolve({ + config: { + markdown: { + sourceDir: 'force-app', + }, + openapi: { + sourceDir: 'force-app', + }, + changelog: { + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + }, + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + assertEither(result, (configs) => { + expect(configs).toHaveLength(3); + expect(configs[0].targetGenerator).toEqual('markdown'); + expect(configs[1].targetGenerator).toEqual('openapi'); + expect(configs[2].targetGenerator).toEqual('changelog'); + + const markdownConfig = configs[0] as UserDefinedMarkdownConfig; + expect(markdownConfig.sourceDir).toEqual('force-app'); + + const openApiConfig = configs[1] as UserDefinedOpenApiConfig; + expect(openApiConfig.sourceDir).toEqual('force-app'); + + const changelogConfig = configs[2] as UserDefinedChangelogConfig; + expect(changelogConfig.previousVersionDir).toEqual('previous'); + expect(changelogConfig.currentVersionDir).toEqual('force-app'); + }); + }); + + it('fails when a non-supported command is provided', async () => { + function getFromProcess() { + return []; + } + + function extractConfig() { + return Promise.resolve({ + config: { + markdown: { + sourceDir: 'force-app', + }, + openapi: { + sourceDir: 'force-app', + }, + changelog: { + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + }, + invalidCommand: { + foo: 'bar', + }, + }, + }); + } + + const result = await extractArgs(getFromProcess, extractConfig); + + expect(E.isLeft(result)).toBeTruthy(); + }); + }); +}); diff --git a/src/cli/args.ts b/src/cli/args.ts index d4992c7f..bce27ac1 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -15,15 +15,22 @@ import { changeLogOptions } from './commands/changelog'; import { pipe } from 'fp-ts/function'; const configOnlyMarkdownDefaults: Partial = { + targetGenerator: 'markdown', excludeTags: [], exclude: [], }; const configOnlyOpenApiDefaults = { + targetGenerator: 'openapi', exclude: [], }; +const configOnlyChangelogDefaults = { + targetGenerator: 'changelog', +}; + type ExtractArgsFromProcess = () => string[]; + function getArgumentsFromProcess() { return process.argv.slice(2); } @@ -32,11 +39,8 @@ type ConfigResult = { config: Record; }; type ExtractConfig = () => Promise; -/** - * Extracts configuration from a configuration file or the package.json - * through cosmiconfig. - */ -async function extractConfig(): Promise { + +async function extractConfigFromPackageJsonOrFile(): Promise { const result = await cosmiconfig('apexdocs', { loaders: { '.ts': TypeScriptLoader(), @@ -50,18 +54,21 @@ async function extractConfig(): Promise { */ export async function extractArgs( extractFromProcessFn: ExtractArgsFromProcess = getArgumentsFromProcess, - extractConfigFn: ExtractConfig = extractConfig, + extractConfigFn: ExtractConfig = extractConfigFromPackageJsonOrFile, ): Promise> { const config = await extractConfigFn(); - const configType = getConfigType(config); - - switch (configType._type) { - case 'no-config': - case 'single-command-config': - return handleSingleCommand(extractFromProcessFn, config); - case 'multi-command-config': - return extractArgsForCommandsProvidedInConfig(config.config as ConfigByGenerator); + + function handle(configType: NoConfig | SingleCommandConfig | MultiCommandConfig) { + switch (configType._type) { + case 'no-config': + case 'single-command-config': + return handleSingleCommand(extractFromProcessFn, config); + case 'multi-command-config': + return extractArgsForCommandsProvidedInConfig(extractFromProcessFn, config.config as ConfigByGenerator); + } } + + return pipe(getConfigType(config), E.flatMap(handle)); } function handleSingleCommand(extractFromProcessFn: ExtractArgsFromProcess, config: ConfigResult) { @@ -87,7 +94,7 @@ function extractArgsForCommandProvidedThroughCli( case 'openapi': return E.right({ ...configOnlyOpenApiDefaults, ...mergedConfig } as unknown as UserDefinedOpenApiConfig); case 'changelog': - return E.right(mergedConfig as unknown as UserDefinedChangelogConfig); + return E.right({ ...configOnlyChangelogDefaults, ...mergedConfig } as unknown as UserDefinedChangelogConfig); default: return E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); } @@ -97,24 +104,26 @@ type ConfigByGenerator = { [key in Generators]: UserDefinedConfig; }; -function extractArgsForCommandsProvidedInConfig(config: ConfigByGenerator) { - // TODO: Error if a command was still passed through the CLI +function extractArgsForCommandsProvidedInConfig( + extractFromProcessFn: ExtractArgsFromProcess, + config: ConfigByGenerator, +) { const configs = Object.entries(config).map(([generator, generatorConfig]) => { switch (generator as Generators) { case 'markdown': return pipe( - validateConfig('markdown', generatorConfig), + validateMultiCommandConfig(extractFromProcessFn, 'markdown', generatorConfig), E.map(() => ({ ...configOnlyMarkdownDefaults, ...generatorConfig })), ); case 'openapi': return pipe( - validateConfig('openapi', generatorConfig), + validateMultiCommandConfig(extractFromProcessFn, 'openapi', generatorConfig), E.map(() => ({ ...configOnlyOpenApiDefaults, ...generatorConfig })), ); case 'changelog': return pipe( - validateConfig('changelog', generatorConfig), - E.map(() => generatorConfig), + validateMultiCommandConfig(extractFromProcessFn, 'changelog', generatorConfig), + E.map(() => ({ ...configOnlyChangelogDefaults, ...generatorConfig })), ); } }); @@ -133,9 +142,9 @@ type MultiCommandConfig = { commands: Generators[]; }; -function getConfigType(config: ConfigResult): NoConfig | SingleCommandConfig | MultiCommandConfig { +function getConfigType(config: ConfigResult): E.Either { if (!config) { - return { _type: 'no-config' }; + return E.right({ _type: 'no-config' }); } // When the config has a shape that looks as follows: @@ -154,25 +163,18 @@ function getConfigType(config: ConfigResult): NoConfig | SingleCommandConfig | M const commands = rootKeys.filter((key) => validRootKeys.includes(key)); const hasInvalidCommands = rootKeys.some((key) => !validRootKeys.includes(key)); if (hasInvalidCommands) { - // TODO: Do not throw - throw new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`); + return E.left(new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`)); } - return { + return E.right({ _type: 'multi-command-config', - // TODO: Throw if the same root key is provided more than once commands: commands as Generators[], - }; + }); } - return { _type: 'single-command-config' }; + return E.right({ _type: 'single-command-config' }); } -/** - * Extracts arguments from the command line, expecting a command to be provided. - * @param extractFromProcessFn The function that extracts the arguments from the process. - * @param config The configuration object from the configuration file, if any. - */ function extractYargsDemandingCommand(extractFromProcessFn: ExtractArgsFromProcess, config: ConfigResult) { return yargs .config(config.config as Record) @@ -189,7 +191,11 @@ function extractYargsDemandingCommand(extractFromProcessFn: ExtractArgsFromProce .parseSync(extractFromProcessFn()); } -function validateConfig(command: Generators, config: UserDefinedConfig) { +function validateMultiCommandConfig( + extractFromProcessFn: ExtractArgsFromProcess, + command: Generators, + config: UserDefinedConfig, +) { function getOptions(generator: Generators) { switch (generator) { case 'markdown': @@ -206,9 +212,21 @@ function validateConfig(command: Generators, config: UserDefinedConfig) { yargs .config(config) .options(options) + .check((argv) => { + // we should not be receiving a command here + // since this is a multi-command config + if (argv._.length > 0) { + throw new Error( + `Unexpected command "${argv._[0]}". + The command name should be provided in the configuration when using the current configuration format.`, + ); + } else { + return true; + } + }) .fail((msg) => { throw new Error(`Invalid configuration for command "${command}": ${msg}`); }) - .parse(); + .parse(extractFromProcessFn()); }, E.toError); } diff --git a/src/core/test-helpers/assert-either.ts b/src/core/test-helpers/assert-either.ts index 8e2bd0b6..990c3151 100644 --- a/src/core/test-helpers/assert-either.ts +++ b/src/core/test-helpers/assert-either.ts @@ -6,3 +6,7 @@ export function assertEither(result: E.Either, assertion: (data: U) (data) => assertion(data), )(result); } + +function fail(error: unknown): never { + throw error; +} From f364ae4698b557bb6460c620a2f907177b611d1a Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:21:11 -0400 Subject: [PATCH 50/61] moving the `reflection` module to a shared space --- src/core/changelog/generate-change-log.ts | 2 +- src/core/markdown/__test__/inheritance-chain.test.ts | 2 +- src/core/markdown/generate-docs.ts | 12 ++++++------ .../reflection/__test__/filter-scope.spec.ts | 0 .../{markdown => }/reflection/__test__/helpers.ts | 2 +- .../reflection/__test__/remove-excluded-tags.spec.ts | 0 src/core/{markdown => }/reflection/filter-scope.ts | 4 ++-- .../reflection/inheritance-chain-expanion.ts | 4 ++-- .../{markdown => }/reflection/inheritance-chain.ts | 0 .../reflection/inherited-member-expansion.ts | 4 ++-- src/core/{markdown => }/reflection/reflect-source.ts | 6 +++--- .../reflection/remove-excluded-tags.ts | 2 +- .../reflection/sort-types-and-members.ts | 2 +- 13 files changed, 20 insertions(+), 20 deletions(-) rename src/core/{markdown => }/reflection/__test__/filter-scope.spec.ts (100%) rename src/core/{markdown => }/reflection/__test__/helpers.ts (87%) rename src/core/{markdown => }/reflection/__test__/remove-excluded-tags.spec.ts (100%) rename src/core/{markdown => }/reflection/filter-scope.ts (79%) rename src/core/{markdown => }/reflection/inheritance-chain-expanion.ts (87%) rename src/core/{markdown => }/reflection/inheritance-chain.ts (100%) rename src/core/{markdown => }/reflection/inherited-member-expansion.ts (97%) rename src/core/{markdown => }/reflection/reflect-source.ts (94%) rename src/core/{markdown => }/reflection/remove-excluded-tags.ts (99%) rename src/core/{markdown => }/reflection/sort-types-and-members.ts (97%) diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 78436c7b..8c3175f8 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -2,7 +2,7 @@ import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../s import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; // TODO: Move the reflection code to outside of the markdown folder since now it is shared with this -import { reflectBundles } from '../markdown/reflection/reflect-source'; +import { reflectBundles } from '../reflection/reflect-source'; import { processChangeLog, VersionManifest } from './process-change-log'; import { convertToRenderableChangeLog } from './renderable-change-log'; // TODO: Also move this file since this is now shared diff --git a/src/core/markdown/__test__/inheritance-chain.test.ts b/src/core/markdown/__test__/inheritance-chain.test.ts index d784a39e..4b97fdaa 100644 --- a/src/core/markdown/__test__/inheritance-chain.test.ts +++ b/src/core/markdown/__test__/inheritance-chain.test.ts @@ -1,5 +1,5 @@ import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; -import { createInheritanceChain } from '../reflection/inheritance-chain'; +import { createInheritanceChain } from '../../reflection/inheritance-chain'; describe('inheritance chain for classes', () => { test('returns an empty list of the class does not extend any other class', () => { diff --git a/src/core/markdown/generate-docs.ts b/src/core/markdown/generate-docs.ts index c872f744..0931d85d 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -19,17 +19,17 @@ import { ParsedFile, } from '../shared/types'; import { parsedFilesToRenderableBundle } from './adapters/renderable-bundle'; -import { reflectBundles } from './reflection/reflect-source'; -import { addInheritanceChainToTypes } from './reflection/inheritance-chain-expanion'; -import { addInheritedMembersToTypes } from './reflection/inherited-member-expansion'; +import { reflectBundles } from '../reflection/reflect-source'; +import { addInheritanceChainToTypes } from '../reflection/inheritance-chain-expanion'; +import { addInheritedMembersToTypes } from '../reflection/inherited-member-expansion'; import { convertToDocumentationBundle } from './adapters/renderable-to-page-data'; -import { filterScope } from './reflection/filter-scope'; +import { filterScope } from '../reflection/filter-scope'; import { Template } from './templates/template'; import { hookableTemplate } from './templates/hookable'; -import { sortTypesAndMembers } from './reflection/sort-types-and-members'; +import { sortTypesAndMembers } from '../reflection/sort-types-and-members'; import { isSkip } from '../shared/utils'; import { parsedFilesToReferenceGuide } from './adapters/reference-guide'; -import { removeExcludedTags } from './reflection/remove-excluded-tags'; +import { removeExcludedTags } from '../reflection/remove-excluded-tags'; import { HookError } from '../errors/errors'; export type MarkdownGeneratorConfig = Omit< diff --git a/src/core/markdown/reflection/__test__/filter-scope.spec.ts b/src/core/reflection/__test__/filter-scope.spec.ts similarity index 100% rename from src/core/markdown/reflection/__test__/filter-scope.spec.ts rename to src/core/reflection/__test__/filter-scope.spec.ts diff --git a/src/core/markdown/reflection/__test__/helpers.ts b/src/core/reflection/__test__/helpers.ts similarity index 87% rename from src/core/markdown/reflection/__test__/helpers.ts rename to src/core/reflection/__test__/helpers.ts index 742dfbb7..15ca7a53 100644 --- a/src/core/markdown/reflection/__test__/helpers.ts +++ b/src/core/reflection/__test__/helpers.ts @@ -1,5 +1,5 @@ -import { ParsedFile } from '../../../shared/types'; import { reflect } from '@cparra/apex-reflection'; +import { ParsedFile } from '../../shared/types'; export function parsedFileFromRawString(raw: string): ParsedFile { const { error, typeMirror } = reflect(raw); diff --git a/src/core/markdown/reflection/__test__/remove-excluded-tags.spec.ts b/src/core/reflection/__test__/remove-excluded-tags.spec.ts similarity index 100% rename from src/core/markdown/reflection/__test__/remove-excluded-tags.spec.ts rename to src/core/reflection/__test__/remove-excluded-tags.spec.ts diff --git a/src/core/markdown/reflection/filter-scope.ts b/src/core/reflection/filter-scope.ts similarity index 79% rename from src/core/markdown/reflection/filter-scope.ts rename to src/core/reflection/filter-scope.ts index 1ee5ab01..bb9c153e 100644 --- a/src/core/markdown/reflection/filter-scope.ts +++ b/src/core/reflection/filter-scope.ts @@ -1,5 +1,5 @@ -import { ParsedFile } from '../../shared/types'; -import Manifest from '../../manifest'; +import Manifest from '../manifest'; +import { ParsedFile } from '../shared/types'; export function filterScope(scopes: string[], parsedFiles: ParsedFile[]): ParsedFile[] { return parsedFiles diff --git a/src/core/markdown/reflection/inheritance-chain-expanion.ts b/src/core/reflection/inheritance-chain-expanion.ts similarity index 87% rename from src/core/markdown/reflection/inheritance-chain-expanion.ts rename to src/core/reflection/inheritance-chain-expanion.ts index c7fd417e..108b84a8 100644 --- a/src/core/markdown/reflection/inheritance-chain-expanion.ts +++ b/src/core/reflection/inheritance-chain-expanion.ts @@ -1,7 +1,7 @@ import { ClassMirror, Type } from '@cparra/apex-reflection'; import { createInheritanceChain } from './inheritance-chain'; -import { ParsedFile } from '../../shared/types'; -import { parsedFilesToTypes } from '../utils'; +import { parsedFilesToTypes } from '../markdown/utils'; +import { ParsedFile } from '../shared/types'; export const addInheritanceChainToTypes = (parsedFiles: ParsedFile[]): ParsedFile[] => parsedFiles.map((parsedFile) => ({ diff --git a/src/core/markdown/reflection/inheritance-chain.ts b/src/core/reflection/inheritance-chain.ts similarity index 100% rename from src/core/markdown/reflection/inheritance-chain.ts rename to src/core/reflection/inheritance-chain.ts diff --git a/src/core/markdown/reflection/inherited-member-expansion.ts b/src/core/reflection/inherited-member-expansion.ts similarity index 97% rename from src/core/markdown/reflection/inherited-member-expansion.ts rename to src/core/reflection/inherited-member-expansion.ts index f7036186..0cfba92a 100644 --- a/src/core/markdown/reflection/inherited-member-expansion.ts +++ b/src/core/reflection/inherited-member-expansion.ts @@ -1,7 +1,7 @@ import { ClassMirror, InterfaceMirror, Type } from '@cparra/apex-reflection'; -import { ParsedFile } from '../../shared/types'; import { pipe } from 'fp-ts/function'; -import { parsedFilesToTypes } from '../utils'; +import { ParsedFile } from '../shared/types'; +import { parsedFilesToTypes } from '../markdown/utils'; export const addInheritedMembersToTypes = (parsedFiles: ParsedFile[]) => parsedFiles.map((parsedFile) => addInheritedMembers(parsedFilesToTypes(parsedFiles), parsedFile)); diff --git a/src/core/markdown/reflection/reflect-source.ts b/src/core/reflection/reflect-source.ts similarity index 94% rename from src/core/markdown/reflection/reflect-source.ts rename to src/core/reflection/reflect-source.ts index e1679283..f83199ee 100644 --- a/src/core/markdown/reflection/reflect-source.ts +++ b/src/core/reflection/reflect-source.ts @@ -1,4 +1,3 @@ -import { ParsedFile, UnparsedSourceFile } from '../../shared/types'; import * as TE from 'fp-ts/TaskEither'; import * as E from 'fp-ts/Either'; import * as T from 'fp-ts/Task'; @@ -6,11 +5,12 @@ import * as A from 'fp-ts/lib/Array'; import { Annotation, reflect as mirrorReflection, Type } from '@cparra/apex-reflection'; import { pipe } from 'fp-ts/function'; import * as O from 'fp-ts/Option'; -import { parseApexMetadata } from '../../parse-apex-metadata'; import { ParsingError } from '@cparra/apex-reflection'; import { apply } from '#utils/fp'; import { Semigroup } from 'fp-ts/Semigroup'; -import { ReflectionError, ReflectionErrors } from '../../errors/errors'; +import { ParsedFile, UnparsedSourceFile } from '../shared/types'; +import { ReflectionError, ReflectionErrors } from '../errors/errors'; +import { parseApexMetadata } from '../parse-apex-metadata'; async function reflectAsync(rawSource: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/core/markdown/reflection/remove-excluded-tags.ts b/src/core/reflection/remove-excluded-tags.ts similarity index 99% rename from src/core/markdown/reflection/remove-excluded-tags.ts rename to src/core/reflection/remove-excluded-tags.ts index 22a39513..9c6a4b1d 100644 --- a/src/core/markdown/reflection/remove-excluded-tags.ts +++ b/src/core/reflection/remove-excluded-tags.ts @@ -1,9 +1,9 @@ import * as O from 'fp-ts/Option'; import { match } from 'fp-ts/boolean'; -import { ParsedFile } from '../../shared/types'; import { ClassMirror, DocComment, InterfaceMirror, Type } from '@cparra/apex-reflection'; import { pipe } from 'fp-ts/function'; import { apply } from '#utils/fp'; +import { ParsedFile } from '../shared/types'; type AppliedRemoveTagFn = (tagName: string, removeFn: RemoveTagFn) => DocComment; type RemoveTagFn = (docComment: DocComment) => DocComment; diff --git a/src/core/markdown/reflection/sort-types-and-members.ts b/src/core/reflection/sort-types-and-members.ts similarity index 97% rename from src/core/markdown/reflection/sort-types-and-members.ts rename to src/core/reflection/sort-types-and-members.ts index 1320c1b1..9c2e3e8e 100644 --- a/src/core/markdown/reflection/sort-types-and-members.ts +++ b/src/core/reflection/sort-types-and-members.ts @@ -1,5 +1,5 @@ import { ClassMirror, EnumMirror, InterfaceMirror, Type } from '@cparra/apex-reflection'; -import { ParsedFile } from '../../shared/types'; +import { ParsedFile } from '../shared/types'; type Named = { name: string }; From 11c814ad5935e7c488376cccebcb0d07e66b4107 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:22:25 -0400 Subject: [PATCH 51/61] moving the template utility to a shared space --- src/core/changelog/generate-change-log.ts | 4 +--- .../adapters/renderable-to-page-data.ts | 2 +- src/core/markdown/generate-docs.ts | 2 +- src/core/{markdown/templates => }/template.ts | 22 +++++++++---------- 4 files changed, 14 insertions(+), 16 deletions(-) rename src/core/{markdown/templates => }/template.ts (76%) diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 8c3175f8..6dc491c3 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -1,12 +1,10 @@ import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../shared/types'; import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; -// TODO: Move the reflection code to outside of the markdown folder since now it is shared with this import { reflectBundles } from '../reflection/reflect-source'; import { processChangeLog, VersionManifest } from './process-change-log'; import { convertToRenderableChangeLog } from './renderable-change-log'; -// TODO: Also move this file since this is now shared -import { CompilationRequest, Template } from '../markdown/templates/template'; +import { CompilationRequest, Template } from '../template'; import { changeLogTemplate } from './templates/change-log-template'; import { ReflectionErrors } from '../errors/errors'; diff --git a/src/core/markdown/adapters/renderable-to-page-data.ts b/src/core/markdown/adapters/renderable-to-page-data.ts index 8b12b885..eff41e45 100644 --- a/src/core/markdown/adapters/renderable-to-page-data.ts +++ b/src/core/markdown/adapters/renderable-to-page-data.ts @@ -1,7 +1,7 @@ import { ReferenceGuideReference, Renderable, RenderableBundle, RenderableEnum } from './types'; import { DocPageData, DocumentationBundle } from '../../shared/types'; import { pipe } from 'fp-ts/function'; -import { CompilationRequest, Template } from '../templates/template'; +import { CompilationRequest, Template } from '../../template'; import { enumMarkdownTemplate } from '../templates/enum-template'; import { interfaceMarkdownTemplate } from '../templates/interface-template'; import { classMarkdownTemplate } from '../templates/class-template'; diff --git a/src/core/markdown/generate-docs.ts b/src/core/markdown/generate-docs.ts index 0931d85d..6bb0d92d 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -24,7 +24,7 @@ import { addInheritanceChainToTypes } from '../reflection/inheritance-chain-expa import { addInheritedMembersToTypes } from '../reflection/inherited-member-expansion'; import { convertToDocumentationBundle } from './adapters/renderable-to-page-data'; import { filterScope } from '../reflection/filter-scope'; -import { Template } from './templates/template'; +import { Template } from '../template'; import { hookableTemplate } from './templates/hookable'; import { sortTypesAndMembers } from '../reflection/sort-types-and-members'; import { isSkip } from '../shared/utils'; diff --git a/src/core/markdown/templates/template.ts b/src/core/template.ts similarity index 76% rename from src/core/markdown/templates/template.ts rename to src/core/template.ts index 5de05230..ba7806d3 100644 --- a/src/core/markdown/templates/template.ts +++ b/src/core/template.ts @@ -1,15 +1,15 @@ import Handlebars from 'handlebars'; -import { CodeBlock, RenderableContent, StringOrLink } from '../adapters/types'; -import { isCodeBlock, isEmptyLine, isInlineCode } from '../adapters/type-utils'; -import { typeDocPartial } from './type-doc-partial'; -import { documentablePartialTemplate } from './documentable-partial-template'; -import { methodsPartialTemplate } from './methods-partial-template'; -import { groupedMembersPartialTemplate } from './grouped-members-partial-template'; -import { constructorsPartialTemplate } from './constructors-partial-template'; -import { fieldsPartialTemplate } from './fieldsPartialTemplate'; -import { classMarkdownTemplate } from './class-template'; -import { enumMarkdownTemplate } from './enum-template'; -import { interfaceMarkdownTemplate } from './interface-template'; +import { CodeBlock, RenderableContent, StringOrLink } from './markdown/adapters/types'; +import { isCodeBlock, isEmptyLine, isInlineCode } from './markdown/adapters/type-utils'; +import { typeDocPartial } from './markdown/templates/type-doc-partial'; +import { documentablePartialTemplate } from './markdown/templates/documentable-partial-template'; +import { methodsPartialTemplate } from './markdown/templates/methods-partial-template'; +import { groupedMembersPartialTemplate } from './markdown/templates/grouped-members-partial-template'; +import { constructorsPartialTemplate } from './markdown/templates/constructors-partial-template'; +import { fieldsPartialTemplate } from './markdown/templates/fieldsPartialTemplate'; +import { classMarkdownTemplate } from './markdown/templates/class-template'; +import { enumMarkdownTemplate } from './markdown/templates/enum-template'; +import { interfaceMarkdownTemplate } from './markdown/templates/interface-template'; export type CompilationRequest = { template: string; From 06cb64956c6d9134cad3c76377dbb75206239e36 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:38:02 -0400 Subject: [PATCH 52/61] can provide a scope to the changelog. --- src/cli/commands/changelog.ts | 10 ++++++++++ src/cli/commands/openapi.ts | 4 ++-- .../__test__/generating-change-log.spec.ts | 16 ++++++++++++++++ src/core/changelog/generate-change-log.ts | 13 ++++++++----- src/core/shared/types.d.ts | 1 + src/defaults.ts | 1 + 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/changelog.ts b/src/cli/commands/changelog.ts index 61a7a555..8c36cb0b 100644 --- a/src/cli/commands/changelog.ts +++ b/src/cli/commands/changelog.ts @@ -25,4 +25,14 @@ export const changeLogOptions: { [key: string]: Options } = { default: changeLogDefaults.fileName, describe: 'The name of the changelog file to be generated.', }, + scope: { + type: 'string', + array: true, + alias: 's', + default: changeLogDefaults.scope, + describe: + 'The list of scope to respect when generating the changelog. ' + + 'Values should be separated by a space, e.g --scope global public namespaceaccessible. ' + + 'Annotations are supported and should be passed lowercased and without the @ symbol, e.g. namespaceaccessible auraenabled.', + }, }; diff --git a/src/cli/commands/openapi.ts b/src/cli/commands/openapi.ts index 1bf39e72..6010e421 100644 --- a/src/cli/commands/openapi.ts +++ b/src/cli/commands/openapi.ts @@ -1,5 +1,5 @@ import { Options } from 'yargs'; -import { markdownDefaults, openApiDefaults } from '../../defaults'; +import { openApiDefaults } from '../../defaults'; export const openApiOptions: { [key: string]: Options } = { sourceDir: { @@ -11,7 +11,7 @@ export const openApiOptions: { [key: string]: Options } = { targetDir: { type: 'string', alias: 't', - default: markdownDefaults.targetDir, + default: openApiDefaults.targetDir, describe: 'The directory location where the OpenApi file will be generated.', }, fileName: { diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index e0f55d26..da065ca2 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -4,6 +4,7 @@ import { assertEither } from '../../test-helpers/assert-either'; const config = { fileName: 'changelog', + scope: ['global', 'public', 'private'], targetDir: '', currentVersionDir: '', previousVersionDir: '', @@ -158,4 +159,19 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).toContain('This is a test enum.')); }); }); + + describe('that includes new types out of scope', () => { + it('should not include them', async () => { + const newClassSource = 'class Test {}'; + + const oldBundle: UnparsedSourceFile[] = []; + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, { ...config, scope: ['global'] })(); + + assertEither(result, (data) => expect(data.content).not.toContain('## New Classes')); + }); + }); }); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 6dc491c3..bccb05f6 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -7,6 +7,8 @@ import { convertToRenderableChangeLog } from './renderable-change-log'; import { CompilationRequest, Template } from '../template'; import { changeLogTemplate } from './templates/change-log-template'; import { ReflectionErrors } from '../errors/errors'; +import { apply } from '#utils/fp'; +import { filterScope } from '../reflection/filter-scope'; export type ChangeLogPageData = { // TODO: This should also support frontmatter (and the hook to add it) @@ -14,21 +16,22 @@ export type ChangeLogPageData = { outputDocPath: string; }; -// TODO: Also provide the ability to have a previously generated manifest as an input -// TODO: Also provide the ability to have 2 git refs as input - // TODO: We should provide the ability to filter out of scope if we are going -// to be relying on source files and not on a previously generated manifest. +// to be relying on source files and not on a previously generated manifest + // TODO: And also the "exclude" property in the config, it should be fairly simple to add. export function generateChangeLog( oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[], config: Omit, ): TE.TaskEither { + const filterOutOfScope = apply(filterScope, config.scope); + return pipe( reflectBundles(oldBundles), + TE.map(filterOutOfScope), TE.bindTo('oldVersion'), - TE.bind('newVersion', () => reflectBundles(newBundles)), + TE.bind('newVersion', () => pipe(reflectBundles(newBundles), TE.map(filterOutOfScope))), TE.map(({ oldVersion, newVersion }) => ({ oldManifest: parsedFilesToManifest(oldVersion), newManifest: parsedFilesToManifest(newVersion), diff --git a/src/core/shared/types.d.ts b/src/core/shared/types.d.ts index 785284a1..c2fa343a 100644 --- a/src/core/shared/types.d.ts +++ b/src/core/shared/types.d.ts @@ -46,6 +46,7 @@ export type UserDefinedChangelogConfig = { currentVersionDir: string; targetDir: string; fileName: string; + scope: string[]; }; export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig | UserDefinedChangelogConfig; diff --git a/src/defaults.ts b/src/defaults.ts index fa01bbfc..29991fad 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -23,4 +23,5 @@ export const openApiDefaults = { export const changeLogDefaults = { ...commonDefaults, fileName: 'changelog', + scope: ['global'], }; From 7a45f5b12c5cc2c12ba8b10426fe8e612107a7bd Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:48:33 -0400 Subject: [PATCH 53/61] Ability to set "exclude" glob patterns --- src/application/Apexdocs.ts | 4 ++-- src/cli/args.ts | 1 + src/core/changelog/generate-change-log.ts | 13 ++++++------- src/core/shared/types.d.ts | 1 + 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index c64da88f..5edd5542 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -68,8 +68,8 @@ async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) async function processChangeLog(config: UserDefinedChangelogConfig) { async function loadFiles(): Promise<[UnparsedSourceFile[], UnparsedSourceFile[]]> { return [ - await readFiles(config.previousVersionDir, false, []), - await readFiles(config.currentVersionDir, false, []), + await readFiles(config.previousVersionDir, false, config.exclude), + await readFiles(config.currentVersionDir, false, config.exclude), ]; } diff --git a/src/cli/args.ts b/src/cli/args.ts index bce27ac1..21119ce9 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -27,6 +27,7 @@ const configOnlyOpenApiDefaults = { const configOnlyChangelogDefaults = { targetGenerator: 'changelog', + exclude: [], }; type ExtractArgsFromProcess = () => string[]; diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index bccb05f6..5a89640f 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -16,10 +16,6 @@ export type ChangeLogPageData = { outputDocPath: string; }; -// TODO: We should provide the ability to filter out of scope if we are going -// to be relying on source files and not on a previously generated manifest - -// TODO: And also the "exclude" property in the config, it should be fairly simple to add. export function generateChangeLog( oldBundles: UnparsedSourceFile[], newBundles: UnparsedSourceFile[], @@ -27,11 +23,14 @@ export function generateChangeLog( ): TE.TaskEither { const filterOutOfScope = apply(filterScope, config.scope); + function reflect(sourceFiles: UnparsedSourceFile[]) { + return pipe(reflectBundles(sourceFiles), TE.map(filterOutOfScope)); + } + return pipe( - reflectBundles(oldBundles), - TE.map(filterOutOfScope), + reflect(oldBundles), TE.bindTo('oldVersion'), - TE.bind('newVersion', () => pipe(reflectBundles(newBundles), TE.map(filterOutOfScope))), + TE.bind('newVersion', () => reflect(newBundles)), TE.map(({ oldVersion, newVersion }) => ({ oldManifest: parsedFilesToManifest(oldVersion), newManifest: parsedFilesToManifest(newVersion), diff --git a/src/core/shared/types.d.ts b/src/core/shared/types.d.ts index c2fa343a..a34d3728 100644 --- a/src/core/shared/types.d.ts +++ b/src/core/shared/types.d.ts @@ -47,6 +47,7 @@ export type UserDefinedChangelogConfig = { targetDir: string; fileName: string; scope: string[]; + exclude: string[]; }; export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig | UserDefinedChangelogConfig; From 3b481e9747db304976533dc6ff8e513621d69707 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:49:09 -0400 Subject: [PATCH 54/61] Ability to set "exclude" glob patterns --- src/core/changelog/__test__/generating-change-log.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index da065ca2..3ab57f51 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -8,6 +8,7 @@ const config = { targetDir: '', currentVersionDir: '', previousVersionDir: '', + exclude: [], }; describe('when generating a changelog', () => { From db161ce97192ca21baf54f7b4914ed8d4fcf8e6b Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 10:56:46 -0400 Subject: [PATCH 55/61] Refactorings --- src/core/changelog/generate-change-log.ts | 51 +++++++++++++-------- src/core/changelog/renderable-change-log.ts | 2 +- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 5a89640f..1b37d3f6 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -3,7 +3,7 @@ import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import { reflectBundles } from '../reflection/reflect-source'; import { processChangeLog, VersionManifest } from './process-change-log'; -import { convertToRenderableChangeLog } from './renderable-change-log'; +import { convertToRenderableChangeLog, RenderableChangeLog } from './renderable-change-log'; import { CompilationRequest, Template } from '../template'; import { changeLogTemplate } from './templates/change-log-template'; import { ReflectionErrors } from '../errors/errors'; @@ -11,7 +11,6 @@ import { apply } from '#utils/fp'; import { filterScope } from '../reflection/filter-scope'; export type ChangeLogPageData = { - // TODO: This should also support frontmatter (and the hook to add it) content: string; outputDocPath: string; }; @@ -27,36 +26,48 @@ export function generateChangeLog( return pipe(reflectBundles(sourceFiles), TE.map(filterOutOfScope)); } + const convertToPageData = apply(toPageData, config.fileName); + return pipe( reflect(oldBundles), TE.bindTo('oldVersion'), TE.bind('newVersion', () => reflect(newBundles)), - TE.map(({ oldVersion, newVersion }) => ({ - oldManifest: parsedFilesToManifest(oldVersion), - newManifest: parsedFilesToManifest(newVersion), - })), + TE.map(toManifests), TE.map(({ oldManifest, newManifest }) => ({ changeLog: processChangeLog(oldManifest, newManifest), newManifest, })), TE.map(({ changeLog, newManifest }) => convertToRenderableChangeLog(changeLog, newManifest.types)), - TE.map((renderable) => { - const compilationRequest: CompilationRequest = { - template: changeLogTemplate, - source: renderable, - }; - - return Template.getInstance().compile(compilationRequest); - }), - TE.map((content) => ({ - content, - outputDocPath: `${config.fileName}.md`, - })), + TE.map(compile), + TE.map(convertToPageData), ); } -function parsedFilesToManifest(parsedFiles: ParsedFile[]): VersionManifest { +function toManifests({ oldVersion, newVersion }: { oldVersion: ParsedFile[]; newVersion: ParsedFile[] }) { + function parsedFilesToManifest(parsedFiles: ParsedFile[]): VersionManifest { + return { + types: parsedFiles.map((parsedFile) => parsedFile.type), + }; + } + + return { + oldManifest: parsedFilesToManifest(oldVersion), + newManifest: parsedFilesToManifest(newVersion), + }; +} + +function compile(renderable: RenderableChangeLog): string { + const compilationRequest: CompilationRequest = { + template: changeLogTemplate, + source: renderable, + }; + + return Template.getInstance().compile(compilationRequest); +} + +function toPageData(fileName: string, content: string): ChangeLogPageData { return { - types: parsedFiles.map((parsedFile) => parsedFile.type), + content, + outputDocPath: `${fileName}.md`, }; } diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index 4aa172f5..ce8ebc46 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -16,7 +16,7 @@ type NewTypeSection = { types: NewTypeRenderable[]; }; -type RenderableChangeLog = { +export type RenderableChangeLog = { newClasses: NewTypeSection<'class'> | null; newInterfaces: NewTypeSection<'interface'> | null; newEnums: NewTypeSection<'enum'> | null; From 7c60ccfdcb898db05b1007c25c5f9069ca035419 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:05:50 -0400 Subject: [PATCH 56/61] Refactorings --- examples/changelog/docs/changelog.md | 24 +------------------ src/core/changelog/renderable-change-log.ts | 14 ++++++----- .../adapters/__tests__/documentables.spec.ts | 2 +- .../adapters/__tests__/references.spec.ts | 2 +- src/core/markdown/adapters/apex-types.ts | 4 ++-- .../adapters/fields-and-properties.ts | 4 ++-- src/core/markdown/adapters/generate-link.ts | 2 +- src/core/markdown/adapters/inline.ts | 2 +- .../adapters/methods-and-constructors.ts | 6 ++--- .../markdown/adapters/renderable-bundle.ts | 4 ++-- .../adapters/renderable-to-page-data.ts | 2 +- src/core/markdown/adapters/type-utils.ts | 2 +- .../adapters => renderables}/documentables.ts | 4 ++-- .../adapters => renderables}/types.d.ts | 2 +- src/core/template.ts | 2 +- 15 files changed, 28 insertions(+), 48 deletions(-) rename src/core/{markdown/adapters => renderables}/documentables.ts (96%) rename src/core/{markdown/adapters => renderables}/types.d.ts (98%) diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index f576225b..5ddad421 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -1,23 +1 @@ -# Changelog - -## New Classes - -These classes are new. - -### AccountService - -This is a new class that does foo and bar and references Baz . - -## New Interfaces - -These interfaces are new. - -### IAnotherExample - -### IExemplificable - -## New Enums - -These enums are new. - -### PossibleValues \ No newline at end of file +# Changelog \ No newline at end of file diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-change-log.ts index ce8ebc46..05ee8faa 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-change-log.ts @@ -1,8 +1,7 @@ import { ChangeLog } from './process-change-log'; import { Type } from '@cparra/apex-reflection'; -// TODO: For these 2 imports, we should move the adapters and documentables to a shared place to not have to get into the markdown folder -import { RenderableContent } from '../markdown/adapters/types'; -import { adaptDescribable } from '../markdown/adapters/documentables'; +import { RenderableContent } from '../renderables/types'; +import { adaptDescribable } from '../renderables/documentables'; type NewTypeRenderable = { name: string; @@ -74,11 +73,14 @@ function typeToRenderable(type: Type): NewTypeRenderable { }; } -// TODO: New Enums -// TODO: New Interfaces - // TODO: Removed Classes // TODO: Removed Enums // TODO: Removed Interfaces // Changes... +// TODO: New and removed enum values +// TODO: new and removed mehtods +// TODO: new and removed class members +// TODO: new and removed inner classes +// TODO: new and removed inner interfaces +// TODO: new and removed inner enums diff --git a/src/core/markdown/adapters/__tests__/documentables.spec.ts b/src/core/markdown/adapters/__tests__/documentables.spec.ts index 4e8e2f3f..d8a623a9 100644 --- a/src/core/markdown/adapters/__tests__/documentables.spec.ts +++ b/src/core/markdown/adapters/__tests__/documentables.spec.ts @@ -1,4 +1,4 @@ -import { adaptDescribable } from '../documentables'; +import { adaptDescribable } from '../../../renderables/documentables'; function linkGenerator(typeName: string) { return typeName; diff --git a/src/core/markdown/adapters/__tests__/references.spec.ts b/src/core/markdown/adapters/__tests__/references.spec.ts index b1907551..4f5fbd81 100644 --- a/src/core/markdown/adapters/__tests__/references.spec.ts +++ b/src/core/markdown/adapters/__tests__/references.spec.ts @@ -1,5 +1,5 @@ import { replaceInlineReferences } from '../inline'; -import { Link } from '../types'; +import { Link } from '../../../renderables/types'; function getFileLink(typeName: string): Link { return { diff --git a/src/core/markdown/adapters/apex-types.ts b/src/core/markdown/adapters/apex-types.ts index c24938a6..35708c37 100644 --- a/src/core/markdown/adapters/apex-types.ts +++ b/src/core/markdown/adapters/apex-types.ts @@ -10,8 +10,8 @@ import { FieldMirrorWithInheritance, PropertyMirrorWithInheritance, GetRenderableContentByTypeName, -} from './types'; -import { adaptDescribable, adaptDocumentable } from './documentables'; +} from '../../renderables/types'; +import { adaptDescribable, adaptDocumentable } from '../../renderables/documentables'; import { adaptConstructor, adaptMethod } from './methods-and-constructors'; import { adaptFieldOrProperty } from './fields-and-properties'; import { MarkdownGeneratorConfig } from '../generate-docs'; diff --git a/src/core/markdown/adapters/fields-and-properties.ts b/src/core/markdown/adapters/fields-and-properties.ts index 76b5689e..f8450ce1 100644 --- a/src/core/markdown/adapters/fields-and-properties.ts +++ b/src/core/markdown/adapters/fields-and-properties.ts @@ -4,8 +4,8 @@ import { PropertyMirrorWithInheritance, RenderableField, GetRenderableContentByTypeName, -} from './types'; -import { adaptDocumentable } from './documentables'; +} from '../../renderables/types'; +import { adaptDocumentable } from '../../renderables/documentables'; export function adaptFieldOrProperty( field: FieldMirrorWithInheritance | PropertyMirrorWithInheritance, diff --git a/src/core/markdown/adapters/generate-link.ts b/src/core/markdown/adapters/generate-link.ts index 79a852b5..36297df7 100644 --- a/src/core/markdown/adapters/generate-link.ts +++ b/src/core/markdown/adapters/generate-link.ts @@ -1,4 +1,4 @@ -import { StringOrLink } from './types'; +import { StringOrLink } from '../../renderables/types'; import path from 'path'; import { LinkingStrategy } from '../../shared/types'; diff --git a/src/core/markdown/adapters/inline.ts b/src/core/markdown/adapters/inline.ts index db551b03..e6003d0a 100644 --- a/src/core/markdown/adapters/inline.ts +++ b/src/core/markdown/adapters/inline.ts @@ -1,4 +1,4 @@ -import { InlineCode, Link, RenderableContent } from './types'; +import { InlineCode, Link, RenderableContent } from '../../renderables/types'; import { pipe } from 'fp-ts/function'; import { apply } from '#utils/fp'; diff --git a/src/core/markdown/adapters/methods-and-constructors.ts b/src/core/markdown/adapters/methods-and-constructors.ts index a1f7c30d..4a749de3 100644 --- a/src/core/markdown/adapters/methods-and-constructors.ts +++ b/src/core/markdown/adapters/methods-and-constructors.ts @@ -5,9 +5,9 @@ import { MethodMirrorWithInheritance, CodeBlock, GetRenderableContentByTypeName, -} from './types'; -import { adaptDescribable, adaptDocumentable } from './documentables'; -import { Documentable } from './types'; +} from '../../renderables/types'; +import { adaptDescribable, adaptDocumentable } from '../../renderables/documentables'; +import { Documentable } from '../../renderables/types'; export function adaptMethod( method: MethodMirror, diff --git a/src/core/markdown/adapters/renderable-bundle.ts b/src/core/markdown/adapters/renderable-bundle.ts index 946c5da9..2b0e580d 100644 --- a/src/core/markdown/adapters/renderable-bundle.ts +++ b/src/core/markdown/adapters/renderable-bundle.ts @@ -1,7 +1,7 @@ import { DocPageReference, ParsedFile } from '../../shared/types'; -import { Link, ReferenceGuideReference, Renderable, RenderableBundle } from './types'; +import { Link, ReferenceGuideReference, Renderable, RenderableBundle } from '../../renderables/types'; import { typeToRenderable } from './apex-types'; -import { adaptDescribable } from './documentables'; +import { adaptDescribable } from '../../renderables/documentables'; import { MarkdownGeneratorConfig } from '../generate-docs'; import { apply } from '#utils/fp'; import { Type } from '@cparra/apex-reflection'; diff --git a/src/core/markdown/adapters/renderable-to-page-data.ts b/src/core/markdown/adapters/renderable-to-page-data.ts index eff41e45..cc291602 100644 --- a/src/core/markdown/adapters/renderable-to-page-data.ts +++ b/src/core/markdown/adapters/renderable-to-page-data.ts @@ -1,4 +1,4 @@ -import { ReferenceGuideReference, Renderable, RenderableBundle, RenderableEnum } from './types'; +import { ReferenceGuideReference, Renderable, RenderableBundle, RenderableEnum } from '../../renderables/types'; import { DocPageData, DocumentationBundle } from '../../shared/types'; import { pipe } from 'fp-ts/function'; import { CompilationRequest, Template } from '../../template'; diff --git a/src/core/markdown/adapters/type-utils.ts b/src/core/markdown/adapters/type-utils.ts index a7396e0a..0aa96208 100644 --- a/src/core/markdown/adapters/type-utils.ts +++ b/src/core/markdown/adapters/type-utils.ts @@ -1,4 +1,4 @@ -import { CodeBlock, EmptyLine, InlineCode, RenderableContent } from './types'; +import { CodeBlock, EmptyLine, InlineCode, RenderableContent } from '../../renderables/types'; export function isEmptyLine(content: RenderableContent): content is EmptyLine { return Object.keys(content).includes('__type') && (content as { __type: string }).__type === 'empty-line'; diff --git a/src/core/markdown/adapters/documentables.ts b/src/core/renderables/documentables.ts similarity index 96% rename from src/core/markdown/adapters/documentables.ts rename to src/core/renderables/documentables.ts index c17bc79f..7d12c8e6 100644 --- a/src/core/markdown/adapters/documentables.ts +++ b/src/core/renderables/documentables.ts @@ -1,7 +1,7 @@ import { CustomTag, RenderableDocumentation, RenderableContent } from './types'; import { Describable, Documentable, GetRenderableContentByTypeName } from './types'; -import { replaceInlineReferences } from './inline'; -import { isEmptyLine } from './type-utils'; +import { replaceInlineReferences } from '../markdown/adapters/inline'; +import { isEmptyLine } from '../markdown/adapters/type-utils'; export function adaptDescribable( describable: Describable, diff --git a/src/core/markdown/adapters/types.d.ts b/src/core/renderables/types.d.ts similarity index 98% rename from src/core/markdown/adapters/types.d.ts rename to src/core/renderables/types.d.ts index a0820904..878bc918 100644 --- a/src/core/markdown/adapters/types.d.ts +++ b/src/core/renderables/types.d.ts @@ -7,7 +7,7 @@ import { MethodMirror, PropertyMirror, } from '@cparra/apex-reflection'; -import { DocPageReference } from '../../shared/types'; +import { DocPageReference } from '../shared/types'; export type Describable = string[] | undefined; diff --git a/src/core/template.ts b/src/core/template.ts index ba7806d3..6e07f3d9 100644 --- a/src/core/template.ts +++ b/src/core/template.ts @@ -1,5 +1,5 @@ import Handlebars from 'handlebars'; -import { CodeBlock, RenderableContent, StringOrLink } from './markdown/adapters/types'; +import { CodeBlock, RenderableContent, StringOrLink } from './renderables/types'; import { isCodeBlock, isEmptyLine, isInlineCode } from './markdown/adapters/type-utils'; import { typeDocPartial } from './markdown/templates/type-doc-partial'; import { documentablePartialTemplate } from './markdown/templates/documentable-partial-template'; From b139df2677e4f8c7428ae6d4d59b24cb8d8c338e Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:06:36 -0400 Subject: [PATCH 57/61] Refactorings --- examples/changelog/docs/changelog.md | 24 +++++++++++++++++++++++- examples/changelog/package.json | 3 ++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index 5ddad421..f576225b 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -1 +1,23 @@ -# Changelog \ No newline at end of file +# Changelog + +## New Classes + +These classes are new. + +### AccountService + +This is a new class that does foo and bar and references Baz . + +## New Interfaces + +These interfaces are new. + +### IAnotherExample + +### IExemplificable + +## New Enums + +These enums are new. + +### PossibleValues \ No newline at end of file diff --git a/examples/changelog/package.json b/examples/changelog/package.json index ed53a483..0fb0b956 100644 --- a/examples/changelog/package.json +++ b/examples/changelog/package.json @@ -14,6 +14,7 @@ }, "apexdocs": { "previousVersionDir": "previous", - "currentVersionDir": "current" + "currentVersionDir": "current", + "scope": ["global", "public", "private"] } } From b1a3974082601a9fcf042002905e009c88c2dc6a Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:15:40 -0400 Subject: [PATCH 58/61] Includes removed types --- ...onverting-to-renderable-change-log.spec.ts | 82 ------------------- .../__test__/generating-change-log.spec.ts | 28 +++++++ .../__test__/processing-changelog.spec.ts | 44 +++++----- src/core/changelog/generate-change-log.ts | 14 ++-- ...ess-change-log.ts => process-changelog.ts} | 4 +- ...-change-log.ts => renderable-changelog.ts} | 19 ++++- ...-log-template.ts => changelog-template.ts} | 12 ++- 7 files changed, 85 insertions(+), 118 deletions(-) delete mode 100644 src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts rename src/core/changelog/{process-change-log.ts => process-changelog.ts} (98%) rename src/core/changelog/{renderable-change-log.ts => renderable-changelog.ts} (80%) rename src/core/changelog/templates/{change-log-template.ts => changelog-template.ts} (77%) diff --git a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts b/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts deleted file mode 100644 index 31a2223c..00000000 --- a/src/core/changelog/__test__/converting-to-renderable-change-log.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ChangeLog } from '../process-change-log'; -import { convertToRenderableChangeLog } from '../renderable-change-log'; -import { ClassMirrorBuilder } from '../../../test-helpers/ClassMirrorBuilder'; -import { InterfaceMirrorBuilder } from '../../../test-helpers/InterfaceMirrorBuilder'; -import { EnumMirrorBuilder } from '../../../test-helpers/EnumMirrorBuilder'; - -describe('when converting a changelog to a renderable changelog', () => { - it('does not include the New Classes section if there are none', () => { - const changeLog: ChangeLog = { - newTypes: [], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, []); - - expect(renderable.newClasses).toBeNull(); - }); - - it('includes the New Classes section if there are any', () => { - const newClasses = [new ClassMirrorBuilder().withName('MyClass').build()]; - const changeLog: ChangeLog = { - newTypes: ['MyClass'], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, newClasses); - - expect(renderable.newClasses).not.toBeNull(); - }); - - it('does not include the New Interfaces section if there are none', () => { - const changeLog: ChangeLog = { - newTypes: [], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, []); - - expect(renderable.newInterfaces).toBeNull(); - }); - - it('includes the New Interfaces section if there are any', () => { - const newInterfaces = [new InterfaceMirrorBuilder().withName('MyInterface').build()]; - const changeLog: ChangeLog = { - newTypes: ['MyInterface'], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, newInterfaces); - - expect(renderable.newInterfaces).not.toBeNull(); - }); - - it('does not include the New Enums section if there are none', () => { - const changeLog: ChangeLog = { - newTypes: [], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, []); - - expect(renderable.newEnums).toBeNull(); - }); - - it('includes the New Enums section if there are any', () => { - const newEnums = [new EnumMirrorBuilder().withName('MyEnum').build()]; - const changeLog: ChangeLog = { - newTypes: ['MyEnum'], - removedTypes: [], - newOrModifiedMembers: [], - }; - - const renderable = convertToRenderableChangeLog(changeLog, newEnums); - - expect(renderable.newEnums).not.toBeNull(); - }); -}); diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index 3ab57f51..b48f712f 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -175,4 +175,32 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).not.toContain('## New Classes')); }); }); + + describe('that includes removed types', () => { + it('should include a section for removed types', async () => { + const oldClassSource = 'class Test {}'; + + const oldBundle: UnparsedSourceFile[] = [ + { content: oldClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + const newBundle: UnparsedSourceFile[] = []; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('## Removed Types')); + }); + + it('should include the removed type name', async () => { + const oldClassSource = 'class Test {}'; + + const oldBundle: UnparsedSourceFile[] = [ + { content: oldClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + const newBundle: UnparsedSourceFile[] = []; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('- Test')); + }); + }); }); diff --git a/src/core/changelog/__test__/processing-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts index 0c4a1d04..9162f06a 100644 --- a/src/core/changelog/__test__/processing-changelog.spec.ts +++ b/src/core/changelog/__test__/processing-changelog.spec.ts @@ -1,4 +1,4 @@ -import { processChangeLog } from '../process-change-log'; +import { processChangelog } from '../process-changelog'; import { reflect, Type } from '@cparra/apex-reflection'; function typeFromRawString(raw: string): Type { @@ -15,7 +15,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([]); }); @@ -24,7 +24,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [] }; const newVersion = { types: [] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([]); }); @@ -35,7 +35,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [anyClass] }; const newVersion = { types: [anyClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([]); }); @@ -46,7 +46,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [anyClass] }; const newVersion = { types: [anyClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([]); }); @@ -59,7 +59,7 @@ describe('when generating a changelog', () => { const newClass = typeFromRawString(newClassBody); const newVersion = { types: [existingClass, newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newTypes).toEqual([newClass.name]); }); @@ -72,7 +72,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [existingClass, existingOnlyInOldClass] }; const newVersion = { types: [existingClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.removedTypes).toEqual([existingOnlyInOldClass.name]); }); @@ -86,7 +86,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldEnum] }; const newVersion = { types: [newEnum] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -110,7 +110,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldEnum] }; const newVersion = { types: [newEnum] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -134,7 +134,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldInterface] }; const newVersion = { types: [newInterface] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -158,7 +158,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -182,7 +182,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldInterface] }; const newVersion = { types: [newInterface] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -206,7 +206,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -230,7 +230,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -254,7 +254,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -278,7 +278,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -302,7 +302,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -326,7 +326,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -350,7 +350,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -374,7 +374,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -398,7 +398,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { @@ -422,7 +422,7 @@ describe('when generating a changelog', () => { const oldVersion = { types: [oldClass] }; const newVersion = { types: [newClass] }; - const changeLog = processChangeLog(oldVersion, newVersion); + const changeLog = processChangelog(oldVersion, newVersion); expect(changeLog.newOrModifiedMembers).toEqual([ { diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts index 1b37d3f6..ec936b86 100644 --- a/src/core/changelog/generate-change-log.ts +++ b/src/core/changelog/generate-change-log.ts @@ -2,10 +2,10 @@ import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../s import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither'; import { reflectBundles } from '../reflection/reflect-source'; -import { processChangeLog, VersionManifest } from './process-change-log'; -import { convertToRenderableChangeLog, RenderableChangeLog } from './renderable-change-log'; +import { processChangelog, VersionManifest } from './process-changelog'; +import { convertToRenderableChangelog, RenderableChangelog } from './renderable-changelog'; import { CompilationRequest, Template } from '../template'; -import { changeLogTemplate } from './templates/change-log-template'; +import { changelogTemplate } from './templates/changelog-template'; import { ReflectionErrors } from '../errors/errors'; import { apply } from '#utils/fp'; import { filterScope } from '../reflection/filter-scope'; @@ -34,10 +34,10 @@ export function generateChangeLog( TE.bind('newVersion', () => reflect(newBundles)), TE.map(toManifests), TE.map(({ oldManifest, newManifest }) => ({ - changeLog: processChangeLog(oldManifest, newManifest), + changeLog: processChangelog(oldManifest, newManifest), newManifest, })), - TE.map(({ changeLog, newManifest }) => convertToRenderableChangeLog(changeLog, newManifest.types)), + TE.map(({ changeLog, newManifest }) => convertToRenderableChangelog(changeLog, newManifest.types)), TE.map(compile), TE.map(convertToPageData), ); @@ -56,9 +56,9 @@ function toManifests({ oldVersion, newVersion }: { oldVersion: ParsedFile[]; new }; } -function compile(renderable: RenderableChangeLog): string { +function compile(renderable: RenderableChangelog): string { const compilationRequest: CompilationRequest = { - template: changeLogTemplate, + template: changelogTemplate, source: renderable, }; diff --git a/src/core/changelog/process-change-log.ts b/src/core/changelog/process-changelog.ts similarity index 98% rename from src/core/changelog/process-change-log.ts rename to src/core/changelog/process-changelog.ts index 26c69caa..7d0aabcf 100644 --- a/src/core/changelog/process-change-log.ts +++ b/src/core/changelog/process-changelog.ts @@ -28,13 +28,13 @@ type NewOrModifiedMember = { modifications: MemberModificationType[]; }; -export type ChangeLog = { +export type Changelog = { newTypes: string[]; removedTypes: string[]; newOrModifiedMembers: NewOrModifiedMember[]; }; -export function processChangeLog(oldVersion: VersionManifest, newVersion: VersionManifest): ChangeLog { +export function processChangelog(oldVersion: VersionManifest, newVersion: VersionManifest): Changelog { return { newTypes: getNewTypes(oldVersion, newVersion), removedTypes: getRemovedTypes(oldVersion, newVersion), diff --git a/src/core/changelog/renderable-change-log.ts b/src/core/changelog/renderable-changelog.ts similarity index 80% rename from src/core/changelog/renderable-change-log.ts rename to src/core/changelog/renderable-changelog.ts index 05ee8faa..c9389fd6 100644 --- a/src/core/changelog/renderable-change-log.ts +++ b/src/core/changelog/renderable-changelog.ts @@ -1,4 +1,4 @@ -import { ChangeLog } from './process-change-log'; +import { Changelog } from './process-changelog'; import { Type } from '@cparra/apex-reflection'; import { RenderableContent } from '../renderables/types'; import { adaptDescribable } from '../renderables/documentables'; @@ -15,14 +15,21 @@ type NewTypeSection = { types: NewTypeRenderable[]; }; -export type RenderableChangeLog = { +type RemovedTypeSection = { + heading: string; + description: string; + types: string[]; +}; + +export type RenderableChangelog = { newClasses: NewTypeSection<'class'> | null; newInterfaces: NewTypeSection<'interface'> | null; newEnums: NewTypeSection<'enum'> | null; + removedTypes: RemovedTypeSection | null; }; -export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: Type[]): RenderableChangeLog { - const allNewTypes = changeLog.newTypes.map( +export function convertToRenderableChangelog(changelog: Changelog, newManifest: Type[]): RenderableChangelog { + const allNewTypes = changelog.newTypes.map( (newType) => newManifest.find((type) => type.name.toLowerCase() === newType.toLowerCase())!, ); @@ -58,6 +65,10 @@ export function convertToRenderableChangeLog(changeLog: ChangeLog, newManifest: types: newEnums.map(typeToRenderable), } : null, + removedTypes: + changelog.removedTypes.length > 0 + ? { heading: 'Removed Types', description: 'These types have been removed.', types: changelog.removedTypes } + : null, }; } diff --git a/src/core/changelog/templates/change-log-template.ts b/src/core/changelog/templates/changelog-template.ts similarity index 77% rename from src/core/changelog/templates/change-log-template.ts rename to src/core/changelog/templates/changelog-template.ts index c1d93094..aab36994 100644 --- a/src/core/changelog/templates/change-log-template.ts +++ b/src/core/changelog/templates/changelog-template.ts @@ -1,4 +1,4 @@ -export const changeLogTemplate = ` +export const changelogTemplate = ` # Changelog {{#if newClasses}} @@ -36,4 +36,14 @@ export const changeLogTemplate = ` {{{renderContent this.description}}} {{/each}} {{/if}} + +{{#if removedTypes}} +## Removed Types + +{{removedTypes.description}} + +{{#each removedTypes.types}} +- {{this}} +{{/each}} +{{/if}} `.trim(); From b5dc42709246b5f7978bfc68afb6c83b26b1d6b5 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:17:12 -0400 Subject: [PATCH 59/61] Includes removed types --- examples/changelog/docs/changelog.md | 8 +++++++- examples/changelog/previous/.gitkeep | 0 examples/changelog/previous/OldImplementation.cls | 1 + src/core/changelog/renderable-changelog.ts | 4 ---- 4 files changed, 8 insertions(+), 5 deletions(-) delete mode 100644 examples/changelog/previous/.gitkeep create mode 100644 examples/changelog/previous/OldImplementation.cls diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index f576225b..26cdda72 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -20,4 +20,10 @@ These interfaces are new. These enums are new. -### PossibleValues \ No newline at end of file +### PossibleValues + +## Removed Types + +These types have been removed. + +- OldImplementation \ No newline at end of file diff --git a/examples/changelog/previous/.gitkeep b/examples/changelog/previous/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/changelog/previous/OldImplementation.cls b/examples/changelog/previous/OldImplementation.cls new file mode 100644 index 00000000..79a73e73 --- /dev/null +++ b/examples/changelog/previous/OldImplementation.cls @@ -0,0 +1 @@ +public class OldImplementation {} diff --git a/src/core/changelog/renderable-changelog.ts b/src/core/changelog/renderable-changelog.ts index c9389fd6..51e64bbc 100644 --- a/src/core/changelog/renderable-changelog.ts +++ b/src/core/changelog/renderable-changelog.ts @@ -84,10 +84,6 @@ function typeToRenderable(type: Type): NewTypeRenderable { }; } -// TODO: Removed Classes -// TODO: Removed Enums -// TODO: Removed Interfaces - // Changes... // TODO: New and removed enum values // TODO: new and removed mehtods From cfc4fb58e4e73ae9cd6d67811e183a81b3e1cca3 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:35:38 -0400 Subject: [PATCH 60/61] Includes changes to existing members --- .../current/classes/SolidService.cls | 9 +++ examples/changelog/docs/changelog.md | 11 +++- examples/changelog/previous/SolidService.cls | 9 +++ .../__test__/generating-change-log.spec.ts | 53 ++++++++++++++++ src/core/changelog/process-changelog.ts | 4 +- src/core/changelog/renderable-changelog.ts | 60 ++++++++++++++++--- .../changelog/templates/changelog-template.ts | 14 +++++ 7 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 examples/changelog/current/classes/SolidService.cls create mode 100644 examples/changelog/previous/SolidService.cls diff --git a/examples/changelog/current/classes/SolidService.cls b/examples/changelog/current/classes/SolidService.cls new file mode 100644 index 00000000..a3517717 --- /dev/null +++ b/examples/changelog/current/classes/SolidService.cls @@ -0,0 +1,9 @@ +public class SolidService { + public void doSomething() { + // do something + } + + public void newMethod() { + // new method + } +} diff --git a/examples/changelog/docs/changelog.md b/examples/changelog/docs/changelog.md index 26cdda72..f25caaf8 100644 --- a/examples/changelog/docs/changelog.md +++ b/examples/changelog/docs/changelog.md @@ -26,4 +26,13 @@ These enums are new. These types have been removed. -- OldImplementation \ No newline at end of file +- OldImplementation + +## New or Modified Members in Existing Types + +These members have been added or modified. + +### SolidService + +- New Method: newMethod +- Removed Method: deprecatedMethod \ No newline at end of file diff --git a/examples/changelog/previous/SolidService.cls b/examples/changelog/previous/SolidService.cls new file mode 100644 index 00000000..0c19adf3 --- /dev/null +++ b/examples/changelog/previous/SolidService.cls @@ -0,0 +1,9 @@ +public class SolidService { + public void doSomething() { + // do something + } + + public void deprecatedMethod() { + // deprecated method + } +} diff --git a/src/core/changelog/__test__/generating-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts index b48f712f..83212b37 100644 --- a/src/core/changelog/__test__/generating-change-log.spec.ts +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -203,4 +203,57 @@ describe('when generating a changelog', () => { assertEither(result, (data) => expect(data.content).toContain('- Test')); }); }); + + describe('that includes modifications to existing members', () => { + it('should include a section for new or modified members', async () => { + const oldClassSource = 'class Test {}'; + const newClassSource = 'class Test { void myMethod() {} }'; + + const oldBundle: UnparsedSourceFile[] = [ + { content: oldClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('## New or Modified Members in Existing Types')); + }); + + it('should include the new or modified type name', async () => { + const oldClassSource = 'class Test {}'; + const newClassSource = 'class Test { void myMethod() {} }'; + + const oldBundle: UnparsedSourceFile[] = [ + { content: oldClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('### Test')); + }); + + it('should include the new or modified member name', async () => { + const oldClassSource = 'class Test {}'; + const newClassSource = 'class Test { void myMethod() {} }'; + + const oldBundle: UnparsedSourceFile[] = [ + { content: oldClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const newBundle: UnparsedSourceFile[] = [ + { content: newClassSource, filePath: 'Test.cls', metadataContent: null }, + ]; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).toContain('myMethod')); + }); + }); }); diff --git a/src/core/changelog/process-changelog.ts b/src/core/changelog/process-changelog.ts index 7d0aabcf..d1e339c9 100644 --- a/src/core/changelog/process-changelog.ts +++ b/src/core/changelog/process-changelog.ts @@ -18,12 +18,12 @@ type ModificationTypes = | 'NewField' | 'RemovedField'; -type MemberModificationType = { +export type MemberModificationType = { __typename: ModificationTypes; name: string; }; -type NewOrModifiedMember = { +export type NewOrModifiedMember = { typeName: string; modifications: MemberModificationType[]; }; diff --git a/src/core/changelog/renderable-changelog.ts b/src/core/changelog/renderable-changelog.ts index 51e64bbc..43aa4982 100644 --- a/src/core/changelog/renderable-changelog.ts +++ b/src/core/changelog/renderable-changelog.ts @@ -1,4 +1,4 @@ -import { Changelog } from './process-changelog'; +import { Changelog, MemberModificationType, NewOrModifiedMember } from './process-changelog'; import { Type } from '@cparra/apex-reflection'; import { RenderableContent } from '../renderables/types'; import { adaptDescribable } from '../renderables/documentables'; @@ -21,11 +21,23 @@ type RemovedTypeSection = { types: string[]; }; +type NewOrModifiedMemberSection = { + typeName: string; + modifications: string[]; +}; + +type NewOrModifiedMembersSection = { + heading: string; + description: string; + modifications: NewOrModifiedMemberSection[]; +}; + export type RenderableChangelog = { newClasses: NewTypeSection<'class'> | null; newInterfaces: NewTypeSection<'interface'> | null; newEnums: NewTypeSection<'enum'> | null; removedTypes: RemovedTypeSection | null; + newOrModifiedMembers: NewOrModifiedMembersSection | null; }; export function convertToRenderableChangelog(changelog: Changelog, newManifest: Type[]): RenderableChangelog { @@ -69,6 +81,14 @@ export function convertToRenderableChangelog(changelog: Changelog, newManifest: changelog.removedTypes.length > 0 ? { heading: 'Removed Types', description: 'These types have been removed.', types: changelog.removedTypes } : null, + newOrModifiedMembers: + changelog.newOrModifiedMembers.length > 0 + ? { + heading: 'New or Modified Members in Existing Types', + description: 'These members have been added or modified.', + modifications: changelog.newOrModifiedMembers.map(toRenderableModification), + } + : null, }; } @@ -84,10 +104,34 @@ function typeToRenderable(type: Type): NewTypeRenderable { }; } -// Changes... -// TODO: New and removed enum values -// TODO: new and removed mehtods -// TODO: new and removed class members -// TODO: new and removed inner classes -// TODO: new and removed inner interfaces -// TODO: new and removed inner enums +function toRenderableModification(newOrModifiedMember: NewOrModifiedMember): NewOrModifiedMemberSection { + return { + typeName: newOrModifiedMember.typeName, + modifications: newOrModifiedMember.modifications.map(toRenderableModificationDescription), + }; +} + +function toRenderableModificationDescription(memberModificationType: MemberModificationType): string { + switch (memberModificationType.__typename) { + case 'NewEnumValue': + return `New Enum Value: ${memberModificationType.name}`; + case 'RemovedEnumValue': + return `Removed Enum Value: ${memberModificationType.name}`; + case 'NewMethod': + return `New Method: ${memberModificationType.name}`; + case 'RemovedMethod': + return `Removed Method: ${memberModificationType.name}`; + case 'NewProperty': + return `New Property: ${memberModificationType.name}`; + case 'RemovedProperty': + return `Removed Property: ${memberModificationType.name}`; + case 'NewField': + return `New Field: ${memberModificationType.name}`; + case 'RemovedField': + return `Removed Field: ${memberModificationType.name}`; + case 'NewType': + return `New Type: ${memberModificationType.name}`; + case 'RemovedType': + return `Removed Type: ${memberModificationType.name}`; + } +} diff --git a/src/core/changelog/templates/changelog-template.ts b/src/core/changelog/templates/changelog-template.ts index aab36994..3d982da2 100644 --- a/src/core/changelog/templates/changelog-template.ts +++ b/src/core/changelog/templates/changelog-template.ts @@ -46,4 +46,18 @@ export const changelogTemplate = ` - {{this}} {{/each}} {{/if}} + +{{#if newOrModifiedMembers}} +## {{newOrModifiedMembers.heading}} + +{{newOrModifiedMembers.description}} + +{{#each newOrModifiedMembers.modifications}} +### {{this.typeName}} + +{{#each this.modifications}} +- {{this}} +{{/each}} +{{/each}} +{{/if}} `.trim(); From 45ea3dde728df4ba0cd9faf09061233d3e04f620 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Sat, 28 Sep 2024 11:50:27 -0400 Subject: [PATCH 61/61] Documentation --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3e8c2ca2..d81cc8de 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,19 @@ annotated with `@RestResource`: apexdocs openapi -s force-app ``` +#### Changelog + +Run the following command to generate a changelog for your Salesforce Apex classes: + +```bash +apexdocs changelog --previousVersionDir force-app-previous --currentVersionDir force-app +``` + ## 🚀 Features * Generate documentation for Salesforce Apex classes as Markdown files * Generate an OpenApi REST specification based on `@RestResource` classes +* Generate a changelog based on the differences between two versions of your Salesforce Apex classes * Support for grouping blocks of related code within a class * Support for ignoring files and members from being documented * Namespace support @@ -146,6 +155,28 @@ apexdocs markdown -s force-app -t docs -p global public namespaceaccessible -n M apexdocs openapi -s force-app -t docs -n MyNamespace --title "My Custom OpenApi Title" ``` +### Changelog + +`changelog` + +#### Flags + +| Flag | Alias | Description | Default | Required | +|------------------------|-------|--------------------------------------------------------------------|-------------|----------| +| `--previousVersionDir` | `-p` | The directory location of the previous version of the source code. | N/A | Yes | +| `--currentVersionDir` | `-t` | The directory location of the current version of the source code. | N/A | Yes | +| `--targetDir` | `-t` | The directory location where the changelog file will be generated. | `./docs/` | No | +| `--fileName` | N/A | The name of the changelog file to be generated. | `changelog` | No | +| `--scope` | N/A | The list of scope to respect when generating the changelog. | ['global'] | No | + +#### Sample Usage + +```bash +apexdocs changelog -p force-app-previous -t force-app +``` + +--- + ## 🔬 Defining a configuration file You can also use a configuration file to define the parameters that will be used when generating the documentation. @@ -187,7 +218,7 @@ CLI will be used, or the default value will be used. ### Config Intellisense -Using the `defineMarkdownConfig` (or the `defineOpenApiConfig` for OpenApi documentation) +Using the `defineMarkdownConfig` (or the `defineOpenApiConfig` for OpenApi documentation) helper will provide Typescript-powered intellisense for the configuration file options. This should work with both Javascript and Typescript files. @@ -202,8 +233,44 @@ export default defineMarkdownConfig({ }); ``` +### Generating Different Types of Documentation + +You might want to generate different types of documentation using a single command. For example, if you are releasing +a new version of your project, you might want to generate updated documentation Markdown files, and at the +same time generate a changelog listing everything new. + +You can do this by providing a configuration file that exports a configuration object which keys are the type of +documentation you want to generate. + +```typescript +import { defineMarkdownConfig, defineChangelogConfig } from '@cparra/apexdocs'; + +export default { + markdown: defineMarkdownConfig({ + sourceDir: 'force-app', + targetDir: 'docs', + scope: ['global', 'public'], + ... + }), + changelog: defineChangelogConfig({ + previousVersionDir: 'force-app-previous', + currentVersionDir: 'force-app', + targetDir: 'docs', + scope: ['global', 'public'], + }) +}; +``` + +Then you only need to run the top level `apexdocs` command, and it will generate both types of documentation. + +```bash +apexdocs +``` + ### Excluding Tags from Appearing in the Documentation +Note: Only works for Markdown documentation. + You can exclude tags from appearing in the documentation by using the `excludeTags` property in the configuration file, which allow you to pass a list of tags that you want to exclude from the documentation. @@ -221,7 +288,7 @@ export default defineMarkdownConfig({ ### Excluding Files from Being Documented -You can exclude one or multiple files from being documented by providing a list of glob patterns to +You can exclude one or multiple files from being documented by providing a list of glob patterns to the `exclude` property in the configuration file. ```typescript