Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option --init-rule-docs #240

Merged
merged 10 commits into from Nov 18, 2022
Merged
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -124,6 +124,7 @@ There's also an optional path argument if you need to point the CLI to an ESLint
| `--config-emoji` | Custom emoji to use for a config. Format is `config-name,emoji`. Default emojis are provided for [common configs](./lib/emojis.ts). To remove a default emoji and rely on a [badge](#badge) instead, provide the config name without an emoji. Option can be repeated. |
| `--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 (default: `false`). |
| `--init-rule-docs` | Whether to create rule doc files if they don't yet exist (default: `false`). |
| `--path-rule-doc` | Path to markdown file for each rule doc. Use `{name}` placeholder for the rule name (default: `docs/rules/{name}.md`). |
| `--path-rule-list` | Path to markdown file with a rules section where the rules table list should live (default: `README.md`). |
| `--rule-doc-notices` | Ordered, comma-separated list of notices to display in rule doc. Non-applicable notices will be hidden. Choices: `configs`, `deprecated`, `fixable` (off by default), `fixableAndHasSuggestions`, `hasSuggestions` (off by default), `options` (off by default), `requiresTypeChecking`, `type` (off by default). Default: `deprecated,configs,fixableAndHasSuggestions,requiresTypeChecking`. |
Expand Down
8 changes: 8 additions & 0 deletions lib/cli.ts
Expand Up @@ -56,6 +56,7 @@ async function loadConfigFileOptions() {
configEmoji: schemaStringArray,
ignoreConfig: schemaStringArray,
ignoreDeprecatedRules: { type: 'boolean' },
initRuleDocs: { type: 'boolean' },
pathRuleDoc: { type: 'string' },
pathRuleList: { type: 'string' },
ruleDocNotices: { type: 'string' },
Expand Down Expand Up @@ -133,6 +134,13 @@ export async function run(
})`,
parseBoolean
)
.option(
'--init-rule-docs [boolean]',
`(optional) Whether to create rule doc files if they don't yet exist. (default: ${
OPTION_DEFAULTS[OPTION_TYPE.INIT_RULE_DOCS]
})`,
parseBoolean
)
.option(
'--path-rule-doc <path>',
`(optional) Path to markdown file for each rule doc. Use \`{name}\` placeholder for the rule name. (default: ${
Expand Down
26 changes: 21 additions & 5 deletions lib/generator.ts
@@ -1,5 +1,5 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { getAllNamedOptions, hasOptions } from './rule-options.js';
import {
loadPlugin,
Expand Down Expand Up @@ -100,6 +100,8 @@ export async function generate(path: string, options?: GenerateOptions) {
const ignoreDeprecatedRules =
options?.ignoreDeprecatedRules ??
OPTION_DEFAULTS[OPTION_TYPE.IGNORE_DEPRECATED_RULES];
const initRuleDocs =
options?.initRuleDocs ?? OPTION_DEFAULTS[OPTION_TYPE.INIT_RULE_DOCS];
const pathRuleDoc =
options?.pathRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC];
const pathRuleList =
Expand Down Expand Up @@ -157,14 +159,22 @@ export async function generate(path: string, options?: GenerateOptions) {
(details) => !ignoreDeprecatedRules || !details.deprecated
);

let initializedRuleDoc = false;

// Update rule doc for each rule.
for (const { name, description, schema } of details) {
const pathToDoc = join(path, pathRuleDoc).replace(/{name}/g, name);

if (!existsSync(pathToDoc)) {
throw new Error(
`Could not find rule doc: ${relative(getPluginRoot(path), pathToDoc)}`
);
if (!initRuleDocs) {
throw new Error(
`Could not find rule doc: ${relative(getPluginRoot(path), pathToDoc)}`
);
}

mkdirSync(dirname(pathToDoc), { recursive: true });
writeFileSync(pathToDoc, '');
initializedRuleDoc = true;
}

// Regenerate the header (title/notices) of each rule doc.
Expand Down Expand Up @@ -267,4 +277,10 @@ export async function generate(path: string, options?: GenerateOptions) {
} else {
writeFileSync(pathToReadme, readmeContentsNew, 'utf8');
}

if (initRuleDocs && !initializedRuleDoc) {
throw new Error(
'--init-rule-docs was enabled, but no rule doc file needed to be created.'
);
}
}
3 changes: 3 additions & 0 deletions lib/options.ts
Expand Up @@ -40,6 +40,7 @@ export enum OPTION_TYPE {
CONFIG_EMOJI = 'configEmoji',
IGNORE_CONFIG = 'ignoreConfig',
IGNORE_DEPRECATED_RULES = 'ignoreDeprecatedRules',
INIT_RULE_DOCS = 'initRuleDocs',
PATH_RULE_DOC = 'pathRuleDoc',
PATH_RULE_LIST = 'pathRuleList',
RULE_DOC_NOTICES = 'ruleDocNotices',
Expand All @@ -61,6 +62,7 @@ export const OPTION_DEFAULTS = {
[OPTION_TYPE.CONFIG_EMOJI]: [],
[OPTION_TYPE.IGNORE_CONFIG]: [],
[OPTION_TYPE.IGNORE_DEPRECATED_RULES]: false,
[OPTION_TYPE.INIT_RULE_DOCS]: false,
[OPTION_TYPE.PATH_RULE_DOC]: 'docs/rules/{name}.md',
[OPTION_TYPE.PATH_RULE_LIST]: 'README.md',
[OPTION_TYPE.RULE_DOC_NOTICES]: Object.entries(
Expand Down Expand Up @@ -88,6 +90,7 @@ export type GenerateOptions = {
configEmoji?: string[];
ignoreConfig?: string[];
ignoreDeprecatedRules?: boolean;
initRuleDocs?: boolean;
pathRuleDoc?: string;
pathRuleList?: string;
ruleDocNotices?: string;
Expand Down
3 changes: 3 additions & 0 deletions test/lib/__snapshots__/cli-test.ts.snap
Expand Up @@ -16,6 +16,7 @@ exports[`cli all CLI options and all config files options merges correctly, with
"ignoredConfigFromCli2",
],
"ignoreDeprecatedRules": true,
"initRuleDocs": false,
"pathRuleDoc": "www.example.com/rule-doc-from-cli",
"pathRuleList": "www.example.com/rule-list-from-cli",
"ruleDocNotices": "type",
Expand Down Expand Up @@ -53,6 +54,7 @@ exports[`cli all CLI options, no config file options is called correctly 1`] = `
"ignoredConfigFromCli2",
],
"ignoreDeprecatedRules": true,
"initRuleDocs": false,
"pathRuleDoc": "www.example.com/rule-doc-from-cli",
"pathRuleList": "www.example.com/rule-list-from-cli",
"ruleDocNotices": "type",
Expand Down Expand Up @@ -86,6 +88,7 @@ exports[`cli all config files options, no CLI options is called correctly 1`] =
"ignoredConfigFromConfigFile2",
],
"ignoreDeprecatedRules": true,
"initRuleDocs": true,
"pathRuleDoc": "www.example.com/rule-doc-from-config-file",
"pathRuleList": "www.example.com/rule-list-from-config-file",
"ruleDocNotices": "type",
Expand Down
3 changes: 3 additions & 0 deletions test/lib/cli-test.ts
Expand Up @@ -11,6 +11,7 @@ const configFileOptionsAll: { [key in OPTION_TYPE]: unknown } = {
'ignoredConfigFromConfigFile2',
],
ignoreDeprecatedRules: true,
initRuleDocs: true,
pathRuleDoc: 'www.example.com/rule-doc-from-config-file',
pathRuleList: 'www.example.com/rule-list-from-config-file',
ruleDocNotices: 'type',
Expand Down Expand Up @@ -43,6 +44,8 @@ const cliOptionsAll: { [key in OPTION_TYPE]: string[] } = {

[OPTION_TYPE.IGNORE_DEPRECATED_RULES]: ['--ignore-deprecated-rules', 'true'],

[OPTION_TYPE.INIT_RULE_DOCS]: ['--init-rule-docs', 'false'],

[OPTION_TYPE.PATH_RULE_DOC]: [
'--path-rule-doc',
'www.example.com/rule-doc-from-cli',
Expand Down
7 changes: 7 additions & 0 deletions test/lib/generate/__snapshots__/file-paths-test.ts.snap
Expand Up @@ -26,3 +26,10 @@ exports[`generate (file paths) lowercase README file generates the documentation

