diff --git a/.idea/.gitignore b/.idea/.gitignore index fb465aba..06f25a75 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ /dataSources.local.xml # Editor-based HTTP Client requests /httpRequests/ +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/README.md b/README.md index ed738d0f..466a4749 100644 --- a/README.md +++ b/README.md @@ -102,22 +102,23 @@ apexdocs-generate The CLI supports the following parameters: -| Parameter | Alias | Description | Default | Required | -|------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|----------| -| --sourceDir | -s | The directory location which contains your apex .cls classes. | N/A | Yes | -| --targetDir | -t | The directory location where documentation will be generated to. | `docs` | No | -| --recursive | -r | Whether .cls classes will be searched for recursively in the directory provided. | `true` | No | -| --scope | -p | A list of scopes to document. Values should be separated by a space, e.g --scope public private. Note that this setting is ignored if generating an OpenApi REST specification since that looks for classes annotated with @RestResource. | `global` | No | -| --targetGenerator | -g | Define the static file generator for which the documents will be created. Currently supports: `jekyll`, `docsify`, `plain-markdown`, and `openapi`. | `jekyll` | No | -| --indexOnly | N/A | Defines whether only the index file should be generated. | `false` | No | -| --defaultGroupName | N/A | Defines the `@group` name to be used when a file does not specify it. | `Miscellaneous` | No | -| --sanitizeHtml | N/A | When on, any special character within your ApexDocs is converted into its HTML code representation. This is specially useful when generic objects are described within the docs, e.g. "List< Foo>", "Map" because otherwise the content within < and > would be treated as HTML tags and not shown in the output. Content in @example blocks are never sanitized. | `Apex REST Api` | No | -| --openApiTitle | N/A | If using "openapi" as the target generator, this allows you to specify the OpenApi title value. | true | No | -| --title | N/A | Allows you to specify the home page main title. If using "openapi" this acts as an alias to the openApiTitle parameter | `Classes` | No | -| --namespace | N/A | The package namespace, if any. If this value is provided the namespace will be added as a prefix to all of the parsed files. If generating an OpenApi definition, it will be added to the file's Server Url. | N/A | No | -| --openApiFileName | N/A | If using "openapi" as the target generator, this allows you to specify the name of the output file. | `openapi` | No | -| --includeMetadata | N/A | Whether to include the file's meta.xml information: Whether it is active and and the API version | false | No | -| --documentationRootDir | N/A | The root directory where the documentation will be generated. This is useful when you want to generate the documentation in a subdirectory of your project. | N/A | No | +| Parameter | Alias | Description | Default | Required | +|-----------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|----------| +| --sourceDir | -s | The directory location which contains your apex .cls classes. | N/A | Yes | +| --targetDir | -t | The directory location where documentation will be generated to. | `docs` | No | +| --recursive | -r | Whether .cls classes will be searched for recursively in the directory provided. | `true` | No | +| --scope | -p | A list of scopes to document. Values should be separated by a space, e.g --scope public private. Note that this setting is ignored if generating an OpenApi REST specification since that looks for classes annotated with @RestResource. | `global` | No | +| --targetGenerator | -g | Define the static file generator for which the documents will be created. Currently supports: `jekyll`, `docsify`, `plain-markdown`, and `openapi`. | `jekyll` | No | +| --indexOnly | N/A | Defines whether only the index file should be generated. | `false` | No | +| --defaultGroupName | N/A | Defines the `@group` name to be used when a file does not specify it. | `Miscellaneous` | No | +| --sanitizeHtml | N/A | When on, any special character within your ApexDocs is converted into its HTML code representation. This is specially useful when generic objects are described within the docs, e.g. "List< Foo>", "Map" because otherwise the content within < and > would be treated as HTML tags and not shown in the output. Content in @example blocks are never sanitized. | `Apex REST Api` | No | +| --openApiTitle | N/A | If using "openapi" as the target generator, this allows you to specify the OpenApi title value. | true | No | +| --title | N/A | Allows you to specify the home page main title. If using "openapi" this acts as an alias to the openApiTitle parameter | `Classes` | No | +| --namespace | N/A | The package namespace, if any. If this value is provided the namespace will be added as a prefix to all of the parsed files. If generating an OpenApi definition, it will be added to the file's Server Url. | N/A | No | +| --openApiFileName | N/A | If using "openapi" as the target generator, this allows you to specify the name of the output file. | `openapi` | No | +| --sortMembersAlphabetically | N/A | Whether to sort the members of a class alphabetically. | `false` | No | + | --includeMetadata | N/A | Whether to include the file's meta.xml information: Whether it is active and and the API version | false | No | + | --documentationRootDir | N/A | The root directory where the documentation will be generated. This is useful when you want to generate the documentation in a subdirectory of your project. | N/A | No | ### Using a configuration file @@ -142,6 +143,16 @@ allow you to override some of the default behavior: The full object definition can be imported from `@cparra/apexdocs/lib/settings` - `onAfterProcess` - A function that will be called after all files have been processed. It receives a `TargetFile[]` array with all of the files that were processed and does not return anything. +- `frontMatterHeader` - A function that will be called before the front matter is written to the file (when using the Jekyll generator). + It receives a `TargetType` object + and should return a list of strings that will be written to the file as the front matter. + The full object definition can be imported from `@cparra/apexdocs/lib/settings` + and contains the following properties: + - `name` - The name of the type + - `typeName` - Can be 'class', 'interface', or 'enum' + - `accessModifier` - The access modifier of the type + - `group` - The group to which the type belongs (if any) + - `description` - The description of the type as defined in the ApexDoc ```typescript import {TargetFile} from "@cparra/apexdocs/lib/settings"; diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index b2be051d..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,16 +0,0 @@ -[ ] Respect access modifiers where the properties/methods are different from the class declaration. For example, -AuraEnabled properties do not live in an AuraEnabled class, so there's no way to just generate docs to showcase the -AuraEnabled properties of a class without some sort of combination of also exposing other public/globals - -[ ] Versioning capabilities. When creating the doc you specify a version number, and a new directory is created for the -files, instead of just overriding - -[ ] New generatortype: To JsDocs from AuraEnabled - -[ ] More unit tests - -[ ] config.js support to allow for injections, home header, etc. - -[ ] Add configuration setting that allows someone to set the "namespace" - -[ ] Homepage support similar to what docsify does diff --git a/apexdocs.config.ts b/apexdocs.config.ts index 9516f54d..83c0a7e7 100644 --- a/apexdocs.config.ts +++ b/apexdocs.config.ts @@ -1,4 +1,4 @@ -import { TargetFile } from './src/settings'; +import { TargetFile, TargetType } from './src/settings'; export default { onBeforeFileWrite: (file: TargetFile): TargetFile => { @@ -8,4 +8,6 @@ export default { onAfterProcess: (files: TargetFile[]) => { console.log('onAfterProcess files', files); }, + frontMatterHeader: (file: TargetType) => [`title: ${file.name}.cls`, `description: ${file.description}`], + sortMembersAlphabetically: true, }; diff --git a/docs/types/Classes/nspc.ChildClass.md b/docs/types/Classes/nspc.ChildClass.md index bee452bf..a281181f 100644 --- a/docs/types/Classes/nspc.ChildClass.md +++ b/docs/types/Classes/nspc.ChildClass.md @@ -21,6 +21,9 @@ ChildClass ## Fields +### `private aPrivateString` → `String` + + ### `private privateStringFromChild` → `String` @@ -48,6 +51,24 @@ This is a protected string, use carefully. --- ## Methods ### `public void doSomething()` +### `public void execute()` + +Executes the command. + +### `public String getValue()` + +Returns a value based on the executed command. + +#### Returns + +|Type|Description| +|---|---| +|`String`|The value| + +### `public virtual String overridableMethod()` + +*Inherited* + ### `public override String overridableMethodOverridden()` This method was overridden. @@ -73,22 +94,4 @@ sequenceDiagram ``` -### `public void execute()` - -Executes the command. - -### `public String getValue()` - -Returns a value based on the executed command. - -#### Returns - -|Type|Description| -|---|---| -|`String`|The value| - -### `public virtual String overridableMethod()` - -*Inherited* - --- diff --git a/docs/types/Main/nspc.SampleClass.md b/docs/types/Main/nspc.SampleClass.md index 3eca85df..0973af9a 100644 --- a/docs/types/Main/nspc.SampleClass.md +++ b/docs/types/Main/nspc.SampleClass.md @@ -62,23 +62,23 @@ Constructs a SampleClass with an argument. ## Fields ### Common Constants -* `public ANOTHER_CONSTANT` → `String` * `public A_CONSTANT` → `String` [`NAMESPACEACCESSIBLE` ] - This is a constant. +* `public ANOTHER_CONSTANT` → `String` * `public listOfStrings` → `List` --- ### 'General' Constants -* `public GENERAL_ANOTHER_CONSTANT` → `String` * `public GENERAL_A_CONSTANT` → `String` [`NAMESPACEACCESSIBLE` ] - This is a constant. ---- -### Other variables - -* `public someVariable` → `String` +* `public GENERAL_ANOTHER_CONSTANT` → `String` --- ### Other * `private somePrivateStuff` → `String` --- +### Other variables + +* `public someVariable` → `String` +--- ## Properties ### `public AnotherProp` → `Decimal` @@ -97,6 +97,13 @@ This is a String property. --- ## Methods ### A method group +##### `public static String anotherSampleMethod(String arg1)` + +Something here + + +**Arg1** The arg1 description + ##### `public static String sampleMethod(String argument1, String argument2)` `NAMESPACEACCESSIBLE` @@ -123,13 +130,6 @@ System.debug(result); ``` -##### `public static String anotherSampleMethod(String arg1)` - -Something here - - -**Arg1** The arg1 description - ##### `public static Map> yetAnotherSampleMethod(String arg1)` --- ### Other diff --git a/examples/force-app/main/default/classes/ChildClass.cls b/examples/force-app/main/default/classes/ChildClass.cls index 6daa36cb..45f5cc0a 100644 --- a/examples/force-app/main/default/classes/ChildClass.cls +++ b/examples/force-app/main/default/classes/ChildClass.cls @@ -4,6 +4,7 @@ */ public class ChildClass extends ParentClass implements SampleInterface { private String privateStringFromChild; + private String aPrivateString; public void doSomething() { System.debug('Do something'); diff --git a/package.json b/package.json index 57684f20..657c002b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cparra/apexdocs", - "version": "2.22.0", + "version": "2.23.0", "description": "Library with CLI capabilities to generate documentation for Salesforce Apex classes.", "keywords": [ "apex", @@ -18,7 +18,7 @@ "scripts": { "test": "jest --coverage", "build": "rimraf ./lib && npm run lint && tsc --declaration", - "lint": "eslint ./src/**/*.{js,ts} --quiet --fix", + "lint": "eslint './src/**/*.{js,ts}' --quiet --fix", "prepare": "npm run build", "version": "npm run format && git add -A src", "postversion": "git push && git push --tags", diff --git a/src/cli/generate.ts b/src/cli/generate.ts index 8c74bba3..98d79ef7 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -8,6 +8,7 @@ import { cosmiconfig } from 'cosmiconfig'; const result = cosmiconfig('apexdocs').search(); result.then((config) => { + yargs.config(config?.config); let argv = yargs.options({ sourceDir: { type: 'string', @@ -85,6 +86,11 @@ result.then((config) => { describe: 'If using "openapi" as the target generator, this allows you to specify the name of the output file.', default: 'openapi', }, + sortMembersAlphabetically: { + type: 'boolean', + describe: 'Whether to sort members alphabetically.', + default: false, + }, includeMetadata: { type: 'boolean', describe: "Whether to include the file's meta.xml information: Whether it is active and and the API version", @@ -114,10 +120,12 @@ result.then((config) => { title: argv.title, namespace: argv.namespace, openApiFileName: argv.openApiFileName, + sortMembersAlphabetically: argv.sortMembersAlphabetically, includeMetadata: argv.includeMetadata, rootDir: argv.documentationRootDir, onAfterProcess: config?.config?.onAfterProcess, onBeforeFileWrite: config?.config?.onBeforeFileWrite, + frontMatterHeader: config?.config?.frontMatterHeader, }); try { diff --git a/src/model/markdown-generation-util/field-declaration-util.ts b/src/model/markdown-generation-util/field-declaration-util.ts index 215cc67d..5e89ca85 100644 --- a/src/model/markdown-generation-util/field-declaration-util.ts +++ b/src/model/markdown-generation-util/field-declaration-util.ts @@ -8,15 +8,9 @@ export function declareField( grouped = false, ) { markdownFile.addBlankLine(); - fields - .sort((propA, propB) => { - if (propA.name < propB.name) return -1; - if (propA.name > propB.name) return 1; - return 0; - }) - .forEach((propertyModel) => { - addFieldSection(markdownFile, propertyModel, startingHeadingLevel, grouped); - }); + fields.forEach((propertyModel) => { + addFieldSection(markdownFile, propertyModel, startingHeadingLevel, grouped); + }); markdownFile.addHorizontalRule(); } diff --git a/src/model/markdown-type-file.ts b/src/model/markdown-type-file.ts index c62398ca..516e87e5 100644 --- a/src/model/markdown-type-file.ts +++ b/src/model/markdown-type-file.ts @@ -91,16 +91,10 @@ export class MarkdownTypeFile extends MarkdownFile implements WalkerListener { private addInnerTypes(title: string, types: Type[], addSeparator = true) { this.addTitle(title, this.headingLevel + 1); - types - .sort((typeA, typeB) => { - if (typeA.name < typeB.name) return -1; - if (typeA.name > typeB.name) return 1; - return 0; - }) - .forEach((currentType) => { - const innerFile = new MarkdownTypeFile(currentType, this.headingLevel + 2, undefined, true); - this.addText(innerFile._contents); - }); + types.forEach((currentType) => { + const innerFile = new MarkdownTypeFile(currentType, this.headingLevel + 2, undefined, true); + this.addText(innerFile._contents); + }); if (addSeparator) { this.addHorizontalRule(); } diff --git a/src/service/walkers/class-walker.ts b/src/service/walkers/class-walker.ts index c4e31236..f2b3ddc2 100644 --- a/src/service/walkers/class-walker.ts +++ b/src/service/walkers/class-walker.ts @@ -5,27 +5,26 @@ export class ClassWalker extends Walker { walk(listener: WalkerListener): void { listener.onTypeDeclaration(this.type); const classMirror = this.type as ClassMirror; - if (classMirror.constructors.length) { listener.onConstructorDeclaration(this.type.name, classMirror.constructors); } if (classMirror.fields.length) { - listener.onFieldsDeclaration(classMirror.fields); + listener.onFieldsDeclaration(this.sortType(classMirror.fields)); } if (classMirror.properties.length) { - listener.onPropertiesDeclaration(classMirror.properties); + listener.onPropertiesDeclaration(this.sortType(classMirror.properties)); } if (classMirror.methods.length) { - listener.onMethodsDeclaration(classMirror.methods); + listener.onMethodsDeclaration(this.sortType(classMirror.methods)); } if (classMirror.enums.length) { - listener.onInnerEnumsDeclaration(classMirror.enums); + listener.onInnerEnumsDeclaration(this.sortType(classMirror.enums)); } if (classMirror.classes.length) { - listener.onInnerClassesDeclaration(classMirror.classes); + listener.onInnerClassesDeclaration(this.sortType(classMirror.classes)); } if (classMirror.interfaces.length) { - listener.onInnerInterfacesDeclaration(classMirror.interfaces); + listener.onInnerInterfacesDeclaration(this.sortType(classMirror.interfaces)); } } } diff --git a/src/service/walkers/enum-walker.ts b/src/service/walkers/enum-walker.ts index 7230fe16..b31dd0f7 100644 --- a/src/service/walkers/enum-walker.ts +++ b/src/service/walkers/enum-walker.ts @@ -1,5 +1,4 @@ import { Walker, WalkerListener } from './walker'; -import { EnumMirror } from '@cparra/apex-reflection'; export class EnumWalker extends Walker { walk(listener: WalkerListener): void { diff --git a/src/service/walkers/interface-walker.ts b/src/service/walkers/interface-walker.ts index 2175bd3c..dd2beead 100644 --- a/src/service/walkers/interface-walker.ts +++ b/src/service/walkers/interface-walker.ts @@ -6,7 +6,7 @@ export class InterfaceWalker extends Walker { listener.onTypeDeclaration(this.type); const interfaceMirror = this.type as InterfaceMirror; if (interfaceMirror.methods.length) { - listener.onMethodsDeclaration(interfaceMirror.methods); + listener.onMethodsDeclaration(this.sortType(interfaceMirror.methods)); } } } diff --git a/src/service/walkers/walker.ts b/src/service/walkers/walker.ts index 7c613adb..f19fcae1 100644 --- a/src/service/walkers/walker.ts +++ b/src/service/walkers/walker.ts @@ -8,7 +8,7 @@ import { PropertyMirror, Type, } from '@cparra/apex-reflection'; -import { Annotation } from '@cparra/apex-reflection/index'; +import { Settings } from '../../settings'; export interface WalkerListener { onTypeDeclaration(typeMirror: Type): void; @@ -32,4 +32,11 @@ export abstract class Walker { constructor(public type: Type) {} abstract walk(listener: WalkerListener): void; + + protected sortType(types: T[]): T[] { + if (Settings.getInstance().sortMembersAlphabetically()) { + return types.sort((a, b) => a.name.localeCompare(b.name)); + } + return types; + } } diff --git a/src/settings.ts b/src/settings.ts index d4a4f6f0..c74074db 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -13,6 +13,14 @@ export type OutputDir = { fileDir: string; }; +export type TargetType = { + name: string; + typeName: 'class' | 'interface' | 'enum'; + accessModifier: string; + description?: string; + group?: string; +}; + export interface SettingsConfig { sourceDirectory: string; recursive: boolean; @@ -28,8 +36,10 @@ export interface SettingsConfig { openApiFileName: string; includeMetadata: boolean; rootDir?: string; + sortMembersAlphabetically?: boolean; onAfterProcess?: (files: TargetFile[]) => void; onBeforeFileWrite?: (file: TargetFile) => TargetFile; + frontMatterHeader?: (file: TargetType) => string[]; } export class Settings { @@ -107,6 +117,10 @@ export class Settings { return this.config.includeMetadata; } + public sortMembersAlphabetically(): boolean { + return this.config.sortMembersAlphabetically ?? false; + } + public getRootDir(): string | undefined { return this.config.rootDir; } @@ -123,4 +137,11 @@ export class Settings { } return file; } + + public frontMatterHeader(file: TargetType): string[] { + if (this.config.frontMatterHeader) { + return this.config.frontMatterHeader(file); + } + return []; + } } diff --git a/src/transpiler/markdown/class-file-generatorHelper.ts b/src/transpiler/markdown/class-file-generatorHelper.ts index bf20bfbf..5a91bb6c 100644 --- a/src/transpiler/markdown/class-file-generatorHelper.ts +++ b/src/transpiler/markdown/class-file-generatorHelper.ts @@ -40,11 +40,11 @@ export default class ClassFileGeneratorHelper { // If the types the same groups then we simply link directly to that file return './'; } else { - // If the types have different groups then we have to go up a directory + // If the types have different groups, then we have to go up a directory return `../${this.getSanitizedGroup(classModel)}/`; } } else { - // If nothing is being processed then we assume we are at the root and links should include the groups + // If nothing is being processed, then we assume we are at the root and links should include the groups return `./${this.getSanitizedGroup(classModel)}/`; } } diff --git a/src/transpiler/markdown/jekyll/jekyll-docsProcessor.ts b/src/transpiler/markdown/jekyll/jekyll-docsProcessor.ts index 1b972435..ad12ecda 100644 --- a/src/transpiler/markdown/jekyll/jekyll-docsProcessor.ts +++ b/src/transpiler/markdown/jekyll/jekyll-docsProcessor.ts @@ -3,6 +3,7 @@ import { Type } from '@cparra/apex-reflection'; import { MarkdownHomeFile } from '../../../model/markdown-home-file'; import { MarkdownTypeFile } from '../../../model/markdown-type-file'; import { LinkingStrategy } from '../../processor-type-transpiler'; +import { Settings } from '../../../settings'; export class JekyllDocsProcessor extends MarkdownTranspilerBase { homeFileName(): string { @@ -10,17 +11,39 @@ export class JekyllDocsProcessor extends MarkdownTranspilerBase { } onBeforeProcess = (types: Type[]) => { - this._fileContainer.pushFile(new MarkdownHomeFile(this.homeFileName(), types, this.frontMatterHeader)); + this._fileContainer.pushFile(new MarkdownHomeFile(this.homeFileName(), types, this.frontMatterForHomeFile)); }; onProcess(type: Type): void { - this._fileContainer.pushFile(new MarkdownTypeFile(type, 1, this.frontMatterHeader)); + this._fileContainer.pushFile(new MarkdownTypeFile(type, 1, this.getFrontMatterHeader(type))); } - get frontMatterHeader(): string { + get frontMatterForHomeFile(): string { return '---\nlayout: default\n---'; } + getFrontMatterHeader(type: Type): string { + const headerLines = ['---']; + // "layout: default" is a required front matter header for Jekyll + headerLines.push('layout: default'); + // Add any additional front matter headers that might have been configured in the settings + const targetType = { + name: type.name, + typeName: type.type_name, + accessModifier: type.access_modifier, + group: type.group, + description: type.docComment?.description, + }; + const configuredHeaders = Settings.getInstance().frontMatterHeader(targetType); + if (configuredHeaders) { + configuredHeaders.forEach((header) => { + headerLines.push(header); + }); + } + headerLines.push('---'); + return headerLines.join('\n'); + } + getLinkingStrategy(): LinkingStrategy { return 'path-relative'; }