Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Oct 1, 2022
1 parent 40e23ce commit ffbcf1c
Show file tree
Hide file tree
Showing 21 changed files with 10,231 additions and 3,369 deletions.
13 changes: 13 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# unconventional js
/vendor/

# compiled output
/dist/
/tmp/

# dependencies
/bower_components/
/node_modules/

# misc
/coverage/
7 changes: 6 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
'plugin:square/typescript',
'plugin:node/recommended',
'plugin:unicorn/recommended', // Turn eslint-plugin-unicorn recommended rules on again because many were turned off by eslint-plugin-square.
'plugin:jest/recommended',
],
env: {
node: true,
Expand All @@ -19,6 +20,10 @@ module.exports = {
},
rules: {
'import/extensions': ['error', 'always'],
'node/no-missing-import': 'off', // Disabled due to a bug: https://github.com/mysticatea/eslint-plugin-node/issues/342
'unicorn/no-array-reduce': 'off',
'unicorn/no-nested-ternary': 'off',
'unicorn/prevent-abbreviations': 'off',
},
overrides: [
{
Expand All @@ -28,7 +33,7 @@ module.exports = {
rules: {
'node/no-unsupported-features/es-syntax': [
'error',
{ ignores: ['modules'] },
{ ignores: ['dynamicImport', 'modules'] },
],
},
},
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:

runs-on: ${{ matrix.os }}-latest

strategy:
matrix:
os: [ ubuntu, windows ]
node-version: [14.x, 16.x, 18.x]

steps:
- uses: actions/checkout@v3

- name: Use Node.js version ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run linters
run: npm run lint

- name: Run tests
run: npm test
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,102 @@
# eslint-doc-generator

[![npm version][npm-image]][npm-url]

Generates the following documentation:

- README rules table
- Rule doc titles and notices

Also performs some basic section consistency checks on rule docs (will eventually be configurable):

- Contains an `## Options` section and mentions each named option (for rules with options)

## Setup

Install it:

```sh
npm run --save-dev eslint-doc-generator
```

Add it as as script in `package.json` (included as a lint script to demonstrate how we can ensure it passes on CI along with other linting):

```json
{
"scripts": {
"lint": "npm-run-all \"lint:*\"",
"lint:docs": "markdownlint \"**/*.md\"",
"lint:eslint-docs": "npm-run-all update:docs && git diff --exit-code",
"lint:js": "eslint .",
"update:eslint-docs": "eslint-doc-generator"
}
}
```

Add the rule list markers in your `README.md` rules section:

```md
<!-- begin rules list -->
<!-- end rules list -->
```

The new title and notices will be added to the top of each rule doc, but you may need to manually remove the old ones.

## Usage

```sh
npm run update:eslint-docs
```

[npm-image]: https://badge.fury.io/js/eslint-doc-generator.svg
[npm-url]: https://www.npmjs.com/package/eslint-doc-generator

## Example

Generated content in a rule doc:

```md
# Disallow use of `foo` (`no-foo`)

💼 This rule is enabled in the following configs: `all`, `recommended`.

🔧 This rule is automatically fixable using the `--fix` [option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) on the command line.

💡 This rule provides [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) that can be applied manually.

❌ This rule is deprecated. It was replaced by [some-new-rule](some-new-rule.md).

<!-- end rule header -->

...
```

Generated rules table in `README.md`:

```md
# eslint-plugin-test

## Rules

✅: Enabled in the `recommended` configuration.\
🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).\
💡: Provides editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\
💭: Requires type information.\
❌: This rule is deprecated.

<!-- begin rules list -->

| Rule | Description | 💼 | 🔧 | 💡 | 💭 |
| -------------------------------------------------------------- | ------------------------------------------------- | ------------- | --- | --- | --- |
| [max-nested-describe](docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | | | |
| [no-alias-methods](docs/rules/no-alias-methods.md) | Disallow alias methods | ✅ ![style][] | 🔧 | | |
| [no-commented-out-tests](docs/rules/no-commented-out-tests.md) | Disallow commented out tests | ✅ | | | |

<!-- end rules list -->

...

<!-- define the badge for any custom configs (besides `recommended`, `all`) here -->

[style]: https://img.shields.io/badge/-style-blue.svg
```
10 changes: 10 additions & 0 deletions bin/eslint-doc-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { run } from '../lib/cli.js';

try {
run();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
process.exitCode = 1;
}
26 changes: 26 additions & 0 deletions jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// https://kulshekhar.github.io/ts-jest/docs/guides/esm-support/

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
testMatch: ['<rootDir>/test/**/*-test.ts'],
globals: {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'#(.*)': '<rootDir>/node_modules/$1',
},
coveragePathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/test/'],
coverageThreshold: {
global: {
branches: 100,
functions: 96.77, // TODO: Should be 100% but unclear what function is missing coverage.
lines: 100,
statements: 100,
},
},
};
34 changes: 34 additions & 0 deletions lib/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Command, Argument } from 'commander';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { generate } from './generator.js';
import type { PackageJson } from 'type-fest';

const __dirname = dirname(fileURLToPath(import.meta.url));

function getCurrentPackageVersion(): string {
const packageJson: PackageJson = JSON.parse(
readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8') // Relative to compiled version of this file in the dist folder.
);
if (!packageJson.version) {
throw new Error('Could not find package.json `version`.');
}
return packageJson.version;
}

export function run() {
const program = new Command();

program
.version(getCurrentPackageVersion())
.addArgument(
new Argument('[path]', 'path to ESLint plugin root').default('.')
)
.action(async function (path) {
await generate(path);
})
.parse(process.argv);

return program;
}
124 changes: 124 additions & 0 deletions lib/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import prettier from 'prettier'; // eslint-disable-line node/no-extraneous-import -- prettier is included by eslint-plugin-square
import { getAllNamedOptions, hasOptions } from './rule-options.js';
import {
loadPlugin,
getPluginPrefix,
getPluginPrettierConfig,
} from './package-json.js';
import { updateRulesList } from './rule-list.js';
import { generateRuleHeaderLines } from './rule-notices.js';
import { END_RULE_HEADER_MARKER } from './markers.js';
import type { RuleModule, RuleDetails } from './types.js';

function format(str: string, pluginPath: string): string {
return prettier.format(str, {
...getPluginPrettierConfig(pluginPath),
parser: 'markdown',
});
}

/**
* Replace the header of a doc up to and including the specified marker.
* Insert at beginning if header doesn't exist.
* @param lines - lines of doc
* @param newHeaderLines - lines of new header including marker
* @param marker - marker to indicate end of header
*/
function replaceOrCreateHeader(
lines: string[],
newHeaderLines: string[],
marker: string
) {
const markerLineIndex = lines.indexOf(marker);

// Replace header section (or create at top if missing).
lines.splice(0, markerLineIndex + 1, ...newHeaderLines);
}

/**
* Ensure a rule doc contains (or doesn't contain) some particular content.
* Upon failure, output the failure and set a failure exit code.
* @param ruleName - which rule we are checking
* @param contents - the rule doc's contents
* @param content - the content we are checking for
* @param expected - whether the content should be present or not present
*/
function expectContent(
ruleName: string,
contents: string,
content: string,
expected: boolean
) {
if (contents.includes(content) !== expected) {
console.error(
`\`${ruleName}\` rule doc should ${
expected ? '' : 'not '
}have included: ${content}`
);
process.exitCode = 1;
}
}

export async function generate(path: string) {
const plugin = await loadPlugin(path);
const pluginPrefix = getPluginPrefix(path);

const pathTo = {
readme: resolve(path, 'README.md'),
rules: resolve(path, 'src', 'rules'),
docs: resolve(path, 'docs'),
};

// Gather details about rules.
const details: RuleDetails[] = Object.entries(plugin.rules)
.filter((nameAndRule): nameAndRule is [string, Required<RuleModule>] =>
Boolean(nameAndRule[1].meta)
)
.map(
([name, rule]): RuleDetails => ({
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,
})
);

// Update rule doc for each rule.
for (const { name, description, schema } of details) {
const pathToDoc = join(pathTo.docs, 'rules', `${name}.md`);
const contents = readFileSync(pathToDoc).toString();
const lines = contents.split('\n');

// Regenerate the header (title/notices) of each rule doc.
const newHeaderLines = generateRuleHeaderLines(
description,
name,
plugin,
pluginPrefix
);

replaceOrCreateHeader(lines, newHeaderLines, END_RULE_HEADER_MARKER);

writeFileSync(pathToDoc, format(lines.join('\n'), path));

// Check for potential issues with the rule doc.

// "Options" section.
expectContent(name, contents, '## Options', hasOptions(schema));
for (const namedOption of getAllNamedOptions(schema)) {
expectContent(name, contents, namedOption, true); // Each rule option is mentioned.
}
}

// Update the rules list in the README.
let readme = readFileSync(pathTo.readme, 'utf8');
readme = updateRulesList(details, readme, plugin, pluginPrefix);
writeFileSync(pathTo.readme, format(readme, path), 'utf8');
}
1 change: 0 additions & 1 deletion lib/index.ts

This file was deleted.

6 changes: 6 additions & 0 deletions lib/markers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Markers so that the README rules list can be automatically updated.
export const BEGIN_RULE_LIST_MARKER = '<!-- begin rules list -->';
export const END_RULE_LIST_MARKER = '<!-- end rules list -->';

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

0 comments on commit ffbcf1c

Please sign in to comment.