Skip to content

Commit 1367a32

Browse files
mxstbrLBLekoArtsmeganesugillkyle
authored
docs(plugins): more in-depth pluginOptionsSchema documentation (#27541)
* Start work on more in-depth pluginOptionsSchema documentation * Add section on unit testing * Slight cleanup * More voice cleanup * Cleanup + external access * Section on custom error messages * Fill out the entire doc * Update docs/docs/validating-plugin-options.md Co-authored-by: LB <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: LB <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: LB <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Lennart <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Lennart <[email protected]> * Cleanup based on review * Benefits * Update validating-plugin-options.md * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Update docs/docs/validating-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Minor updates * Slightly more meaty intro * Add description of the testPluginOptions method * Move validating plugin options back to configuring usage with plugin options doc * Update docs/docs/configuring-usage-with-plugin-options.md Co-authored-by: Kyle Gill <[email protected]> * Update docs/docs/configuring-usage-with-plugin-options.md Co-authored-by: Michaël De Boey <[email protected]> * Update docs/docs/configuring-usage-with-plugin-options.md Co-authored-by: Michaël De Boey <[email protected]> * Update docs/docs/configuring-usage-with-plugin-options.md Co-authored-by: Michaël De Boey <[email protected]> * Update docs/docs/configuring-usage-with-plugin-options.md Co-authored-by: Megan Sullivan <[email protected]> * Run prettier Co-authored-by: LB <[email protected]> Co-authored-by: Lennart <[email protected]> Co-authored-by: Megan Sullivan <[email protected]> Co-authored-by: Kyle Gill <[email protected]> Co-authored-by: Michaël De Boey <[email protected]>
1 parent 9022a98 commit 1367a32

File tree

1 file changed

+213
-7
lines changed

1 file changed

+213
-7
lines changed

docs/docs/configuring-usage-with-plugin-options.md

Lines changed: 213 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,230 @@ The following table lists possible options values and an example plugin that mak
5454

5555
**Note**: Themes (which are a type of plugin) are able to receive options from a site's `gatsby-config` to be used in its `gatsby-config` in order to allow themes to be composed together. This is done by exporting the `gatsby-config` as a function instead of an object. You can see an example of this in the [`gatsby-theme-blog`](https://github.com/gatsbyjs/themes/tree/master/packages/gatsby-theme-blog) and [`gatsby-theme-blog-core`](https://github.com/gatsbyjs/themes/tree/master/packages/gatsby-theme-blog-core) repositories. Plugins are not capable of this functionality.
5656

57-
## How to validate options
57+
## How to validate plugin options
5858

59-
In order to make configuration easier for users, plugins can optionally define a [Joi](https://joi.dev) schema to enforce data types for each option using the [`pluginOptionsSchema` API](/docs/node-apis/#pluginOptionsSchema) in `gatsby-node.js`. When users of the plugin pass in configuration options, Gatsby will validate that the options match the schema.
59+
To help users [configure plugins](/docs/configuring-usage-with-plugin-options/) correctly, a plugin can optionally define a schema to enforce a type for each option. Gatsby will validate that the options users pass match the schema to help them correctly set up their site.
6060

61-
For example, here is what the schema for `gatsby-plugin-console-log` looks like:
61+
### How to define an options schema
62+
63+
You should use the [`pluginOptionsSchema`](/docs/node-apis/#pluginOptionsSchema) API to define your plugins' options schema. It gets passed an instance of [Joi](https://joi.dev), which you use to return a [`Joi.object`](https://joi.dev/api/?v=17.3.0#object) schema for the options you expect users to pass.
64+
65+
For example, imagine you were creating a plugin called `gatsby-plugin-console-log`. You decide you want users to configure your plugin using the following options:
66+
67+
```javascript:title=gatsby-config.js
68+
module.exports = {
69+
plugins: [
70+
{
71+
resolve: `gatsby-plugin-console-log`,
72+
options: {
73+
optionA: true,
74+
message: "Hello world"
75+
optionB: false, // Optional.
76+
},
77+
},
78+
],
79+
}
80+
```
81+
82+
You want users to pass in a boolean to `optionA` and a string to `message`, and they can optionally pass a boolean to `optionB`. To enforce these rules, you would create the following `pluginOptionsSchema`:
6283

6384
```javascript:title=plugins/gatsby-plugin-console-log/gatsby-node.js
6485
exports.pluginOptionsSchema = ({ Joi }) => {
6586
return Joi.object({
66-
optionA: Joi.boolean().required(),
67-
message: Joi.string().required().default(`default message`),
68-
optionB: Joi.boolean(),
87+
optionA: Joi.boolean().required().description(`Enables optionA.`),
88+
message: Joi.string()
89+
.required()
90+
.description(`The message logged to the console.`),
91+
optionB: Joi.boolean().description(`Enables optionB.`),
6992
})
7093
}
7194
```
7295

73-
This ensures users pass a boolean to `optionA` and `optionB`, and a string to `message`. If they pass options that do not match the schema, the validation will show an error when they run `gatsby develop`. It also defaults the `message` option to "default message" in case users do not pass their own.
96+
If users pass options that do not match the schema, the validation will show an error when they run `gatsby develop` and prompt them to fix their configuration.
97+
98+
For example, if an integer is passed into `message` (which is marked as a required string) this message would be shown:
99+
100+
```
101+
ERROR #11331 PLUGIN
102+
103+
Invalid plugin options for "gatsby-plugin-console-log":
104+
105+
- "message" must be a string
106+
```
107+
108+
### Best practices for option schemas
109+
110+
The [Joi API documentation](https://joi.dev/api/) is a great reference to use while working on a `pluginOptionsSchema`, as it shows all the available types and methods.
111+
112+
Here are some specific Joi best practices for `pluginOptionsSchema`:
113+
114+
- [Add descriptions](#add-descriptions)
115+
- [Set default options](#set-default-options)
116+
- [Validate external access](#validate-external-access) where necessary
117+
- [Add custom error messages](#add-custom-error-messages) where useful
118+
- [Deprecate options](#deprecating-options) in a major version release rather than removing them
119+
120+
#### Add descriptions
121+
122+
Make sure that every option and field has a [`.description()`](https://joi.dev/api/?v=17.3.0#anydescriptiondesc) explaining its purpose. This is helpful for documentation as users can look at the schema and understand all the options. There might also be tooling in the future that auto-generates plugin option documentation from the schema.
123+
124+
#### Set default options
125+
126+
You can use the [`.default()` method](https://joi.dev/api/?v=17.3.0#anydefaultvalue) to set a default value for an option. For example, in the `gatsby-plugin-console-log` plugin above, you could have the `message` option default to `"default message"` if a user does not pass their own `message` value:
127+
128+
```javascript:title=plugins/gatsby-plugin-console-log/gatsby-node.js
129+
exports.pluginOptionsSchema = ({ Joi }) => {
130+
return Joi.object({
131+
optionA: Joi.boolean().required().description(`Enables optionA.`),
132+
message: Joi.string()
133+
.default(`default message`) // highlight-line
134+
.description(`The message logged to the console.`),
135+
optionB: Joi.boolean().description(`Enables optionB.`),
136+
})
137+
}
138+
```
139+
140+
Accessing `pluginOptions.message` would then log `"default message"` in all plugin APIs if the user does not supply their own value.
141+
142+
#### Validate external access
143+
144+
Some plugins (particularly source plugins) query external APIs. With the [`.external()`](https://joi.dev/api/?v=17.3.0#anyexternalmethod-description) method, you can asynchronously validate that the user has access to the API, providing a better experience if they pass invalid secrets.
145+
146+
For example, this is how the [Contentful source plugin](/plugins/gatsby-source-contentful/) might validate that the user has access to the space they are trying to query:
147+
148+
```javascript:title=gatsby-source-contentful/gatsby-node.js
149+
exports.pluginOptionsSchema = ({ Joi }) => {
150+
return Joi.object({
151+
accessToken: Joi.string().required(),
152+
spaceId: Joi.string().required(),
153+
// ...more options here...
154+
}).external(async pluginOptions => {
155+
try {
156+
await contentful
157+
.createClient({
158+
space: pluginOptions.spaceId,
159+
accessToken: pluginOptions.accessToken,
160+
})
161+
.getSpace()
162+
} catch (err) {
163+
throw new Error(
164+
`Cannot access Contentful space "${pluginOptions.spaceId}" with the provided access token. Double check they are correct and try again!`
165+
)
166+
}
167+
})
168+
}
169+
```
170+
171+
#### Add custom error messages
172+
173+
Sometimes you might want to provide more detailed error messages when validation fails for a specific field. Joi provides a [`.messages()` method](https://joi.dev/api/?v=17.3.0#anymessagesmessages) which lets you override error messages for specific [error types](https://joi.dev/api/?v=17.3.0#list-of-errors) (e.g. `"any.required"` when a `.required()` call fails).
174+
175+
For example, in the `gatsby-plugin-console-log` plugin above, this is how you would provide a custom error message if users do not specify `optionA`:
176+
177+
```javascript:title=plugins/gatsby-plugin-console-log/gatsby-node.js
178+
exports.pluginOptionsSchema = ({ Joi }) => {
179+
return Joi.object({
180+
optionA: Joi.boolean()
181+
.required()
182+
.description(`Enables optionA.`)
183+
// highlight-start
184+
.messages({
185+
// Override the error message if the .required() call fails
186+
"any.required": `"optionA" needs to be specified to true or false. Get the correct value from your dashboard settings.`,
187+
}),
188+
// highlight-end
189+
message: Joi.string()
190+
.default(`default message`)
191+
.description(`The message logged to the console.`),
192+
optionB: Joi.boolean().description(`Enables optionB.`),
193+
})
194+
}
195+
```
196+
197+
#### Deprecating options
198+
199+
While you can simply remove options from the schema in major versions, that causes cryptic error messages for users upgrading with existing configuration. Instead, deprecate them using the [`.forbidden()` method](https://joi.dev/api/?v=17.3.0#anyforbidden) in a major version release. Then, [add a custom error message](#add-custom-error-messages) explaining how users should upgrade the functionality using `.messages()`.
200+
201+
For example:
202+
203+
```javascript:title=plugins/gatsby-plugin-console-log/gatsby-node.js
204+
return Joi.object({
205+
optionA: Joi.boolean()
206+
.required()
207+
.description(`Enables optionA.`)
208+
// highlight-start
209+
.forbidden()
210+
.messages({
211+
// Override the error message if the .forbidden() call fails
212+
"any.unknown": `"optionA" is no longer supported. Use "optionB" instead by setting it to the same value you had before on "optionA".`,
213+
}),
214+
// highlight-end
215+
message: Joi.string()
216+
.default(`default message`)
217+
.description(`The message logged to the console.`),
218+
optionB: Joi.boolean().description(`Enables optionB.`),
219+
})
220+
}
221+
```
222+
223+
### Unit testing an options schema
224+
225+
To verify that a `pluginOptionsSchema` behaves as expected, unit test it with different configurations using the [`gatsby-plugin-utils` package](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-utils#testpluginoptionsschema).
226+
227+
1. Add the `gatsby-plugin-utils` package to your site:
228+
229+
```shell
230+
npm install --dev gatsby-plugin-utils
231+
```
232+
233+
2. Use the `testPluginOptionsSchema` function exported from the package in your test file. It takes two parameters, the plugin's actual Joi schema and an example options object to test. It returns an object with an `isValid` boolean, which will be true or false based on whether or not the options object fits the actual Joi schema, and an `errors` array, which will contain the error messages if the validation failed.
234+
235+
For example, with [Jest](https://jestjs.io), your tests might look something like this:
236+
237+
```javascript:title=plugins/gatsby-plugin-console/__tests__/gatsby-node.js
238+
// This is an example using Jest (https://jestjs.io/)
239+
import { testPluginOptionsSchema } from "gatsby-plugin-utils"
240+
import { pluginOptionsSchema } from "../gatsby-node"
241+
242+
describe(`pluginOptionsSchema`, () => {
243+
it(`should invalidate incorrect options`, () => {
244+
const options = {
245+
optionA: undefined, // Should be a boolean
246+
message: 123, // Should be a string
247+
optionB: `not a boolean`, // Should be a boolean
248+
}
249+
const { isValid, errors } = testPluginOptionsSchema(
250+
pluginOptionsSchema,
251+
options
252+
)
253+
254+
expect(isValid).toBe(false)
255+
expect(errors).toEqual([
256+
`"optionA" is required`,
257+
`"message" must be a string`,
258+
`"optionB" must be a boolean`,
259+
])
260+
})
261+
262+
it(`should validate correct options`, () => {
263+
const options = {
264+
optionA: false,
265+
message: "string",
266+
optionB: true,
267+
}
268+
const { isValid, errors } = testPluginOptionsSchema(
269+
pluginOptionsSchema,
270+
options
271+
)
272+
273+
expect(isValid).toBe(true)
274+
expect(errors).toEqual([])
275+
})
276+
})
277+
```
74278

75279
## Additional resources
76280

77281
- [Example Gatsby site using plugin options with a local plugin](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-plugin-options)
282+
- [Joi API documentation](https://joi.dev/api/)
283+
- [`pluginOptionsSchema` for the Contentful source plugin](https://github.com/gatsbyjs/gatsby/blob/af973d4647dc14c85555a2ad8f1aff08028ee3b7/packages/gatsby-source-contentful/src/gatsby-node.js#L75-L159)

0 commit comments

Comments
 (0)