diff --git a/README.md b/README.md index 8161eb1a..2ebe3553 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. @@ -219,6 +286,23 @@ 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 +the `exclude` property in the configuration file. + +```typescript +import { defineMarkdownConfig } from "@cparra/apexdocs"; + +export default defineMarkdownConfig({ + sourceDir: 'force-app', + targetDir: 'docs', + scope: ['global', 'public'], + exclude: ['**/MyClass.cls', '**/MyOtherClass.cls'], + ... +}); +``` + ### Configuration Hooks When defining a `.js` or `.ts` configuration file, your object export can also make use @@ -370,12 +454,12 @@ If using Typescript, ApexDocs provides all necessary type definitions. ## 📖 Documentation Guide -See the [wiki](https://github.com/cesarParra/apexdocs/wiki/%F0%9F%93%96-Documenting-Apex-code) +See the [wiki](https://github.com/cesarParra/apexdocs/wiki/2.-%F0%9F%93%96-Documenting-Apex-code) for an in-depth guide on how to document your Apex code to get the most out of ApexDocs. ## 📄 Generating OpenApi REST Definitions ApexDocs can also generate OpenApi REST definitions for your Salesforce Apex classes annotated with `@RestResource`. -See the [wiki](https://github.com/cesarParra/apexdocs/wiki/%F0%9F%93%84-Generating-OpenApi-REST-Definitions) +See the [wiki](https://github.com/cesarParra/apexdocs/wiki/3.-%F0%9F%93%84-Generating-OpenApi-REST-Definitions) for more information. diff --git a/examples/changelog/current/classes/AccountService.cls b/examples/changelog/current/classes/AccountService.cls new file mode 100644 index 00000000..16eeef9c --- /dev/null +++ b/examples/changelog/current/classes/AccountService.cls @@ -0,0 +1,8 @@ +/** + * @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/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/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/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 new file mode 100644 index 00000000..f25caaf8 --- /dev/null +++ b/examples/changelog/docs/changelog.md @@ -0,0 +1,38 @@ +# 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 + +## Removed Types + +These types have been removed. + +- 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/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/package.json b/examples/changelog/package.json new file mode 100644 index 00000000..0fb0b956 --- /dev/null +++ b/examples/changelog/package.json @@ -0,0 +1,20 @@ +{ + "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", + "scope": ["global", "public", "private"] + } +} 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/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/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/apexdocs.config.ts b/examples/vitepress/apexdocs.config.ts index 20184640..4a76b64f 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 { @@ -25,70 +25,77 @@ 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 { + changelog: defineChangelogConfig({ + previousVersionDir: 'previous', + currentVersionDir: 'force-app', + scope: ['global', 'public', 'protected', 'private', 'namespaceaccessible'], + }), + 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/.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/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..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/examples/vitepress/package.json b/examples/vitepress/package.json index 51bad928..5ceb6e5e 100644 --- a/examples/vitepress/package.json +++ b/examples/vitepress/package.json @@ -2,11 +2,8 @@ "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", - "docs:build": "vitepress build docs", - "docs:gen": "npm run docs:clean && npm run docs:build", - "docs:dev": "vitepress dev docs", - "docs:preview": "vitepress preview docs" + "apexdocs:build": "npm run docs:clean && ts-node ../../src/cli/generate.ts", + "docs:dev": "vitepress dev docs" }, "devDependencies": { "ts-node": "^10.9.2", diff --git a/examples/vitepress/previous/.gitkeep b/examples/vitepress/previous/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json index 90bbd436..3071226c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "fp-ts": "^2.16.8", "handlebars": "^4.7.8", "js-yaml": "^4.1.0", + "minimatch": "^10.0.1", "yargs": "^17.7.2" }, "bin": { @@ -29,7 +30,6 @@ "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", "@types/node": "^20.14.10", - "@types/shelljs": "^0.8.15", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", @@ -37,7 +37,7 @@ "lint-staged": "^15.2.7", "pkgroll": "^2.4.2", "prettier": "^3.3.2", - "rimraf": "^6.0.0", + "rimraf": "^6.0.1", "ts-jest": "^29.2.0", "typescript": "^5.5.3", "typescript-eslint": "^7.16.0" @@ -1034,6 +1034,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -1050,6 +1060,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -1089,6 +1111,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2010,16 +2054,6 @@ } } }, - "node_modules/@rollup/plugin-commonjs/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@rollup/plugin-commonjs/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2508,16 +2542,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2573,12 +2597,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true - }, "node_modules/@types/node": { "version": "20.14.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", @@ -2594,16 +2612,6 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, - "node_modules/@types/shelljs": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.15.tgz", - "integrity": "sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==", - "dev": true, - "dependencies": { - "@types/glob": "~7.2.0", - "@types/node": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -2776,16 +2784,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3020,17 +3018,14 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -3444,7 +3439,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/cosmiconfig": { @@ -3824,6 +3819,16 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3883,6 +3888,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4376,6 +4393,28 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -6541,15 +6580,17 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -7075,13 +7116,14 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.0.tgz", - "integrity": "sha512-u+yqhM92LW+89cxUQK0SRyvXYQmyuKHx0jkx4W7KfwLGLqJnQM5031Uv1trE4gB9XEXBM/s6MxKlfW95IidqaA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, "license": "ISC", "dependencies": { - "glob": "^11.0.0" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" @@ -7093,16 +7135,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/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==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/rimraf/node_modules/glob": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", @@ -7127,22 +7159,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", @@ -7489,6 +7505,28 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 500bb71d..5a8dc20d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cparra/apexdocs", - "version": "3.1.1", + "version": "3.2.1", "description": "Library with CLI capabilities to generate documentation for Salesforce Apex classes.", "keywords": [ "apex", @@ -38,7 +38,6 @@ "@types/eslint__js": "^8.42.3", "@types/jest": "^29.5.12", "@types/node": "^20.14.10", - "@types/shelljs": "^0.8.15", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", @@ -46,7 +45,7 @@ "lint-staged": "^15.2.7", "pkgroll": "^2.4.2", "prettier": "^3.3.2", - "rimraf": "^6.0.0", + "rimraf": "^6.0.1", "ts-jest": "^29.2.0", "typescript": "^5.5.3", "typescript-eslint": "^7.16.0" @@ -72,6 +71,7 @@ "fp-ts": "^2.16.8", "handlebars": "^4.7.8", "js-yaml": "^4.1.0", + "minimatch": "^10.0.1", "yargs": "^17.7.2" }, "imports": { diff --git a/src/application/Apexdocs.ts b/src/application/Apexdocs.ts index d41e3da8..5edd5542 100644 --- a/src/application/Apexdocs.ts +++ b/src/application/Apexdocs.ts @@ -1,14 +1,24 @@ +import { pipe } from 'fp-ts/function'; +import * as TE from 'fp-ts/TaskEither'; +import * as E from 'fp-ts/Either'; + 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 { UnparsedSourceFile, UserDefinedConfig, UserDefinedMarkdownConfig } 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 { + UnparsedSourceFile, + UserDefinedChangelogConfig, + UserDefinedConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../core/shared/types'; +import { ReflectionError, ReflectionErrors, HookError } from '../core/errors/errors'; +import { FileReadingError, FileWritingError } from './errors'; +import { apply } from '#utils/fp'; /** * Application entry-point to generate documentation out of Apex source files. @@ -21,18 +31,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, - ); - 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]); @@ -40,28 +46,55 @@ export class Apexdocs { } } -function generateMarkdownDocumentation( - fileBodies: UnparsedSourceFile[], - config: UserDefinedMarkdownConfig, -): TE.TaskEither { +const readFiles = apply(processFiles, new DefaultFileSystem()); + +async function processMarkdown(config: UserDefinedMarkdownConfig) { 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((error) => { - if (error._tag === 'HookError') { - return ['Error(s) occurred while processing hooks. Please review the following issues:', error.error]; - } + TE.mapLeft(toErrors), + ); +} - if (error._tag === 'FileWritingError') { - return ['Error(s) occurred while writing files. Please review the following issues:', error.error]; - } +async function processOpenApi(config: UserDefinedOpenApiConfig, logger: Logger) { + const fileBodies = await readFiles(config.sourceDir, false, config.exclude); + return openApi(logger, fileBodies, config); +} +async function processChangeLog(config: UserDefinedChangelogConfig) { + async function loadFiles(): Promise<[UnparsedSourceFile[], UnparsedSourceFile[]]> { + return [ + await readFiles(config.previousVersionDir, false, config.exclude), + await readFiles(config.currentVersionDir, false, config.exclude), + ]; + } + + return pipe( + 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 | 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), ]; - }), - ); + } } function formatReflectionError(error: ReflectionError) { diff --git a/src/application/__tests__/apex-file-reader.spec.ts b/src/application/__tests__/apex-file-reader.spec.ts index 8cb0a0df..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,12 +120,43 @@ 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{}'); }); + it('skips files that match the excluded glob pattern', async () => { + const fileSystem = new TestFileSystem([ + { + type: 'directory', + path: '', + files: [ + { + type: 'file', + path: 'SomeFile.cls', + content: 'public class MyClass{}', + }, + { + type: 'directory', + path: 'subdir', + files: [ + { + type: 'file', + path: 'AnotherFile.cls', + content: 'public class AnotherClass{}', + }, + ], + }, + ], + }, + ]); + + const result = await processFiles(fileSystem, '', false, ['**/AnotherFile.cls']); + expect(result.length).toBe(1); + expect(result[0].content).toBe('public class MyClass{}'); + }); + it('returns the file contents for all Apex when there are multiple directories', async () => { const fileSystem = new TestFileSystem([ { @@ -174,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/application/apex-file-reader.ts b/src/application/apex-file-reader.ts index 6207fc1c..4ddd3697 100644 --- a/src/application/apex-file-reader.ts +++ b/src/application/apex-file-reader.ts @@ -1,56 +1,63 @@ import { FileSystem } from './file-system'; import { UnparsedSourceFile } from '../core/shared/types'; +import { minimatch } from 'minimatch'; +import { pipe } from 'fp-ts/function'; +import { apply } from '#utils/fp'; 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, - ): Promise { - const filePaths = await this.getFilePaths(fileSystem, rootPath); - const apexFilePaths = filePaths.filter((filePath) => this.isApexFile(filePath)); - const filePromises = apexFilePaths.map((filePath) => this.processFile(fileSystem, filePath, includeMetadata)); - return Promise.all(filePromises); - } +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; } + return paths; +} - private static async processFile( - fileSystem: FileSystem, - filePath: string, - includeMetadata: boolean, - ): 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); } diff --git a/src/application/errors.ts b/src/application/errors.ts new file mode 100644 index 00000000..57b2f680 --- /dev/null +++ b/src/application/errors.ts @@ -0,0 +1,17 @@ +export class FileReadingError { + readonly _tag = 'FileReadingError'; + + constructor( + public message: string, + public error: unknown, + ) {} +} + +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 new file mode 100644 index 00000000..0a245407 --- /dev/null +++ b/src/application/generators/changelog.ts @@ -0,0 +1,27 @@ +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'; +import { FileWritingError } from '../errors'; + +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..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'; - -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( 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/__tests__/args/no-config.spec.ts b/src/cli/__tests__/args/no-config.spec.ts new file mode 100644 index 00000000..0bc0d21d --- /dev/null +++ b/src/cli/__tests__/args/no-config.spec.ts @@ -0,0 +1,125 @@ +import { extractArgs } from '../../args'; +import { assertEither } from '../../../core/test-helpers/assert-either'; +import { + UserDefinedChangelogConfig, + UserDefinedMarkdownConfig, + UserDefinedOpenApiConfig, +} from '../../../core/shared/types'; +import * as E from 'fp-ts/Either'; + +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(); + }); + + 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'); + }); + }); + + 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'); + }); + }); + + 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'); + }); + }); + + 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(() => {}); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(exitFake); + function getFromProcess() { + return []; + } + + try { + await extractArgs(getFromProcess); + } catch (error) { + // Do nothing + } + + 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(() => {}); + const mockExit = jest.spyOn(process, 'exit').mockImplementation(exitFake); + function getFromProcess() { + return ['markdown']; + } + + try { + await extractArgs(getFromProcess); + } catch (error) { + // Do nothing + } + + expect(consoleSpy).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + mockExit.mockRestore(); + }); + }); +}); 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(); + } + }); + }); +}); diff --git a/src/cli/args.ts b/src/cli/args.ts index ee03bf0d..21119ce9 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -1,55 +1,233 @@ -import { cosmiconfig, CosmiconfigResult } from 'cosmiconfig'; +import { cosmiconfig } from 'cosmiconfig'; import * as yargs from 'yargs'; -import { UserDefinedConfig, UserDefinedMarkdownConfig } from '../core/shared/types'; +import * as E from 'fp-ts/Either'; +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'; +import { changeLogOptions } from './commands/changelog'; +import { pipe } from 'fp-ts/function'; -const configOnlyDefaults: Partial = { +const configOnlyMarkdownDefaults: Partial = { + targetGenerator: 'markdown', excludeTags: [], + exclude: [], }; -/** - * Extracts configuration from a configuration file or the package.json - * through cosmiconfig. - */ -function _extractConfig(): Promise { - return cosmiconfig('apexdocs', { +const configOnlyOpenApiDefaults = { + targetGenerator: 'openapi', + exclude: [], +}; + +const configOnlyChangelogDefaults = { + targetGenerator: 'changelog', + exclude: [], +}; + +type ExtractArgsFromProcess = () => string[]; + +function getArgumentsFromProcess() { + return process.argv.slice(2); +} + +type ConfigResult = { + config: Record; +}; +type ExtractConfig = () => Promise; + +async function extractConfigFromPackageJsonOrFile(): Promise { + const result = await cosmiconfig('apexdocs', { loaders: { '.ts': TypeScriptLoader(), }, }).search(); + return { config: result?.config ?? {} }; } /** - * Extracts arguments from the command line. - * @param config The configuration object from the configuration file, if any. + * Combines the extracted configuration and arguments. */ -function _extractYargs(config?: CosmiconfigResult) { +export async function extractArgs( + extractFromProcessFn: ExtractArgsFromProcess = getArgumentsFromProcess, + extractConfigFn: ExtractConfig = extractConfigFromPackageJsonOrFile, +): Promise> { + const config = await extractConfigFn(); + + 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) { + return pipe( + E.right(config), + E.flatMap((config) => extractArgsForCommandProvidedThroughCli(extractFromProcessFn, config)), + E.map((config) => [config]), + ); +} + +function extractArgsForCommandProvidedThroughCli( + extractFromProcessFn: ExtractArgsFromProcess, + config: ConfigResult, +): E.Either { + const cliArgs = extractYargsDemandingCommand(extractFromProcessFn, config); + const commandName = cliArgs._[0]; + + const mergedConfig = { ...config.config, ...cliArgs, targetGenerator: commandName as Generators }; + + switch (mergedConfig.targetGenerator) { + case 'markdown': + return E.right({ ...configOnlyMarkdownDefaults, ...mergedConfig } as UserDefinedMarkdownConfig); + case 'openapi': + return E.right({ ...configOnlyOpenApiDefaults, ...mergedConfig } as unknown as UserDefinedOpenApiConfig); + case 'changelog': + return E.right({ ...configOnlyChangelogDefaults, ...mergedConfig } as unknown as UserDefinedChangelogConfig); + default: + return E.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); + } +} + +type ConfigByGenerator = { + [key in Generators]: UserDefinedConfig; +}; + +function extractArgsForCommandsProvidedInConfig( + extractFromProcessFn: ExtractArgsFromProcess, + config: ConfigByGenerator, +) { + const configs = Object.entries(config).map(([generator, generatorConfig]) => { + switch (generator as Generators) { + case 'markdown': + return pipe( + validateMultiCommandConfig(extractFromProcessFn, 'markdown', generatorConfig), + E.map(() => ({ ...configOnlyMarkdownDefaults, ...generatorConfig })), + ); + case 'openapi': + return pipe( + validateMultiCommandConfig(extractFromProcessFn, 'openapi', generatorConfig), + E.map(() => ({ ...configOnlyOpenApiDefaults, ...generatorConfig })), + ); + case 'changelog': + return pipe( + validateMultiCommandConfig(extractFromProcessFn, 'changelog', generatorConfig), + E.map(() => ({ ...configOnlyChangelogDefaults, ...generatorConfig })), + ); + } + }); + + return E.sequenceArray(configs); +} + +type NoConfig = { + _type: 'no-config'; +}; +type SingleCommandConfig = { + _type: 'single-command-config'; +}; +type MultiCommandConfig = { + _type: 'multi-command-config'; + commands: Generators[]; +}; + +function getConfigType(config: ConfigResult): E.Either { + if (!config) { + return E.right({ _type: 'no-config' }); + } + + // When the config has a shape that looks as follows: + // 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 + // 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) { + const commands = rootKeys.filter((key) => validRootKeys.includes(key)); + const hasInvalidCommands = rootKeys.some((key) => !validRootKeys.includes(key)); + if (hasInvalidCommands) { + return E.left(new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`)); + } + + return E.right({ + _type: 'multi-command-config', + commands: commands as Generators[], + }); + } + + return E.right({ _type: 'single-command-config' }); +} + +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), ) .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(); + .parseSync(extractFromProcessFn()); } -/** - * 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 'markdown' | 'openapi' }; - if (mergedConfig.targetGenerator === 'markdown') { - return { ...configOnlyDefaults, ...mergedConfig }; - } else { - return mergedConfig; +function validateMultiCommandConfig( + extractFromProcessFn: ExtractArgsFromProcess, + 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); + return E.tryCatch(() => { + 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(extractFromProcessFn()); + }, E.toError); } diff --git a/src/cli/commands/changelog.ts b/src/cli/commands/changelog.ts new file mode 100644 index 00000000..8c36cb0b --- /dev/null +++ b/src/cli/commands/changelog.ts @@ -0,0 +1,38 @@ +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.', + }, + 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/cli/generate.ts b/src/cli/generate.ts index c13606e6..b0eee5ae 100644 --- a/src/cli/generate.ts +++ b/src/cli/generate.ts @@ -3,29 +3,30 @@ 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 generating the documentation: ${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((config) => 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); } 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-change-log.spec.ts b/src/core/changelog/__test__/generating-change-log.spec.ts new file mode 100644 index 00000000..83212b37 --- /dev/null +++ b/src/core/changelog/__test__/generating-change-log.spec.ts @@ -0,0 +1,259 @@ +import { UnparsedSourceFile } from '../../shared/types'; +import { generateChangeLog } from '../generate-change-log'; +import { assertEither } from '../../test-helpers/assert-either'; + +const config = { + fileName: 'changelog', + scope: ['global', 'public', 'private'], + targetDir: '', + currentVersionDir: '', + previousVersionDir: '', + exclude: [], +}; + +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[] = []; + const newBundle: UnparsedSourceFile[] = []; + + const result = await generateChangeLog(oldBundle, newBundle, config)(); + + assertEither(result, (data) => expect(data.content).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, config)(); + + 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.')); + }); + }); + + 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.')); + }); + }); + + 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.')); + }); + }); + + 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')); + }); + }); + + 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')); + }); + }); + + 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/__test__/processing-changelog.spec.ts b/src/core/changelog/__test__/processing-changelog.spec.ts new file mode 100644 index 00000000..9162f06a --- /dev/null +++ b/src/core/changelog/__test__/processing-changelog.spec.ts @@ -0,0 +1,439 @@ +import { processChangelog } from '../process-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 changelog', () => { + it('has no new types when both the old and new versions are empty', () => { + const oldVersion = { types: [] }; + const newVersion = { types: [] }; + + const changeLog = processChangelog(oldVersion, newVersion); + + expect(changeLog.newTypes).toEqual([]); + }); + + it('has no removed types when the old and new versions are empty', () => { + const oldVersion = { types: [] }; + const newVersion = { types: [] }; + + const changeLog = processChangelog(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] }; + const newVersion = { types: [anyClass] }; + + const changeLog = processChangelog(oldVersion, newVersion); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.removedTypes).toEqual([]); + }); + + 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: [existingClass, newClass] }; + + const changeLog = processChangelog(oldVersion, newVersion); + + 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 = processChangelog(oldVersion, newVersion); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyEnum', + modifications: [ + { + __typename: 'NewEnumValue', + name: 'VALUE2', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyEnum', + modifications: [ + { + __typename: 'RemovedEnumValue', + name: 'VALUE2', + }, + ], + }, + ]); + }); + + 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 = processChangelog(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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewMethod', + name: 'newMethod', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyInterface', + modifications: [ + { + __typename: 'RemovedMethod', + name: 'oldMethod', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewProperty', + name: 'newProperty', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedProperty', + name: 'oldProperty', + }, + ], + }, + ]); + }); + + 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 = processChangelog(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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedField', + name: 'oldField', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'NewType', + name: 'NewInnerClass', + }, + ], + }, + ]); + }); + + 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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldInnerClass', + }, + ], + }, + ]); + }); + + 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 = processChangelog(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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldInterface', + }, + ], + }, + ]); + }); + + 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 = processChangelog(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 = processChangelog(oldVersion, newVersion); + + expect(changeLog.newOrModifiedMembers).toEqual([ + { + typeName: 'MyClass', + modifications: [ + { + __typename: 'RemovedType', + name: 'OldEnum', + }, + ], + }, + ]); + }); +}); diff --git a/src/core/changelog/generate-change-log.ts b/src/core/changelog/generate-change-log.ts new file mode 100644 index 00000000..ec936b86 --- /dev/null +++ b/src/core/changelog/generate-change-log.ts @@ -0,0 +1,73 @@ +import { ParsedFile, UnparsedSourceFile, UserDefinedChangelogConfig } from '../shared/types'; +import { pipe } from 'fp-ts/function'; +import * as TE from 'fp-ts/TaskEither'; +import { reflectBundles } from '../reflection/reflect-source'; +import { processChangelog, VersionManifest } from './process-changelog'; +import { convertToRenderableChangelog, RenderableChangelog } from './renderable-changelog'; +import { CompilationRequest, Template } from '../template'; +import { changelogTemplate } from './templates/changelog-template'; +import { ReflectionErrors } from '../errors/errors'; +import { apply } from '#utils/fp'; +import { filterScope } from '../reflection/filter-scope'; + +export type ChangeLogPageData = { + content: string; + outputDocPath: string; +}; + +export function generateChangeLog( + oldBundles: UnparsedSourceFile[], + newBundles: UnparsedSourceFile[], + config: Omit, +): TE.TaskEither { + const filterOutOfScope = apply(filterScope, config.scope); + + function reflect(sourceFiles: UnparsedSourceFile[]) { + 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(toManifests), + TE.map(({ oldManifest, newManifest }) => ({ + changeLog: processChangelog(oldManifest, newManifest), + newManifest, + })), + TE.map(({ changeLog, newManifest }) => convertToRenderableChangelog(changeLog, newManifest.types)), + TE.map(compile), + TE.map(convertToPageData), + ); +} + +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 { + content, + outputDocPath: `${fileName}.md`, + }; +} diff --git a/src/core/changelog/method-changes-checker.ts b/src/core/changelog/method-changes-checker.ts new file mode 100644 index 00000000..bd916b8b --- /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.toLowerCase() !== method2.name.toLowerCase()) { + 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; +} diff --git a/src/core/changelog/process-changelog.ts b/src/core/changelog/process-changelog.ts new file mode 100644 index 00000000..d1e339c9 --- /dev/null +++ b/src/core/changelog/process-changelog.ts @@ -0,0 +1,198 @@ +import { ClassMirror, EnumMirror, MethodMirror, Type } from '@cparra/apex-reflection'; +import { pipe } from 'fp-ts/function'; +import { areMethodsEqual } from './method-changes-checker'; + +export type VersionManifest = { + types: Type[]; +}; + +type ModificationTypes = + | 'NewType' + | 'RemovedType' + | 'NewEnumValue' + | 'RemovedEnumValue' + | 'NewMethod' + | 'RemovedMethod' + | 'NewProperty' + | 'RemovedProperty' + | 'NewField' + | 'RemovedField'; + +export type MemberModificationType = { + __typename: ModificationTypes; + name: string; +}; + +export type NewOrModifiedMember = { + typeName: string; + modifications: MemberModificationType[]; +}; + +export type Changelog = { + newTypes: string[]; + removedTypes: string[]; + newOrModifiedMembers: NewOrModifiedMember[]; +}; + +export function processChangelog(oldVersion: VersionManifest, newVersion: VersionManifest): Changelog { + return { + newTypes: getNewTypes(oldVersion, newVersion), + removedTypes: getRemovedTypes(oldVersion, newVersion), + newOrModifiedMembers: getNewOrModifiedMembers(oldVersion, newVersion), + }; +} + +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); +} + +function getNewOrModifiedMembers(oldVersion: VersionManifest, newVersion: VersionManifest): NewOrModifiedMember[] { + return pipe( + getTypesInBothVersions(oldVersion, newVersion), + (typesInBoth) => [ + ...getNewOrModifiedEnumValues(typesInBoth), + ...getNewOrModifiedMethods(typesInBoth), + ...getNewOrModifiedClassMembers(typesInBoth), + ], + (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: newType.name, + modifications: [ + ...getNewValues(oldEnum, newEnum, 'values', 'NewEnumValue'), + ...getRemovedValues(oldEnum, newEnum, 'values', 'RemovedEnumValue'), + ], + }; + }), + ); +} + +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: [ + ...getNewValues( + oldMethodAware, + newMethodAware, + 'methods', + 'NewMethod', + areMethodsEqual, + ), + ...getRemovedValues( + oldMethodAware, + newMethodAware, + 'methods', + 'RemovedMethod', + areMethodsEqual, + ), + ], + }; + }), + ); +} + +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: [ + ...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'), + ], + }; + }), + ); +} + +type TypeInBoth = { + oldType: Type; + newType: Type; +}; + +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[]; +} + +type NameAware = { + name: string; +}; + +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) => areEqualFn(oldValue, newValue))) + .map((value) => value.name) + .map((name) => ({ __typename: typeName, name })); +} + +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) => areEqualFn(oldValue, newValue))) + .map((value) => value.name) + .map((name) => ({ __typename: typeName, name })); +} + +type MethodAware = { + methods: MethodMirror[]; +}; diff --git a/src/core/changelog/renderable-changelog.ts b/src/core/changelog/renderable-changelog.ts new file mode 100644 index 00000000..43aa4982 --- /dev/null +++ b/src/core/changelog/renderable-changelog.ts @@ -0,0 +1,137 @@ +import { Changelog, MemberModificationType, NewOrModifiedMember } from './process-changelog'; +import { Type } from '@cparra/apex-reflection'; +import { RenderableContent } from '../renderables/types'; +import { adaptDescribable } from '../renderables/documentables'; + +type NewTypeRenderable = { + name: string; + description?: RenderableContent[]; +}; + +type NewTypeSection = { + __type: T; + heading: string; + description: string; + types: NewTypeRenderable[]; +}; + +type RemovedTypeSection = { + heading: string; + description: string; + 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 { + const allNewTypes = changelog.newTypes.map( + (newType) => newManifest.find((type) => type.name.toLowerCase() === newType.toLowerCase())!, + ); + + 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: + newClasses.length > 0 + ? { + __type: 'class', + heading: 'New Classes', + description: 'These classes are new.', + types: newClasses.map(typeToRenderable), + } + : null, + newInterfaces: + newInterfaces.length > 0 + ? { + __type: 'interface', + heading: 'New Interfaces', + description: 'These interfaces are new.', + types: newInterfaces.map(typeToRenderable), + } + : null, + newEnums: + newEnums.length > 0 + ? { + __type: 'enum', + heading: 'New Enums', + description: 'These enums are new.', + types: newEnums.map(typeToRenderable), + } + : null, + removedTypes: + 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, + }; +} + +function typeToRenderable(type: Type): NewTypeRenderable { + function adapt() { + const describable = adaptDescribable(type.docComment?.descriptionLines, (typeName) => typeName); + return describable.description; + } + + return { + name: type.name, + description: adapt(), + }; +} + +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 new file mode 100644 index 00000000..3d982da2 --- /dev/null +++ b/src/core/changelog/templates/changelog-template.ts @@ -0,0 +1,63 @@ +export const changelogTemplate = ` +# Changelog + +{{#if newClasses}} +## {{newClasses.heading}} + +{{newClasses.description}} + +{{#each newClasses.types}} +### {{this.name}} + +{{{renderContent this.description}}} +{{/each}} +{{/if}} + +{{#if newInterfaces}} +## {{newInterfaces.heading}} + +{{newInterfaces.description}} + +{{#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}} + +{{#if removedTypes}} +## Removed Types + +{{removedTypes.description}} + +{{#each removedTypes.types}} +- {{this}} +{{/each}} +{{/if}} + +{{#if newOrModifiedMembers}} +## {{newOrModifiedMembers.heading}} + +{{newOrModifiedMembers.description}} + +{{#each newOrModifiedMembers.modifications}} +### {{this.typeName}} + +{{#each this.modifications}} +- {{this}} +{{/each}} +{{/each}} +{{/if}} +`.trim(); 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/__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..9c4ab2c5 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(() => { @@ -358,5 +359,3 @@ describe('When generating documentation for a class', () => { }); }); }); - -// TODO: Skips tags at the member level 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/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/__test__/test-helpers.ts b/src/core/markdown/__test__/test-helpers.ts index 3cd48a00..a3cd95a8 100644 --- a/src/core/markdown/__test__/test-helpers.ts +++ b/src/core/markdown/__test__/test-helpers.ts @@ -20,6 +20,7 @@ export function generateDocs(apexBundles: UnparsedSourceFile[], config?: Partial linkingStrategy: 'relative', excludeTags: [], referenceGuideTitle: 'Apex Reference Guide', + exclude: [], ...config, }); } 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__/interface-adapter.spec.ts b/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts index 39b7d646..6b818cea 100644 --- a/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts +++ b/src/core/markdown/adapters/__tests__/interface-adapter.spec.ts @@ -17,6 +17,7 @@ const defaultMarkdownGeneratorConfig: MarkdownGeneratorConfig = { sortAlphabetically: false, linkingStrategy: 'relative', referenceGuideTitle: 'Apex Reference Guide', + exclude: [], excludeTags: [], }; 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 8b12b885..cc291602 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 { ReferenceGuideReference, Renderable, RenderableBundle, RenderableEnum } from '../../renderables/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/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/generate-docs.ts b/src/core/markdown/generate-docs.ts index d397c8d0..6bb0d92d 100644 --- a/src/core/markdown/generate-docs.ts +++ b/src/core/markdown/generate-docs.ts @@ -19,17 +19,18 @@ 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 { Template } from './templates/template'; +import { filterScope } from '../reflection/filter-scope'; +import { Template } from '../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< 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/__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 90% rename from src/core/markdown/reflection/reflect-source.ts rename to src/core/reflection/reflect-source.ts index e515c3b9..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,23 +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'; - -export class ReflectionErrors { - readonly _tag = 'ReflectionErrors'; - - constructor(public errors: ReflectionError[]) {} -} - -export class ReflectionError { - constructor( - public file: string, - public message: string, - ) {} -} +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 }; 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 6e03769f..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, @@ -60,6 +60,7 @@ export function describableToRenderableContent( }, ]; } + return ( content // If the last element is an empty line, remove it 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/shared/types.d.ts b/src/core/shared/types.d.ts index 732f09a6..a34d3728 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. @@ -23,6 +26,7 @@ export type UserDefinedMarkdownConfig = { linkingStrategy: LinkingStrategy; excludeTags: string[]; referenceGuideTitle: string; + exclude: string[]; } & Partial; export type UserDefinedOpenApiConfig = { @@ -33,9 +37,20 @@ export type UserDefinedOpenApiConfig = { namespace?: string; title: string; apiVersion: string; + exclude: string[]; +}; + +export type UserDefinedChangelogConfig = { + targetGenerator: 'changelog'; + previousVersionDir: string; + currentVersionDir: string; + targetDir: string; + fileName: string; + scope: string[]; + exclude: string[]; }; -export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig; +export type UserDefinedConfig = UserDefinedMarkdownConfig | UserDefinedOpenApiConfig | UserDefinedChangelogConfig; export type UnparsedSourceFile = { filePath: string; @@ -86,7 +101,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/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..6e07f3d9 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 './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'; +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; diff --git a/src/core/test-helpers/assert-either.ts b/src/core/test-helpers/assert-either.ts new file mode 100644 index 00000000..990c3151 --- /dev/null +++ b/src/core/test-helpers/assert-either.ts @@ -0,0 +1,12 @@ +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); +} + +function fail(error: unknown): never { + throw error; +} diff --git a/src/defaults.ts b/src/defaults.ts index 9ff35677..f1931087 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -11,6 +11,7 @@ export const markdownDefaults = { linkingStrategy: 'relative' as const, referenceGuideTitle: 'Apex Reference Guide', excludeTags: [], + exclude: [], }; export const openApiDefaults = { @@ -18,4 +19,12 @@ export const openApiDefaults = { fileName: 'openapi', title: 'Apex REST API', apiVersion: '1.0.0', + exclude: [], +}; + +export const changeLogDefaults = { + ...commonDefaults, + fileName: 'changelog', + scope: ['global'], + exclude: [], }; diff --git a/src/index.ts b/src/index.ts index 65e79267..37b5ae82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,12 +12,10 @@ import type { TransformReference, ConfigurableDocPageReference, UserDefinedOpenApiConfig, - UserDefinedConfig, + UserDefinedChangelogConfig, } from './core/shared/types'; -import { markdownDefaults, openApiDefaults } from './defaults'; -import { NoLogger } from '#utils/logger'; -import { Apexdocs } from './application/Apexdocs'; -import * as E from 'fp-ts/Either'; +import { changeLogDefaults, markdownDefaults, openApiDefaults } from './defaults'; +import { process } from './node/process'; type ConfigurableMarkdownConfig = Omit, 'targetGenerator'>; @@ -47,46 +45,27 @@ function defineOpenApiConfig(config: ConfigurableOpenApiConfig): Partial, 'targetGenerator'>; + /** - * Represents a file to be skipped. + * Helper function to define a configuration to generate a changelog. + * @param config The configuration to use. */ -function skip(): Skip { +function defineChangelogConfig(config: ConfigurableChangelogConfig): Partial { return { - _tag: 'Skip', - }; -} - -type CallableConfig = Partial & { sourceDir: string; targetGenerator: 'markdown' | 'openapi' }; - -async function process(config: CallableConfig): Promise { - const logger = new NoLogger(); - const configWithDefaults = { - ...getDefault(config), + ...changeLogDefaults, ...config, + targetGenerator: 'changelog' as const, }; - - if (!configWithDefaults.sourceDir) { - throw new Error('sourceDir is required'); - } - - const result = await Apexdocs.generate(configWithDefaults as UserDefinedConfig, logger); - E.match( - (errors) => { - throw errors; - }, - () => {}, - )(result); } -function getDefault(config: CallableConfig) { - switch (config.targetGenerator) { - case 'markdown': - return markdownDefaults; - case 'openapi': - return openApiDefaults; - default: - throw new Error('Unknown target generator'); - } +/** + * Represents a file to be skipped. + */ +function skip(): Skip { + return { + _tag: 'Skip', + }; } export { @@ -94,6 +73,8 @@ export { ConfigurableMarkdownConfig, defineOpenApiConfig, ConfigurableOpenApiConfig, + defineChangelogConfig, + ConfigurableChangelogConfig, skip, TransformReferenceGuide, TransformDocs, diff --git a/src/node/process.ts b/src/node/process.ts new file mode 100644 index 00000000..006fdf5a --- /dev/null +++ b/src/node/process.ts @@ -0,0 +1,44 @@ +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 { changeLogDefaults, markdownDefaults, openApiDefaults } from '../defaults'; + +type CallableConfig = Partial & { sourceDir: string; targetGenerator: Generators }; + +/** + * Processes the documentation generation, similar to the main function in the CLI. + * @param config The configuration to use. + */ +export async function process(config: CallableConfig): Promise { + const logger = new NoLogger(); + const configWithDefaults = { + ...getDefault(config), + ...config, + }; + + if (!configWithDefaults.sourceDir) { + throw new Error('sourceDir is required'); + } + + const result = await Apexdocs.generate(configWithDefaults as UserDefinedConfig, logger); + E.match( + (errors) => { + throw errors; + }, + () => {}, + )(result); +} + +function getDefault(config: CallableConfig) { + switch (config.targetGenerator) { + case 'markdown': + return markdownDefaults; + case 'openapi': + return openApiDefaults; + case 'changelog': + return changeLogDefaults; + default: + throw new Error('Unknown target generator'); + } +} 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: [], + }; + } +} 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.