Skip to content

Commit

Permalink
feat: support function for urlRuleDoc option
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Dec 26, 2022
1 parent eae9a5f commit ac73b38
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 53 deletions.
2 changes: 1 addition & 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
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
21 changes: 13 additions & 8 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 @@ -62,7 +62,12 @@ export function getUrlToRule(
: ruleName;

return urlRuleDoc
? replaceRulePlaceholder(urlRuleDoc, ruleNameWithoutPluginPrefix)
? replaceRulePlaceholder(
typeof urlRuleDoc === 'function'
? urlRuleDoc(ruleName, relative(pathPlugin, pathCurrentPage))
: urlRuleDoc,
ruleNameWithoutPluginPrefix
)
: pathToUrl(
join(
relativePathPluginRoot,
Expand All @@ -80,10 +85,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 +117,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
13 changes: 11 additions & 2 deletions lib/types.ts
Expand Up @@ -126,6 +126,15 @@ export type RuleListSplitFunction = (rules: RuleNamesAndRules) => readonly {
rules: RuleNamesAndRules;
}[];

/**
* Function for splitting the rule list into multiple sections.
* 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
*/
export type UrlRuleDocFunction = (name: string, path: 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 = {
Expand Down Expand Up @@ -194,9 +203,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 it) 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.
*/
readonly urlRuleDoc?: string;
readonly urlRuleDoc?: string | UrlRuleDocFunction;
};
46 changes: 46 additions & 0 deletions test/lib/generate/__snapshots__/option-url-rule-doc-test.ts.snap
Expand Up @@ -30,3 +30,49 @@ exports[`generate (--url-rule-doc) basic uses the right URLs 3`] = `
<!-- 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 -->
"
`;
93 changes: 76 additions & 17 deletions test/lib/generate/option-url-rule-doc-test.ts
Expand Up @@ -20,24 +20,24 @@ describe('generate (--url-rule-doc)', function () {
}),

'index.js': `
export default {
rules: {
'no-foo': {
meta: {
docs: { description: 'Description for no-foo.' },
deprecated: true,
replacedBy: ['no-bar']
},
create(context) {}
},
'no-bar': {
meta: {
docs: { description: 'Description for no-bar.' }
},
create(context) {}
},
export default {
rules: {
'no-foo': {
meta: {
docs: { description: 'Description for no-foo.' },
deprecated: true,
replacedBy: ['no-bar']
},
};`,
create(context) {}
},
'no-bar': {
meta: {
docs: { description: 'Description for no-bar.' }
},
create(context) {}
},
},
};`,

'README.md': '## Rules\n',

Expand All @@ -63,4 +63,63 @@ describe('generate (--url-rule-doc)', function () {
expect(readFileSync('docs/rules/no-bar.md', 'utf8')).toMatchSnapshot();
});
});

describe('function', function () {
beforeEach(function () {
mockFs({
'package.json': JSON.stringify({
name: 'eslint-plugin-test',
exports: 'index.js',
type: 'module',
}),

'index.js': `
export default {
rules: {
'no-foo': {
meta: {
docs: { description: 'Description for no-foo.' },
deprecated: true,
replacedBy: ['no-bar']
},
create(context) {}
},
'no-bar': {
meta: {
docs: { description: 'Description for no-bar.' }
},
create(context) {}
},
},
};`,

'README.md': '## Rules\n',
'nested/README.md': '## Rules\n',

'docs/rules/no-foo.md': '',
'docs/rules/no-bar.md': '',

// Needed for some of the test infrastructure to work.
node_modules: mockFs.load(PATH_NODE_MODULES),
});
});

afterEach(function () {
mockFs.restore();
jest.resetModules();
});

it('uses the custom URL', async function () {
await generate('.', {
pathRuleList: ['README.md', 'nested/README.md'],
urlRuleDoc(name, path) {
return `https://example.com/rule-docs/name:${name}/path:${path}`;
},
});
expect(readFileSync('README.md', 'utf8')).toMatchSnapshot();
expect(readFileSync('nested/README.md', 'utf8')).toMatchSnapshot();
expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot();
expect(readFileSync('docs/rules/no-bar.md', 'utf8')).toMatchSnapshot();
});
});
});

0 comments on commit ac73b38

Please sign in to comment.