Skip to content

Commit 9bcb366

Browse files
authoredJun 14, 2022
feat: ensure readme is in correct format (#997)
Uses modules from the `mdast`/`remark` ecosystem to ensure our readmes are in a predictable format while running `aegir check-project`. Ensure the following are present and consistent: - Header (taken from `package.json` name field) - CI link (assumes unified CI) - Strap line (taken from `package.json` description field) - Table of contents - Installation instructions - License - Contributing Existing header/strap line/etc is overwritten, any other existing content is added after the installation instructions. Also formats GH tables nicely and removes all instances of that weird whitespace character you get when you hold down `option` and press `space` which messes up the headers in some of our READMEs.
1 parent 0b82fee commit 9bcb366

File tree

3 files changed

+234
-0
lines changed

3 files changed

+234
-0
lines changed
 

‎package.json

+13
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,20 @@
240240
"kleur": "^4.1.4",
241241
"lilconfig": "^2.0.5",
242242
"listr": "~0.14.2",
243+
"mdast-util-from-markdown": "^1.2.0",
244+
"mdast-util-gfm": "^2.0.1",
245+
"mdast-util-gfm-footnote": "^1.0.1",
246+
"mdast-util-gfm-strikethrough": "^1.0.1",
247+
"mdast-util-gfm-table": "^1.0.4",
248+
"mdast-util-gfm-task-list-item": "^1.0.1",
249+
"mdast-util-to-markdown": "^1.3.0",
250+
"mdast-util-toc": "^6.1.0",
243251
"merge-options": "^3.0.4",
252+
"micromark-extension-gfm": "^2.0.1",
253+
"micromark-extension-gfm-footnote": "^1.0.4",
254+
"micromark-extension-gfm-strikethrough": "^1.0.4",
255+
"micromark-extension-gfm-table": "^1.0.5",
256+
"micromark-extension-gfm-task-list-item": "^1.0.3",
244257
"mocha": "^9.0.2",
245258
"npm-package-json-lint": "^6.3.0",
246259
"nyc": "^15.1.0",

‎src/check-project/check-readme.js

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
2+
/* eslint-disable no-console */
3+
4+
import fs from 'fs'
5+
import path from 'path'
6+
import {
7+
ensureFileHasContents
8+
} from './utils.js'
9+
import { fromMarkdown } from 'mdast-util-from-markdown'
10+
import { toMarkdown } from 'mdast-util-to-markdown'
11+
import { toc as makeToc } from 'mdast-util-toc'
12+
import { gfm } from 'micromark-extension-gfm'
13+
import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm'
14+
import { gfmTable } from 'micromark-extension-gfm-table'
15+
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table'
16+
import { gfmFootnote } from 'micromark-extension-gfm-footnote'
17+
import { gfmFootnoteFromMarkdown, gfmFootnoteToMarkdown } from 'mdast-util-gfm-footnote'
18+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
19+
import { gfmStrikethroughFromMarkdown, gfmStrikethroughToMarkdown } from 'mdast-util-gfm-strikethrough'
20+
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item'
21+
import { gfmTaskListItemFromMarkdown, gfmTaskListItemToMarkdown } from 'mdast-util-gfm-task-list-item'
22+
23+
/**
24+
* @param {*} pkg
25+
* @param {string} repoUrl
26+
* @param {string} defaultBranch
27+
*/
28+
const HEADER = (pkg, repoUrl, defaultBranch) => {
29+
return `
30+
# ${pkg.name} <!-- omit in toc -->
31+
32+
[![test & maybe release](${repoUrl}/actions/workflows/js-test-and-release.yml/badge.svg?branch=${defaultBranch})](${repoUrl}/actions/workflows/js-test-and-release.yml)
33+
34+
> ${pkg.description}
35+
36+
## Table of contents <!-- omit in toc -->
37+
`
38+
}
39+
40+
/**
41+
* @param {*} pkg
42+
*/
43+
const INSTALL = (pkg) => {
44+
return `
45+
## Install
46+
47+
\`\`\`console
48+
$ npm i ${pkg.name}
49+
\`\`\`
50+
`
51+
}
52+
53+
const LICENSE = `
54+
## License
55+
56+
Licensed under either of
57+
58+
* Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0)
59+
* MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT)
60+
61+
## Contribution
62+
63+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
64+
`
65+
66+
/**
67+
* @param {string} md
68+
*/
69+
function parseMarkdown (md) {
70+
return fromMarkdown(md, {
71+
extensions: [
72+
gfm(),
73+
gfmTable,
74+
gfmFootnote(),
75+
gfmStrikethrough(),
76+
gfmTaskListItem
77+
],
78+
mdastExtensions: [
79+
gfmFromMarkdown(),
80+
gfmTableFromMarkdown,
81+
gfmFootnoteFromMarkdown(),
82+
gfmStrikethroughFromMarkdown,
83+
gfmTaskListItemFromMarkdown
84+
]
85+
})
86+
}
87+
88+
/**
89+
*
90+
* @param {import('mdast').Root | import('mdast').Content} tree
91+
*/
92+
function writeMarkdown (tree) {
93+
return toMarkdown(tree, {
94+
extensions: [
95+
gfmToMarkdown(),
96+
gfmTableToMarkdown(),
97+
gfmFootnoteToMarkdown(),
98+
gfmStrikethroughToMarkdown,
99+
gfmTaskListItemToMarkdown
100+
],
101+
bullet: '-',
102+
listItemIndent: 'one'
103+
})
104+
}
105+
106+
/**
107+
* @param {string} projectDir
108+
* @param {string} repoUrl
109+
* @param {string} defaultBranch
110+
*/
111+
export async function checkReadme (projectDir, repoUrl, defaultBranch) {
112+
console.info('Check README files')
113+
114+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), {
115+
encoding: 'utf-8'
116+
}))
117+
118+
const readmePath = path.join(projectDir, 'README.md')
119+
let readmeContents = ''
120+
121+
if (fs.existsSync(readmePath)) {
122+
readmeContents = fs.readFileSync(path.join(projectDir, 'README.md'), {
123+
encoding: 'utf-8'
124+
})
125+
}
126+
127+
// replace the magic OPTION+SPACE character that messes up headers
128+
readmeContents = readmeContents.replaceAll(' ', ' ')
129+
130+
// parse the project's readme file
131+
const file = parseMarkdown(readmeContents)
132+
133+
// create basic readme with heading, CI link, etc
134+
const readme = parseMarkdown(HEADER(pkg, repoUrl, defaultBranch))
135+
136+
// remove existing header, CI link, etc
137+
/** @type {import('mdast').Root} */
138+
const parsedReadme = {
139+
type: 'root',
140+
children: []
141+
}
142+
143+
let tocIndex = -1
144+
let installIndex = -1
145+
let licenseFound = false
146+
147+
file.children.forEach((child, index) => {
148+
const rendered = writeMarkdown(child).toLowerCase()
149+
150+
if (child.type === 'heading' && index === 0) {
151+
// skip heading
152+
return
153+
}
154+
155+
if (child.type === 'paragraph' && index === 1) {
156+
// skip badges
157+
return
158+
}
159+
160+
if (child.type === 'blockquote' && tocIndex === -1 && installIndex === -1) {
161+
// skip project overview
162+
return
163+
}
164+
165+
if (rendered.includes('## table of')) {
166+
// skip toc header
167+
tocIndex = index
168+
return
169+
}
170+
171+
if (tocIndex !== -1 && index === tocIndex + 1) {
172+
// skip toc header
173+
return
174+
}
175+
176+
if (child.type === 'heading' && rendered.includes('install')) {
177+
// skip install
178+
installIndex = index
179+
return
180+
}
181+
182+
if (installIndex !== -1 && index === installIndex + 1) {
183+
// skip install
184+
return
185+
}
186+
187+
if ((child.type === 'heading' && rendered.includes('license')) || licenseFound) {
188+
licenseFound = true
189+
return
190+
}
191+
192+
parsedReadme.children.push(child)
193+
})
194+
195+
const installation = parseMarkdown(INSTALL(pkg))
196+
const license = parseMarkdown(LICENSE)
197+
198+
parsedReadme.children = [
199+
...installation.children,
200+
...parsedReadme.children,
201+
...license.children
202+
]
203+
204+
const toc = makeToc(parsedReadme, {
205+
tight: true
206+
})
207+
208+
if (toc.map == null) {
209+
throw new Error('Could not create TOC for README.md')
210+
}
211+
212+
readme.children = [
213+
...readme.children,
214+
toc.map,
215+
...parsedReadme.children
216+
]
217+
218+
await ensureFileHasContents(projectDir, 'README.md', writeMarkdown(readme))
219+
}

‎src/check-project/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { typedCJSManifest } from './manifests/typed-cjs.js'
1313
import { checkLicenseFiles } from './check-licence-files.js'
1414
import { checkBuildFiles } from './check-build-files.js'
1515
import { checkMonorepoFiles } from './check-monorepo-files.js'
16+
import { checkReadme } from './check-readme.js'
1617
import {
1718
sortManifest,
1819
ensureFileHasContents
@@ -394,6 +395,7 @@ async function processModule (projectDir, manifest, branchName, repoUrl, homePag
394395

395396
await ensureFileHasContents(projectDir, 'package.json', JSON.stringify(proposedManifest, null, 2))
396397
await checkLicenseFiles(projectDir)
398+
await checkReadme(projectDir, repoUrl, branchName)
397399
}
398400

399401
export default new Listr([

0 commit comments

Comments
 (0)
Please sign in to comment.