Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

eslint-doc-generator

Package Overview
Dependencies
Maintainers
1
Versions
62
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eslint-doc-generator - npm Package Compare versions

Comparing version 1.1.0 to 1.2.0

31

dist/lib/cli.js

@@ -86,3 +86,10 @@ import { Command, Argument, Option } from 'commander';

ruleListColumns: schemaStringArray,
ruleListSplit: { type: 'string' },
ruleListSplit:
/* 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.ruleListSplit === 'function'
? {
/* Functions are allowed but JSON Schema can't validate them so no-op in this case. */
}
: { anyOf: [{ type: 'string' }, schemaStringArray] },
urlConfigs: { type: 'string' },

@@ -107,13 +114,16 @@ urlRuleDoc: { type: 'string' },

// Additional validation that couldn't be handled by ajv.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
/* eslint-disable @typescript-eslint/no-unsafe-member-access -- disabled for same reason above */
if (config.postprocess && typeof config.postprocess !== 'function') {
throw new Error('postprocess must be a function');
throw new Error('postprocess must be a function.');
}
// Perform any normalization.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (typeof config.pathRuleList === 'string') {
config.pathRuleList = [config.pathRuleList]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
config.pathRuleList = [config.pathRuleList];
}
if (typeof config.ruleListSplit === 'string') {
config.ruleListSplit = [config.ruleListSplit];
}
return explorerResults.config;
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
return {};

@@ -139,3 +149,3 @@ }

.option('--path-rule-doc <path>', `(optional) Path to markdown file for each rule doc. Use \`{name}\` placeholder for the rule name. (default: ${OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC]})`)
.option('--path-rule-list <path>', `(optional) Path to markdown file where the rules table list should live. Option can be repeated. Defaults to ${OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]} if not provided.`, collect, [])
.option('--path-rule-list <path>', `(optional) Path to markdown file where the rules table list should live. Option can be repeated. Defaults to ${String(OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST])} if not provided.`, collect, [])
.option('--rule-doc-notices <notices>', `(optional) Ordered, comma-separated list of notices to display in rule doc. Non-applicable notices will be hidden. (choices: "${Object.values(NOTICE_TYPE).join('", "')}") (default: ${String(OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_NOTICES])})`, collectCSV, [])

@@ -147,3 +157,3 @@ .option('--rule-doc-section-exclude <section>', '(optional) Disallowed section in each rule doc (option can be repeated).', collect, [])

.option('--rule-list-columns <columns>', `(optional) Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. (choices: "${Object.values(COLUMN_TYPE).join('", "')})" (default: ${String(OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_COLUMNS])})`, collectCSV, [])
.option('--rule-list-split <property>', '(optional) Rule property to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`.')
.option('--rule-list-split <property>', '(optional) Rule property(s) to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. To specify a function, use a JavaScript-based config file.', collectCSV, [])
.option('--url-configs <url>', '(optional) Link to documentation about the ESLint configurations exported by the plugin.')

@@ -158,2 +168,9 @@ .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.')

const generateOptions = merge(configFileOptions, options); // Recursive merge.
// Options with both a CLI/config-file variant will lose the function value during the merge, so restore it here.
// TODO: figure out a better way to handle this.
/* istanbul ignore next -- TODO: haven't tested JavaScript config files yet https://github.com/bmish/eslint-doc-generator/issues/366 */
if (typeof configFileOptions.ruleListSplit === 'function') {
// @ts-expect-error -- The array is supposed to be read-only at this point.
generateOptions.ruleListSplit = configFileOptions.ruleListSplit;
}
// Invoke callback.

@@ -160,0 +177,0 @@ await cb(path, generateOptions);

@@ -51,2 +51,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';

}
function stringOrArrayToArrayWithFallback(stringOrArray, fallback) {
const asArray = stringOrArray instanceof Array // eslint-disable-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array.
? stringOrArray
: stringOrArray
? [stringOrArray]
: [];
const csvStringItem = asArray.find((item) => item.includes(','));
if (csvStringItem) {
throw new Error(`Provide property as array, not a CSV string: ${csvStringItem}`);
}
return asArray && asArray.length > 0 ? asArray : fallback;
}
// eslint-disable-next-line complexity

@@ -68,3 +80,3 @@ export async function generate(path, options) {

const pathRuleDoc = options?.pathRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_DOC];
const pathRuleList = stringOrArrayWithFallback(options?.pathRuleList, OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]);
const pathRuleList = stringOrArrayToArrayWithFallback(options?.pathRuleList, OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]);
const postprocess = options?.postprocess ?? OPTION_DEFAULTS[OPTION_TYPE.POSTPROCESS];

