Skip to content

Commit ae76db7

Browse files
authoredJun 14, 2022
feat: parse monorepo project readme (#1001)
1 parent fc4d7b9 commit ae76db7

File tree

8 files changed

+315
-97
lines changed

8 files changed

+315
-97
lines changed
 
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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 { toc as makeToc } from 'mdast-util-toc'
10+
import { parseMarkdown, writeMarkdown } from './readme/utils.js'
11+
import { HEADER } from './readme/header.js'
12+
import { LICENSE } from './readme/license.js'
13+
import { STRUCTURE } from './readme/structure.js'
14+
15+
/**
16+
* @param {string} projectDir
17+
* @param {string} repoUrl
18+
* @param {string} defaultBranch
19+
* @param {string[]} projectDirs
20+
*/
21+
export async function checkMonorepoReadme (projectDir, repoUrl, defaultBranch, projectDirs) {
22+
const repoParts = repoUrl.split('/')
23+
const repoName = repoParts.pop()
24+
const repoOwner = repoParts.pop()
25+
26+
if (repoName == null || repoOwner == null) {
27+
throw new Error(`Could not parse repo owner & name from ${repoUrl}`)
28+
}
29+
30+
console.info('Check README files')
31+
32+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), {
33+
encoding: 'utf-8'
34+
}))
35+
36+
const readmePath = path.join(projectDir, 'README.md')
37+
let readmeContents = ''
38+
39+
if (fs.existsSync(readmePath)) {
40+
readmeContents = fs.readFileSync(path.join(projectDir, 'README.md'), {
41+
encoding: 'utf-8'
42+
})
43+
}
44+
45+
// replace the magic OPTION+SPACE character that messes up headers
46+
readmeContents = readmeContents.replaceAll(' ', ' ')
47+
48+
// parse the project's readme file
49+
const file = parseMarkdown(readmeContents)
50+
51+
// create basic readme with heading, CI link, etc
52+
const readme = parseMarkdown(HEADER(pkg, repoOwner, repoName, defaultBranch))
53+
54+
// remove existing header, CI link, etc
55+
/** @type {import('mdast').Root} */
56+
const parsedReadme = {
57+
type: 'root',
58+
children: []
59+
}
60+
61+
let structureIndex = -1
62+
let tocIndex = -1
63+
let licenseFound = false
64+
65+
file.children.forEach((child, index) => {
66+
const rendered = writeMarkdown(child).toLowerCase()
67+
68+
if (child.type === 'heading' && index === 0) {
69+
// skip heading
70+
return
71+
}
72+
73+
if (child.type === 'paragraph' && index === 1) {
74+
// skip badges
75+
return
76+
}
77+
78+
if (child.type === 'blockquote' && tocIndex === -1 && tocIndex === -1) {
79+
// skip project overview
80+
return
81+
}
82+
83+
if (rendered.includes('## table of')) {
84+
// skip toc header
85+
tocIndex = index
86+
return
87+
}
88+
89+
if (tocIndex !== -1 && index === tocIndex + 1) {
90+
// skip toc
91+
return
92+
}
93+
94+
if (child.type === 'heading' && rendered.includes('structure')) {
95+
// skip structure header
96+
structureIndex = index
97+
return
98+
}
99+
100+
if (structureIndex !== -1 && index === structureIndex + 1) {
101+
// skip structure
102+
return
103+
}
104+
105+
if ((child.type === 'heading' && rendered.includes('license')) || licenseFound) {
106+
licenseFound = true
107+
return
108+
}
109+
110+
parsedReadme.children.push(child)
111+
})
112+
113+
const license = parseMarkdown(LICENSE[repoOwner])
114+
const structure = parseMarkdown(STRUCTURE(projectDir, projectDirs))
115+
116+
parsedReadme.children = [
117+
...structure.children,
118+
...parsedReadme.children,
119+
...license.children
120+
]
121+
122+
const toc = makeToc(parsedReadme, {
123+
tight: true
124+
})
125+
126+
if (toc.map == null) {
127+
throw new Error('Could not create TOC for README.md')
128+
}
129+
130+
readme.children = [
131+
...readme.children,
132+
toc.map,
133+
...parsedReadme.children
134+
]
135+
136+
await ensureFileHasContents(projectDir, 'README.md', writeMarkdown(readme))
137+
}

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

