diff --git a/README.md b/README.md
index 6d807ddd0..278d3478d 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-# aegir
-
[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech)
[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech)
[![codecov](https://img.shields.io/codecov/c/github/ipfs/aegir.svg?style=flat-square)](https://codecov.io/gh/ipfs/aegir)
@@ -7,21 +5,6 @@
> JavaScript project management
-## Table of contents
-
-- [Install](#install)
- - [Browser `
- \`\`\`
+\`\`\`html
+
+\`\`\`
`
const scripts = pkg.scripts ?? {}
diff --git a/src/check-project/readme/structure.js b/src/check-project/readme/structure.js
index c83af9585..2525ebc0c 100644
--- a/src/check-project/readme/structure.js
+++ b/src/check-project/readme/structure.js
@@ -18,7 +18,7 @@ export const STRUCTURE = (monorepoDir, projectDirs) => {
}
return `
-## Structure
+## Packages
${Object.entries(packages).map(([key, value]) => {
return `* [\`${key}\`](.${key}) ${value}`
diff --git a/src/docs.js b/src/docs.js
index aa79cc516..cef3941a9 100644
--- a/src/docs.js
+++ b/src/docs.js
@@ -54,6 +54,8 @@ const docs = async (ctx, task) => {
fromAegir('src/docs/type-indexer-plugin.js'),
'--plugin',
'typedoc-plugin-mdn-links',
+ '--plugin',
+ fromAegir('src/docs/readme-updater-plugin.js'),
...forwardOptions
],
{
diff --git a/src/docs/readme-updater-plugin.js b/src/docs/readme-updater-plugin.js
new file mode 100644
index 000000000..a2e6e999c
--- /dev/null
+++ b/src/docs/readme-updater-plugin.js
@@ -0,0 +1,197 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import * as td from 'typedoc'
+import { parseMarkdown, writeMarkdown } from '../check-project/readme/utils.js'
+import { isMonorepoParent, parseProjects, pkg } from '../utils.js'
+
+/**
+ * A plugin that updates the `About` section of a README with the rendered
+ * contents of a `@packageDocumentation` tag.
+ *
+ * @param {td.Application} app
+ */
+export function load (app) {
+ /** @type {Record} */
+ let projects = {}
+
+ if (isMonorepoParent) {
+ projects = parseProjects(process.cwd(), pkg.workspaces)
+ }
+
+ // when rendering has finished, work out which UrlMappings refer to the index
+ // pages of the current module or monorepo packages
+ app.renderer.on(td.RendererEvent.END, (/** @type {td.RendererEvent} */ evt) => {
+ const urlMappings = evt.urls?.filter(urlMapping => {
+ // skip anything without a model-level comment
+ if (urlMapping.model?.comment == null) {
+ return false
+ }
+
+ // single-module repo
+ if (urlMapping.url === 'modules.html') {
+ return true
+ }
+
+ // mono-repo and the model name matches a package name
+ if (isMonorepoParent && projects[urlMapping.model.name] != null) {
+ return true
+ }
+
+ return false
+ }).map(urlMapping => {
+ if (urlMapping.model?.comment == null) {
+ throw new Error('Model comment was null')
+ }
+
+ if (isMonorepoParent) {
+ return {
+ comment: urlMapping.model.comment,
+ manifestPath: path.join(projects[urlMapping.model.name].dir, 'package.json'),
+ readmePath: path.join(projects[urlMapping.model.name].dir, 'README.md')
+ }
+ }
+
+ return {
+ comment: urlMapping.model.comment,
+ manifestPath: path.join(process.cwd(), 'package.json'),
+ readmePath: path.join(process.cwd(), 'README.md')
+ }
+ })
+
+ urlMappings?.forEach(urlMapping => updateModule(urlMapping.comment, urlMapping.manifestPath, urlMapping.readmePath, app))
+ })
+}
+
+/**
+ * Turn the comment into markdown
+ *
+ * @param {td.Models.Comment} comment
+ * @param {string} manifestPath
+ * @param {string} readmePath
+ * @param {td.Application} app
+ * @returns {void}
+ */
+function updateModule (comment, manifestPath, readmePath, app) {
+ const about = `
+## About
+
+${
+ comment.summary
+ .map(item => item.text)
+ .join('')
+}
+${
+ comment.blockTags
+ .map(item => {
+ if (item.tag === '@example') {
+ return `
+### Example
+
+${
+ item.content
+ .map(item => item.text)
+ .join('')
+}
+`
+ }
+
+ app.logger.warn(`Unknown block tag: ${item.tag}`)
+
+ return ''
+ })
+}
+`.trim()
+
+ try {
+ updateReadme(about, manifestPath, readmePath, app)
+ } catch (/** @type {any} */ err) {
+ app.logger.error(`Could not update README about section: ${err.stack}`)
+ }
+}
+
+/**
+ * Accept a markdown string and add it to the README, replacing any existing
+ * "About" section
+ *
+ * @param {string} aboutMd
+ * @param {string} manifestPath
+ * @param {string} readmePath
+ * @param {td.Application} app
+ * @returns {void}
+ */
+function updateReadme (aboutMd, manifestPath, readmePath, app) {
+ if (!fs.existsSync(readmePath)) {
+ app.logger.error(`Could not read readme from ${readmePath}`)
+ }
+
+ const readmeContents = fs.readFileSync(readmePath, {
+ encoding: 'utf-8'
+ })
+ // replace the magic OPTION+SPACE character that messes up headers
+ .replaceAll(' ', ' ')
+
+ if (!fs.existsSync(manifestPath)) {
+ app.logger.error(`Could not read manifest from ${manifestPath}`)
+ }
+
+ const manifestContents = fs.readFileSync(manifestPath, {
+ encoding: 'utf-8'
+ })
+ const manifest = JSON.parse(manifestContents)
+
+ // parse the project's readme file
+ const readme = parseMarkdown(readmeContents)
+ const about = parseMarkdown(aboutMd)
+
+ // remove existing header, CI link, etc
+ /** @type {import('mdast').Root} */
+ const parsedReadme = {
+ type: 'root',
+ children: []
+ }
+
+ let foundAbout = false
+ let aboutHeadingLevel = -1
+
+ readme.children.forEach((child, index) => {
+ const rendered = writeMarkdown(child).toLowerCase()
+
+ if (child.type === 'blockquote' && rendered === `> ${manifest.description.toLowerCase()}\n`) {
+ // insert the `About` section beneath the project description
+ parsedReadme.children.push(
+ child,
+ ...about.children
+ )
+
+ return
+ }
+
+ if (child.type === 'heading' && rendered.includes('# about')) {
+ // we have found an existing `About` section - skip all content until we
+ // hit another heading equal or higher depth than `About
+ foundAbout = true
+ aboutHeadingLevel = child.depth
+
+ // skip about
+ return
+ }
+
+ if (foundAbout) {
+ if (child.type === 'heading' && child.depth <= aboutHeadingLevel) {
+ foundAbout = false
+ } else {
+ // skip all content until we hit another heading equal or higher depth
+ // than `About`
+ return
+ }
+ }
+
+ parsedReadme.children.push(child)
+ })
+
+ const updatedReadmeContents = writeMarkdown(parsedReadme)
+
+ fs.writeFileSync(readmePath, updatedReadmeContents, {
+ encoding: 'utf-8'
+ })
+}
diff --git a/src/utils.js b/src/utils.js
index 298920b71..c21ab040f 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -328,7 +328,7 @@ export async function everyMonorepoProject (projectDir, fn) {
}
/** @type {Record} */
- const projects = await parseProjects(projectDir, workspaces)
+ const projects = parseProjects(projectDir, workspaces)
checkForCircularDependencies(projects)