Skip to content

Commit db90a41

Browse files
authored
chore: add doc-renderer for generating & updating detailed rule docs (#442)
### Problems: - The new rule document (generated by `npm run new`) was unclear about which part is the header and which part is the body. - In `tools/update-docs.ts`, when generating notes and adding them to the header section: - The process is too much like cutting and pasting manually on the string, it reduces the readability - The code for producing the new header (with notes included) is quite fragmented and long - Includes legacy/redundant code `header.replace(/\$/g, "$$$$")` - Inefficient code, it creates two new items for the notes then `join()` it with `'\n'` later to create the trailing `'\n\n'` ```ts if (notes.length >= 1) { notes.push("", "") } ... const header = `\n${title}\n\n${notes.join("\n")}` ``` ### This PR: - Create renderRuleHeader function and a RuleDocHeader type to clarify the structure of the header. Then applies this to both the generation and update tools for the detailed rule Markdown files ```ts type RuleDocHeader = { ruleId: string description: string notes: string[] } const header = renderRuleHeader({ ruleId, description, notes }) ``` - Make it clear there are `\n\n` between each part of the header in the `renderRuleHeader` function
1 parent ae7c6a4 commit db90a41

File tree

3 files changed

+109
-83
lines changed

3 files changed

+109
-83
lines changed

tools/lib/doc-renderer.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { RuleModule } from "src/types"
2+
3+
type RuleDocHeader = {
4+
ruleId: string
5+
description: string
6+
notes: string[]
7+
}
8+
/**
9+
* Render the header of the doc file of a rule.
10+
*
11+
* example header:
12+
* ```
13+
* # astro/no-unused-vars
14+
*
15+
* > description
16+
*
17+
* - note1
18+
* - note2
19+
*
20+
* ```
21+
* Note that there are newlines between the parts of the header
22+
* and there is a trailing newline at the end.
23+
*/
24+
export function renderRuleHeader({
25+
ruleId,
26+
description,
27+
notes,
28+
}: RuleDocHeader): string {
29+
const hasNotes = notes.length > 0
30+
const notesStr = hasNotes ? `${notes.join("\n")}\n\n` : ""
31+
return `# ${ruleId}\n\n> ${description}\n\n${notesStr}`
32+
}
33+
34+
//eslint-disable-next-line jsdoc/require-jsdoc -- tools
35+
function formatItems(items: string[]) {
36+
if (items.length <= 2) {
37+
return items.join(" and ")
38+
}
39+
return `all of ${items.slice(0, -1).join(", ")} and ${
40+
items[items.length - 1]
41+
}`
42+
}
43+
44+
/**
45+
* Build notes from a rule for rendering the header of the doc file.
46+
*/
47+
export function buildNotesFromRule(rule: RuleModule, isNew: boolean): string[] {
48+
const {
49+
meta: {
50+
fixable,
51+
hasSuggestions,
52+
deprecated,
53+
replacedBy,
54+
docs: { recommended },
55+
},
56+
} = rule
57+
const notes = []
58+
59+
if (deprecated) {
60+
if (replacedBy) {
61+
const replacedRules = replacedBy.map(
62+
(name) => `[astro/${name}](${name}.md) rule`,
63+
)
64+
notes.push(
65+
`- ⚠️ This rule was **deprecated** and replaced by ${formatItems(
66+
replacedRules,
67+
)}.`,
68+
)
69+
} else {
70+
notes.push("- ⚠️ This rule was **deprecated**.")
71+
}
72+
} else if (recommended) {
73+
if (recommended === "base") {
74+
notes.push(
75+
'- ⚙ This rule is included in `"plugin:astro/base"` and `"plugin:astro/recommended"`.',
76+
)
77+
} else {
78+
notes.push('- ⚙ This rule is included in `"plugin:astro/recommended"`.')
79+
}
80+
}
81+
if (fixable) {
82+
notes.push(
83+
"- 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.",
84+
)
85+
}
86+
if (hasSuggestions) {
87+
notes.push(
88+
"- 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).",
89+
)
90+
}
91+
if (isNew) {
92+
notes.push(
93+
`- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>`,
94+
)
95+
}
96+
return notes
97+
}

tools/new-rule.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from "path"
22
import fs from "fs"
33
import cp from "child_process"
4+
import { renderRuleHeader } from "./lib/doc-renderer"
45
const logger = console
56

67
// main
@@ -77,9 +78,7 @@ tester.run("${ruleId}", rule as any, loadTestCases("${ruleId}"))
7778
)
7879
fs.writeFileSync(
7980
docFile,
80-
`# (astro/${ruleId})
81-
82-
> description
81+
`${renderRuleHeader({ ruleId: `astro/${ruleId}`, description: "description", notes: [] })}
8382
8483
## 📖 Rule Details
8584

tools/update-docs.ts

Lines changed: 10 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,7 @@ import { rules } from "../src/rules"
44
import type { RuleModule } from "../src/types"
55
import { getNewVersion } from "./lib/changesets-util"
66
import { formatAndSave } from "./lib/utils"
7-
8-
//eslint-disable-next-line jsdoc/require-jsdoc -- tools
9-
function formatItems(items: string[]) {
10-
if (items.length <= 2) {
11-
return items.join(" and ")
12-
}
13-
return `all of ${items.slice(0, -1).join(", ")} and ${
14-
items[items.length - 1]
15-
}`
16-
}
7+
import { buildNotesFromRule, renderRuleHeader } from "./lib/doc-renderer"
178

189
//eslint-disable-next-line jsdoc/require-jsdoc -- tools
1910
function yamlValue(val: unknown) {
@@ -60,9 +51,7 @@ class DocFile {
6051
this.filePath = path.join(ROOT, `${rule.meta.docs.ruleName}.md`)
6152
this.content = fs.existsSync(this.filePath)
6253
? fs.readFileSync(this.filePath, "utf8")
63-
: `
64-
65-
`
54+
: "\n\n"
6655
this.since = pickSince(this.content)
6756
}
6857

@@ -71,74 +60,15 @@ class DocFile {
7160
}
7261

7362
public updateHeader() {
74-
const {
75-
meta: {
76-
fixable,
77-
hasSuggestions,
78-
deprecated,
79-
replacedBy,
80-
docs: { ruleId, description, recommended },
81-
},
82-
} = this.rule
83-
const title = `# ${ruleId}\n\n> ${description}`
84-
const notes = []
85-
86-
if (deprecated) {
87-
if (replacedBy) {
88-
const replacedRules = replacedBy.map(
89-
(name) => `[astro/${name}](${name}.md) rule`,
90-
)
91-
notes.push(
92-
`- ⚠️ This rule was **deprecated** and replaced by ${formatItems(
93-
replacedRules,
94-
)}.`,
95-
)
96-
} else {
97-
notes.push("- ⚠️ This rule was **deprecated**.")
98-
}
99-
} else if (recommended) {
100-
if (recommended === "base") {
101-
notes.push(
102-
'- ⚙ This rule is included in `"plugin:astro/base"` and `"plugin:astro/recommended"`.',
103-
)
104-
} else {
105-
notes.push(
106-
'- ⚙ This rule is included in `"plugin:astro/recommended"`.',
107-
)
108-
}
109-
}
110-
if (fixable) {
111-
notes.push(
112-
"- 🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.",
113-
)
114-
}
115-
if (hasSuggestions) {
116-
notes.push(
117-
"- 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).",
118-
)
119-
}
120-
if (!this.since) {
121-
notes.unshift(
122-
`- ❗ <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>`,
123-
)
124-
}
125-
126-
// Add an empty line after notes.
127-
if (notes.length >= 1) {
128-
notes.push("", "")
129-
}
130-
63+
const ruleDocs = this.rule.meta.docs
64+
const { ruleId, description } = ruleDocs
65+
const isNewRule = !this.since
66+
const notes = buildNotesFromRule(this.rule, isNewRule)
13167
const headerPattern = /(?:^|\n)#.+\n+[^\n]*\n+(?:- .+\n+)*\n*/u
132-
133-
const header = `\n${title}\n\n${notes.join("\n")}`
134-
if (headerPattern.test(this.content)) {
135-
this.content = this.content.replace(
136-
headerPattern,
137-
header.replace(/\$/g, "$$$$"),
138-
)
139-
} else {
140-
this.content = `${header}${this.content.trim()}\n`
141-
}
68+
const header = renderRuleHeader({ ruleId, description, notes })
69+
this.content = headerPattern.test(this.content)
70+
? this.content.replace(headerPattern, header)
71+
: `${header}${this.content.trim()}\n`
14272

14373
return this
14474
}

0 commit comments

Comments
 (0)