+14-97
Original file line numberDiff line numberDiff line change
@@ -6,109 +6,26 @@ import path from 'path'
66
import {
77
ensureFileHasContents
88
} from './utils.js'
9-
import { fromMarkdown } from 'mdast-util-from-markdown'
10-
import { toMarkdown } from 'mdast-util-to-markdown'
119
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-
}
10+
import { parseMarkdown, writeMarkdown } from './readme/utils.js'
11+
import { HEADER } from './readme/header.js'
12+
import { LICENSE } from './readme/license.js'
13+
import { INSTALL } from './readme/install.js'
10514

10615
/**
10716
* @param {string} projectDir
10817
* @param {string} repoUrl
10918
* @param {string} defaultBranch
11019
*/
11120
export async function checkReadme (projectDir, repoUrl, defaultBranch) {
21+
const repoParts = repoUrl.split('/')
22+
const repoName = repoParts.pop()
23+
const repoOwner = repoParts.pop()
24+
25+
if (repoName == null || repoOwner == null) {
26+
throw new Error(`Could not parse repo owner & name from ${repoUrl}`)
27+
}
28+
11229
console.info('Check README files')
11330

11431
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), {
@@ -131,7 +48,7 @@ export async function checkReadme (projectDir, repoUrl, defaultBranch) {
13148
const file = parseMarkdown(readmeContents)
13249

13350
// create basic readme with heading, CI link, etc
134-
const readme = parseMarkdown(HEADER(pkg, repoUrl, defaultBranch))
51+
const readme = parseMarkdown(HEADER(pkg, repoOwner, repoName, defaultBranch))
13552

13653
// remove existing header, CI link, etc
13754
/** @type {import('mdast').Root} */
@@ -193,7 +110,7 @@ export async function checkReadme (projectDir, repoUrl, defaultBranch) {
193110
})
194111

195112
const installation = parseMarkdown(INSTALL(pkg))
196-
const license = parseMarkdown(LICENSE)
113+
const license = parseMarkdown(LICENSE[repoOwner])
197114

198115
parsedReadme.children = [
199116
...installation.children,

‎src/check-project/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { checkLicenseFiles } from './check-licence-files.js'
1414
import { checkBuildFiles } from './check-build-files.js'
1515
import { checkMonorepoFiles } from './check-monorepo-files.js'
1616
import { checkReadme } from './check-readme.js'
17+
import { checkMonorepoReadme } from './check-monorepo-readme.js'
1718
import {
1819
sortManifest,
1920
ensureFileHasContents
@@ -127,6 +128,7 @@ async function processMonorepo (projectDir, manifest, branchName, repoUrl) {
127128
await ensureFileHasContents(projectDir, 'package.json', JSON.stringify(proposedManifest, null, 2))
128129
await checkLicenseFiles(projectDir)
129130
await checkBuildFiles(projectDir, branchName, repoUrl)
131+
await checkMonorepoReadme(projectDir, repoUrl, branchName, projectDirs)
130132
await checkMonorepoFiles(projectDir)
131133
}
132134

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

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @type {Record<string, (repoOwner: string, repoName: string, defaultBranch: string) => string>}
3+
*/
4+
const BADGES = {
5+
libp2p: (repoOwner, repoName, defaultBranch) => `
6+
[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/)
7+
[![IRC](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p)
8+
[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io)
9+
[![codecov](https://img.shields.io/codecov/c/github/${repoOwner}/${repoName}.svg?style=flat-square)](https://codecov.io/gh/${repoOwner}/${repoName})
10+
[![CI](https://img.shields.io/github/workflow/status/libp2p/js-libp2p-interfaces/test%20&%20maybe%20release/${defaultBranch}?style=flat-square)](https://github.com/${repoOwner}/${repoName}/actions/workflows/js-test-and-release.yml)
11+
`,
12+
ipfs: (repoOwner, repoName, defaultBranch) => `
13+
[![ipfs.io](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io)
14+
[![IRC](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs)
15+
[![Discord](https://img.shields.io/discord/806902334369824788?style=flat-square)](https://discord.gg/ipfs)
16+
[![codecov](https://img.shields.io/codecov/c/github/${repoOwner}/${repoName}.svg?style=flat-square)](https://codecov.io/gh/${repoOwner}/${repoName})
17+
[![CI](https://img.shields.io/github/workflow/status/libp2p/js-libp2p-interfaces/test%20&%20maybe%20release/${defaultBranch}?style=flat-square)](https://github.com/${repoOwner}/${repoName}/actions/workflows/js-test-and-release.yml)
18+
`
19+
}
20+
21+
/**
22+
* @param {*} pkg
23+
* @param {string} repoOwner
24+
* @param {string} repoName
25+
* @param {string} defaultBranch
26+
*/
27+
export const HEADER = (pkg, repoOwner, repoName, defaultBranch) => {
28+
return `
29+
# ${pkg.name} <!-- omit in toc -->
30+
31+
${BADGES[repoOwner](repoOwner, repoName, defaultBranch).trim()}
32+
33+
> ${pkg.description}
34+
35+
## Table of contents <!-- omit in toc -->
36+
`
37+
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @param {*} pkg
3+
*/
4+
export const INSTALL = (pkg) => {
5+
return `
6+
## Install
7+
8+
\`\`\`console
9+
$ npm i ${pkg.name}
10+
\`\`\`
11+
`
12+
}

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

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
/** @type {Record<string, string>} */
3+
export const LICENSE = {
4+
libp2p: `
5+
## License
6+
7+
Licensed under either of
8+
9+
* Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0)
10+
* MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT)
11+
12+
## Contribution
13+
14+
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.
15+
`,
16+
ipfs: `
17+
## License
18+
19+
Licensed under either of
20+
21+
* Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0)
22+
* MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT)
23+
24+
## Contribute
25+
26+
Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipfs-unixfs-importer/issues)!
27+
28+
This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md).
29+
30+
[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md)
31+
`
32+
}

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

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
/**
5+
* @param {string} monorepoDir
6+
* @param {string[]} projectDirs
7+
*/
8+
export const STRUCTURE = (monorepoDir, projectDirs) => {
9+
/** @type {Record<string, string>} */
10+
const packages = {}
11+
12+
for (const projectDir of projectDirs.sort()) {
13+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), {
14+
encoding: 'utf-8'
15+
}))
16+
17+
const key = projectDir.replace(monorepoDir, '')
18+
19+
packages[key] = pkg.description
20+
}
21+
22+
return `
23+
## Structure
24+
25+
${Object.entries(packages).map(([key, value]) => {
26+
return `* [\`${key}\`](.${key}) ${value}`
27+
}).join('\n')}
28+
`
29+
}

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

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { fromMarkdown } from 'mdast-util-from-markdown'
2+
import { toMarkdown } from 'mdast-util-to-markdown'
3+
import { gfm } from 'micromark-extension-gfm'
4+
import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm'
5+
import { gfmTable } from 'micromark-extension-gfm-table'
6+
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table'
7+
import { gfmFootnote } from 'micromark-extension-gfm-footnote'
8+
import { gfmFootnoteFromMarkdown, gfmFootnoteToMarkdown } from 'mdast-util-gfm-footnote'
9+
import { gfmStrikethrough } from 'micromark-extension-gfm-strikethrough'
10+
import { gfmStrikethroughFromMarkdown, gfmStrikethroughToMarkdown } from 'mdast-util-gfm-strikethrough'
11+
import { gfmTaskListItem } from 'micromark-extension-gfm-task-list-item'
12+
import { gfmTaskListItemFromMarkdown, gfmTaskListItemToMarkdown } from 'mdast-util-gfm-task-list-item'
13+
14+
/**
15+
* @param {string} md
16+
*/
17+
export function parseMarkdown (md) {
18+
return fromMarkdown(md, {
19+
extensions: [
20+
gfm(),
21+
gfmTable,
22+
gfmFootnote(),
23+
gfmStrikethrough(),
24+
gfmTaskListItem
25+
],
26+
mdastExtensions: [
27+
gfmFromMarkdown(),
28+
gfmTableFromMarkdown,
29+
gfmFootnoteFromMarkdown(),
30+
gfmStrikethroughFromMarkdown,
31+
gfmTaskListItemFromMarkdown
32+
]
33+
})
34+
}
35+
36+
/**
37+
*
38+
* @param {import('mdast').Root | import('mdast').Content} tree
39+
*/
40+
export function writeMarkdown (tree) {
41+
return toMarkdown(tree, {
42+
extensions: [
43+
gfmToMarkdown(),
44+
gfmTableToMarkdown(),
45+
gfmFootnoteToMarkdown(),
46+
gfmStrikethroughToMarkdown,
47+
gfmTaskListItemToMarkdown
48+
],
49+
bullet: '-',
50+
listItemIndent: 'one'
51+
})
52+
}

0 commit comments

Comments
 (0)
Please sign in to comment.