diff --git a/docs/docs/debugging-replace-renderer-api.md b/docs/docs/debugging-replace-renderer-api.md
new file mode 100644
index 0000000000000..999fc07e9dcb2
--- /dev/null
+++ b/docs/docs/debugging-replace-renderer-api.md
@@ -0,0 +1,113 @@
+---
+title: Debugging replaceRenderer API
+---
+
+## What is the `replaceRenderer` API?
+
+The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) extension APIs](/docs/ssr-apis/#replaceRenderer). This API is called when you run `gatsby build` and is used to customise how Gatsby renders your static content. It can be implemented by any Gatsby plugin or your `gatsby-ssr.js` file - adding support for Redux, CSS-in-JS libraries or any code that needs to change Gatsby's default HTML output.
+
+## Why does it cause build errors?
+
+If multiple plugins implement `replaceRenderer` in your project, only the last plugin implementing the API can be called - which will break your site builds.
+
+Note that `replaceRenderer` is only used during `gatsby build`. It won't cause problems as you work on your site with `gatsby develop`.
+
+If multiple plugins implement `replaceRenderer`, `gatsby build` will warn you:
+
+```
+The "replaceRenderer" API is implemented by several enabled plugins.
+This could be an error, see https://gatsbyjs.org/docs/debugging-replace-renderer-api for workarounds.
+Check the following plugins for "replaceRenderer" implementations:
+/path/to/my/site/node_modules/gatsby-plugin-styled-components/gatsby-ssr.js
+/path/to/my/site/gatsby-ssr.js
+```
+
+## Fixing `replaceRenderer` build errors
+
+If you see errors during your build, you can fix them with the following steps.
+
+### 1. Identify the plugins using `replaceRenderer`
+
+Your error message should list the files that use `replaceRenderer`
+
+```shell
+Check the following files for "replaceRenderer" implementations:
+/path/to/my/site/node_modules/gatsby-plugin-styled-components/gatsby-ssr.js
+/path/to/my/site/gatsby-ssr.js
+```
+
+In this example, your `gatsby-ssr.js` file and `gatsby-plugin-styled-components` are both using `replaceRenderer`.
+
+### 2. Copy the plugins' `replaceRenderer` functionality to your site's `gatsby-ssr.js` file
+
+You'll need to override your plugins' `replaceRenderer` code in your `gatsby-ssr.js` file. This step will be different for each project, keep reading to see an example.
+
+## Example
+
+### Initial setup
+
+In this example project we're using [`redux`](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-redux) and [Gatsby's Styled Components plugin](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-styled-components).
+
+`gatsby-config.js`
+
+```js
+module.exports = {
+ plugins: [`gatsby-plugin-styled-components`],
+}
+```
+
+`gatsby-ssr.js` (based on the [using Redux example](https://github.com/gatsbyjs/gatsby/blob/master/examples/using-redux/gatsby-ssr.js))
+
+```js
+import React from "react"
+import { Provider } from "react-redux"
+import { renderToString } from "react-dom/server"
+
+import createStore from "./src/state/createStore"
+
+exports.replaceRenderer = ({ bodyComponent, replaceBodyHTMLString }) => {
+ const store = createStore()
+
+ const ConnectedBody = () => {bodyComponent}
+ replaceBodyHTMLString(renderToString())
+}
+```
+
+Note that the Styled Components plugin uses `replaceRenderer`, and the code in `gatsby-ssr.js` also uses `replaceRenderer`.
+
+### Fixing the `replaceRenderer` error
+
+Our `gatsby-config.js` file will remain unchanged. However, oour `gatsby-ssr.js` file will update to include the [`replaceRenderer` functionality from the Styled Components plugin](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-plugin-styled-components/src/gatsby-ssr.js)
+
+`gatsby-ssr.js`
+
+```js
+import React from "react"
+import { Provider } from "react-redux"
+import { renderToString } from "react-dom/server"
+import { ServerStyleSheet, StyleSheetManager } from "styled-components"
+import createStore from "./src/state/createStore"
+
+exports.replaceRenderer = ({
+ bodyComponent,
+ replaceBodyHTMLString,
+ setHeadComponents,
+}) => {
+ const sheet = new ServerStyleSheet()
+ const store = createStore()
+
+ const app = () => (
+
+
+ {bodyComponent}
+
+
+ )
+ replaceBodyHTMLString(renderToString())
+ setHeadComponents([sheet.getStyleElement()])
+}
+```
+
+Now `gatsby-ssr.js` implements the Styled Components and Redux functionality using one `replaceRenderer` instance. Run `gatsby build` and the site will build without errors.
+
+All the code from this example is [available on GitHub](https://github.com/m-allanson/gatsby-replace-renderer-example/commits/master).
diff --git a/packages/gatsby/cache-dir/api-runner-browser.js b/packages/gatsby/cache-dir/api-runner-browser.js
index b68e5ba1970c7..bd0b255d90c8f 100644
--- a/packages/gatsby/cache-dir/api-runner-browser.js
+++ b/packages/gatsby/cache-dir/api-runner-browser.js
@@ -1,8 +1,14 @@
// During bootstrap, we write requires at top of this file which looks
// basically like:
// var plugins = [
-// require('/path/to/plugin1/gatsby-browser.js'),
-// require('/path/to/plugin2/gatsby-browser.js'),
+// {
+// plugin: require("/path/to/plugin1/gatsby-browser.js"),
+// options: { ... },
+// },
+// {
+// plugin: require("/path/to/plugin2/gatsby-browser.js"),
+// options: { ... },
+// },
// ]
export function apiRunner(api, args, defaultReturn) {
diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js
index 3fc53b5a72a1e..f5891ff43eaa2 100644
--- a/packages/gatsby/cache-dir/api-runner-ssr.js
+++ b/packages/gatsby/cache-dir/api-runner-ssr.js
@@ -1,15 +1,23 @@
// During bootstrap, we write requires at top of this file which looks like:
// var plugins = [
-// require('/path/to/plugin1/gatsby-ssr.js'),
-// require('/path/to/plugin2/gatsby-ssr.js'),
+// {
+// plugin: require("/path/to/plugin1/gatsby-ssr.js"),
+// options: { ... },
+// },
+// {
+// plugin: require("/path/to/plugin2/gatsby-ssr.js"),
+// options: { ... },
+// },
// ]
const apis = require(`./api-ssr-docs`)
+// Run the specified API in any plugins that have implemented it
module.exports = (api, args, defaultReturn) => {
if (!apis[api]) {
console.log(`This API doesn't exist`, api)
}
+
// Run each plugin in series.
let results = plugins.map(plugin => {
if (plugin.plugin[api]) {
diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js
index 2c4512665720a..c763978352eac 100644
--- a/packages/gatsby/cache-dir/develop-static-entry.js
+++ b/packages/gatsby/cache-dir/develop-static-entry.js
@@ -1,8 +1,8 @@
import React from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { merge } from "lodash"
-import apiRunner from "./api-runner-ssr"
import testRequireError from "./test-require-error"
+import apiRunner from "./api-runner-ssr"
let HTML
try {
@@ -17,7 +17,6 @@ try {
}
module.exports = (locals, callback) => {
- // const apiRunner = require(`${directory}/.cache/api-runner-ssr`)
let headComponents = []
let htmlAttributes = {}
let bodyAttributes = {}
diff --git a/packages/gatsby/src/bootstrap/__mocks__/resolve-module-exports.js b/packages/gatsby/src/bootstrap/__mocks__/resolve-module-exports.js
new file mode 100644
index 0000000000000..8bd33b3a6a031
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/resolve-module-exports.js
@@ -0,0 +1,19 @@
+'use strict'
+
+let mockResults = {}
+
+module.exports = input => {
+ // return a mocked result
+ if (typeof input === `string`) {
+ return mockResults[input]
+ }
+
+ // return default result
+ if (typeof input !== `object`) {
+ return []
+ }
+
+ // set mock results
+ mockResults = Object.assign({}, input)
+ return undefined
+}
diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js
index 89aa3ef84cabf..2c2b5b9157242 100644
--- a/packages/gatsby/src/bootstrap/index.js
+++ b/packages/gatsby/src/bootstrap/index.js
@@ -1,13 +1,13 @@
/* @flow */
const Promise = require(`bluebird`)
-const glob = require(`glob`)
const _ = require(`lodash`)
const slash = require(`slash`)
const fs = require(`fs-extra`)
const md5File = require(`md5-file/promise`)
const crypto = require(`crypto`)
const del = require(`del`)
+const path = require(`path`)
const apiRunnerNode = require(`../utils/api-runner-node`)
const { graphql } = require(`graphql`)
@@ -175,9 +175,17 @@ module.exports = async (args: BootstrapArgs) => {
// Find plugins which implement gatsby-browser and gatsby-ssr and write
// out api-runners for them.
- const hasAPIFile = (env, plugin) =>
- // TODO make this async...
- glob.sync(`${plugin.resolve}/gatsby-${env}*`)[0]
+ const hasAPIFile = (env, plugin) => {
+ // The plugin loader has disabled SSR APIs for this plugin. Usually due to
+ // multiple implementations of an API that can only be implemented once
+ if (env === `ssr` && plugin.skipSSR === true) return undefined
+
+ const envAPIs = plugin[`${env}APIs`]
+ if (envAPIs && Array.isArray(envAPIs) && envAPIs.length > 0 ) {
+ return path.join(plugin.resolve, `gatsby-${env}.js`)
+ }
+ return undefined
+ }
const ssrPlugins = _.filter(
flattenedPlugins.map(plugin => {
diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js
deleted file mode 100644
index 3203d144c4d47..0000000000000
--- a/packages/gatsby/src/bootstrap/load-plugins.js
+++ /dev/null
@@ -1,342 +0,0 @@
-const _ = require(`lodash`)
-const slash = require(`slash`)
-const fs = require(`fs`)
-const path = require(`path`)
-const crypto = require(`crypto`)
-const glob = require(`glob`)
-
-const { store } = require(`../redux`)
-const nodeAPIs = require(`../utils/api-node-docs`)
-const browserAPIs = require(`../utils/api-browser-docs`)
-const ssrAPIs = require(`../../cache-dir/api-ssr-docs`)
-const resolveModuleExports = require(`./resolve-module-exports`)
-
-// Given a plugin object, an array of the API names it exports and an
-// array of valid API names, return an array of invalid API exports.
-const getBadExports = (plugin, pluginAPIKeys, apis) => {
- let badExports = []
- // Discover any exports from plugins which are not "known"
- badExports = badExports.concat(
- _.difference(pluginAPIKeys, apis).map(e => {
- return {
- exportName: e,
- pluginName: plugin.name,
- pluginVersion: plugin.version,
- }
- })
- )
- return badExports
-}
-
-const getBadExportsMessage = (badExports, exportType, apis) => {
- const { stripIndent } = require(`common-tags`)
- const stringSimiliarity = require(`string-similarity`)
- let capitalized = `${exportType[0].toUpperCase()}${exportType.slice(1)}`
- if (capitalized === `Ssr`) capitalized = `SSR`
-
- let message = `\n`
- message += stripIndent`
- Your plugins must export known APIs from their gatsby-${exportType}.js.
- The following exports aren't APIs. Perhaps you made a typo or
- your plugin is outdated?
-
- See https://www.gatsbyjs.org/docs/${exportType}-apis/ for the list of Gatsby ${capitalized} APIs`
-
- badExports.forEach(bady => {
- const similarities = stringSimiliarity.findBestMatch(bady.exportName, apis)
- message += `\n — `
- if (bady.pluginName == `default-site-plugin`) {
- message += `Your site's gatsby-${exportType}.js is exporting a variable named "${
- bady.exportName
- }" which isn't an API.`
- } else {
- message += `The plugin "${bady.pluginName}@${
- bady.pluginVersion
- }" is exporting a variable named "${bady.exportName}" which isn't an API.`
- }
- if (similarities.bestMatch.rating > 0.5) {
- message += ` Perhaps you meant to export "${
- similarities.bestMatch.target
- }"?`
- }
- })
-
- return message
-}
-
-function createFileContentHash(root, globPattern) {
- const hash = crypto.createHash(`md5`)
- const files = glob.sync(`${root}/${globPattern}`, { nodir: true })
-
- files.forEach(filepath => {
- hash.update(fs.readFileSync(filepath))
- })
-
- return hash.digest(`hex`)
-}
-
-/**
- * @typedef {Object} PluginInfo
- * @property {string} resolve The absolute path to the plugin
- * @property {string} name The plugin name
- * @property {string} version The plugin version (can be content hash)
- */
-
-/**
- * resolvePlugin
- * @param {string} pluginName
- * This can be a name of a local plugin, the name of a plugin located in
- * node_modules, or a Gatsby internal plugin. In the last case the pluginName
- * will be an absolute path.
- * @return {PluginInfo}
- */
-function resolvePlugin(pluginName) {
- // Only find plugins when we're not given an absolute path
- if (!fs.existsSync(pluginName)) {
- // Find the plugin in the local plugins folder
- const resolvedPath = slash(path.resolve(`./plugins/${pluginName}`))
-
- if (fs.existsSync(resolvedPath)) {
- if (fs.existsSync(`${resolvedPath}/package.json`)) {
- const packageJSON = JSON.parse(
- fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
- )
-
- return {
- resolve: resolvedPath,
- name: packageJSON.name || pluginName,
- id: `Plugin ${packageJSON.name || pluginName}`,
- version:
- packageJSON.version || createFileContentHash(resolvedPath, `**`),
- }
- } else {
- // Make package.json a requirement for local plugins too
- throw new Error(`Plugin ${pluginName} requires a package.json file`)
- }
- }
- }
-
- /**
- * Here we have an absolute path to an internal plugin, or a name of a module
- * which should be located in node_modules.
- */
- try {
- const resolvedPath = slash(path.dirname(require.resolve(pluginName)))
-
- const packageJSON = JSON.parse(
- fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
- )
-
- return {
- resolve: resolvedPath,
- id: `Plugin ${packageJSON.name}`,
- name: packageJSON.name,
- version: packageJSON.version,
- }
- } catch (err) {
- throw new Error(`Unable to find plugin "${pluginName}"`)
- }
-}
-
-module.exports = async (config = {}) => {
- // Instantiate plugins.
- const plugins = []
-
- // Create fake little site with a plugin for testing this
- // w/ snapshots. Move plugin processing to its own module.
- // Also test adding to redux store.
- const processPlugin = plugin => {
- if (_.isString(plugin)) {
- const info = resolvePlugin(plugin)
-
- return {
- ...info,
- pluginOptions: {
- plugins: [],
- },
- }
- } else {
- // Plugins can have plugins.
- const subplugins = []
- if (plugin.options && plugin.options.plugins) {
- plugin.options.plugins.forEach(p => {
- subplugins.push(processPlugin(p))
- })
-
- plugin.options.plugins = subplugins
- }
-
- // Add some default values for tests as we don't actually
- // want to try to load anything during tests.
- if (plugin.resolve === `___TEST___`) {
- return {
- name: `TEST`,
- pluginOptions: {
- plugins: [],
- },
- }
- }
-
- const info = resolvePlugin(plugin.resolve)
-
- return {
- ...info,
- pluginOptions: _.merge({ plugins: [] }, plugin.options),
- }
- }
- }
-
- // Add internal plugins
- plugins.push(
- processPlugin(path.join(__dirname, `../internal-plugins/dev-404-page`))
- )
- plugins.push(
- processPlugin(
- path.join(__dirname, `../internal-plugins/component-page-creator`)
- )
- )
- plugins.push(
- processPlugin(
- path.join(__dirname, `../internal-plugins/component-layout-creator`)
- )
- )
- plugins.push(
- processPlugin(
- path.join(__dirname, `../internal-plugins/internal-data-bridge`)
- )
- )
- plugins.push(
- processPlugin(path.join(__dirname, `../internal-plugins/prod-404`))
- )
- plugins.push(
- processPlugin(path.join(__dirname, `../internal-plugins/query-runner`))
- )
-
- // Add plugins from the site config.
- if (config.plugins) {
- config.plugins.forEach(plugin => {
- plugins.push(processPlugin(plugin))
- })
- }
-
- // Add the site's default "plugin" i.e. gatsby-x files in root of site.
- plugins.push({
- resolve: slash(process.cwd()),
- id: `Plugin default-site-plugin`,
- name: `default-site-plugin`,
- version: createFileContentHash(process.cwd(), `gatsby-*`),
- pluginOptions: {
- plugins: [],
- },
- })
-
- // Create a "flattened" array of plugins with all subplugins
- // brought to the top-level. This simplifies running gatsby-* files
- // for subplugins.
- const flattenedPlugins = []
- const extractPlugins = plugin => {
- plugin.pluginOptions.plugins.forEach(subPlugin => {
- flattenedPlugins.push(subPlugin)
- extractPlugins(subPlugin)
- })
- }
-
- plugins.forEach(plugin => {
- flattenedPlugins.push(plugin)
- extractPlugins(plugin)
- })
-
- // Validate plugins before saving. Plugins can only export known APIs. The known
- // APIs that a plugin supports are saved along with the plugin in the store for
- // easier filtering later. If there are bad exports (either typos, outdated, or
- // plain incorrect), then we output a readable error & quit.
- const apis = {}
- apis.node = _.keys(nodeAPIs)
- apis.browser = _.keys(browserAPIs)
- apis.ssr = _.keys(ssrAPIs)
-
- const allAPIs = [...apis.node, ...apis.browser, ...apis.ssr]
-
- const apiToPlugins = allAPIs.reduce((acc, value) => {
- acc[value] = []
- return acc
- }, {})
-
- const badExports = {
- node: [],
- browser: [],
- ssr: [],
- }
-
- flattenedPlugins.forEach(plugin => {
- plugin.nodeAPIs = []
- plugin.browserAPIs = []
- plugin.ssrAPIs = []
-
- // Discover which APIs this plugin implements and store an array against
- // the plugin node itself *and* in an API to plugins map for faster lookups
- // later.
- const pluginNodeExports = resolveModuleExports(
- `${plugin.resolve}/gatsby-node`
- )
- const pluginBrowserExports = resolveModuleExports(
- `${plugin.resolve}/gatsby-browser`
- )
- const pluginSSRExports = resolveModuleExports(
- `${plugin.resolve}/gatsby-ssr`
- )
-
- if (pluginNodeExports.length > 0) {
- plugin.nodeAPIs = _.intersection(pluginNodeExports, apis.node)
- plugin.nodeAPIs.map(nodeAPI => apiToPlugins[nodeAPI].push(plugin.name))
- badExports.node = getBadExports(plugin, pluginNodeExports, apis.node) // Collate any bad exports
- }
-
- if (pluginBrowserExports.length > 0) {
- plugin.browserAPIs = _.intersection(pluginBrowserExports, apis.browser)
- plugin.browserAPIs.map(browserAPI =>
- apiToPlugins[browserAPI].push(plugin.name)
- )
- badExports.browser = getBadExports(
- plugin,
- pluginBrowserExports,
- apis.browser
- ) // Collate any bad exports
- }
-
- if (pluginSSRExports.length > 0) {
- plugin.ssrAPIs = _.intersection(pluginSSRExports, apis.ssr)
- plugin.ssrAPIs.map(ssrAPI => apiToPlugins[ssrAPI].push(plugin.name))
- badExports.ssr = getBadExports(plugin, pluginSSRExports, apis.ssr) // Collate any bad exports
- }
- })
-
- // Output error messages for all bad exports
- let bad = false
- _.toPairs(badExports).forEach(bad => {
- const [exportType, entries] = bad
- if (entries.length > 0) {
- bad = true
- console.log(getBadExportsMessage(entries, exportType, apis[exportType]))
- }
- })
-
- if (bad) process.exit()
-
- store.dispatch({
- type: `SET_SITE_PLUGINS`,
- payload: plugins,
- })
-
- store.dispatch({
- type: `SET_SITE_FLATTENED_PLUGINS`,
- payload: flattenedPlugins,
- })
-
- store.dispatch({
- type: `SET_SITE_API_TO_PLUGINS`,
- payload: apiToPlugins,
- })
-
- return flattenedPlugins
-}
diff --git a/packages/gatsby/src/bootstrap/__tests__/__snapshots__/load-plugins.js.snap b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.js.snap
similarity index 98%
rename from packages/gatsby/src/bootstrap/__tests__/__snapshots__/load-plugins.js.snap
rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.js.snap
index 4609e91b310ea..7148fa293ff8e 100644
--- a/packages/gatsby/src/bootstrap/__tests__/__snapshots__/load-plugins.js.snap
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/load-plugins.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Load plugins Loads plugins defined with an object but without an option key 1`] = `
+exports[`Load plugins Load plugins for a site 1`] = `
Array [
Object {
"browserAPIs": Array [],
@@ -88,16 +88,6 @@ Array [
"ssrAPIs": Array [],
"version": "1.0.0",
},
- Object {
- "browserAPIs": Array [],
- "name": "TEST",
- "nodeAPIs": Array [],
- "pluginOptions": Object {
- "plugins": Array [],
- },
- "resolve": "",
- "ssrAPIs": Array [],
- },
Object {
"browserAPIs": Array [],
"id": "Plugin default-site-plugin",
@@ -113,7 +103,7 @@ Array [
]
`;
-exports[`Load plugins load plugins for a site 1`] = `
+exports[`Load plugins Loads plugins defined with an object but without an option key 1`] = `
Array [
Object {
"browserAPIs": Array [],
@@ -201,6 +191,16 @@ Array [
"ssrAPIs": Array [],
"version": "1.0.0",
},
+ Object {
+ "browserAPIs": Array [],
+ "name": "TEST",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "",
+ "ssrAPIs": Array [],
+ },
Object {
"browserAPIs": Array [],
"id": "Plugin default-site-plugin",
diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap
new file mode 100644
index 0000000000000..28d5ce3f1054d
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap
@@ -0,0 +1,259 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`collatePluginAPIs Identifies APIs used by a site's plugins 1`] = `
+Object {
+ "apiToPlugins": Object {
+ "browser-1": Array [
+ "foo-plugin",
+ ],
+ "browser-2": Array [
+ "foo-plugin",
+ "default-site-plugin",
+ ],
+ "browser-3": Array [
+ "default-site-plugin",
+ ],
+ "browser-4": Array [],
+ "node-1": Array [
+ "foo-plugin",
+ ],
+ "node-2": Array [
+ "foo-plugin",
+ "default-site-plugin",
+ ],
+ "node-3": Array [
+ "default-site-plugin",
+ ],
+ "node-4": Array [],
+ "ssr-1": Array [
+ "foo-plugin",
+ ],
+ "ssr-2": Array [
+ "foo-plugin",
+ "default-site-plugin",
+ ],
+ "ssr-3": Array [
+ "default-site-plugin",
+ ],
+ "ssr-4": Array [],
+ },
+ "badExports": Object {
+ "browser": Array [],
+ "node": Array [],
+ "ssr": Array [],
+ },
+ "flattenedPlugins": Array [
+ Object {
+ "browserAPIs": Array [
+ "browser-1",
+ "browser-2",
+ ],
+ "id": "Plugin foo",
+ "name": "foo-plugin",
+ "nodeAPIs": Array [
+ "node-1",
+ "node-2",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "/foo",
+ "ssrAPIs": Array [
+ "ssr-1",
+ "ssr-2",
+ ],
+ "version": "1.0.0",
+ },
+ Object {
+ "browserAPIs": Array [
+ "browser-2",
+ "browser-3",
+ ],
+ "id": "Plugin default-site-plugin",
+ "name": "default-site-plugin",
+ "nodeAPIs": Array [
+ "node-2",
+ "node-3",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "/bar",
+ "ssrAPIs": Array [
+ "ssr-2",
+ "ssr-3",
+ ],
+ "version": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ ],
+}
+`;
+
+exports[`collatePluginAPIs Identifies incorrect APIs used by a site's plugins 1`] = `
+Object {
+ "apiToPlugins": Object {
+ "browser-1": Array [
+ "foo-plugin",
+ ],
+ "browser-2": Array [
+ "foo-plugin",
+ ],
+ "browser-3": Array [],
+ "browser-4": Array [],
+ "node-1": Array [
+ "foo-plugin",
+ ],
+ "node-2": Array [
+ "foo-plugin",
+ ],
+ "node-3": Array [],
+ "node-4": Array [],
+ "ssr-1": Array [
+ "foo-plugin",
+ ],
+ "ssr-2": Array [
+ "foo-plugin",
+ ],
+ "ssr-3": Array [],
+ "ssr-4": Array [],
+ },
+ "badExports": Object {
+ "browser": Array [
+ Object {
+ "exportName": "bad-browser-2",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ Object {
+ "exportName": "bad-browser-3",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ ],
+ "node": Array [
+ Object {
+ "exportName": "bad-node-2",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ Object {
+ "exportName": "bad-node-3",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ ],
+ "ssr": Array [
+ Object {
+ "exportName": "bad-ssr-2",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ Object {
+ "exportName": "bad-ssr-3",
+ "pluginName": "default-site-plugin",
+ "pluginVersion": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ ],
+ },
+ "flattenedPlugins": Array [
+ Object {
+ "browserAPIs": Array [
+ "browser-1",
+ "browser-2",
+ ],
+ "id": "Plugin foo",
+ "name": "foo-plugin",
+ "nodeAPIs": Array [
+ "node-1",
+ "node-2",
+ ],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "/foo",
+ "ssrAPIs": Array [
+ "ssr-1",
+ "ssr-2",
+ ],
+ "version": "1.0.0",
+ },
+ Object {
+ "browserAPIs": Array [],
+ "id": "Plugin default-site-plugin",
+ "name": "default-site-plugin",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "/bad-apis",
+ "ssrAPIs": Array [],
+ "version": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+ ],
+}
+`;
+
+exports[`handleMultipleReplaceRenderers Does nothing when replaceRenderers is implemented once 1`] = `
+Array [
+ Object {
+ "browserAPIs": Array [],
+ "id": "Plugin foo",
+ "name": "foo-plugin",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "___TEST___",
+ "ssrAPIs": Array [
+ "replaceRenderer",
+ ],
+ "version": "1.0.0",
+ },
+ Object {
+ "browserAPIs": Array [],
+ "id": "Plugin default-site-plugin",
+ "name": "default-site-plugin",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "___TEST___",
+ "ssrAPIs": Array [],
+ "version": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+]
+`;
+
+exports[`handleMultipleReplaceRenderers Sets skipSSR when replaceRenderers is implemented more than once 1`] = `
+Array [
+ Object {
+ "browserAPIs": Array [],
+ "id": "Plugin foo",
+ "name": "foo-plugin",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "___TEST___",
+ "skipSSR": true,
+ "ssrAPIs": Array [
+ "replaceRenderer",
+ ],
+ "version": "1.0.0",
+ },
+ Object {
+ "browserAPIs": Array [],
+ "id": "Plugin default-site-plugin",
+ "name": "default-site-plugin",
+ "nodeAPIs": Array [],
+ "pluginOptions": Object {
+ "plugins": Array [],
+ },
+ "resolve": "___TEST___",
+ "ssrAPIs": Array [
+ "replaceRenderer",
+ ],
+ "version": "ec21d02c31ab044d027a1d2fcaeb4a79",
+ },
+]
+`;
diff --git a/packages/gatsby/src/bootstrap/__tests__/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js
similarity index 90%
rename from packages/gatsby/src/bootstrap/__tests__/load-plugins.js
rename to packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js
index 75922f986e18f..7e15fc064615d 100644
--- a/packages/gatsby/src/bootstrap/__tests__/load-plugins.js
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.js
@@ -1,7 +1,7 @@
-const loadPlugins = require(`../load-plugins`)
+const loadPlugins = require(`../index`)
describe(`Load plugins`, () => {
- it(`load plugins for a site`, async () => {
+ it(`Load plugins for a site`, async () => {
let plugins = await loadPlugins({ plugins: [] })
// Delete the resolve path as that varies depending
diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js
new file mode 100644
index 0000000000000..2f09e0d09b88c
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js
@@ -0,0 +1,189 @@
+jest.mock(`../../resolve-module-exports`)
+
+const {
+ collatePluginAPIs,
+ handleBadExports,
+ handleMultipleReplaceRenderers,
+} = require(`../validate`)
+
+describe(`collatePluginAPIs`, () => {
+ const MOCK_RESULTS = {
+ "/foo/gatsby-node": [`node-1`, `node-2`],
+ "/foo/gatsby-browser": [`browser-1`, `browser-2`],
+ "/foo/gatsby-ssr": [`ssr-1`, `ssr-2`],
+ "/bar/gatsby-node": [`node-2`, `node-3`],
+ "/bar/gatsby-browser": [`browser-2`, `browser-3`],
+ "/bar/gatsby-ssr": [`ssr-2`, `ssr-3`],
+ "/bad-apis/gatsby-node": [`bad-node-2`, `bad-node-3`],
+ "/bad-apis/gatsby-browser": [`bad-browser-2`, `bad-browser-3`],
+ "/bad-apis/gatsby-ssr": [`bad-ssr-2`, `bad-ssr-3`],
+ }
+
+ beforeEach(() => {
+ const resolveModuleExports = require(`../../resolve-module-exports`)
+ resolveModuleExports(MOCK_RESULTS)
+ })
+
+ it(`Identifies APIs used by a site's plugins`, async () => {
+ const apis = {
+ node: [`node-1`, `node-2`, `node-3`, `node-4`],
+ browser: [`browser-1`, `browser-2`, `browser-3`, `browser-4`],
+ ssr: [`ssr-1`, `ssr-2`, `ssr-3`, `ssr-4`],
+ }
+ const flattenedPlugins = [
+ {
+ resolve: `/foo`,
+ id: `Plugin foo`,
+ name: `foo-plugin`,
+ version: `1.0.0`,
+ pluginOptions: { plugins: [] },
+ },
+ {
+ resolve: `/bar`,
+ id: `Plugin default-site-plugin`,
+ name: `default-site-plugin`,
+ version: `ec21d02c31ab044d027a1d2fcaeb4a79`,
+ pluginOptions: { plugins: [] },
+ },
+ ]
+
+ let result = collatePluginAPIs({ apis, flattenedPlugins })
+ expect(result).toMatchSnapshot()
+ })
+
+ it(`Identifies incorrect APIs used by a site's plugins`, async () => {
+ const apis = {
+ node: [`node-1`, `node-2`, `node-3`, `node-4`],
+ browser: [`browser-1`, `browser-2`, `browser-3`, `browser-4`],
+ ssr: [`ssr-1`, `ssr-2`, `ssr-3`, `ssr-4`],
+ }
+ const flattenedPlugins = [
+ {
+ resolve: `/foo`,
+ id: `Plugin foo`,
+ name: `foo-plugin`,
+ version: `1.0.0`,
+ pluginOptions: { plugins: [] },
+ },
+ {
+ resolve: `/bad-apis`,
+ id: `Plugin default-site-plugin`,
+ name: `default-site-plugin`,
+ version: `ec21d02c31ab044d027a1d2fcaeb4a79`,
+ pluginOptions: { plugins: [] },
+ },
+ ]
+
+ let result = collatePluginAPIs({ apis, flattenedPlugins })
+ expect(result).toMatchSnapshot()
+ })
+})
+
+describe(`handleBadExports`, () => {
+ it(`Does nothing when there are no bad exports`, async () => {
+ const result = handleBadExports({
+ apis: {
+ node: [`these`, `can`, `be`],
+ browser: [`anything`, `as there`],
+ ssr: [`are no`, `bad errors`],
+ },
+ badExports: {
+ node: [],
+ browser: [],
+ ssr: [],
+ },
+ })
+
+ expect(result).toEqual(false)
+ })
+
+ it(`Returns true and logs a message when bad exports are detected`, async () => {
+ const result = handleBadExports({
+ apis: {
+ node: [``],
+ browser: [``],
+ ssr: [`notFoo`, `bar`],
+ },
+ badExports: {
+ node: [],
+ browser: [],
+ ssr: [
+ {
+ exportName: `foo`,
+ pluginName: `default-site-plugin`,
+ },
+ ],
+ },
+ })
+ // TODO: snapshot console.log()'s from handleBadExports?
+ expect(result).toEqual(true)
+ })
+})
+
+describe(`handleMultipleReplaceRenderers`, () => {
+ it(`Does nothing when replaceRenderers is implemented once`, async () => {
+ const apiToPlugins = {
+ replaceRenderer: [`foo-plugin`],
+ }
+
+ const flattenedPlugins = [
+ {
+ resolve: `___TEST___`,
+ id: `Plugin foo`,
+ name: `foo-plugin`,
+ version: `1.0.0`,
+ pluginOptions: { plugins: [] },
+ nodeAPIs: [],
+ browserAPIs: [],
+ ssrAPIs: [`replaceRenderer`],
+ },
+ {
+ resolve: `___TEST___`,
+ id: `Plugin default-site-plugin`,
+ name: `default-site-plugin`,
+ version: `ec21d02c31ab044d027a1d2fcaeb4a79`,
+ pluginOptions: { plugins: [] },
+ nodeAPIs: [],
+ browserAPIs: [],
+ ssrAPIs: [],
+ },
+ ]
+
+ const result = handleMultipleReplaceRenderers({ apiToPlugins, flattenedPlugins })
+
+ expect(result).toMatchSnapshot()
+ })
+
+ it(`Sets skipSSR when replaceRenderers is implemented more than once`, async () => {
+ const apiToPlugins = {
+ replaceRenderer: [`foo-plugin`, `default-site-plugin`],
+ }
+
+ const flattenedPlugins = [
+ {
+ resolve: `___TEST___`,
+ id: `Plugin foo`,
+ name: `foo-plugin`,
+ version: `1.0.0`,
+ pluginOptions: { plugins: [] },
+ nodeAPIs: [],
+ browserAPIs: [],
+ ssrAPIs: [`replaceRenderer`],
+ },
+ {
+ resolve: `___TEST___`,
+ id: `Plugin default-site-plugin`,
+ name: `default-site-plugin`,
+ version: `ec21d02c31ab044d027a1d2fcaeb4a79`,
+ pluginOptions: { plugins: [] },
+ nodeAPIs: [],
+ browserAPIs: [],
+ ssrAPIs: [`replaceRenderer`],
+ },
+ ]
+
+ const result = handleMultipleReplaceRenderers({ apiToPlugins, flattenedPlugins })
+
+ expect(result).toMatchSnapshot()
+ })
+})
diff --git a/packages/gatsby/src/bootstrap/load-plugins/index.js b/packages/gatsby/src/bootstrap/load-plugins/index.js
new file mode 100644
index 0000000000000..c197f7ddba7b8
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/load-plugins/index.js
@@ -0,0 +1,82 @@
+const _ = require(`lodash`)
+
+const { store } = require(`../../redux`)
+const nodeAPIs = require(`../../utils/api-node-docs`)
+const browserAPIs = require(`../../utils/api-browser-docs`)
+const ssrAPIs = require(`../../../cache-dir/api-ssr-docs`)
+const loadPlugins = require(`./load`)
+const {
+ collatePluginAPIs,
+ handleBadExports,
+ handleMultipleReplaceRenderers,
+} = require(`./validate`)
+
+const apis = {
+ node: _.keys(nodeAPIs),
+ browser: _.keys(browserAPIs),
+ ssr: _.keys(ssrAPIs),
+}
+
+// Create a "flattened" array of plugins with all subplugins
+// brought to the top-level. This simplifies running gatsby-* files
+// for subplugins.
+const flattenPlugins = plugins => {
+ const flattened = []
+ const extractPlugins = plugin => {
+ plugin.pluginOptions.plugins.forEach(subPlugin => {
+ flattened.push(subPlugin)
+ extractPlugins(subPlugin)
+ })
+ }
+
+ plugins.forEach(plugin => {
+ flattened.push(plugin)
+ extractPlugins(plugin)
+ })
+
+ return flattened
+}
+
+module.exports = async (config = {}) => {
+ // Collate internal plugins, site config plugins, site default plugins
+ const plugins = await loadPlugins(config)
+
+ // Create a flattened array of the plugins
+ let flattenedPlugins = flattenPlugins(plugins)
+
+ // Work out which plugins use which APIs, including those which are not
+ // valid Gatsby APIs, aka 'badExports'
+ const x = collatePluginAPIs({ apis, flattenedPlugins })
+ flattenedPlugins = x.flattenedPlugins
+ const apiToPlugins = x.apiToPlugins
+ const badExports = x.badExports
+
+ // Show errors for any non-Gatsby APIs exported from plugins
+ const isBad = handleBadExports({ apis, badExports })
+ if (isBad && process.env.NODE_ENV === `production`) process.exit(1) // TODO: change to panicOnBuild
+
+ // Show errors when ReplaceRenderer has been implemented multiple times
+ flattenedPlugins = handleMultipleReplaceRenderers({
+ apiToPlugins,
+ flattenedPlugins,
+ })
+
+ // If we get this far, everything looks good. Update the store
+ store.dispatch({
+ type: `SET_SITE_FLATTENED_PLUGINS`,
+ payload: flattenedPlugins,
+ })
+
+ store.dispatch({
+ type: `SET_SITE_API_TO_PLUGINS`,
+ payload: apiToPlugins,
+ })
+
+ // TODO: Is this used? plugins and flattenedPlugins may be out of sync
+ store.dispatch({
+ type: `SET_SITE_PLUGINS`,
+ payload: plugins,
+ })
+
+ return flattenedPlugins
+}
diff --git a/packages/gatsby/src/bootstrap/load-plugins/load.js b/packages/gatsby/src/bootstrap/load-plugins/load.js
new file mode 100644
index 0000000000000..139bc8e30a59f
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/load-plugins/load.js
@@ -0,0 +1,163 @@
+const _ = require(`lodash`)
+const slash = require(`slash`)
+const fs = require(`fs`)
+const path = require(`path`)
+const crypto = require(`crypto`)
+const glob = require(`glob`)
+
+function createFileContentHash(root, globPattern) {
+ const hash = crypto.createHash(`md5`)
+ const files = glob.sync(`${root}/${globPattern}`, { nodir: true })
+
+ files.forEach(filepath => {
+ hash.update(fs.readFileSync(filepath))
+ })
+
+ return hash.digest(`hex`)
+}
+
+/**
+ * @typedef {Object} PluginInfo
+ * @property {string} resolve The absolute path to the plugin
+ * @property {string} name The plugin name
+ * @property {string} version The plugin version (can be content hash)
+ */
+
+/**
+ * resolvePlugin
+ * @param {string} pluginName
+ * This can be a name of a local plugin, the name of a plugin located in
+ * node_modules, or a Gatsby internal plugin. In the last case the pluginName
+ * will be an absolute path.
+ * @return {PluginInfo}
+ */
+function resolvePlugin(pluginName) {
+ // Only find plugins when we're not given an absolute path
+ if (!fs.existsSync(pluginName)) {
+ // Find the plugin in the local plugins folder
+ const resolvedPath = slash(path.resolve(`../plugins/${pluginName}`))
+
+ if (fs.existsSync(resolvedPath)) {
+ if (fs.existsSync(`${resolvedPath}/package.json`)) {
+ const packageJSON = JSON.parse(
+ fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
+ )
+
+ return {
+ resolve: resolvedPath,
+ name: packageJSON.name || pluginName,
+ id: `Plugin ${packageJSON.name || pluginName}`,
+ version:
+ packageJSON.version || createFileContentHash(resolvedPath, `**`),
+ }
+ } else {
+ // Make package.json a requirement for local plugins too
+ throw new Error(`Plugin ${pluginName} requires a package.json file`)
+ }
+ }
+ }
+
+ /**
+ * Here we have an absolute path to an internal plugin, or a name of a module
+ * which should be located in node_modules.
+ */
+ try {
+ const resolvedPath = slash(path.dirname(require.resolve(pluginName)))
+
+ const packageJSON = JSON.parse(
+ fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
+ )
+
+ return {
+ resolve: resolvedPath,
+ id: `Plugin ${packageJSON.name}`,
+ name: packageJSON.name,
+ version: packageJSON.version,
+ }
+ } catch (err) {
+ throw new Error(`Unable to find plugin "${pluginName}"`)
+ }
+}
+
+module.exports = async (config = {}) => {
+ // Instantiate plugins.
+ const plugins = []
+
+ // Create fake little site with a plugin for testing this
+ // w/ snapshots. Move plugin processing to its own module.
+ // Also test adding to redux store.
+ const processPlugin = plugin => {
+ if (_.isString(plugin)) {
+ const info = resolvePlugin(plugin)
+
+ return {
+ ...info,
+ pluginOptions: {
+ plugins: [],
+ },
+ }
+ } else {
+ // Plugins can have plugins.
+ const subplugins = []
+ if (plugin.options && plugin.options.plugins) {
+ plugin.options.plugins.forEach(p => {
+ subplugins.push(processPlugin(p))
+ })
+
+ plugin.options.plugins = subplugins
+ }
+
+ // Add some default values for tests as we don't actually
+ // want to try to load anything during tests.
+ if (plugin.resolve === `___TEST___`) {
+ return {
+ name: `TEST`,
+ pluginOptions: {
+ plugins: [],
+ },
+ }
+ }
+
+ const info = resolvePlugin(plugin.resolve)
+
+ return {
+ ...info,
+ pluginOptions: _.merge({ plugins: [] }, plugin.options),
+ }
+ }
+ }
+
+ // Add internal plugins
+ const internalPlugins = [
+ `../../internal-plugins/dev-404-page`,
+ `../../internal-plugins/component-page-creator`,
+ `../../internal-plugins/component-layout-creator`,
+ `../../internal-plugins/internal-data-bridge`,
+ `../../internal-plugins/prod-404`,
+ `../../internal-plugins/query-runner`,
+ ]
+ internalPlugins.forEach(relPath => {
+ const absPath = path.join(__dirname, relPath)
+ plugins.push(processPlugin(absPath))
+ })
+
+ // Add plugins from the site config.
+ if (config.plugins) {
+ config.plugins.forEach(plugin => {
+ plugins.push(processPlugin(plugin))
+ })
+ }
+
+ // Add the site's default "plugin" i.e. gatsby-x files in root of site.
+ plugins.push({
+ resolve: slash(process.cwd()),
+ id: `Plugin default-site-plugin`,
+ name: `default-site-plugin`,
+ version: createFileContentHash(process.cwd(), `gatsby-*`),
+ pluginOptions: {
+ plugins: [],
+ },
+ })
+
+ return plugins
+}
diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.js b/packages/gatsby/src/bootstrap/load-plugins/validate.js
new file mode 100644
index 0000000000000..8da4ae65cf2ac
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/load-plugins/validate.js
@@ -0,0 +1,189 @@
+const _ = require(`lodash`)
+
+const reporter = require(`gatsby-cli/lib/reporter`)
+const resolveModuleExports = require(`../resolve-module-exports`)
+
+// Given a plugin object, an array of the API names it exports and an
+// array of valid API names, return an array of invalid API exports.
+const getBadExports = (plugin, pluginAPIKeys, apis) => {
+ let badExports = []
+ // Discover any exports from plugins which are not "known"
+ badExports = badExports.concat(
+ _.difference(pluginAPIKeys, apis).map(e => {
+ return {
+ exportName: e,
+ pluginName: plugin.name,
+ pluginVersion: plugin.version,
+ }
+ })
+ )
+ return badExports
+}
+
+const getBadExportsMessage = (badExports, exportType, apis) => {
+ const { stripIndent } = require(`common-tags`)
+ const stringSimiliarity = require(`string-similarity`)
+ let capitalized = `${exportType[0].toUpperCase()}${exportType.slice(1)}`
+ if (capitalized === `Ssr`) capitalized = `SSR`
+
+ let message = `\n`
+ message += stripIndent`
+ Your plugins must export known APIs from their gatsby-${exportType}.js.
+ The following exports aren't APIs. Perhaps you made a typo or
+ your plugin is outdated?
+
+ See https://www.gatsbyjs.org/docs/${exportType}-apis/ for the list of Gatsby ${capitalized} APIs`
+
+ badExports.forEach(bady => {
+ const similarities = stringSimiliarity.findBestMatch(bady.exportName, apis)
+ message += `\n — `
+ if (bady.pluginName == `default-site-plugin`) {
+ message += `Your site's gatsby-${exportType}.js is exporting a variable named "${
+ bady.exportName
+ }" which isn't an API.`
+ } else {
+ message += `The plugin "${bady.pluginName}@${
+ bady.pluginVersion
+ }" is exporting a variable named "${bady.exportName}" which isn't an API.`
+ }
+ if (similarities.bestMatch.rating > 0.5) {
+ message += ` Perhaps you meant to export "${
+ similarities.bestMatch.target
+ }"?`
+ }
+ })
+
+ return message
+}
+
+const handleBadExports = ({ apis, badExports }) => {
+ // Output error messages for all bad exports
+ let isBad = false
+ _.toPairs(badExports).forEach(badItem => {
+ const [exportType, entries] = badItem
+ if (entries.length > 0) {
+ isBad = true
+ console.log(getBadExportsMessage(entries, exportType, apis[exportType]))
+ }
+ })
+ return isBad
+}
+
+/**
+ * Identify which APIs each plugin exports
+ */
+const collatePluginAPIs = ({ apis, flattenedPlugins }) => {
+ const allAPIs = [...apis.node, ...apis.browser, ...apis.ssr]
+ const apiToPlugins = allAPIs.reduce((acc, value) => {
+ acc[value] = []
+ return acc
+ }, {})
+
+ // Get a list of bad exports
+ const badExports = {
+ node: [],
+ browser: [],
+ ssr: [],
+ }
+
+ flattenedPlugins.forEach(plugin => {
+ plugin.nodeAPIs = []
+ plugin.browserAPIs = []
+ plugin.ssrAPIs = []
+
+ // Discover which APIs this plugin implements and store an array against
+ // the plugin node itself *and* in an API to plugins map for faster lookups
+ // later.
+ const pluginNodeExports = resolveModuleExports(
+ `${plugin.resolve}/gatsby-node`
+ )
+ const pluginBrowserExports = resolveModuleExports(
+ `${plugin.resolve}/gatsby-browser`
+ )
+ const pluginSSRExports = resolveModuleExports(
+ `${plugin.resolve}/gatsby-ssr`
+ )
+
+ if (pluginNodeExports.length > 0) {
+ plugin.nodeAPIs = _.intersection(pluginNodeExports, apis.node)
+ plugin.nodeAPIs.map(nodeAPI => apiToPlugins[nodeAPI].push(plugin.name))
+ badExports.node = getBadExports(plugin, pluginNodeExports, apis.node) // Collate any bad exports
+ }
+
+ if (pluginBrowserExports.length > 0) {
+ plugin.browserAPIs = _.intersection(pluginBrowserExports, apis.browser)
+ plugin.browserAPIs.map(browserAPI =>
+ apiToPlugins[browserAPI].push(plugin.name)
+ )
+ badExports.browser = getBadExports(
+ plugin,
+ pluginBrowserExports,
+ apis.browser
+ ) // Collate any bad exports
+ }
+
+ if (pluginSSRExports.length > 0) {
+ plugin.ssrAPIs = _.intersection(pluginSSRExports, apis.ssr)
+ plugin.ssrAPIs.map(ssrAPI => apiToPlugins[ssrAPI].push(plugin.name))
+ badExports.ssr = getBadExports(plugin, pluginSSRExports, apis.ssr) // Collate any bad exports
+ }
+ })
+
+ return { apiToPlugins, flattenedPlugins, badExports }
+}
+
+const handleMultipleReplaceRenderers = ({ apiToPlugins, flattenedPlugins }) => {
+ // multiple replaceRenderers may cause problems at build time
+ if (apiToPlugins.replaceRenderer.length > 1) {
+ const rendererPlugins = [...apiToPlugins.replaceRenderer]
+
+ if (rendererPlugins.includes(`default-site-plugin`)) {
+ reporter.warn(`replaceRenderer API found in these plugins:`)
+ reporter.warn(rendererPlugins.join(`, `))
+ reporter.warn(
+ `This might be an error, see: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`
+ )
+ } else {
+ console.log(``)
+ reporter.error(
+ `Gatsby's replaceRenderer API is implemented by multiple plugins:`
+ )
+ reporter.error(rendererPlugins.join(`, `))
+ reporter.error(`This will break your build`)
+ reporter.error(
+ `See: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`
+ )
+ if (process.env.NODE_ENV === `production`) process.exit(1)
+ }
+
+ // Now update plugin list so only final replaceRenderer will run
+ const ignorable = rendererPlugins.slice(0, -1)
+
+ // For each plugin in ignorable, set a skipSSR flag to true
+ // This prevents apiRunnerSSR() from attempting to run it later
+ const messages = []
+ flattenedPlugins.forEach((fp, i) => {
+ if (ignorable.includes(fp.name)) {
+ messages.push(
+ `Duplicate replaceRenderer found, skipping gatsby-ssr.js for plugin: ${
+ fp.name
+ }`
+ )
+ flattenedPlugins[i].skipSSR = true
+ }
+ })
+ if (messages.length > 0) {
+ console.log(``)
+ messages.forEach(m => reporter.warn(m))
+ console.log(``)
+ }
+ }
+
+ return flattenedPlugins
+}
+
+module.exports = {
+ collatePluginAPIs,
+ handleBadExports,
+ handleMultipleReplaceRenderers,
+}