UNPKG

@cparra/apexdocs

Version:

Library with CLI capabilities to generate documentation for Salesforce Apex classes.

555 lines (546 loc) 21.1 kB
#!/usr/bin/env node 'use strict'; var logger$1 = require('../logger-D4Q3KA6D.js'); var require$$0 = require('yargs'); var cosmiconfig = require('cosmiconfig'); var E = require('fp-ts/Either'); var cosmiconfigTypescriptLoader = require('cosmiconfig-typescript-loader'); var _function = require('fp-ts/function'); require('fp-ts/TaskEither'); require('js-yaml'); require('path'); require('fp-ts/Task'); require('fp-ts/lib/Array'); require('@cparra/apex-reflection'); require('fp-ts/Option'); require('fast-xml-parser'); require('handlebars'); require('fp-ts/boolean'); require('fp-ts/Array'); require('fs'); require('fp-ts/lib/TaskEither'); require('minimatch'); require('@salesforce/source-deploy-retrieve'); require('chalk'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var E__namespace = /*#__PURE__*/_interopNamespaceDefault(E); function validateMarkdownArgs(argv) { const hasSourceDir = argv.sourceDir && (typeof argv.sourceDir === "string" || Array.isArray(argv.sourceDir) && argv.sourceDir.length > 0); const hasUseSfdxProjectJson = argv.useSfdxProjectJson; if (!hasSourceDir && !hasUseSfdxProjectJson) { throw new Error("Must specify one of: --sourceDir or --useSfdxProjectJson"); } return true; } const markdownOptions = { sourceDir: { type: "string", array: true, alias: "s", demandOption: false, describe: "The directory location(s) which contain your apex .cls classes. Can specify a single directory or multiple directories. Cannot be used with useSfdxProjectJson.", conflicts: ["useSfdxProjectJson"] }, useSfdxProjectJson: { type: "boolean", demandOption: false, describe: "Read source directories from sfdx-project.json packageDirectories. Cannot be used with sourceDir.", conflicts: ["sourceDir"] }, sfdxProjectPath: { type: "string", demandOption: false, describe: "Path to the directory containing sfdx-project.json (defaults to current working directory). Only used with useSfdxProjectJson.", implies: "useSfdxProjectJson" }, targetDir: { type: "string", alias: "t", default: logger$1.markdownDefaults.targetDir, describe: "The directory location where documentation will be generated to." }, scope: { type: "string", array: true, alias: "p", default: logger$1.markdownDefaults.scope, describe: "A list of scopes to document. 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." }, customObjectVisibility: { type: "string", array: true, alias: "v", default: logger$1.markdownDefaults.customObjectVisibility, choices: ["public", "protected", "packageprotected"], describe: "Controls which custom objects are documented. Values should be separated by a space." }, defaultGroupName: { type: "string", default: logger$1.markdownDefaults.defaultGroupName, describe: "Defines the @group name to be used when a file does not specify it." }, customObjectsGroupName: { type: "string", default: logger$1.markdownDefaults.customObjectsGroupName, describe: "The name under which custom objects will be grouped in the Reference Guide" }, triggersGroupName: { type: "string", default: logger$1.markdownDefaults.triggersGroupName, describe: "The name under which triggers will be grouped in the Reference Guide" }, namespace: { type: "string", describe: "The package namespace, if any. If provided, it will be added to the generated files." }, sortAlphabetically: { type: "boolean", describe: "Whether to sort files and members alphabetically.", default: logger$1.markdownDefaults.sortAlphabetically }, includeMetadata: { type: "boolean", describe: "Whether to include the file's meta.xml information: Whether it is active and and the API version", default: logger$1.markdownDefaults.includeMetadata }, linkingStrategy: { type: "string", describe: "The strategy to use when linking to other documentation pages.", choices: ["relative", "no-link", "none"], default: logger$1.markdownDefaults.linkingStrategy }, referenceGuideTitle: { type: "string", describe: "The title of the reference guide.", default: logger$1.markdownDefaults.referenceGuideTitle }, includeFieldSecurityMetadata: { type: "boolean", describe: "Whether to include the compliance category and security classification for fields in the generated files.", default: logger$1.markdownDefaults.includeFieldSecurityMetadata }, includeInlineHelpTextMetadata: { type: "boolean", describe: "Whether to include the inline help text for fields in the generated files." } }; function validateOpenApiArgs(argv) { const hasSourceDir = argv.sourceDir && (typeof argv.sourceDir === "string" || Array.isArray(argv.sourceDir) && argv.sourceDir.length > 0); const hasUseSfdxProjectJson = argv.useSfdxProjectJson; if (!hasSourceDir && !hasUseSfdxProjectJson) { throw new Error("Must specify one of: --sourceDir or --useSfdxProjectJson"); } return true; } const openApiOptions = { sourceDir: { type: "string", array: true, alias: "s", demandOption: false, describe: "The directory location(s) which contain your apex .cls classes. Can specify a single directory or multiple directories. Cannot be used with useSfdxProjectJson.", conflicts: ["useSfdxProjectJson"] }, useSfdxProjectJson: { type: "boolean", demandOption: false, describe: "Read source directories from sfdx-project.json packageDirectories. Cannot be used with sourceDir.", conflicts: ["sourceDir"] }, sfdxProjectPath: { type: "string", demandOption: false, describe: "Path to the directory containing sfdx-project.json (defaults to current working directory). Only used with useSfdxProjectJson.", implies: "useSfdxProjectJson" }, targetDir: { type: "string", alias: "t", default: logger$1.openApiDefaults.targetDir, describe: "The directory location where the OpenApi file will be generated." }, fileName: { type: "string", default: logger$1.openApiDefaults.fileName, describe: "The name of the OpenApi file to be generated." }, namespace: { type: "string", describe: "The package namespace, if any. This will be added to the API file Server Url." }, title: { type: "string", default: logger$1.openApiDefaults.title, describe: "The title of the OpenApi file." }, apiVersion: { type: "string", default: logger$1.openApiDefaults.apiVersion, describe: "The version of the OpenApi file." } }; function validateChangelogArgs(argv) { const hasPreviousVersionDir = argv.previousVersionDir && (typeof argv.previousVersionDir === "string" || Array.isArray(argv.previousVersionDir) && argv.previousVersionDir.length > 0); const hasCurrentVersionDir = argv.currentVersionDir && (typeof argv.currentVersionDir === "string" || Array.isArray(argv.currentVersionDir) && argv.currentVersionDir.length > 0); if (!hasPreviousVersionDir) { throw new Error("Must specify --previousVersionDir"); } if (!hasCurrentVersionDir) { throw new Error("Must specify --currentVersionDir"); } return true; } const changeLogOptions = { previousVersionDir: { type: "string", array: true, alias: "p", demandOption: false, describe: "The directory location(s) of the previous version of the source code. Can specify a single directory or multiple directories." }, currentVersionDir: { type: "string", array: true, alias: "c", demandOption: false, describe: "The directory location(s) of the current version of the source code. Can specify a single directory or multiple directories." }, targetDir: { type: "string", alias: "t", default: logger$1.changeLogDefaults.targetDir, describe: "The directory location where the changelog file will be generated." }, fileName: { type: "string", default: logger$1.changeLogDefaults.fileName, describe: "The name of the changelog file to be generated." }, scope: { type: "string", array: true, alias: "s", default: logger$1.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." }, customObjectVisibility: { type: "string", array: true, alias: "v", default: logger$1.changeLogDefaults.customObjectVisibility, choices: ["public", "protected", "packageprotected"], describe: "Controls which custom objects are documented. Values should be separated by a space." }, skipIfNoChanges: { type: "boolean", default: logger$1.changeLogDefaults.skipIfNoChanges, describe: "Skip the changelog generation if there are no changes between the previous and current version." } }; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __async$1 = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; const yargs = require$$0; const configOnlyMarkdownDefaults = { targetGenerator: "markdown", excludeTags: [], exclude: [] }; const configOnlyOpenApiDefaults = { targetGenerator: "openapi", exclude: [] }; const configOnlyChangelogDefaults = { targetGenerator: "changelog", exclude: [] }; function getArgumentsFromProcess() { return process.argv.slice(2); } function extractConfigFromPackageJsonOrFile() { return __async$1(this, null, function* () { var _a; const result = yield cosmiconfig.cosmiconfig("apexdocs", { loaders: { ".ts": cosmiconfigTypescriptLoader.TypeScriptLoader() } }).search(); return { config: (_a = result == null ? void 0 : result.config) != null ? _a : {} }; }); } function extractArgs() { return __async$1(this, arguments, function* (extractFromProcessFn = getArgumentsFromProcess, extractConfigFn = extractConfigFromPackageJsonOrFile) { const config = yield extractConfigFn(); function handle(configType) { switch (configType._type) { case "no-config": case "single-command-config": return handleSingleCommand(extractFromProcessFn, config); case "multi-command-config": return extractArgsForCommandsProvidedInConfig(extractFromProcessFn, config.config); } } return _function.pipe(getConfigType(config), E__namespace.flatMap(handle)); }); } function handleSingleCommand(extractFromProcessFn, config) { return _function.pipe( E__namespace.right(config), E__namespace.flatMap((config2) => extractArgsForCommandProvidedThroughCli(extractFromProcessFn, config2)), E__namespace.map((config2) => [config2]) ); } function extractArgsForCommandProvidedThroughCli(extractFromProcessFn, config) { const cliArgs = extractYargsDemandingCommand(extractFromProcessFn, config); const commandName = cliArgs._[0]; const mergedConfig = __spreadProps(__spreadValues(__spreadValues({}, config.config), cliArgs), { targetGenerator: commandName }); switch (mergedConfig.targetGenerator) { case "markdown": return _function.pipe( logger$1.validateSourceDirectoryConfig(extractSourceDirectoryConfig(mergedConfig)), E__namespace.mapLeft((error) => new Error(`Invalid markdown configuration: ${error.message}`)), E__namespace.map(() => __spreadValues(__spreadValues({}, configOnlyMarkdownDefaults), mergedConfig)) ); case "openapi": return _function.pipe( logger$1.validateSourceDirectoryConfig(extractSourceDirectoryConfig(mergedConfig)), E__namespace.mapLeft((error) => new Error(`Invalid openapi configuration: ${error.message}`)), E__namespace.map(() => __spreadValues(__spreadValues({}, configOnlyOpenApiDefaults), mergedConfig)) ); case "changelog": return _function.pipe( validateChangelogConfig(mergedConfig), E__namespace.mapLeft((error) => new Error(`Invalid changelog configuration: ${error.message}`)), E__namespace.map(() => __spreadValues(__spreadValues({}, configOnlyChangelogDefaults), mergedConfig)) ); default: return E__namespace.left(new Error(`Invalid command provided: ${mergedConfig.targetGenerator}`)); } } function extractArgsForCommandsProvidedInConfig(extractFromProcessFn, config) { const providedThroughCli = yargs.parseSync(extractFromProcessFn()); const hasACommandBeenProvided = providedThroughCli._.length > 0; const configs = Object.entries(config).filter(([generator]) => hasACommandBeenProvided ? providedThroughCli._[0] === generator : true).map(([generator, generatorConfig]) => { switch (generator) { case "markdown": return _function.pipe( extractMultiCommandConfig(extractFromProcessFn, "markdown", generatorConfig), E__namespace.map((cliArgs) => { return cliArgs; }), E__namespace.flatMap((cliArgs) => { const mergedConfig = __spreadValues(__spreadValues(__spreadValues({}, configOnlyMarkdownDefaults), generatorConfig), cliArgs); return _function.pipe( logger$1.validateSourceDirectoryConfig(extractSourceDirectoryConfig(mergedConfig)), E__namespace.mapLeft((error) => new Error(`Invalid markdown configuration: ${error.message}`)), E__namespace.map(() => mergedConfig) ); }) ); case "openapi": return _function.pipe( extractMultiCommandConfig(extractFromProcessFn, "openapi", generatorConfig), E__namespace.flatMap((cliArgs) => { const mergedConfig = __spreadValues(__spreadValues(__spreadValues({}, configOnlyOpenApiDefaults), generatorConfig), cliArgs); return _function.pipe( logger$1.validateSourceDirectoryConfig(extractSourceDirectoryConfig(mergedConfig)), E__namespace.mapLeft((error) => new Error(`Invalid openapi configuration: ${error.message}`)), E__namespace.map(() => mergedConfig) ); }) ); case "changelog": return _function.pipe( extractMultiCommandConfig(extractFromProcessFn, "changelog", generatorConfig), E__namespace.map((cliArgs) => { return cliArgs; }), E__namespace.flatMap((cliArgs) => { const mergedConfig = __spreadValues(__spreadValues(__spreadValues({}, configOnlyChangelogDefaults), generatorConfig), cliArgs); return _function.pipe( validateChangelogConfig(mergedConfig), E__namespace.mapLeft((error) => new Error(`Invalid changelog configuration: ${error.message}`)), E__namespace.map(() => mergedConfig) ); }) ); } }); return E__namespace.sequenceArray(configs); } function getConfigType(config) { if (!config) { return E__namespace.right({ _type: "no-config" }); } 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__namespace.left(new Error(`Invalid command(s) provided in the configuration file: ${rootKeys}`)); } return E__namespace.right({ _type: "multi-command-config", commands }); } return E__namespace.right({ _type: "single-command-config" }); } function extractYargsDemandingCommand(extractFromProcessFn, config) { return yargs.config(config.config).command( "markdown", "Generate documentation from Apex classes as a Markdown site.", (yargs2) => yargs2.options(markdownOptions).check(validateMarkdownArgs) ).command( "openapi", "Generate an OpenApi REST specification from Apex classes.", (yargs2) => yargs2.options(openApiOptions).check(validateOpenApiArgs) ).command( "changelog", "Generate a changelog from 2 versions of the source code.", (yargs2) => yargs2.options(changeLogOptions).check(validateChangelogArgs) ).demandCommand().parseSync(extractFromProcessFn()); } function extractMultiCommandConfig(extractFromProcessFn, command, config) { function getOptions(generator) { switch (generator) { case "markdown": return markdownOptions; case "openapi": return openApiOptions; case "changelog": return changeLogOptions; } } function getValidationFunction(generator) { switch (generator) { case "markdown": return validateMarkdownArgs; case "openapi": return validateOpenApiArgs; case "changelog": return validateChangelogArgs; } } const options = getOptions(command); const validator = getValidationFunction(command); return E__namespace.tryCatch(() => { return yargs(extractFromProcessFn()).config(config).options(options).check(validator).fail((msg) => { throw new Error(`Invalid configuration for command "${command}": ${msg}`); }).parseSync(); }, E__namespace.toError); } function extractSourceDirectoryConfig(config) { return { sourceDir: config.sourceDir, useSfdxProjectJson: config.useSfdxProjectJson, sfdxProjectPath: config.sfdxProjectPath }; } function validateChangelogConfig(config) { const previousVersionConfig = { sourceDir: config.previousVersionDir }; const currentVersionConfig = { sourceDir: config.currentVersionDir }; return _function.pipe( E__namespace.Do, E__namespace.bind("previousValid", () => logger$1.validateSourceDirectoryConfig(previousVersionConfig)), E__namespace.bind("currentValid", () => logger$1.validateSourceDirectoryConfig(currentVersionConfig)), E__namespace.map(() => config) ); } var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; const logger = new logger$1.StdOutLogger(); function main() { function parseResult(result) { E__namespace.match(catchUnexpectedError, (successMessage) => { logger.logSingle(successMessage); })(result); } function catchUnexpectedError(error) { logger.error(`\u274C An error occurred while processing the request: ${error}`); process.exit(1); } extractArgs().then((maybeConfigs) => __async(null, null, function* () { E__namespace.match(catchUnexpectedError, (configs) => __async(null, null, function* () { for (const config of configs) { yield logger$1.Apexdocs.generate(config, logger).then(parseResult); } }))(maybeConfigs); })).catch(catchUnexpectedError); } main();