Skip to content

Commit

Permalink
feat: support auto-generated config lists
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Oct 6, 2023
1 parent d27c509 commit b120c89
Show file tree
Hide file tree
Showing 7 changed files with 599 additions and 12 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Automatic documentation generator for [ESLint](https://eslint.org/) plugins and
Generates the following documentation covering a [wide variety](#column-and-notice-types) of rule metadata:

- `README.md` rules table
- `README.md` configs table
- Rule doc titles and notices

Also performs [configurable](#configuration-options) section consistency checks on rule docs:
Expand All @@ -20,6 +21,7 @@ Also performs [configurable](#configuration-options) section consistency checks
- [Usage](#usage)
- [Examples](#examples)
- [Rules list table](#rules-list-table)
- [Configs list table](#configs-list-table)
- [Rule doc notices](#rule-doc-notices)
- [Users](#users)
- [Configuration options](#configuration-options)
Expand Down Expand Up @@ -75,6 +77,13 @@ Delete any old rules list from your `README.md`. A new one will be automatically
<!-- end auto-generated rules list -->
```

Optionally, add these marker comments to your `README.md` where you would like the configs list to go (uses the `description` property exported by each config if available):

```md
<!-- begin auto-generated configs list -->
<!-- end auto-generated configs list -->
```

Delete any old recommended/fixable/etc. notices from your rule docs. A new title and notices will be automatically added to the top of each rule doc (along with a marker comment if it doesn't already exist).

```md
Expand Down Expand Up @@ -102,6 +111,10 @@ For examples, see our [users](#users) or the in-house examples below. Note that

See the generated rules table and legend in our example [`README.md`](./docs/examples/eslint-plugin-test/README.md#rules).

### Configs list table

See the generated configs table in our example [`README.md`](./docs/examples/eslint-plugin-test/README.md#configs).

### Rule doc notices

See the generated rule doc title and notices in our example rule docs [`no-foo.md`](./docs/examples/eslint-plugin-test/docs/rules/no-foo.md), [`prefer-bar.md`](./docs/examples/eslint-plugin-test/docs/rules/prefer-bar.md), [`require-baz.md`](./docs/examples/eslint-plugin-test/docs/rules/require-baz.md).
Expand Down
10 changes: 9 additions & 1 deletion docs/examples/eslint-plugin-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ This plugin is for x purpose.

## Configs

Configs section would normally go here.
<!-- begin auto-generated configs list -->

| | Name | Description |
| :- | :------------ | :----------------------------------------------- |
|| `recommended` | These rules are recommended for everyone. |
| 🎨 | `stylistic` | These rules are more about code style than bugs. |
| ⌨️ | `typescript` | These are good rules to use with TypeScript. |

<!-- end auto-generated configs list -->

## Rules

Expand Down
6 changes: 6 additions & 0 deletions lib/comment-markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export const END_RULE_LIST_MARKER = '<!-- end auto-generated rules list -->';

// Marker so that rule doc header (title/notices) can be automatically updated.
export const END_RULE_HEADER_MARKER = '<!-- end auto-generated rule header -->';

// Markers so that the configs table list can be automatically updated.
export const BEGIN_CONFIG_LIST_MARKER =
'<!-- begin auto-generated configs list -->';
export const END_CONFIG_LIST_MARKER =
'<!-- end auto-generated configs list -->';
98 changes: 98 additions & 0 deletions lib/config-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
BEGIN_CONFIG_LIST_MARKER,
END_CONFIG_LIST_MARKER,
} from './comment-markers.js';
import { markdownTable } from 'markdown-table';
import type { ConfigsToRules, ConfigEmojis, Plugin } from './types.js';
import { ConfigFormat, configNameToDisplay } from './config-format.js';

function generateConfigListMarkdown(
plugin: Plugin,
configsToRules: ConfigsToRules,
pluginPrefix: string,
configEmojis: ConfigEmojis,
configFormat: ConfigFormat,
ignoreConfig: readonly string[]
): string {
/* istanbul ignore next -- configs are sure to exist at this point */
const configs = Object.values(plugin.configs || {});
const hasDescription = configs.some(
// @ts-expect-error -- description is not an official config property.
(config) => config.description
);
const listHeaderRow = ['', 'Name'];
if (hasDescription) {
listHeaderRow.push('Description');
}

return markdownTable(
[
listHeaderRow,
...Object.keys(configsToRules)
.filter((configName) => !ignoreConfig.includes(configName))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map((configName) => {
return [
configEmojis.find((obj) => obj.config === configName)?.emoji || '',
`\`${configNameToDisplay(
configName,
configFormat,
pluginPrefix
)}\``,
hasDescription
? // @ts-expect-error -- description is not an official config property.
(plugin.configs?.[configName]?.description as
| string
| undefined) || ''
: undefined,
].filter((col) => col !== undefined);
}),
],
{ align: 'l' } // Left-align headers.
);
}

export function updateConfigsList(
markdown: string,
plugin: Plugin,
configsToRules: ConfigsToRules,
pluginPrefix: string,
configEmojis: ConfigEmojis,
configFormat: ConfigFormat,
ignoreConfig: readonly string[]
): string {
const listStartIndex = markdown.indexOf(BEGIN_CONFIG_LIST_MARKER);
let listEndIndex = markdown.indexOf(END_CONFIG_LIST_MARKER);

if (listStartIndex === -1 || listEndIndex === -1) {
// No config list found.
return markdown;
}

if (
Object.keys(configsToRules).filter(
(configName) => !ignoreConfig.includes(configName)
).length === 0
) {
// No non-ignored configs found.
return markdown;
}

// Account for length of pre-existing marker.
listEndIndex += END_CONFIG_LIST_MARKER.length;

const preList = markdown.slice(0, Math.max(0, listStartIndex));
const postList = markdown.slice(Math.max(0, listEndIndex));

// New config list.
const list = generateConfigListMarkdown(
plugin,
configsToRules,
pluginPrefix,
configEmojis,
configFormat,
ignoreConfig
);

return `${preList}${BEGIN_CONFIG_LIST_MARKER}\n\n${list}\n\n${END_CONFIG_LIST_MARKER}${postList}`;
}
31 changes: 20 additions & 11 deletions lib/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getPathWithExactFileNameCasing,
} from './package-json.js';
import { updateRulesList } from './rule-list.js';
import { updateConfigsList } from './config-list.js';
import { generateRuleHeaderLines } from './rule-doc-notices.js';
import {
parseRuleDocNoticesOption,
Expand Down Expand Up @@ -260,22 +261,30 @@ export async function generate(path: string, options?: GenerateOptions) {
// Update the rules list in this file.
const fileContents = readFileSync(pathToFile, 'utf8');
const fileContentsNew = await postprocess(
updateRulesList(
ruleNamesAndRules,
fileContents,
updateConfigsList(
updateRulesList(
ruleNamesAndRules,
fileContents,
plugin,
configsToRules,
pluginPrefix,
pathRuleDoc,
pathToFile,
path,
configEmojis,
configFormat,
ignoreConfig,
ruleListColumns,
ruleListSplit,
urlConfigs,
urlRuleDoc
),
plugin,
configsToRules,
pluginPrefix,
pathRuleDoc,
pathToFile,
path,
configEmojis,
configFormat,
ignoreConfig,
ruleListColumns,
ruleListSplit,
urlConfigs,
urlRuleDoc
ignoreConfig
),
resolve(pathToFile)
);
Expand Down
126 changes: 126 additions & 0 deletions test/lib/generate/__snapshots__/configs-list-test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generate (configs list) basic generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
| | Name |
| :- | :------------ |
| ✅ | \`recommended\` |
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) when a config exports a description generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
| | Name | Description |
| :- | :------------ | :--------------------------------------- |
| | \`foo\` | |
| ✅ | \`recommended\` | This config has the recommended rules... |
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) when all configs are ignored generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) when there are no configs generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) with --config-format generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
| | Name |
| :- | :----------------- |
| ✅ | \`test/recommended\` |
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) with --ignore-config generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
| | Name |
| :- | :------------ |
| ✅ | \`recommended\` |
<!-- end auto-generated configs list -->"
`;

exports[`generate (configs list) with configs not defined in alphabetical order generates the documentation 1`] = `
"## Rules
<!-- begin auto-generated rules list -->
| Name | Description |
| :----------------------------- | :--------------------- |
| [no-foo](docs/rules/no-foo.md) | Description of no-foo. |
<!-- end auto-generated rules list -->
## Configs
<!-- begin auto-generated configs list -->
| | Name |
| :- | :------------ |
| | \`foo\` |
| ✅ | \`recommended\` |
<!-- end auto-generated configs list -->"
`;
Loading

0 comments on commit b120c89

Please sign in to comment.