From f2fd013b237677107a3657e7fa684f38f3c20667 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Tue, 14 Nov 2023 17:17:29 +0800 Subject: [PATCH] Support a user-defined function for `--path-rule-doc` option (#502) --- README.md | 17 ++++++++++++++++- lib/cli.ts | 11 +++++++++-- lib/generator.ts | 2 +- lib/rule-doc-notices.ts | 7 ++++--- lib/rule-link.ts | 19 +++++++++++++++---- lib/rule-list.ts | 9 +++++---- lib/types.ts | 16 ++++++++++++++-- .../__snapshots__/file-paths-test.ts.snap | 17 +++++++++++++++++ test/lib/generate/file-paths-test.ts | 10 ++++++++++ 9 files changed, 91 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 614cdd10..458c3922 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ There's also a `postprocess` option that's only available via a [config file](#c | `--ignore-config` | Config to ignore from being displayed. Often used for an `all` config. Option can be repeated. | | | `--ignore-deprecated-rules` | Whether to ignore deprecated rules from being checked, displayed, or updated. | `false` | | `--init-rule-docs` | Whether to create rule doc files if they don't yet exist. | `false` | -| `--path-rule-doc` | Path to markdown file for each rule doc. Use `{name}` placeholder for the rule name. | `docs/rules/{name}.md` | +| `--path-rule-doc` | Path to markdown file for each rule doc. Use `{name}` placeholder for the rule name. A function can also be provided for this option via a [config file](#configuration-file). | `docs/rules/{name}.md` | | `--path-rule-list` | Path to markdown file where the rules table list should live. Option can be repeated. | `README.md` | | `--rule-doc-notices` | Ordered, comma-separated list of notices to display in rule doc. Non-applicable notices will be hidden. See choices in below [table](#column-and-notice-types). | `deprecated`, `configs`, `fixableAndHasSuggestions`, `requiresTypeChecking` | | `--rule-doc-section-exclude` | Disallowed section in each rule doc. Exit with failure if present. Option can be repeated. | @@ -261,6 +261,21 @@ const config = { module.exports = config; ``` +Example `.eslint-doc-generatorrc.js` with `pathRuleDoc` function: + +```js +/** @type {import('eslint-doc-generator').GenerateOptions} */ +const config = { + pathRuleDoc(name) { + // e.g. rule name format is `some-plugin/some-rule`, and rule is in a monorepo under different package. + const [plugin, rule] = name.split("/"); + return `packages/eslint-plugin-${plugin}/src/rules/${rule}.md`; + }, +}; + +module.exports = config; +``` + Example `.eslint-doc-generatorrc.js` with `ruleListSplit` function: ```js diff --git a/lib/cli.ts b/lib/cli.ts index 71e798e0..19ed293c 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -96,7 +96,14 @@ async function loadConfigFileOptions(): Promise { ignoreConfig: schemaStringArray, ignoreDeprecatedRules: { type: 'boolean' }, initRuleDocs: { type: 'boolean' }, - pathRuleDoc: { type: 'string' }, + pathRuleDoc: + /* istanbul ignore next -- TODO: haven't tested JavaScript config files yet https://github.com/bmish/eslint-doc-generator/issues/366 */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + typeof explorerResults.config.pathRuleDoc === 'function' + ? { + /* Functions are allowed but JSON Schema can't validate them so no-op in this case. */ + } + : { type: 'string' }, pathRuleList: { anyOf: [{ type: 'string' }, schemaStringArray] }, postprocess: { /* JSON Schema can't validate functions so check this later */ @@ -226,7 +233,7 @@ export async function run( ) .option( '--path-rule-doc ', - `(optional) Path to markdown file for each rule doc. Use \`{name}\` placeholder for the rule name. (default: ${ + `(optional) Path to markdown file for each rule doc. Use \`{name}\` placeholder for the rule name. To specify a function, use a JavaScript-based config file. (default: ${ OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC] })` ) diff --git a/lib/generator.ts b/lib/generator.ts index 47e2e6fe..1d633c55 100644 --- a/lib/generator.ts +++ b/lib/generator.ts @@ -151,7 +151,7 @@ export async function generate(path: string, options?: GenerateOptions) { for (const [name, rule] of ruleNamesAndRules) { const schema = rule.meta?.schema; const description = rule.meta?.docs?.description; - const pathToDoc = replaceRulePlaceholder(join(path, pathRuleDoc), name); + const pathToDoc = join(path, replaceRulePlaceholder(pathRuleDoc, name)); const ruleHasOptions = hasOptions(schema); if (!existsSync(pathToDoc)) { diff --git a/lib/rule-doc-notices.ts b/lib/rule-doc-notices.ts index fe92ce9e..fd77ea47 100644 --- a/lib/rule-doc-notices.ts +++ b/lib/rule-doc-notices.ts @@ -16,6 +16,7 @@ import { SEVERITY_TYPE, NOTICE_TYPE, UrlRuleDocFunction, + PathRuleDocFunction, } from './types.js'; import { RULE_TYPE, RULE_TYPE_MESSAGES_NOTICES } from './rule-type.js'; import { RuleDocTitleFormat } from './rule-doc-title-format.js'; @@ -104,7 +105,7 @@ const RULE_NOTICES: { plugin: Plugin; pluginPrefix: string; pathPlugin: string; - pathRuleDoc: string; + pathRuleDoc: string | PathRuleDocFunction; type?: `${RULE_TYPE}`; urlRuleDoc?: string | UrlRuleDocFunction; }) => string); @@ -303,7 +304,7 @@ function getRuleNoticeLines( configsToRules: ConfigsToRules, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, configEmojis: ConfigEmojis, configFormat: ConfigFormat, ignoreConfig: readonly string[], @@ -495,7 +496,7 @@ export function generateRuleHeaderLines( configsToRules: ConfigsToRules, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, configEmojis: ConfigEmojis, configFormat: ConfigFormat, ignoreConfig: readonly string[], diff --git a/lib/rule-link.ts b/lib/rule-link.ts index 6614788b..8fb9f9b2 100644 --- a/lib/rule-link.ts +++ b/lib/rule-link.ts @@ -1,8 +1,19 @@ import { join, sep, relative, dirname } from 'node:path'; -import { Plugin, RULE_SOURCE, UrlRuleDocFunction } from './types.js'; +import { + PathRuleDocFunction, + Plugin, + RULE_SOURCE, + UrlRuleDocFunction, +} from './types.js'; import { getPluginRoot } from './package-json.js'; -export function replaceRulePlaceholder(pathOrUrl: string, ruleName: string) { +export function replaceRulePlaceholder( + pathOrUrl: string | PathRuleDocFunction, + ruleName: string +) { + if (typeof pathOrUrl === 'function') { + return pathOrUrl(ruleName); + } return pathOrUrl.replace(/\{name\}/gu, ruleName); } @@ -22,7 +33,7 @@ export function getUrlToRule( ruleSource: RULE_SOURCE, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathCurrentPage: string, urlRuleDoc?: string | UrlRuleDocFunction ) { @@ -76,7 +87,7 @@ export function getLinkToRule( plugin: Plugin, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathCurrentPage: string, includeBackticks: boolean, includePrefix: boolean, diff --git a/lib/rule-list.ts b/lib/rule-list.ts index b768aaa8..9b5820e6 100644 --- a/lib/rule-list.ts +++ b/lib/rule-list.ts @@ -28,6 +28,7 @@ import type { ConfigsToRules, ConfigEmojis, RuleNamesAndRules, + PathRuleDocFunction, } from './types.js'; import { EMOJIS_TYPE } from './rule-type.js'; import { hasOptions } from './rule-options.js'; @@ -118,7 +119,7 @@ function buildRuleRow( plugin: Plugin, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathRuleList: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], @@ -203,7 +204,7 @@ function generateRulesListMarkdown( plugin: Plugin, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathRuleList: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], @@ -258,7 +259,7 @@ function generateRuleListMarkdownForRulesAndHeaders( plugin: Plugin, pluginPrefix: string, pathPlugin: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathRuleList: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], @@ -396,7 +397,7 @@ export function updateRulesList( plugin: Plugin, configsToRules: ConfigsToRules, pluginPrefix: string, - pathRuleDoc: string, + pathRuleDoc: string | PathRuleDocFunction, pathRuleList: string, pathPlugin: string, configEmojis: ConfigEmojis, diff --git a/lib/types.ts b/lib/types.ts index 0c0e574d..31e3fdaa 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -138,6 +138,14 @@ export type UrlRuleDocFunction = ( path: string ) => string | undefined; +/** + * Function for generating the path to markdown file for each rule doc. + * Can be provided via a JavaScript-based config file using the `pathRuleDoc` option. + * @param name - the name of the rule + * @returns the path to the rule doc + */ +export type PathRuleDocFunction = (name: string) => string; + // JSDocs for options should be kept in sync with README.md and the CLI runner in cli.ts. /** The type for the config file (e.g. `.eslint-doc-generatorrc.js`) and internal `generate()` function. */ export type GenerateOptions = { @@ -166,8 +174,12 @@ export type GenerateOptions = { readonly ignoreDeprecatedRules?: boolean; /** Whether to create rule doc files if they don't yet exist. Default: `false`. */ readonly initRuleDocs?: boolean; - /** Path to markdown file for each rule doc. Use `{name}` placeholder for the rule name. Default: `docs/rules/{name}.md`. */ - readonly pathRuleDoc?: string; + /** + * Path (or function to generate a path) to to markdown file for each rule doc. + * For the string version, use `{name}` placeholder for the rule name. + * Default: `docs/rules/{name}.md`. + */ + readonly pathRuleDoc?: string | PathRuleDocFunction; /** Path to markdown file(s) where the rules table list should live. Default: `README.md`. */ readonly pathRuleList?: string | readonly string[]; /** diff --git a/test/lib/generate/__snapshots__/file-paths-test.ts.snap b/test/lib/generate/__snapshots__/file-paths-test.ts.snap index 83184cc4..5d32768f 100644 --- a/test/lib/generate/__snapshots__/file-paths-test.ts.snap +++ b/test/lib/generate/__snapshots__/file-paths-test.ts.snap @@ -17,6 +17,23 @@ exports[`generate (file paths) custom path to rule docs and rules list generates " `; +exports[`generate (file paths) custom path to rule docs and rules list generates the documentation using a function for pathRuleDoc 1`] = ` +" + +| Name | +| :------------------------------- | +| [no-foo](rules/no-foo/no-foo.md) | + +" +`; + +exports[`generate (file paths) custom path to rule docs and rules list generates the documentation using a function for pathRuleDoc 2`] = ` +"# test/no-foo + + +" +`; + exports[`generate (file paths) empty array of rule lists (happens when CLI option is not passed) falls back to default rules list 1`] = ` " diff --git a/test/lib/generate/file-paths-test.ts b/test/lib/generate/file-paths-test.ts index f601dd0d..de7eca6e 100644 --- a/test/lib/generate/file-paths-test.ts +++ b/test/lib/generate/file-paths-test.ts @@ -245,6 +245,8 @@ describe('generate (file paths)', function () { }, };`, + 'README.md': + '', 'rules/list.md': '', 'rules/no-foo/no-foo.md': '', @@ -267,6 +269,14 @@ describe('generate (file paths)', function () { expect(readFileSync('rules/list.md', 'utf8')).toMatchSnapshot(); expect(readFileSync('rules/no-foo/no-foo.md', 'utf8')).toMatchSnapshot(); }); + + it('generates the documentation using a function for pathRuleDoc', async function () { + await generate('.', { + pathRuleDoc: (ruleName) => join('rules', ruleName, `${ruleName}.md`), + }); + expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); + expect(readFileSync('rules/no-foo/no-foo.md', 'utf8')).toMatchSnapshot(); + }); }); describe('multiple rules lists', function () {