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

docs: update rules table during build #274

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ export default [

### Rules

| **Rule Name** | **Description** |
|---------------|-----------------|
| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Enforce fenced code blocks to specify a language. |
| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. |
| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document. |
| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. |
| [`no-html`](./docs/rules/no-html.md) | Enforce fenced code blocks to specify a language. |
| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. |
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. |
<!-- NOTE: The following table is autogenerated. Do not manually edit. -->

<!-- Rule Table Start -->
| **Rule Name** | **Description** | **Recommended** |
| :- | :- | :-: |
| [`fenced-code-language`](./docs/rules/fenced-code-language.md) | Require languages for fenced code blocks. | yes |
| [`heading-increment`](./docs/rules/heading-increment.md) | Enforce heading levels increment by one. | yes |
| [`no-duplicate-headings`](./docs/rules/no-duplicate-headings.md) | Disallow duplicate headings in the same document. | no |
| [`no-empty-links`](./docs/rules/no-empty-links.md) | Disallow empty links. | yes |
| [`no-html`](./docs/rules/no-html.md) | Disallow HTML tags. | no |
| [`no-invalid-label-refs`](./docs/rules/no-invalid-label-refs.md) | Disallow invalid label references. | yes |
| [`no-missing-label-refs`](./docs/rules/no-missing-label-refs.md) | Disallow missing label references. | yes |
<!-- Rule Table End -->

**Note:** This plugin does not provide formatting rules. We recommend using a source code formatter such as [Prettier](https://prettier.io) for that purpose.

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"eslint --fix",
"prettier --write"
],
"!(*.{js,md})": "prettier --write --ignore-unknown"
"!(*.{js,md})": "prettier --write --ignore-unknown",
"{src/rules/*.js,tools/update-rules-docs.js,README.md}": "npm run build:update-rules-docs"
},
"scripts": {
"lint": "eslint . && eslint -c eslint.config-content.js .",
Expand All @@ -50,7 +51,8 @@
"fmt:check": "prettier --check .",
"build:dedupe-types": "node tools/dedupe-types.js dist/esm/index.js",
"build:rules": "node tools/build-rules.js",
"build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json",
"build:update-rules-docs": "node tools/update-rules-docs.js",
"build": "npm run build:rules && rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:update-rules-docs",
"prepare": "node ./npm-prepare.cjs && npm run build",
"test": "c8 mocha \"tests/**/*.test.js\" --timeout 30000"
},
Expand Down
103 changes: 103 additions & 0 deletions tools/update-rules-docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* @fileoverview Updates the rules table in README.md with rule names,
* descriptions, and whether the rules are recommended or not.
*
* Usage:
* node tools/update-rules-docs.js
*
* @author Francesco Trotta
*/

//-----------------------------------------------------------------------------
// Imports
//-----------------------------------------------------------------------------

import { fromMarkdown } from "mdast-util-from-markdown";
import fs from "node:fs/promises";
import path from "node:path";

//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------

/** @typedef {import("eslint").AST.Range} Range */

//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const docsFileURL = new URL("../README.md", import.meta.url);
const rulesDirURL = new URL("../src/rules/", import.meta.url);

/**
* Formats a table row from a rule filename.
* @param {string} ruleFilename The filename of the rule module without directory.
* @returns {Promise<string>} The formatted markdown text of the table row.
*/
async function formatTableRowFromFilename(ruleFilename) {
const ruleURL = new URL(ruleFilename, rulesDirURL);
const { default: rule } = await import(ruleURL);
const ruleName = path.parse(ruleFilename).name;
const docs = rule?.meta?.docs;
const ruleLink = `[\`${ruleName}\`](./docs/rules/${ruleName}.md)`;
const description = docs?.description || "_no description_";
const recommendedText = docs?.recommended ? "yes" : "no";

return `| ${ruleLink} | ${description} | ${recommendedText} |`;
}

/**
* Generates the markdown text for the rules table.
* @returns {Promise<string>} The formatted markdown text of the rules table.
*/
async function createRulesTableText() {
const filenames = await fs.readdir(rulesDirURL);
const ruleFilenames = filenames.filter(
filename => path.extname(filename) === ".js",
);
const text = [
"| **Rule Name** | **Description** | **Recommended** |",
"| :- | :- | :-: |",
...(await Promise.all(ruleFilenames.map(formatTableRowFromFilename))),
].join("\n");

return text;
}

/**
* Returns start and end offset of the rules table as indicated by "Rule Table Start" and
* "Rule Table End" HTML comments in the markdown text.
* @param {string} text The markdown text.
* @returns {Range | null} The offset range of the rules table, or `null`.
*/
function getRulesTableRange(text) {
const tree = fromMarkdown(text);
const htmlNodes = tree.children.filter(({ type }) => type === "html");
const startComment = htmlNodes.find(
({ value }) => value === "<!-- Rule Table Start -->",
);
const endComment = htmlNodes.find(
({ value }) => value === "<!-- Rule Table End -->",
);

return startComment && endComment
? [startComment.position.end.offset, endComment.position.start.offset]
: null;
}

//-----------------------------------------------------------------------------
// Main
//-----------------------------------------------------------------------------

let docsText = await fs.readFile(docsFileURL, "utf-8");
const rulesTableRange = getRulesTableRange(docsText);

if (!rulesTableRange) {
throw Error("Rule Table Start/End comments not found, unable to update.");
}

const tableText = await createRulesTableText();

docsText = `${docsText.slice(0, rulesTableRange[0])}\n${tableText}\n${docsText.slice(rulesTableRange[1])}`;

await fs.writeFile(docsFileURL, docsText);