From 9c9284279ad366489ee9dcd98a5cef9cf8c514a2 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:44:11 -0500 Subject: [PATCH] Support multiple potential property locations for config description (#508) --- README.md | 2 +- lib/config-list.ts | 33 +++- .../__snapshots__/configs-list-test.ts.snap | 46 ++++- test/lib/generate/configs-list-test.ts | 162 ++++++++++++++---- 4 files changed, 198 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 458c3922..521fb598 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Delete any old rules list from your `README.md`. A new one will be automatically ``` -Optionally, add these marker comments to your `README.md` in a `## Configs` section or similar location (uses the `description` property exported by each config if available): +Optionally, add these marker comments to your `README.md` in a `## Configs` section or similar location (uses the `meta.docs.description` property exported by each config if available): ```md diff --git a/lib/config-list.ts b/lib/config-list.ts index 9eef5e4c..23a7ad48 100644 --- a/lib/config-list.ts +++ b/lib/config-list.ts @@ -3,10 +3,31 @@ import { END_CONFIG_LIST_MARKER, } from './comment-markers.js'; import { markdownTable } from 'markdown-table'; -import type { ConfigsToRules, ConfigEmojis, Plugin } from './types.js'; +import type { ConfigsToRules, ConfigEmojis, Plugin, Config } from './types.js'; import { ConfigFormat, configNameToDisplay } from './config-format.js'; import { sanitizeMarkdownTable } from './string.js'; +/** + * Check potential locations for the config description. + * These are not official properties. + * The recommended/allowed way to add a description is still pending the outcome of: https://github.com/eslint/eslint/issues/17842 + * @param config + * @returns the description if available + */ +function configToDescription(config: Config): string | undefined { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return ( + // @ts-expect-error -- description is not an official config property. + config.description || + // @ts-expect-error -- description is not an official config property. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + config.meta?.description || + // @ts-expect-error -- description is not an official config property. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + config.meta?.docs?.description + ); +} + function generateConfigListMarkdown( plugin: Plugin, configsToRules: ConfigsToRules, @@ -17,10 +38,7 @@ function generateConfigListMarkdown( ): 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 hasDescription = configs.some((config) => configToDescription(config)); const listHeaderRow = ['', 'Name']; if (hasDescription) { listHeaderRow.push('Description'); @@ -33,8 +51,9 @@ function generateConfigListMarkdown( .filter((configName) => !ignoreConfig.includes(configName)) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) .map((configName) => { - const description = // @ts-expect-error -- description is not an official config property. - plugin.configs?.[configName]?.description as string | undefined; + const config = plugin.configs?.[configName]; + /* istanbul ignore next -- config should exist at this point */ + const description = config ? configToDescription(config) : undefined; return [ configEmojis.find((obj) => obj.config === configName)?.emoji || '', `\`${configNameToDisplay( diff --git a/test/lib/generate/__snapshots__/configs-list-test.ts.snap b/test/lib/generate/__snapshots__/configs-list-test.ts.snap index 4d134540..a38479d8 100644 --- a/test/lib/generate/__snapshots__/configs-list-test.ts.snap +++ b/test/lib/generate/__snapshots__/configs-list-test.ts.snap @@ -38,7 +38,7 @@ exports[`generate (configs list) when a config description needs to be escaped i " `; -exports[`generate (configs list) when a config exports a description generates the documentation 1`] = ` +exports[`generate (configs list) when a config exports a description property=description generates the documentation 1`] = ` "## Rules @@ -47,8 +47,48 @@ exports[`generate (configs list) when a config exports a description generates t | [no-foo](docs/rules/no-foo.md) | Description of no-foo. | -## Configs - + ## Configs + + +| | Name | Description | +| :- | :------------ | :--------------------------------------- | +| | \`foo\` | | +| ✅ | \`recommended\` | This config has the recommended rules... | + +" +`; + +exports[`generate (configs list) when a config exports a description property=meta.description generates the documentation 1`] = ` +"## Rules + + +| Name | Description | +| :----------------------------- | :--------------------- | +| [no-foo](docs/rules/no-foo.md) | Description of no-foo. | + + + ## Configs + + +| | Name | Description | +| :- | :------------ | :--------------------------------------- | +| | \`foo\` | | +| ✅ | \`recommended\` | This config has the recommended rules... | + +" +`; + +exports[`generate (configs list) when a config exports a description property=meta.docs.description generates the documentation 1`] = ` +"## Rules + + +| Name | Description | +| :----------------------------- | :--------------------- | +| [no-foo](docs/rules/no-foo.md) | Description of no-foo. | + + + ## Configs + | | Name | Description | | :- | :------------ | :--------------------------------------- | diff --git a/test/lib/generate/configs-list-test.ts b/test/lib/generate/configs-list-test.ts index 2813bd21..52369836 100644 --- a/test/lib/generate/configs-list-test.ts +++ b/test/lib/generate/configs-list-test.ts @@ -193,48 +193,142 @@ describe('generate (configs list)', function () { }); describe('when a config exports a description', 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 of no-foo.' }, }, - create(context) {} + describe('property=description', 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 of no-foo.' }, }, + create(context) {} + }, }, - }, - configs: { - foo: {}, - recommended: { description: 'This config has the recommended rules...' }, - } - };`, - - 'README.md': `## Rules -## Configs - -`, + configs: { + foo: {}, + recommended: { description: 'This config has the recommended rules...' }, + } + };`, + + 'README.md': `## Rules + ## Configs + + `, + + 'docs/rules/no-foo.md': '', + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); - 'docs/rules/no-foo.md': '', + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); - // Needed for some of the test infrastructure to work. - node_modules: mockFs.load(PATH_NODE_MODULES), + it('generates the documentation', async function () { + await generate('.'); + expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); }); }); - afterEach(function () { - mockFs.restore(); - jest.resetModules(); + describe('property=meta.description', 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 of no-foo.' }, }, + create(context) {} + }, + }, + configs: { + foo: {}, + recommended: { meta: { description: 'This config has the recommended rules...' } }, + } + };`, + + 'README.md': `## Rules + ## Configs + + `, + + 'docs/rules/no-foo.md': '', + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); + + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); + + it('generates the documentation', async function () { + await generate('.'); + expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); + }); }); - it('generates the documentation', async function () { - await generate('.'); - expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); + describe('property=meta.docs.description', 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 of no-foo.' }, }, + create(context) {} + }, + }, + configs: { + foo: {}, + recommended: { meta: { docs: { description: 'This config has the recommended rules...' } } }, + } + };`, + + 'README.md': `## Rules + ## Configs + + `, + + 'docs/rules/no-foo.md': '', + + // Needed for some of the test infrastructure to work. + node_modules: mockFs.load(PATH_NODE_MODULES), + }); + }); + + afterEach(function () { + mockFs.restore(); + jest.resetModules(); + }); + + it('generates the documentation', async function () { + await generate('.'); + expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); + }); }); });