<!-- end auto-generated rules list -->"
`;

exports[`generate (file paths) missing rule doc when initRuleDocs is true creates the rule doc 1`] = `
"# test/no-foo

<!-- end auto-generated rule header -->
"
`;
72 changes: 61 additions & 11 deletions test/lib/generate/file-paths-test.ts
Expand Up @@ -20,14 +20,65 @@ describe('generate (file paths)', function () {
}),

'index.js': `
export default {
rules: {
'no-foo': {
meta: { },
create(context) {}
},
},
};`,
export default {
rules: {
'no-foo': {
meta: { },
create(context) {}
},
},
};`,

'README.md':
'<!-- begin auto-generated rules list --><!-- end auto-generated rules list -->',

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

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

describe('when initRuleDocs is false', () => {
it('throws an error', async function () {
// Use join to handle both Windows and Unix paths.
await expect(generate('.')).rejects.toThrow(
`Could not find rule doc: ${join('docs', 'rules', 'no-foo.md')}`
);
});
});

describe('when initRuleDocs is true', () => {
it('creates the rule doc', async function () {
await generate('.', { initRuleDocs: true });
expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot();
});
});
});

describe('no missing rule doc but --init-rule-docs enabled', function () {
beforeEach(function () {
mockFs({
'docs/rules/no-foo.md': '',

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

'index.js': `
export default {
rules: {
'no-foo': {
meta: { },
create(context) {}
},
},
};`,

'README.md':
'<!-- begin auto-generated rules list --><!-- end auto-generated rules list -->',
Expand All @@ -43,9 +94,8 @@ describe('generate (file paths)', function () {
});

it('throws an error', async function () {
// Use join to handle both Windows and Unix paths.
await expect(generate('.')).rejects.toThrow(
`Could not find rule doc: ${join('docs', 'rules', 'no-foo.md')}`
await expect(generate('.', { initRuleDocs: true })).rejects.toThrow(
'--init-rule-docs was enabled, but no rule doc file needed to be created.'
);
});
});
Expand Down