@@ -79,41 +91,36 @@ const ruleDocNotices = parseRuleDocNoticesOption(options?.ruleDocNotices);

const ruleListColumns = parseRuleListColumnsOption(options?.ruleListColumns);
const ruleListSplit = options?.ruleListSplit ?? OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT];
const ruleListSplit = typeof options?.ruleListSplit === 'function'
? options.ruleListSplit
: stringOrArrayToArrayWithFallback(options?.ruleListSplit, OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_SPLIT]);
const urlConfigs = options?.urlConfigs ?? OPTION_DEFAULTS[OPTION_TYPE.URL_CONFIGS];
const urlRuleDoc = options?.urlRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.URL_RULE_DOC];
// Gather details about rules.
const ruleDetails = Object.entries(plugin.rules)
.map(([name, rule]) => {
return typeof rule === 'object'
? // Object-style rule.
{
name,
description: rule.meta?.docs?.description,
fixable: rule.meta?.fixable
? ['code', 'whitespace'].includes(rule.meta.fixable)
: false,
hasSuggestions: rule.meta?.hasSuggestions ?? false,
requiresTypeChecking: rule.meta?.docs?.requiresTypeChecking ?? false,
deprecated: rule.meta?.deprecated ?? false,
schema: rule.meta?.schema,
type: rule.meta?.type,
}
: // Deprecated function-style rule (does not support most of these features).
{
name,
description: undefined,
fixable: false,
hasSuggestions: false,
requiresTypeChecking: false,
deprecated: false,
schema: [],
type: undefined,
};
// Gather normalized list of rules.
const ruleNamesAndRules = Object.entries(plugin.rules)
.map(([name, ruleModule]) => {
// Convert deprecated function-style rules to object-style rules so that we don't have to handle function-style rules everywhere throughout the codebase.
// @ts-expect-error -- this type unfortunately requires us to choose a `meta.type` even though the deprecated function-style rule won't have one.
const ruleModuleAsObject = typeof ruleModule === 'function'
? {
// Deprecated function-style rule don't support most of the properties that object-style rules support, so we'll just use the bare minimum.
meta: {
// @ts-expect-error -- type is missing for this property
schema: ruleModule.schema,
// @ts-expect-error -- type is missing for this property
deprecated: ruleModule.deprecated, // eslint-disable-line @typescript-eslint/no-unsafe-assignment -- type is missing for this property
},
create: ruleModule,
}
: ruleModule;
const tuple = [name, ruleModuleAsObject];
return tuple;
})
.filter(
// Filter out deprecated rules from being checked, displayed, or updated if the option is set.
(ruleDetails) => !ignoreDeprecatedRules || !ruleDetails.deprecated)
.sort(({ name: a }, { name: b }) => a.toLowerCase().localeCompare(b.toLowerCase()));
([, rule]) => !ignoreDeprecatedRules || !rule.meta.deprecated)
.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
// Update rule doc for each rule.
let initializedRuleDoc = false;
for (const { name, description, schema } of ruleDetails) {
for (const [name, rule] of ruleNamesAndRules) {
const schema = rule.meta?.schema;
const description = rule.meta?.docs?.description;
const pathToDoc = replaceRulePlaceholder(join(path, pathRuleDoc), name);

@@ -162,6 +169,3 @@ if (!existsSync(pathToDoc)) {

}
// eslint-disable-next-line unicorn/no-instanceof-array -- using Array.isArray() loses type information about the array.
for (const pathRuleListItem of pathRuleList instanceof Array
? pathRuleList
: [pathRuleList]) {
for (const pathRuleListItem of pathRuleList) {
// Find the exact filename.

@@ -174,3 +178,3 @@ const pathToFile = getPathWithExactFileNameCasing(join(path, pathRuleListItem));

const fileContents = readFileSync(pathToFile, 'utf8');
const fileContentsNew = await postprocess(updateRulesList(ruleDetails, fileContents, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathToFile, path, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc), resolve(pathToFile));
const fileContentsNew = await postprocess(updateRulesList(ruleNamesAndRules, fileContents, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathToFile, path, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc), resolve(pathToFile));
if (check) {

@@ -177,0 +181,0 @@ if (fileContentsNew !== fileContents) {

@@ -6,3 +6,3 @@ import { COLUMN_TYPE, NOTICE_TYPE } from './types.js';

*/
export declare function parseConfigEmojiOptions(plugin: Plugin, configEmoji?: readonly (readonly string[])[]): ConfigEmojis;
export declare function parseConfigEmojiOptions(plugin: Plugin, configEmoji?: readonly ([configName: string, emoji: string] | [configName: string])[]): ConfigEmojis;
/**

@@ -9,0 +9,0 @@ * Parse the option, check for errors, and set defaults.

@@ -15,3 +15,3 @@ import { COLUMN_TYPE, NOTICE_TYPE } from './types.js';

pathRuleDoc: string;
pathRuleList: string;
pathRuleList: string[];
postprocess: (content: string) => string;

@@ -24,5 +24,5 @@ ruleDocNotices: string[];

ruleListColumns: string[];
ruleListSplit: undefined;
ruleListSplit: never[];
urlConfigs: undefined;
urlRuleDoc: undefined;
};

@@ -40,3 +40,3 @@ import { join } from 'node:path';

[OPTION_TYPE.PATH_RULE_DOC]: join('docs', 'rules', '{name}.md'),
[OPTION_TYPE.PATH_RULE_LIST]: 'README.md',
[OPTION_TYPE.PATH_RULE_LIST]: ['README.md'],
[OPTION_TYPE.POSTPROCESS]: (content) => content,

@@ -53,5 +53,5 @@ [OPTION_TYPE.RULE_DOC_NOTICES]: Object.entries(NOTICE_TYPE_DEFAULT_PRESENCE_AND_ORDERING)

.map(([col]) => col),
[OPTION_TYPE.RULE_LIST_SPLIT]: undefined,
[OPTION_TYPE.RULE_LIST_SPLIT]: [],
[OPTION_TYPE.URL_CONFIGS]: undefined,
[OPTION_TYPE.URL_RULE_DOC]: undefined,
}; // Satisfies is used to ensure all options are included, but without losing type information.
import { COLUMN_TYPE } from './types.js';
import type { RuleDetails, ConfigsToRules, Plugin } from './types.js';
import type { ConfigsToRules, Plugin, RuleNamesAndRules } from './types.js';
/**

@@ -8,3 +8,3 @@ * An object containing the column header for each column (as a string or function to generate the string).

[key in COLUMN_TYPE]: string | ((data: {
ruleDetails: readonly RuleDetails[];
ruleNamesAndRules: RuleNamesAndRules;
}) => string);

@@ -16,2 +16,2 @@ };

*/
export declare function getColumns(plugin: Plugin, ruleDetails: readonly RuleDetails[], configsToRules: ConfigsToRules, ruleListColumns: readonly COLUMN_TYPE[], pluginPrefix: string, ignoreConfig: readonly string[]): Record<COLUMN_TYPE, boolean>;
export declare function getColumns(plugin: Plugin, ruleNamesAndRules: RuleNamesAndRules, configsToRules: ConfigsToRules, ruleListColumns: readonly COLUMN_TYPE[], pluginPrefix: string, ignoreConfig: readonly string[]): Record<COLUMN_TYPE, boolean>;

@@ -10,6 +10,6 @@ import { EMOJI_DEPRECATED, EMOJI_FIXABLE, EMOJI_HAS_SUGGESTIONS, EMOJI_REQUIRES_TYPE_CHECKING, EMOJI_TYPE, EMOJI_CONFIG_FROM_SEVERITY, EMOJI_OPTIONS, } from './emojis.js';

export const COLUMN_HEADER = {
[COLUMN_TYPE.NAME]: ({ ruleDetails }) => {
const ruleNames = ruleDetails.map((ruleDetail) => ruleDetail.name);
[COLUMN_TYPE.NAME]: ({ ruleNamesAndRules }) => {
const ruleNames = ruleNamesAndRules.map(([name]) => name);
const longestRuleNameLength = Math.max(...ruleNames.map(({ length }) => length));
const ruleDescriptions = ruleDetails.map((ruleDetail) => ruleDetail.description);
const ruleDescriptions = ruleNamesAndRules.map(([, rule]) => rule.meta?.docs?.description);
const longestRuleDescriptionLength = Math.max(...ruleDescriptions.map((description) => description ? description.length : 0));

@@ -43,3 +43,3 @@ const title = 'Name';

*/
export function getColumns(plugin, ruleDetails, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig) {
export function getColumns(plugin, ruleNamesAndRules, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig) {
const columns = {

@@ -50,12 +50,12 @@ // Alphabetical order.

[COLUMN_TYPE.CONFIGS_WARN]: getConfigsThatSetARule(plugin, configsToRules, pluginPrefix, ignoreConfig, SEVERITY_TYPE.warn).length > 0,
[COLUMN_TYPE.DEPRECATED]: ruleDetails.some((ruleDetail) => ruleDetail.deprecated),
[COLUMN_TYPE.DESCRIPTION]: ruleDetails.some((ruleDetail) => ruleDetail.description),
[COLUMN_TYPE.FIXABLE]: ruleDetails.some((ruleDetail) => ruleDetail.fixable),
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: ruleDetails.some((ruleDetail) => ruleDetail.fixable || ruleDetail.hasSuggestions),
[COLUMN_TYPE.HAS_SUGGESTIONS]: ruleDetails.some((ruleDetail) => ruleDetail.hasSuggestions),
[COLUMN_TYPE.DEPRECATED]: ruleNamesAndRules.some(([, rule]) => rule.meta?.deprecated),
[COLUMN_TYPE.DESCRIPTION]: ruleNamesAndRules.some(([, rule]) => rule.meta?.docs?.description),
[COLUMN_TYPE.FIXABLE]: ruleNamesAndRules.some(([, rule]) => rule.meta?.fixable),
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: ruleNamesAndRules.some(([, rule]) => rule.meta?.fixable || rule.meta?.hasSuggestions),
[COLUMN_TYPE.HAS_SUGGESTIONS]: ruleNamesAndRules.some(([, rule]) => rule.meta?.hasSuggestions),
[COLUMN_TYPE.NAME]: true,
[COLUMN_TYPE.OPTIONS]: ruleDetails.some((ruleDetail) => hasOptions(ruleDetail.schema)),
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: ruleDetails.some((ruleDetail) => ruleDetail.requiresTypeChecking),
[COLUMN_TYPE.OPTIONS]: ruleNamesAndRules.some(([, rule]) => hasOptions(rule.meta?.schema)),
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: ruleNamesAndRules.some(([, rule]) => rule.meta?.docs?.requiresTypeChecking),
// Show type column only if we found at least one rule with a standard type.
[COLUMN_TYPE.TYPE]: ruleDetails.some((ruleDetail) => ruleDetail.type && RULE_TYPES.includes(ruleDetail.type)),
[COLUMN_TYPE.TYPE]: ruleNamesAndRules.some(([, rule]) => rule.meta?.type && RULE_TYPES.includes(rule.meta?.type)),
};

@@ -62,0 +62,0 @@ // Recreate object using the ordering and presence of columns specified in ruleListColumns.

@@ -60,3 +60,3 @@ import { EMOJI_DEPRECATED, EMOJI_FIXABLE, EMOJI_HAS_SUGGESTIONS, EMOJI_OPTIONS, EMOJI_REQUIRES_TYPE_CHECKING, EMOJI_TYPE, EMOJI_CONFIG_FROM_SEVERITY, } from './emojis.js';

for (const ruleType of RULE_TYPES) {
const hasThisRuleType = Object.values(rules).some((rule) => typeof rule === 'object' && rule.meta.type === ruleType);
const hasThisRuleType = Object.values(rules).some((rule) => typeof rule === 'object' && rule.meta?.type === ruleType);
if (hasThisRuleType) {

@@ -63,0 +63,0 @@ if (!hasAnyRuleType) {

@@ -1,3 +0,3 @@

import { COLUMN_TYPE } from './types.js';
import type { Plugin, RuleDetails, ConfigsToRules, ConfigEmojis } from './types.js';
export declare function updateRulesList(ruleDetails: readonly RuleDetails[], markdown: string, plugin: Plugin, configsToRules: ConfigsToRules, pluginPrefix: string, pathRuleDoc: string, pathRuleList: string, pathPlugin: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], ruleListColumns: readonly COLUMN_TYPE[], ruleListSplit?: string, urlConfigs?: string, urlRuleDoc?: string): string;
import { COLUMN_TYPE, RuleListSplitFunction } from './types.js';
import type { Plugin, ConfigsToRules, ConfigEmojis, RuleNamesAndRules } from './types.js';
export declare function updateRulesList(ruleNamesAndRules: RuleNamesAndRules, markdown: string, plugin: Plugin, configsToRules: ConfigsToRules, pluginPrefix: string, pathRuleDoc: string, pathRuleList: string, pathPlugin: string, configEmojis: ConfigEmojis, ignoreConfig: readonly string[], ruleListColumns: readonly COLUMN_TYPE[], ruleListSplit: readonly string[] | RuleListSplitFunction, urlConfigs?: string, urlRuleDoc?: string): string;

@@ -9,3 +9,3 @@ import { BEGIN_RULE_LIST_MARKER, END_RULE_LIST_MARKER, } from './comment-markers.js';

import { relative } from 'node:path';
import { COLUMN_TYPE, SEVERITY_TYPE } from './types.js';
import { COLUMN_TYPE, SEVERITY_TYPE, } from './types.js';
import { markdownTable } from 'markdown-table';

@@ -19,2 +19,3 @@ import { EMOJIS_TYPE } from './rule-type.js';

import { boolean, isBooleanable } from 'boolean';
import Ajv from 'ajv';
function isBooleanableTrue(value) {

@@ -40,28 +41,28 @@ return isBooleanable(value) && boolean(value);

}
function getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, severityType) {
function getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, severityType) {
const configsToRulesWithoutIgnored = Object.fromEntries(Object.entries(configsToRules).filter(([configName]) => !ignoreConfig?.includes(configName)));
// Collect the emojis for the configs that set the rule to this severity level.
return getEmojisForConfigsSettingRuleToSeverity(rule.name, configsToRulesWithoutIgnored, pluginPrefix, configEmojis, severityType).join(' ');
return getEmojisForConfigsSettingRuleToSeverity(ruleName, configsToRulesWithoutIgnored, pluginPrefix, configEmojis, severityType).join(' ');
}
function buildRuleRow(columnsEnabled, rule, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) {
function buildRuleRow(ruleName, rule, columnsEnabled, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) {
const columns = {
// Alphabetical order.
[COLUMN_TYPE.CONFIGS_ERROR]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.error),
[COLUMN_TYPE.CONFIGS_OFF]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.off),
[COLUMN_TYPE.CONFIGS_WARN]: getConfigurationColumnValueForRule(rule, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.warn),
[COLUMN_TYPE.DEPRECATED]: rule.deprecated ? EMOJI_DEPRECATED : '',
[COLUMN_TYPE.DESCRIPTION]: rule.description || '',
[COLUMN_TYPE.FIXABLE]: rule.fixable ? EMOJI_FIXABLE : '',
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: `${rule.fixable ? EMOJI_FIXABLE : ''}${rule.hasSuggestions ? EMOJI_HAS_SUGGESTIONS : ''}`,
[COLUMN_TYPE.HAS_SUGGESTIONS]: rule.hasSuggestions
[COLUMN_TYPE.CONFIGS_ERROR]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.error),
[COLUMN_TYPE.CONFIGS_OFF]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.off),
[COLUMN_TYPE.CONFIGS_WARN]: getConfigurationColumnValueForRule(ruleName, configsToRules, pluginPrefix, configEmojis, ignoreConfig, SEVERITY_TYPE.warn),
[COLUMN_TYPE.DEPRECATED]: rule.meta?.deprecated ? EMOJI_DEPRECATED : '',
[COLUMN_TYPE.DESCRIPTION]: rule.meta?.docs?.description || '',
[COLUMN_TYPE.FIXABLE]: rule.meta?.fixable ? EMOJI_FIXABLE : '',
[COLUMN_TYPE.FIXABLE_AND_HAS_SUGGESTIONS]: `${rule.meta?.fixable ? EMOJI_FIXABLE : ''}${rule.meta?.hasSuggestions ? EMOJI_HAS_SUGGESTIONS : ''}`,
[COLUMN_TYPE.HAS_SUGGESTIONS]: rule.meta?.hasSuggestions
? EMOJI_HAS_SUGGESTIONS
: '',
[COLUMN_TYPE.NAME]() {
return getLinkToRule(rule.name, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, false, false, urlRuleDoc);
return getLinkToRule(ruleName, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, false, false, urlRuleDoc);
},
[COLUMN_TYPE.OPTIONS]: hasOptions(rule.schema) ? EMOJI_OPTIONS : '',
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: rule.requiresTypeChecking
[COLUMN_TYPE.OPTIONS]: hasOptions(rule.meta?.schema) ? EMOJI_OPTIONS : '',
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: rule.meta?.docs?.requiresTypeChecking
? EMOJI_REQUIRES_TYPE_CHECKING
: '',
[COLUMN_TYPE.TYPE]: rule.type ? EMOJIS_TYPE[rule.type] : '',
[COLUMN_TYPE.TYPE]: rule.meta?.type ? EMOJIS_TYPE[rule.meta?.type] : '',
};

@@ -80,3 +81,3 @@ // List columns using the ordering and presence of columns specified in columnsEnabled.

}
function generateRulesListMarkdown(columns, ruleDetails, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) {
function generateRulesListMarkdown(ruleNamesAndRules, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) {
const listHeaderRow = Object.entries(columns).flatMap(([columnType, enabled]) => {

@@ -89,3 +90,3 @@ if (!enabled) {

typeof headerStrOrFn === 'function'
? headerStrOrFn({ ruleDetails })
? headerStrOrFn({ ruleNamesAndRules })
: headerStrOrFn,

@@ -96,46 +97,77 @@ ];

listHeaderRow,
...ruleDetails.map((rule) => buildRuleRow(columns, rule, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)),
...ruleNamesAndRules.map(([name, rule]) => buildRuleRow(name, rule, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc)),
], { align: 'l' } // Left-align headers.
);
}
function generateRuleListMarkdownForRulesAndHeaders(rulesAndHeaders, headerLevel, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc) {
const parts = [];
for (const { title, rules } of rulesAndHeaders) {
if (title) {
parts.push(`${'#'.repeat(headerLevel)} ${title}`);
}
parts.push(generateRulesListMarkdown(rules, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc));
}
return parts.join('\n\n');
}
/**
* Generate multiple rule lists given the `ruleListSplit` property.
* Get the pairs of rules and headers for a given split property.
*/
function generateRulesListMarkdownWithRuleListSplit(columns, ruleDetails, plugin, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, ruleListSplit, headerLevel, urlRuleDoc) {
const values = new Set(ruleDetails.map((ruleDetail) => getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit)));
const valuesAll = [...values.values()];
if (values.size === 1 && isConsideredFalse(valuesAll[0])) {
throw new Error(`No rules found with --rule-list-split property "${ruleListSplit}".`);
function getRulesAndHeadersForSplit(ruleNamesAndRules, plugin, ruleListSplit) {
const rulesAndHeaders = [];
// Initially, all rules are unused.
let unusedRules = ruleNamesAndRules;
// Loop through each split property.
for (const ruleListSplitItem of ruleListSplit) {
// Store the rules and headers for this split property.
const rulesAndHeadersForThisSplit = [];
// Check what possible values this split property can have.
const valuesForThisPropertyFromUnusedRules = [
...new Set(unusedRules.map(([name]) => getPropertyFromRule(plugin, name, ruleListSplitItem))).values(),
];
const valuesForThisPropertyFromAllRules = [
...new Set(ruleNamesAndRules.map(([name]) => getPropertyFromRule(plugin, name, ruleListSplitItem))).values(),
];
// Throw an exception if there are no possible rules with this split property.
if (valuesForThisPropertyFromAllRules.length === 1 &&
isConsideredFalse(valuesForThisPropertyFromAllRules[0])) {
throw new Error(`No rules found with --rule-list-split property "${ruleListSplitItem}".`);
}
// For each possible non-disabled value, show a header and list of corresponding rules.
const valuesNotFalseAndNotTrue = valuesForThisPropertyFromUnusedRules.filter((val) => !isConsideredFalse(val) && !isBooleanableTrue(val));
const valuesTrue = valuesForThisPropertyFromUnusedRules.filter((val) => isBooleanableTrue(val));
const valuesNew = [
...valuesNotFalseAndNotTrue,
...(valuesTrue.length > 0 ? [true] : []), // If there are multiple true values, combine them all into one.
];
for (const value of valuesNew.sort((a, b) => String(a).toLowerCase().localeCompare(String(b).toLowerCase()))) {
// Rules with the property set to this value.
const rulesForThisValue = unusedRules.filter(([name]) => {
const property = getPropertyFromRule(plugin, name, ruleListSplitItem);
return (property === value || (value === true && isBooleanableTrue(property)));
});
// Turn ruleListSplit into a title.
// E.g. meta.docs.requiresTypeChecking to "Requires Type Checking".
const ruleListSplitParts = ruleListSplitItem.split('.');
const ruleListSplitFinalPart = ruleListSplitParts[ruleListSplitParts.length - 1];
const ruleListSplitTitle = noCase(ruleListSplitFinalPart, {
transform: (str) => capitalizeOnlyFirstLetter(str),
});
// Add a list for the rules with property set to this value.
rulesAndHeadersForThisSplit.push({
title: String(isBooleanableTrue(value) ? ruleListSplitTitle : value),
rules: rulesForThisValue,
});
// Remove these rules from the unused rules.
unusedRules = unusedRules.filter((rule) => !rulesForThisValue.includes(rule));
}
// Add the rules and headers for this split property to the beginning of the list of all rules and headers.
rulesAndHeaders.unshift(...rulesAndHeadersForThisSplit);
}
const parts = [];
// Show any rules that don't have a value for this rule-list-split property first, or for which the boolean property is off.
if (valuesAll.some((val) => isConsideredFalse(val))) {
const rulesForThisValue = ruleDetails.filter((ruleDetail) => isConsideredFalse(getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit)));
parts.push(generateRulesListMarkdown(columns, rulesForThisValue, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc));
// All remaining unused rules go at the beginning.
if (unusedRules.length > 0) {
rulesAndHeaders.unshift({ rules: unusedRules });
}
// For each possible non-disabled value, show a header and list of corresponding rules.
const valuesNotFalseAndNotTrue = valuesAll.filter((val) => !isConsideredFalse(val) && !isBooleanableTrue(val));
const valuesTrue = valuesAll.filter((val) => isBooleanableTrue(val));
const valuesNew = [
...valuesNotFalseAndNotTrue,
...(valuesTrue.length > 0 ? [true] : []), // If there are multiple true values, combine them all into one.
];
for (const value of valuesNew.sort((a, b) => String(a).toLowerCase().localeCompare(String(b).toLowerCase()))) {
const rulesForThisValue = ruleDetails.filter((ruleDetail) => {
const property = getPropertyFromRule(plugin, ruleDetail.name, ruleListSplit);
return (property === value || (value === true && isBooleanableTrue(property)));
});
// Turn ruleListSplit into a title.
// E.g. meta.docs.requiresTypeChecking to "Requires Type Checking".
const ruleListSplitParts = ruleListSplit.split('.');
const ruleListSplitFinalPart = ruleListSplitParts[ruleListSplitParts.length - 1];
const ruleListSplitTitle = noCase(ruleListSplitFinalPart, {
transform: (str) => capitalizeOnlyFirstLetter(str),
});
parts.push(`${'#'.repeat(headerLevel)} ${isBooleanableTrue(value) ? ruleListSplitTitle : value // eslint-disable-line @typescript-eslint/restrict-template-expressions -- TODO: better handling to ensure value is a string.
}`, generateRulesListMarkdown(columns, rulesForThisValue, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc));
}
return parts.join('\n\n');
return rulesAndHeaders;
}
export function updateRulesList(ruleDetails, markdown, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathRuleList, pathPlugin, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc) {
export function updateRulesList(ruleNamesAndRules, markdown, plugin, configsToRules, pluginPrefix, pathRuleDoc, pathRuleList, pathPlugin, configEmojis, ignoreConfig, ruleListColumns, ruleListSplit, urlConfigs, urlRuleDoc) {
let listStartIndex = markdown.indexOf(BEGIN_RULE_LIST_MARKER);

@@ -171,11 +203,62 @@ let listEndIndex = markdown.indexOf(END_RULE_LIST_MARKER);

// Determine columns to include in the rules list.
const columns = getColumns(plugin, ruleDetails, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig);
const columns = getColumns(plugin, ruleNamesAndRules, configsToRules, ruleListColumns, pluginPrefix, ignoreConfig);
// New legend.
const legend = generateLegend(columns, plugin, configsToRules, configEmojis, pluginPrefix, ignoreConfig, urlConfigs);
// Determine the pairs of rules and headers based on any split property.
const rulesAndHeaders = [];
if (typeof ruleListSplit === 'function') {
const userDefinedLists = ruleListSplit(ruleNamesAndRules);
// Schema for the user-defined lists.
const schema = {
// Array of rule lists.
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' },
rules: {
type: 'array',
items: {
type: 'array',
items: [
{ type: 'string' },
{ type: 'object' }, // The rule object (won't bother trying to validate deeper than this).
],
minItems: 2,
maxItems: 2,
},
minItems: 1,
uniqueItems: true,
},
},
required: ['rules'],
additionalProperties: false,
},
minItems: 1,
uniqueItems: true,
};
// Validate the user-defined lists.
const ajv = new Ajv();
const validate = ajv.compile(schema);
const valid = validate(userDefinedLists);
if (!valid) {
throw new Error(validate.errors
? ajv.errorsText(validate.errors, {
dataVar: 'ruleListSplit return value',
})
: /* istanbul ignore next -- this shouldn't happen */
'Invalid ruleListSplit return value');
}
rulesAndHeaders.push(...userDefinedLists);
}
else if (ruleListSplit.length > 0) {
rulesAndHeaders.push(...getRulesAndHeadersForSplit(ruleNamesAndRules, plugin, ruleListSplit));
}
else {
rulesAndHeaders.push({ rules: ruleNamesAndRules });
}
// New rule list.
const list = ruleListSplit
? generateRulesListMarkdownWithRuleListSplit(columns, ruleDetails, plugin, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, ruleListSplit, ruleListSplitHeaderLevel, urlRuleDoc)
: generateRulesListMarkdown(columns, ruleDetails, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc);
const list = generateRuleListMarkdownForRulesAndHeaders(rulesAndHeaders, ruleListSplitHeaderLevel, columns, configsToRules, pluginPrefix, pathPlugin, pathRuleDoc, pathRuleList, configEmojis, ignoreConfig, urlRuleDoc);
const newContent = `${legend ? `${legend}\n\n` : ''}${list}`;
return `${preList}${BEGIN_RULE_LIST_MARKER}\n\n${newContent}\n\n${END_RULE_LIST_MARKER}${postList}`;
}
import type { RuleDocTitleFormat } from './rule-doc-title-format.js';
import type { TSESLint, JSONSchema } from '@typescript-eslint/utils';
import type { RULE_TYPE } from './rule-type.js';
import type { TSESLint } from '@typescript-eslint/utils';
export type RuleModule = TSESLint.RuleModule<string, readonly unknown[]>;

@@ -21,15 +20,12 @@ export type Rules = TSESLint.Linter.RulesRecord;

export type ConfigsToRules = Record<string, Rules>;
export interface RuleDetails {
name: string;
description?: string;
fixable: boolean;
hasSuggestions: boolean;
requiresTypeChecking: boolean;
deprecated: boolean;
schema: JSONSchema.JSONSchema4;
type?: `${RULE_TYPE}`;
}
/**
* Some configs may have an emoji defined.
* List of rules in the form of tuples (rule name and the actual rule).
*/
export type RuleNamesAndRules = readonly (readonly [
name: string,
rule: RuleModule
])[];
/**
* The emoji for each config that has one after option parsing and defaults have been applied.
*/
export type ConfigEmojis = readonly {

@@ -92,2 +88,12 @@ config: string;

}
/**
* Function for splitting the rule list into multiple sections.
* Can be provided via a JavaScript-based config file using the `ruleListSplit` option.
* @param rules - all rules from the plugin
* @returns an array of sections, each with a title (optional) and list of rules
*/
export type RuleListSplitFunction = (rules: RuleNamesAndRules) => readonly {
title?: string;
rules: RuleNamesAndRules;
}[];
/** The type for the config file (e.g. `.eslint-doc-generatorrc.js`) and internal `generate()` function. */

@@ -103,3 +109,3 @@ export type GenerateOptions = {

*/
readonly configEmoji?: readonly (readonly string[])[];
readonly configEmoji?: readonly ([configName: string, emoji: string] | [configName: string])[];
/** Configs to ignore from being displayed. Often used for an `all` config. */

@@ -118,3 +124,3 @@ readonly ignoreConfig?: readonly string[];

* Useful for applying custom transformations such as formatting with tools like prettier.
* Only available via a JavaScript config file.
* Only available via a JavaScript-based config file.
*/

@@ -125,3 +131,3 @@ readonly postprocess?: (content: string, pathToFile: string) => string | Promise<string>;

* 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).
* Choices: `configs`, `deprecated`, `description` (off by default), `fixable` (off by default), `fixableAndHasSuggestions`, `hasSuggestions` (off by default), `options` (off by default), `requiresTypeChecking`, `type` (off by default).
* Default: `['deprecated', 'configs', 'fixableAndHasSuggestions', 'requiresTypeChecking']`.

@@ -146,7 +152,7 @@ */

/**
* Rule property to split the rules list by.
* Rule property(s) or function to split the rules list by.
* A separate list and header will be created for each value.
* Example: `meta.type`.
*/
readonly ruleListSplit?: string;
readonly ruleListSplit?: string | readonly string[] | RuleListSplitFunction;
/** Link to documentation about the ESLint configurations exported by the plugin. */

@@ -153,0 +159,0 @@ readonly urlConfigs?: string;

{
"name": "eslint-doc-generator",
"version": "1.1.0",
"version": "1.2.0",
"description": "Automatic documentation generator for ESLint plugins and rules.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -145,3 +145,3 @@ # eslint-doc-generator<!-- omit from toc -->

| `--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 to split the rules list by. A separate list and header will be created for each value. Example: `meta.type`. |
| `--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. |

@@ -191,4 +191,7 @@ | `--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. |

Using a JavaScript-based config file also allows you to provide a `postprocess` function to be called with the generated content and file path for each processed file. This is useful for applying custom transformations such as formatting with tools like prettier (see [prettier example](#prettier)).
Some options are exclusive to a JavaScript-based config file:
- `postprocess` - A function-only option useful for applying custom transformations such as formatting with tools like prettier. See [prettier example](#prettier).
- [`ruleListSplit`](#configuration-options) with a function - This is useful for customizing the grouping of rules into lists.
Example `.eslint-doc-generatorrc.js`:

@@ -205,2 +208,28 @@

Example `.eslint-doc-generatorrc.js` with `ruleListSplit` function:
```js
/** @type {import('eslint-doc-generator').GenerateOptions} */
const config = {
ruleListSplit(rules) {
return [
{
// No header for this list.
rules: rules.filter(([name, rule]) => !rule.meta.someProp),
},
{
title: 'Foo',
rules: rules.filter(([name, rule]) => rule.meta.someProp === 'foo'),
},
{
title: 'Bar',
rules: rules.filter(([name, rule]) => rule.meta.someProp === 'bar'),
},
];
},
};
module.exports = config;
```
### Badges

@@ -207,0 +236,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc