Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gatsby): release plugin option validation #27437

Merged
merged 12 commits into from
Nov 2, 2020
1 change: 0 additions & 1 deletion .jestSetup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
process.env.GATSBY_RECIPES_NO_COLOR = "true"
process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION = "true"
7 changes: 1 addition & 6 deletions packages/gatsby-admin/gatsby-config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-react-helmet",
options: {
test: false,
},
},
"gatsby-plugin-react-helmet",
{
resolve: "gatsby-plugin-webfonts",
options: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,3 @@ exports[`Test plugin feed custom properties work properly 1`] = `"<?xml version=
exports[`Test plugin feed custom query runs 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><rss xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\" xmlns:atom=\\"http://www.w3.org/2005/Atom\\" version=\\"2.0\\"><channel><title><![CDATA[my feed]]></title><description><![CDATA[a description]]></description><link>http://github.com/dylang/node-rss</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 01 Jan 2018 00:00:00 GMT</lastBuildDate><item><title><![CDATA[No title]]></title><description><![CDATA[post description]]></description><link>http://dummy.url/a-custom-path</link><guid isPermaLink=\\"true\\">http://dummy.url/a-custom-path</guid></item><item><title><![CDATA[No title]]></title><description><![CDATA[post description]]></description><link>http://dummy.url/another-custom-path</link><guid isPermaLink=\\"true\\">http://dummy.url/another-custom-path</guid></item></channel></rss>"`;

exports[`Test plugin feed default settings work properly 1`] = `"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><rss xmlns:dc=\\"http://purl.org/dc/elements/1.1/\\" xmlns:content=\\"http://purl.org/rss/1.0/modules/content/\\" xmlns:atom=\\"http://www.w3.org/2005/Atom\\" version=\\"2.0\\"><channel><title><![CDATA[a sample title]]></title><description><![CDATA[a description]]></description><link>http://github.com/dylang/node-rss</link><generator>GatsbyJS</generator><lastBuildDate>Mon, 01 Jan 2018 00:00:00 GMT</lastBuildDate><item><title><![CDATA[No title]]></title><description><![CDATA[post description]]></description><link>http://dummy.url/a-slug</link><guid isPermaLink=\\"false\\">http://dummy.url/a-slug</guid><content:encoded></content:encoded></item></channel></rss>"`;

exports[`Test plugin feed options validation throws when invalid plugin options 1`] = `[Error: [Config Validation]: "feeds[0].output" is required]`;

exports[`Test plugin feed options validation throws when invalid plugin options 2`] = `[Error: [Config Validation]: "feeds[0].query" is required]`;
104 changes: 1 addition & 103 deletions packages/gatsby-plugin-feed/src/__tests__/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
jest.mock(`fs-extra`)
const fs = require(`fs-extra`)
const path = require(`path`)
const { onPreBootstrap, onPostBuild } = require(`../gatsby-node`)
const { onPostBuild } = require(`../gatsby-node`)
const DATE_TO_USE = new Date(`2018`)
const _Date = Date
global.Date = jest.fn(() => DATE_TO_USE)
Expand All @@ -15,108 +15,6 @@ describe(`Test plugin feed`, () => {
fs.mkdirp = jest.fn().mockResolvedValue()
})

describe(`options validation`, () => {
const setup = async options => {
const reporter = {
stripIndent: jest.fn(value => value.trim()),
warn: jest.fn(),
}
await onPreBootstrap({ reporter }, options)

return [reporter, options]
}

const deprecationNotice = `This behavior will be removed in the next major release of gatsby-plugin-feed`

it(`removes plugins`, async () => {
const options = { plugins: [] }

await setup(options)

expect(options.plugins).toBeUndefined()
})

it(`warns when feeds is not supplied`, async () => {
const options = {}

const [reporter] = await setup(options)

expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenCalledWith(
expect.stringContaining(deprecationNotice)
)
})

it(`warns when individual feed does not have title`, async () => {
const options = {
feeds: [
{
output: `rss.xml`,
query: `{}`,
serialize: () => {},
},
],
}

const [reporter] = await setup(options)

expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenCalledWith(
expect.stringContaining(`title`)
)
})

it(`warns when individual feed does not have serialize function`, async () => {
const options = {
feeds: [
{
output: `rss.xml`,
query: `{}`,
title: `my feed`,
},
],
}

const [reporter] = await setup(options)

expect(reporter.warn).toHaveBeenCalledTimes(1)
expect(reporter.warn).toHaveBeenCalledWith(
expect.stringContaining(deprecationNotice)
)
})

it(`throws when invalid plugin options`, async () => {
const invalidOptions = [
{
feeds: [
{
// output is missing
query: `{}`,
},
],
},
{
feeds: [
{
output: `rss.xml`,
// query is missing
},
],
},
]

for (let options of invalidOptions) {
try {
await setup(options)
} catch (e) {
expect(e).toMatchSnapshot()
}
}

expect.assertions(invalidOptions.length)
})
})

it(`default settings work properly`, async () => {
fs.writeFile = jest.fn()
fs.writeFile.mockResolvedValue(true)
Expand Down
61 changes: 1 addition & 60 deletions packages/gatsby-plugin-feed/src/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@ import fs from "fs-extra"
import path from "path"
import RSS from "rss"
import merge from "lodash.merge"
import { Joi } from "gatsby-plugin-utils"

import { defaultOptions, runQuery } from "./internals"
import pluginOptionsSchema from "./plugin-options"

const publicPath = `./public`

const warnMessage = (error, behavior) => `
gatsby-plugin-feed was initialized in gatsby-config.js without a ${error}.
This means that the plugin will use ${behavior}, which may not match your use case.
This behavior will be removed in the next major release of gatsby-plugin-feed.
For more info, check out: https://gatsby.dev/adding-rss-feed
`

if (process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION) {
exports.pluginOptionsSchema = pluginOptionsSchema
}
exports.pluginOptionsSchema = pluginOptionsSchema

// TODO: remove in the next major release
// A default function to transform query data into feed entries.
Expand All @@ -33,55 +23,6 @@ const serialize = ({ query: { site, allMarkdownRemark } }) =>
}
})

exports.onPreBootstrap = async function onPreBootstrap(
{ reporter },
pluginOptions
) {
delete pluginOptions.plugins

try {
// TODO: remove this once pluginOptionsSchema is stable
const { value: normalized, error } = await pluginOptionsSchema({
Joi,
}).validate(pluginOptions, {
externals: false,
})

if (error) throw error

if (!normalized.feeds) {
reporter.warn(
reporter.stripIndent(
warnMessage(`feeds option`, `the internal RSS feed creation`)
)
)
} else if (normalized.feeds.some(feed => typeof feed.title !== `string`)) {
reporter.warn(
reporter.stripIndent(
warnMessage(`title in a feed`, `the default feed title`)
)
)
} else if (
normalized.feeds.some(feed => typeof feed.serialize !== `function`)
) {
reporter.warn(
reporter.stripIndent(
warnMessage(
`serialize function in a feed`,
`the internal serialize function`
)
)
)
}
} catch (e) {
throw new Error(
e.details
.map(detail => `[Config Validation]: ${detail.message}`)
.join(`\n`)
)
}
}

exports.onPostBuild = async ({ graphql }, pluginOptions) => {
/*
* Run the site settings query to gather context, then
Expand Down
82 changes: 36 additions & 46 deletions packages/gatsby-plugin-google-analytics/src/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,38 @@
if (process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION) {
exports.pluginOptionsSchema = ({ Joi }) =>
// TODO: make sure that trackingId gets required() when releasing a major version
Joi.object({
trackingId: Joi.string().description(
`The property ID; the tracking code won't be generated without it`
exports.pluginOptionsSchema = ({ Joi }) =>
// TODO: make sure that trackingId gets required() when releasing a major version
Joi.object({
trackingId: Joi.string().description(
`The property ID; the tracking code won't be generated without it`
),
head: Joi.boolean()
.default(false)
.description(
`Defines where to place the tracking script - \`true\` in the head and \`false\` in the body`
),
head: Joi.boolean()
.default(false)
.description(
`Defines where to place the tracking script - \`true\` in the head and \`false\` in the body`
),
anonymize: Joi.boolean().default(false),
respectDNT: Joi.boolean().default(false),
exclude: Joi.array()
.items(Joi.string())
.default([])
.description(`Avoids sending pageview hits from custom paths`),
pageTransitionDelay: Joi.number()
.default(0)
.description(
`Delays sending pageview hits on route update (in milliseconds)`
),
optimizeId: Joi.string().description(
`Enables Google Optimize using your container Id`
anonymize: Joi.boolean().default(false),
respectDNT: Joi.boolean().default(false),
exclude: Joi.array()
.items(Joi.string())
.default([])
.description(`Avoids sending pageview hits from custom paths`),
pageTransitionDelay: Joi.number()
.default(0)
.description(
`Delays sending pageview hits on route update (in milliseconds)`
),
experimentId: Joi.string().description(
`Enables Google Optimize Experiment ID`
),
variationId: Joi.string().description(
`Set Variation ID. 0 for original 1,2,3....`
),
defer: Joi.boolean().description(
`Defers execution of google analytics script after page load`
),
sampleRate: Joi.number(),
siteSpeedSampleRate: Joi.number(),
cookieDomain: Joi.string(),
})
} else {
exports.onPreInit = ({ reporter }, { trackingId } = {}) => {
if (!trackingId) {
reporter.warn(
`The Google Analytics plugin requires a tracking ID. Did you mean to add it?`
)
}
}
}
optimizeId: Joi.string().description(
`Enables Google Optimize using your container Id`
),
experimentId: Joi.string().description(
`Enables Google Optimize Experiment ID`
),
variationId: Joi.string().description(
`Set Variation ID. 0 for original 1,2,3....`
),
defer: Joi.boolean().description(
`Defers execution of google analytics script after page load`
),
sampleRate: Joi.number(),
siteSpeedSampleRate: Joi.number(),
cookieDomain: Joi.string(),
})
52 changes: 25 additions & 27 deletions packages/gatsby-plugin-google-tagmanager/src/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,31 @@ exports.onPreInit = (args, options) => {
}
}

if (process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION) {
exports.pluginOptionsSchema = ({ Joi }) =>
Joi.object({
id: Joi.string().description(
`Google Tag Manager ID that can be found in your Tag Manager dashboard.`
exports.pluginOptionsSchema = ({ Joi }) =>
Joi.object({
id: Joi.string().description(
`Google Tag Manager ID that can be found in your Tag Manager dashboard.`
),
includeInDevelopment: Joi.boolean()
.default(false)
.description(
`Include Google Tag Manager when running in development mode.`
),
includeInDevelopment: Joi.boolean()
.default(false)
.description(
`Include Google Tag Manager when running in development mode.`
),
defaultDataLayer: Joi.object()
.default(null)
.description(
`Data layer to be set before Google Tag Manager is loaded. Should be an object or a function.`
),
gtmAuth: Joi.string().description(
`Google Tag Manager environment auth string.`
defaultDataLayer: Joi.object()
.default(null)
.description(
`Data layer to be set before Google Tag Manager is loaded. Should be an object or a function.`
),
gtmPreview: Joi.string().description(
`Google Tag Manager environment preview name.`
gtmAuth: Joi.string().description(
`Google Tag Manager environment auth string.`
),
gtmPreview: Joi.string().description(
`Google Tag Manager environment preview name.`
),
dataLayerName: Joi.string().description(`Data layer name.`),
routeChangeEventName: Joi.string()
.default(`gatsby-route-change`)
.description(
`Name of the event that is triggered on every Gatsby route change.`
),
dataLayerName: Joi.string().description(`Data layer name.`),
routeChangeEventName: Joi.string()
.default(`gatsby-route-change`)
.description(
`Name of the event that is triggered on every Gatsby route change.`
),
})
}
})
Loading