Skip to content

Commit

Permalink
Support a user-defined function for --url-rule-doc option (#369)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Dec 26, 2022
1 parent 57b8e22 commit 4e57a37
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 61 deletions.
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -146,7 +146,7 @@ There's also a `postprocess` option that's only available via a [config file](#c
| `--rule-list-columns` | Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. See choices in below [table](#column-and-notice-types). Default: `name,description,configsError,configsWarn,configsOff,fixable,hasSuggestions,requiresTypeChecking,deprecated`. |
| `--rule-list-split` | Rule property(s) to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. A function can also be provided for this option via a [config file](#configuration-file). |
| `--url-configs` | Link to documentation about the ESLint configurations exported by the plugin. |
| `--url-rule-doc` | Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name. |
| `--url-rule-doc` | Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name. A function can also be provided for this option via a [config file](#configuration-file). |

### Column and notice types

Expand Down Expand Up @@ -241,6 +241,22 @@ const config = {
module.exports = config;
```

Example `.eslint-doc-generatorrc.js` with `urlRuleDoc` function:

```js
/** @type {import('eslint-doc-generator').GenerateOptions} */
const config = {
urlRuleDoc(name, page) {
if (page === 'README.md') {
// Use URLs only in the readme.
return `https://example.com/rules/${name}.html`;
}
},
};

module.exports = config;
```

### Badges

While config emojis are the recommended representations of configs that a rule belongs to (see [`--config-emoji`](#configuration-options)), you can alternatively define badges for configs at the bottom of your `README.md`.
Expand Down
11 changes: 9 additions & 2 deletions lib/cli.ts
Expand Up @@ -116,7 +116,14 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
}
: { anyOf: [{ type: 'string' }, schemaStringArray] },
urlConfigs: { type: 'string' },
urlRuleDoc: { type: 'string' },
urlRuleDoc:
/* 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.urlRuleDoc === 'function'
? {
/* Functions are allowed but JSON Schema can't validate them so no-op in this case. */
}
: { type: 'string' },
};
const schema = {
type: 'object',
Expand Down Expand Up @@ -290,7 +297,7 @@ export async function run(
)
.option(
'--url-rule-doc <url>',
'(optional) Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name.'
'(optional) Link to documentation for each rule. Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use). Use `{name}` placeholder for the rule name. To specify a function, use a JavaScript-based config file.'
)
.action(async function (path: string, options: GenerateOptions) {
// Load config file options and merge with CLI options.
Expand Down
25 changes: 6 additions & 19 deletions lib/rule-doc-notices.ts
Expand Up @@ -15,12 +15,12 @@ import {
ConfigEmojis,
SEVERITY_TYPE,
NOTICE_TYPE,
RULE_SOURCE,
UrlRuleDocFunction,
} from './types.js';
import { RULE_TYPE, RULE_TYPE_MESSAGES_NOTICES } from './rule-type.js';
import { RuleDocTitleFormat } from './rule-doc-title-format.js';
import { hasOptions } from './rule-options.js';
import { getLinkToRule, getUrlToRule } from './rule-link.js';
import { getLinkToRule, replaceRulePlaceholder } from './rule-link.js';
import {
toSentenceCase,
removeTrailingPeriod,
Expand Down Expand Up @@ -102,7 +102,7 @@ const RULE_NOTICES: {
pathPlugin: string;
pathRuleDoc: string;
type?: `${RULE_TYPE}`;
urlRuleDoc?: string;
urlRuleDoc?: string | UrlRuleDocFunction;
}) => string);
} = {
// Configs notice varies based on whether the rule is configured in one or more configs.
Expand Down Expand Up @@ -189,27 +189,14 @@ const RULE_NOTICES: {
ruleName,
urlRuleDoc,
}) => {
const urlCurrentPage = getUrlToRule(
ruleName,
RULE_SOURCE.self,
pluginPrefix,
pathPlugin,
pathRuleDoc,
pathPlugin,
urlRuleDoc
);
/* istanbul ignore next -- this shouldn't happen */
if (!urlCurrentPage) {
throw new Error('Missing URL to our own rule');
}
const replacementRuleList = (replacedBy ?? []).map((replacementRuleName) =>
getLinkToRule(
replacementRuleName,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
urlCurrentPage,
replaceRulePlaceholder(pathRuleDoc, ruleName),
true,
true,
urlRuleDoc
Expand Down Expand Up @@ -318,7 +305,7 @@ function getRuleNoticeLines(
ignoreConfig: readonly string[],
ruleDocNotices: readonly NOTICE_TYPE[],
urlConfigs?: string,
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
) {
const lines: string[] = [];

Expand Down Expand Up @@ -502,7 +489,7 @@ export function generateRuleHeaderLines(
ruleDocNotices: readonly NOTICE_TYPE[],
ruleDocTitleFormat: RuleDocTitleFormat,
urlConfigs?: string,
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
): string {
return [
makeRuleDocTitle(name, description, pluginPrefix, ruleDocTitleFormat),
Expand Down
42 changes: 27 additions & 15 deletions lib/rule-link.ts
@@ -1,6 +1,6 @@
import { countOccurrencesInString } from './string.js';
import { join, sep, relative } from 'node:path';
import { Plugin, RULE_SOURCE } from './types.js';
import { Plugin, RULE_SOURCE, UrlRuleDocFunction } from './types.js';

export function replaceRulePlaceholder(pathOrUrl: string, ruleName: string) {
return pathOrUrl.replace(/\{name\}/gu, ruleName);
Expand Down Expand Up @@ -35,8 +35,8 @@ export function getUrlToRule(
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
urlCurrentPage: string,
urlRuleDoc?: string
pathCurrentPage: string,
urlRuleDoc?: string | UrlRuleDocFunction
) {
switch (ruleSource) {
case RULE_SOURCE.eslintCore:
Expand All @@ -50,7 +50,7 @@ export function getUrlToRule(
}

const nestingDepthOfCurrentPage = countOccurrencesInString(
relative(pathPlugin, urlCurrentPage),
relative(pathPlugin, pathCurrentPage),
sep
);
const relativePathPluginRoot = goUpLevel(nestingDepthOfCurrentPage);
Expand All @@ -61,14 +61,26 @@ export function getUrlToRule(
? ruleName.slice(pluginPrefix.length + 1)
: ruleName;

return urlRuleDoc
? replaceRulePlaceholder(urlRuleDoc, ruleNameWithoutPluginPrefix)
: pathToUrl(
join(
relativePathPluginRoot,
replaceRulePlaceholder(pathRuleDoc, ruleNameWithoutPluginPrefix)
)
);
// If the URL is a function, evaluate it.
const urlRuleDocFunctionEvaluated =
typeof urlRuleDoc === 'function'
? urlRuleDoc(ruleName, pathToUrl(relative(pathPlugin, pathCurrentPage)))
: undefined;

return (
// If the function returned a URL, use it.
urlRuleDocFunctionEvaluated ??
(typeof urlRuleDoc === 'string'
? // Otherwise, use the URL if it's a string.
replaceRulePlaceholder(urlRuleDoc, ruleNameWithoutPluginPrefix)
: // Finally, fallback to the relative path.
pathToUrl(
join(
relativePathPluginRoot,
replaceRulePlaceholder(pathRuleDoc, ruleNameWithoutPluginPrefix)
)
))
);
}

/**
Expand All @@ -80,10 +92,10 @@ export function getLinkToRule(
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
urlCurrentPage: string,
pathCurrentPage: string,
includeBackticks: boolean,
includePrefix: boolean,
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
) {
const ruleNameWithoutPluginPrefix = ruleName.startsWith(`${pluginPrefix}/`)
? ruleName.slice(pluginPrefix.length + 1)
Expand Down Expand Up @@ -112,7 +124,7 @@ export function getLinkToRule(
pluginPrefix,
pathPlugin,
pathRuleDoc,
urlCurrentPage,
pathCurrentPage,
urlRuleDoc
);

Expand Down
9 changes: 5 additions & 4 deletions lib/rule-list.ts
Expand Up @@ -20,6 +20,7 @@ import {
RuleListSplitFunction,
RuleModule,
SEVERITY_TYPE,
UrlRuleDocFunction,
} from './types.js';
import { markdownTable } from 'markdown-table';
import type {
Expand Down Expand Up @@ -107,7 +108,7 @@ function buildRuleRow(
pathRuleList: string,
configEmojis: ConfigEmojis,
ignoreConfig: readonly string[],
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
): readonly string[] {
const columns: {
[key in COLUMN_TYPE]: string | (() => string);
Expand Down Expand Up @@ -190,7 +191,7 @@ function generateRulesListMarkdown(
pathRuleList: string,
configEmojis: ConfigEmojis,
ignoreConfig: readonly string[],
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
): string {
const listHeaderRow = (
Object.entries(columns) as readonly [COLUMN_TYPE, boolean][]
Expand Down Expand Up @@ -245,7 +246,7 @@ function generateRuleListMarkdownForRulesAndHeaders(
pathRuleList: string,
configEmojis: ConfigEmojis,
ignoreConfig: readonly string[],
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
): string {
const parts: string[] = [];

Expand Down Expand Up @@ -388,7 +389,7 @@ export function updateRulesList(
ruleListColumns: readonly COLUMN_TYPE[],
ruleListSplit: readonly string[] | RuleListSplitFunction,
urlConfigs?: string,
urlRuleDoc?: string
urlRuleDoc?: string | UrlRuleDocFunction
): string {
let listStartIndex = markdown.indexOf(BEGIN_RULE_LIST_MARKER);
let listEndIndex = markdown.indexOf(END_RULE_LIST_MARKER);
Expand Down
18 changes: 15 additions & 3 deletions lib/types.ts
Expand Up @@ -126,6 +126,18 @@ export type RuleListSplitFunction = (rules: RuleNamesAndRules) => readonly {
rules: RuleNamesAndRules;
}[];

/**
* Function for generating the URL to a rule doc.
* Can be provided via a JavaScript-based config file using the `urlRuleDoc` option.
* @param name - the name of the rule
* @param path - the file path to the current page displaying the link, relative to the project root
* @returns the URL to the rule doc, or `undefined` to fallback to the default logic (relative URL)
*/
export type UrlRuleDocFunction = (
name: string,
path: string
) => string | undefined;

// 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 = {
Expand Down Expand Up @@ -194,9 +206,9 @@ export type GenerateOptions = {
/** Link to documentation about the ESLint configurations exported by the plugin. */
readonly urlConfigs?: string;
/**
* Link to documentation for each rule.
* Link (or function to generate a link) to documentation for each rule.
* Useful when it differs from the rule doc path on disk (e.g. custom documentation site in use).
* Use `{name}` placeholder for the rule name.
* For the string version, use `{name}` placeholder for the rule name.
*/
readonly urlRuleDoc?: string;
readonly urlRuleDoc?: string | UrlRuleDocFunction;
};
92 changes: 92 additions & 0 deletions test/lib/generate/__snapshots__/option-url-rule-doc-test.ts.snap
Expand Up @@ -30,3 +30,95 @@ exports[`generate (--url-rule-doc) basic uses the right URLs 3`] = `
<!-- end auto-generated rule header -->
"
`;

exports[`generate (--url-rule-doc) function returns undefined should fallback to the normal URL 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
❌ Deprecated.
| Name | Description | ❌ |
| :----------------------------- | :---------------------- | :- |
| [no-bar](docs/rules/no-bar.md) | Description for no-bar. | |
| [no-foo](docs/rules/no-foo.md) | Description for no-foo. | ❌ |
<!-- end auto-generated rules list -->
"
`;

exports[`generate (--url-rule-doc) function returns undefined should fallback to the normal URL 2`] = `
"## Rules
<!-- begin auto-generated rules list -->
❌ Deprecated.
| Name | Description | ❌ |
| :-------------------------------- | :---------------------- | :- |
| [no-bar](../docs/rules/no-bar.md) | Description for no-bar. | |
| [no-foo](../docs/rules/no-foo.md) | Description for no-foo. | ❌ |
<!-- end auto-generated rules list -->
"
`;

exports[`generate (--url-rule-doc) function returns undefined should fallback to the normal URL 3`] = `
"# Description for no-foo (\`test/no-foo\`)
❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](../../docs/rules/no-bar.md).
<!-- end auto-generated rule header -->
"
`;

exports[`generate (--url-rule-doc) function returns undefined should fallback to the normal URL 4`] = `
"# Description for no-bar (\`test/no-bar\`)
<!-- end auto-generated rule header -->
"
`;

exports[`generate (--url-rule-doc) function uses the custom URL 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
❌ Deprecated.
| Name | Description | ❌ |
| :----------------------------------------------------------------- | :---------------------- | :- |
| [no-bar](https://example.com/rule-docs/name:no-bar/path:README.md) | Description for no-bar. | |
| [no-foo](https://example.com/rule-docs/name:no-foo/path:README.md) | Description for no-foo. | ❌ |
<!-- end auto-generated rules list -->
"
`;

exports[`generate (--url-rule-doc) function uses the custom URL 2`] = `
"## Rules
<!-- begin auto-generated rules list -->
❌ Deprecated.
| Name | Description | ❌ |
| :------------------------------------------------------------------------ | :---------------------- | :- |
| [no-bar](https://example.com/rule-docs/name:no-bar/path:nested/README.md) | Description for no-bar. | |
| [no-foo](https://example.com/rule-docs/name:no-foo/path:nested/README.md) | Description for no-foo. | ❌ |
<!-- end auto-generated rules list -->
"
`;

exports[`generate (--url-rule-doc) function uses the custom URL 3`] = `
"# Description for no-foo (\`test/no-foo\`)
❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](https://example.com/rule-docs/name:no-bar/path:docs/rules/no-foo.md).
<!-- end auto-generated rule header -->
"
`;

exports[`generate (--url-rule-doc) function uses the custom URL 4`] = `
"# Description for no-bar (\`test/no-bar\`)
<!-- end auto-generated rule header -->
"
`;

0 comments on commit 4e57a37

Please sign in to comment.