Skip to content

Generating READMEs From Gatsby's pluginOptionsSchema


Created: Sep 24, 2022 – Last Updated: Sep 24, 2022

Tags: Gatsby

Digital Garden

For my latest Gatsby plugin gatsby-source-flickr (opens in a new tab) I created — of course 🙄 — a boilerplate first before doing the actual work. The result is my personal gatsby-plugin-starter (opens in a new tab). Feel free to also use it for your projects! I want to showcase one feature I built in this blog post: The ability to automatically generate the project’s README from the pluginOptionsSchema (opens in a new tab) of your plugin. This is pretty cool as it means that the source of truth for the documentation always is code-first, you just have to regenerate the README.

You can follow this post best if you already have set up a project, e.g. by following the Creating a Source Plugin guide (opens in a new tab).

If you want to see the TypeScript version of this, have a look at the gatsby-plugin-starter (opens in a new tab).

#Setup

You should have a gatsby-node.js file for your plugin and an NPM project already initialized.

sh
your-project/
├─ package.json
├─ gatsby-node.js

Install the necessary dependencies:

sh
npm install gatsby-plugin-utils fs-extra prettier handlebars lodash.startcase markdown-toc

Create an empty generate-readme.js file and an empty plugin-options-schema.js file at the root.

Add a generate-readme script to the package.json:

package.json
json
{
"scripts": {
"generate-readme": "node generate-readme.js"
}
}

#Creating pluginOptionsSchema

Suppose the plugin you’re creating has two options, api_key is required and username is optional. You can enforce these options via pluginOptionsSchema for your users.

Open the plugin-options-schema.js file and add the following:

plugin-options-schema.js
js
const wrapOptions = (innerOptions) => `{
resolve: \`name-of-your-plugin\`,
options: {
${innerOptions.trim()}
},
}
`
const pluginOptionsSchema = ({ Joi }) =>
Joi.object({
api_key: Joi.string()
.required()
.description(`API Key required for login`)
.meta({ example: wrapOptions(`api_key: "123456789",`) }),
username: Joi.string()
.default(``)
.description(`Optional username for other stuff`)
.meta({ example: wrapOptions(`username: "hello",`) }),
})
exports.pluginOptionsSchema = pluginOptionsSchema

You’ve successfully defined the schema for your plugin options! The .meta() portion will be accessed by the README generation script later.

As a last step you need to define the pluginOptionsSchema in your gatsby-node.js:

gatsby-node.js
js
const { pluginOptionsSchema } = require("./plugin-options-schema")
exports.pluginOptionsSchema = pluginOptionsSchema

#Generating the README

Now that your pluginOptionsSchema is easily accessible you can switch over to the generate-readme.js file and add the following:

generate-readme.js
js
const { Joi } = require("gatsby-plugin-utils")
const fs = require("fs-extra")
const prettier = require("prettier")
const Handlebars = require("handlebars")
const startCase = require("lodash.startcase")
const toc = require("markdown-toc")
const { pluginOptionsSchema } = require("./src/plugin-options-schema")
const PLUGIN_NAME = `name-of-your-plugin`
const DEFAULT_README = `# ${PLUGIN_NAME}
Your description goes here.
## Install
\`\`\`shell
npm install ${PLUGIN_NAME}
\`\`\`
## How to use
Add the plugin to your \`gatsby-config\` file:
\`\`\`js:title=gatsby-config.js
module.exports = {
plugins: [
{
resolve: \`${PLUGIN_NAME}\`,
options: {}
}
]
}
\`\`\`
## Plugin Options
`
const PRETTIER_CONFIG = {
printWidth: 80,
semi: false,
trailingComma: `es5`,
}
async function writeReadme() {
console.info(`Writing README.md...`)
try {
const mdString = await getMdString()
await fs.writeFile(`./README.md`, mdString)
console.info(`Successfully created README.md`)
} catch (error) {
console.error(error)
}
}
if (process.env.NODE_ENV !== `test`) {
writeReadme()
}
async function getMdString() {
const schema = pluginOptionsSchema({ Joi }).describe()
const mdString = generateMdStringFromSchemaDescription(schema)
return mdString
}
async function generateMdStringFromSchemaDescription(schema) {
const template = Handlebars.compile(`{{{defaultReadme}}}
{{{tableOfContents}}}
{{{docs}}}`)
const docs = joiKeysToMD({
keys: schema.keys,
})
const tableOfContents = toc(docs).content
const mdContents = template({
defaultReadme: DEFAULT_README,
tableOfContents,
docs,
})
const mdStringFormatted = prettier.format(mdContents, {
parser: `markdown`,
...PRETTIER_CONFIG,
})
return mdStringFormatted
}
function joiKeysToMD({
keys,
inputMdString = ``,
level = 2,
parent = null,
parentMetas = [],
}) {
if (
!keys ||
(parentMetas.length && parentMetas.find((meta) => meta.portableOptions))
) {
return inputMdString
}
let mdString = inputMdString
Object.entries(keys).forEach(([key, value]) => {
const isRequired = value.flags && value.flags.presence === `required`
const title = `${parent ? `${parent}.` : ``}${key}${
isRequired ? ` (**required**)` : ``
}`
mdString += `${`#`.repeat(level + 1)} ${title}`
if (value.flags.description) {
mdString += `\n\n`
const description = value.flags.description.trim()
mdString += description.endsWith(`.`) ? description : `${description}.`
}
if (value.type) {
const { trueType } =
(value.metas && value.metas.find((meta) => `trueType` in meta)) || {}
mdString += `\n\n`
mdString += `**Field type**: \`${(trueType || value.type)
.split(`|`)
.map((typename) => startCase(typename))
.join(` | `)}\``
}
if (
(value.flags && `default` in value.flags) ||
(value.metas && value.metas.find((meta) => `default` in meta))
) {
const defaultValue =
((value.metas && value.metas.find((meta) => `default` in meta)) || {})
.default || value.flags.default
let printedValue
if (typeof defaultValue === `string`) {
printedValue = defaultValue
} else if (Array.isArray(defaultValue)) {
printedValue = `[${defaultValue.join(`, `)}]`
} else if (
[`boolean`, `function`, `number`].includes(typeof defaultValue)
) {
printedValue = defaultValue.toString()
} else if (defaultValue === null) {
printedValue = `null`
}
if (typeof printedValue === `string`) {
mdString += `\n\n`
mdString += `**Default value**: ${
printedValue.includes(`\n`)
? `\n\`\`\`js\n${printedValue}\n\`\`\``
: `\`${printedValue}\``
}`
}
}
if (value.metas) {
const examples = value.metas.filter((meta) => `example` in meta)
examples.forEach(({ example }) => {
mdString += `\n\n\`\`\`js\n${example}\`\`\`\n`
})
}
mdString += `\n\n`
if (value.keys) {
mdString = joiKeysToMD({
keys: value.keys,
inputMdString: mdString,
level: level + 1,
parent: title,
parentMetas: value.metas,
})
}
if (value.items && value.items.length) {
value.items.forEach((item) => {
if (item.keys) {
mdString = joiKeysToMD({
keys: item.keys,
inputMdString: mdString,
level: level + 1,
parent: `${title}[]`,
parentMetas: value.metas,
})
}
})
}
})
return mdString
}

Try it out if it works by running the script in your terminal:

sh
npm run generate-readme

You should now have a README.md file with the desired contents.


Want to learn more? Browse my Digital Garden