Generating READMEs From Gatsby's pluginOptionsSchema
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.
your-project/├─ package.json├─ gatsby-node.js
Install the necessary dependencies:
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
:
{ "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:
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
:
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:
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
\`\`\`shellnpm install ${PLUGIN_NAME}\`\`\`
## How to use
Add the plugin to your \`gatsby-config\` file:
\`\`\`js:title=gatsby-config.jsmodule.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:
npm run generate-readme
You should now have a README.md
file with the desired contents.