From acfe558a5911931926bcd6da78b75863a1ead66f Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Wed, 7 Feb 2018 12:43:12 +0000 Subject: [PATCH 01/17] Warn when replaceRenderer is implemented multiple times --- packages/gatsby/cache-dir/api-runner-ssr.js | 40 +++++++++++++++++- packages/gatsby/cache-dir/static-entry.js | 45 ++++++++++++--------- packages/gatsby/src/bootstrap/index.js | 1 + 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index 3fc53b5a72a1e..a58529d691790 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -6,10 +6,45 @@ const apis = require(`./api-ssr-docs`) -module.exports = (api, args, defaultReturn) => { +/** + * Some apis should only be implemented once. Given a list of plugins, and an + * `api` to check, this will return [] for apis that are implemented 1 or 0 times. + * + * For apis that have been implemented multiple times, return an array of paths + * pointing to the file implementing `api`. + * + * @param {Array} pluginList + * @param {String} api + */ +const duplicatedApis = (pluginList, api) => { + let implementsApi = [] + + pluginList.forEach(p => { + if (p.plugin[api]) implementsApi.push(p.path) + }) + + if (implementsApi.length < 2) return [] // no dupes + return implementsApi // paths to dupes +} + +// Run the specified api in any plugins that have implemented it +const apiRunner = ({ api, args, defaultReturn, checkDupes = false }) => { if (!apis[api]) { console.log(`This API doesn't exist`, api) } + + if (checkDupes) { + const dupes = duplicatedApis(plugins, api) + if (dupes.length > 0) { + let m = `\nThe "${api}" api has been implemented multiple times. Only the last implementation will be used.` + let m2 = `This is probably an error, see https://github.com/gatsbyjs/gatsby/issues/2005#issuecomment-326787567 for workarounds.` + console.log(m) + console.log(m2) + console.log(`Check the following files for "${api}" implementations:`) + dupes.map(d => console.log(d)) + } + } + // Run each plugin in series. let results = plugins.map(plugin => { if (plugin.plugin[api]) { @@ -27,3 +62,6 @@ module.exports = (api, args, defaultReturn) => { return [defaultReturn] } } + +exports.apiRunner = apiRunner +exports.duplicatedApis = duplicatedApis diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 3884e46673fdc..a897a90344dde 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -3,7 +3,7 @@ import { renderToString, renderToStaticMarkup } from "react-dom/server" import { StaticRouter, Route, withRouter } from "react-router-dom" import { kebabCase, get, merge, isArray, isString } from "lodash" -import apiRunner from "./api-runner-ssr" +import { apiRunner } from "./api-runner-ssr" import pages from "./pages.json" import syncRequires from "./sync-requires" import testRequireError from "./test-require-error" @@ -110,15 +110,19 @@ module.exports = (locals, callback) => { ) // Let the site or plugin render the page component. - apiRunner(`replaceRenderer`, { - bodyComponent, - replaceBodyHTMLString, - setHeadComponents, - setHtmlAttributes, - setBodyAttributes, - setPreBodyComponents, - setPostBodyComponents, - setBodyProps, + apiRunner({ + api: `replaceRenderer`, + args: { + bodyComponent, + replaceBodyHTMLString, + setHeadComponents, + setHtmlAttributes, + setBodyAttributes, + setPreBodyComponents, + setPostBodyComponents, + setBodyProps, + }, + checkDupes: true, }) // If no one stepped up, we'll handle it. @@ -126,15 +130,18 @@ module.exports = (locals, callback) => { bodyHtml = renderToString(bodyComponent) } - apiRunner(`onRenderBody`, { - setHeadComponents, - setHtmlAttributes, - setBodyAttributes, - setPreBodyComponents, - setPostBodyComponents, - setBodyProps, - pathname: locals.path, - bodyHtml, + apiRunner({ + api: `onRenderBody`, + args: { + setHeadComponents, + setHtmlAttributes, + setBodyAttributes, + setPreBodyComponents, + setPostBodyComponents, + setBodyProps, + pathname: locals.path, + bodyHtml, + }, }) let stats diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index 89aa3ef84cabf..5ec7e6d86abaa 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -234,6 +234,7 @@ module.exports = async (args: BootstrapArgs) => { plugin => `{ plugin: require('${plugin.resolve}'), + path: "${plugin.resolve}", options: ${JSON.stringify(plugin.options)}, }` ) From 66af1b90b000feefb9adad21a1478574a6cd843c Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Wed, 7 Feb 2018 12:43:30 +0000 Subject: [PATCH 02/17] Remove unused import --- packages/gatsby/cache-dir/develop-static-entry.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js index 2c4512665720a..eca3eba09d272 100644 --- a/packages/gatsby/cache-dir/develop-static-entry.js +++ b/packages/gatsby/cache-dir/develop-static-entry.js @@ -1,7 +1,6 @@ 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" let HTML @@ -17,7 +16,6 @@ try { } module.exports = (locals, callback) => { - // const apiRunner = require(`${directory}/.cache/api-runner-ssr`) let headComponents = [] let htmlAttributes = {} let bodyAttributes = {} From 959d7f486b8696bb8c1b6b841048ddb9724331f7 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Wed, 7 Feb 2018 13:05:22 +0000 Subject: [PATCH 03/17] Add tests --- .../cache-dir/__tests__/api-runner-ssr.js | 76 +++++++++++++++++++ packages/gatsby/cache-dir/api-runner-ssr.js | 4 +- 2 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 packages/gatsby/cache-dir/__tests__/api-runner-ssr.js diff --git a/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js b/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js new file mode 100644 index 0000000000000..89026c4c322d4 --- /dev/null +++ b/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js @@ -0,0 +1,76 @@ +const { duplicatedApis } = require(`../api-runner-ssr`) + +describe(`duplicatedApis`, () => { + it(`identifies duplicate apis`, () => { + const result = duplicatedApis( + [ + { + plugin: { + replaceRenderer: () => {}, + otherApi: () => {}, + }, + path: `/path/to/foo.js`, + }, + { + plugin: { + replaceRenderer: () => {}, + differentApi: () => {}, + }, + path: `/path/to/bar.js`, + }, + ], + `replaceRenderer` + ) + expect(result).toEqual([`/path/to/foo.js`, `/path/to/bar.js`]) + }) + + it(`only identifies the specified duplicate`, () => { + const result = duplicatedApis( + [ + { + plugin: { + replaceRenderer: () => {}, + }, + path: `/path/to/foo.js`, + }, + { + plugin: { + otherDuplicate: () => {}, + replaceRenderer: () => {}, + }, + path: `/path/to/bar.js`, + }, + { + plugin: { + otherDuplicate: () => {}, + replaceRenderer: () => {}, + }, + path: `/path/to/baz.js`, + }, + ], + `otherDuplicate` + ) + expect(result).toEqual([`/path/to/bar.js`, `/path/to/baz.js`]) + }) + + it(`correctly identifies no duplicates`, () => { + const result = duplicatedApis( + [ + { + plugin: { + uniqueApi1: () => {}, + }, + path: `/path/to/foo.js`, + }, + { + plugin: { + uniqueApi2: () => {}, + }, + path: `/path/to/bar.js`, + }, + ], + `uniqueApi1` + ) + expect(result).toEqual([]) + }) +}) diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index a58529d691790..fec2fb15dbaae 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -13,8 +13,8 @@ const apis = require(`./api-ssr-docs`) * For apis that have been implemented multiple times, return an array of paths * pointing to the file implementing `api`. * - * @param {Array} pluginList - * @param {String} api + * @param {array} pluginList + * @param {string} api */ const duplicatedApis = (pluginList, api) => { let implementsApi = [] From 5dae1563d822c007874a94a9854999c8ffc6a9d2 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 11:57:21 +0000 Subject: [PATCH 04/17] Back out 'named arguments' change from apiRunner --- packages/gatsby/cache-dir/api-runner-ssr.js | 4 +- packages/gatsby/cache-dir/static-entry.js | 43 +++++++++------------ 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index fec2fb15dbaae..44ffd9095d8ac 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -28,12 +28,12 @@ const duplicatedApis = (pluginList, api) => { } // Run the specified api in any plugins that have implemented it -const apiRunner = ({ api, args, defaultReturn, checkDupes = false }) => { +const apiRunner = (api, args, defaultReturn) => { if (!apis[api]) { console.log(`This API doesn't exist`, api) } - if (checkDupes) { + if (api === `replaceRenderer`) { const dupes = duplicatedApis(plugins, api) if (dupes.length > 0) { let m = `\nThe "${api}" api has been implemented multiple times. Only the last implementation will be used.` diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index a897a90344dde..3a554e25f4003 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -110,19 +110,15 @@ module.exports = (locals, callback) => { ) // Let the site or plugin render the page component. - apiRunner({ - api: `replaceRenderer`, - args: { - bodyComponent, - replaceBodyHTMLString, - setHeadComponents, - setHtmlAttributes, - setBodyAttributes, - setPreBodyComponents, - setPostBodyComponents, - setBodyProps, - }, - checkDupes: true, + apiRunner(`replaceRenderer`, { + bodyComponent, + replaceBodyHTMLString, + setHeadComponents, + setHtmlAttributes, + setBodyAttributes, + setPreBodyComponents, + setPostBodyComponents, + setBodyProps, }) // If no one stepped up, we'll handle it. @@ -130,18 +126,15 @@ module.exports = (locals, callback) => { bodyHtml = renderToString(bodyComponent) } - apiRunner({ - api: `onRenderBody`, - args: { - setHeadComponents, - setHtmlAttributes, - setBodyAttributes, - setPreBodyComponents, - setPostBodyComponents, - setBodyProps, - pathname: locals.path, - bodyHtml, - }, + apiRunner(`onRenderBody`, { + setHeadComponents, + setHtmlAttributes, + setBodyAttributes, + setPreBodyComponents, + setPostBodyComponents, + setBodyProps, + pathname: locals.path, + bodyHtml, }) let stats From c577de692d61696500baa68432ec5d5a213723d9 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 15:19:31 +0000 Subject: [PATCH 05/17] Re-add required import --- packages/gatsby/cache-dir/develop-static-entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js index eca3eba09d272..54873fa9374fb 100644 --- a/packages/gatsby/cache-dir/develop-static-entry.js +++ b/packages/gatsby/cache-dir/develop-static-entry.js @@ -2,6 +2,7 @@ import React from "react" import { renderToStaticMarkup } from "react-dom/server" import { merge } from "lodash" import testRequireError from "./test-require-error" +import { apiRunner } from "./api-runner-ssr" let HTML try { From 881f066354ac40dc0068a1e33e0f04b89f462062 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 15:20:51 +0000 Subject: [PATCH 06/17] Add first draft of docs page --- docs/docs/debugging-replace-renderer-api.md | 113 ++++++++++++++++++++ packages/gatsby/cache-dir/api-runner-ssr.js | 2 +- 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 docs/docs/debugging-replace-renderer-api.md diff --git a/docs/docs/debugging-replace-renderer-api.md b/docs/docs/debugging-replace-renderer-api.md new file mode 100644 index 0000000000000..4e39e72bbbfce --- /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) hooks](https://www.gatsbyjs.org/docs/ssr-apis/#replaceRenderer). It's used to customise how Gatsby renders your static content. It can be implemented by any Gatsby plugin or your `gatsby-ssr.js` file to add 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? + +When using `replaceRenderer` multiple times in your project, only the newest instance will be used - which can cause unexpected problems with your site. + +Note that it's only used during `gatsby build` and not `gatsby develop`, so you may not notice any problems while developing your site. + +When `gatsby build` detects a project using `replaceRenderer` multiple times, it will show an error like this: + +``` +The "replaceRenderer" api has been implemented multiple times. Only the last implementation will be used. +This is probably an error, see https://example.com for workarounds. +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 +``` + +## 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. Move their `replaceRenderer` functionality to your `gatsby-ssr.js` file + +You'll need to manually combine the `replaceRenderer` functionality from your plugins into 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 you'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 + +Your `gatsby-config.js` file will remain unchanged, but your `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 a replaceRenderer instance. Now run `gatsby build` and your site will build without errors. + +There's a full repo of this example at: https://github.com/m-allanson/gatsby-replace-renderer-example/commits/master diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index 44ffd9095d8ac..55e6b522b5daa 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -37,7 +37,7 @@ const apiRunner = (api, args, defaultReturn) => { const dupes = duplicatedApis(plugins, api) if (dupes.length > 0) { let m = `\nThe "${api}" api has been implemented multiple times. Only the last implementation will be used.` - let m2 = `This is probably an error, see https://github.com/gatsbyjs/gatsby/issues/2005#issuecomment-326787567 for workarounds.` + let m2 = `This is probably an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api.md for details.` console.log(m) console.log(m2) console.log(`Check the following files for "${api}" implementations:`) From e9de6c412e1e9fa6e0eca482046b1f7c75b215a8 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 15:44:49 +0000 Subject: [PATCH 07/17] Simplify language based on results from Hemingway App See http://www.hemingwayapp.com/ --- docs/docs/debugging-replace-renderer-api.md | 16 ++++++++-------- packages/gatsby/cache-dir/api-runner-ssr.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docs/debugging-replace-renderer-api.md b/docs/docs/debugging-replace-renderer-api.md index 4e39e72bbbfce..c1022ee4574ae 100644 --- a/docs/docs/debugging-replace-renderer-api.md +++ b/docs/docs/debugging-replace-renderer-api.md @@ -4,15 +4,15 @@ title: Debugging replaceRenderer API ## What is the `replaceRenderer` API? -The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) hooks](https://www.gatsbyjs.org/docs/ssr-apis/#replaceRenderer). It's used to customise how Gatsby renders your static content. It can be implemented by any Gatsby plugin or your `gatsby-ssr.js` file to add support for Redux, CSS-in-JS libraries or any code that needs to change Gatsby's default HTML output. +The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) hooks](https://www.gatsbyjs.org/docs/ssr-apis/#replaceRenderer). It's 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? -When using `replaceRenderer` multiple times in your project, only the newest instance will be used - which can cause unexpected problems with your site. +When using `replaceRenderer` many times in your project, only the newest instance will be used - which can cause unexpected problems with your site. -Note that it's only used during `gatsby build` and not `gatsby develop`, so you may not notice any problems while developing your site. +Note that it's only used during `gatsby build`. It won't cause problems as you build your site with `gatsby develop`. -When `gatsby build` detects a project using `replaceRenderer` multiple times, it will show an error like this: +If your project uses replaceRenderer more than once, gatsby build will warn you: ``` The "replaceRenderer" api has been implemented multiple times. Only the last implementation will be used. @@ -40,7 +40,7 @@ In this example, your `gatsby-ssr.js` file and `gatsby-plugin-styled-components` ### 2. Move their `replaceRenderer` functionality to your `gatsby-ssr.js` file -You'll need to manually combine the `replaceRenderer` functionality from your plugins into your `gatsby-ssr.js` file. This step will be different for each project, keep reading to see an example. +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 @@ -77,7 +77,7 @@ Note that the Styled Components plugin uses `replaceRenderer`, and the code in ` ### Fixing the `replaceRenderer` error -Your `gatsby-config.js` file will remain unchanged, but your `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) +Your `gatsby-config.js` file will remain unchanged. However, your `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` @@ -108,6 +108,6 @@ exports.replaceRenderer = ({ } ``` -Now `gatsby-ssr.js` implements the Styled Components and Redux functionality using a replaceRenderer instance. Now run `gatsby build` and your site will build without errors. +Now `gatsby-ssr.js` implements the Styled Components and Redux functionality using one replaceRenderer instance. Now run `gatsby build` and your site will build without errors. -There's a full repo of this example at: https://github.com/m-allanson/gatsby-replace-renderer-example/commits/master +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-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index 55e6b522b5daa..cd686a8ef5635 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -37,7 +37,7 @@ const apiRunner = (api, args, defaultReturn) => { const dupes = duplicatedApis(plugins, api) if (dupes.length > 0) { let m = `\nThe "${api}" api has been implemented multiple times. Only the last implementation will be used.` - let m2 = `This is probably an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api.md for details.` + let m2 = `This could be an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api.md for details.` console.log(m) console.log(m2) console.log(`Check the following files for "${api}" implementations:`) From 6298c41a5456a685a44f8ae7aca2736f8007f8e3 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 21:01:44 +0000 Subject: [PATCH 08/17] Implement docs feedback --- docs/docs/debugging-replace-renderer-api.md | 22 ++++++++++----------- packages/gatsby/cache-dir/api-runner-ssr.js | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/docs/debugging-replace-renderer-api.md b/docs/docs/debugging-replace-renderer-api.md index c1022ee4574ae..f7579cdb30119 100644 --- a/docs/docs/debugging-replace-renderer-api.md +++ b/docs/docs/debugging-replace-renderer-api.md @@ -4,20 +4,20 @@ title: Debugging replaceRenderer API ## What is the `replaceRenderer` API? -The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) hooks](https://www.gatsbyjs.org/docs/ssr-apis/#replaceRenderer). It's 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. +The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) hooks](/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? -When using `replaceRenderer` many times in your project, only the newest instance will be used - which can cause unexpected problems with your site. +When using `replaceRenderer` many times in your project, only the last plugin implementing the API can be called - which will break your site builds. -Note that it's only used during `gatsby build`. It won't cause problems as you build your site with `gatsby develop`. +Note that `replaceRenderer` is only used during `gatsby build`. It won't cause problems as you work on your site with `gatsby develop`. -If your project uses replaceRenderer more than once, gatsby build will warn you: +If your project uses `replaceRenderer` more than once, `gatsby build` will warn you: ``` -The "replaceRenderer" api has been implemented multiple times. Only the last implementation will be used. -This is probably an error, see https://example.com for workarounds. -Check the following files for "replaceRenderer" implementations: +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 ``` @@ -38,7 +38,7 @@ Check the following files for "replaceRenderer" implementations: In this example, your `gatsby-ssr.js` file and `gatsby-plugin-styled-components` are both using `replaceRenderer`. -### 2. Move their `replaceRenderer` functionality to your `gatsby-ssr.js` file +### 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. @@ -46,7 +46,7 @@ You'll need to override your plugins' `replaceRenderer` code in your `gatsby-ssr ### Initial setup -In this example project you'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). +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` @@ -77,7 +77,7 @@ Note that the Styled Components plugin uses `replaceRenderer`, and the code in ` ### Fixing the `replaceRenderer` error -Your `gatsby-config.js` file will remain unchanged. However, your `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) +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` @@ -108,6 +108,6 @@ exports.replaceRenderer = ({ } ``` -Now `gatsby-ssr.js` implements the Styled Components and Redux functionality using one replaceRenderer instance. Now run `gatsby build` and your site will build without errors. +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-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index cd686a8ef5635..830a9a372e0ed 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -36,11 +36,11 @@ const apiRunner = (api, args, defaultReturn) => { if (api === `replaceRenderer`) { const dupes = duplicatedApis(plugins, api) if (dupes.length > 0) { - let m = `\nThe "${api}" api has been implemented multiple times. Only the last implementation will be used.` - let m2 = `This could be an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api.md for details.` + let m = `\nThe "${api}" API is implemented by several enabled plugins.` + let m2 = `This could be an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/ for details.` console.log(m) console.log(m2) - console.log(`Check the following files for "${api}" implementations:`) + console.log(`Check the following plugins for "${api}" implementations:`) dupes.map(d => console.log(d)) } } From 44c30a496ec20c842098055b89359c7c38b88344 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Thu, 8 Feb 2018 21:03:31 +0000 Subject: [PATCH 09/17] Capitalisation --- packages/gatsby/cache-dir/api-runner-ssr.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index 830a9a372e0ed..63308dc93ed75 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -7,10 +7,10 @@ const apis = require(`./api-ssr-docs`) /** - * Some apis should only be implemented once. Given a list of plugins, and an - * `api` to check, this will return [] for apis that are implemented 1 or 0 times. + * Some APIs should only be implemented once. Given a list of plugins, and an + * `api` to check, this will return [] for APIs that are implemented 1 or 0 times. * - * For apis that have been implemented multiple times, return an array of paths + * For APIs that have been implemented multiple times, return an array of paths * pointing to the file implementing `api`. * * @param {array} pluginList @@ -27,7 +27,7 @@ const duplicatedApis = (pluginList, api) => { return implementsApi // paths to dupes } -// Run the specified api in any plugins that have implemented it +// Run the specified API in any plugins that have implemented it const apiRunner = (api, args, defaultReturn) => { if (!apis[api]) { console.log(`This API doesn't exist`, api) From 39132070232c09225f76880bef6c355dd3576347 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Fri, 9 Feb 2018 15:29:15 +0000 Subject: [PATCH 10/17] Copy tweaks --- docs/docs/debugging-replace-renderer-api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/debugging-replace-renderer-api.md b/docs/docs/debugging-replace-renderer-api.md index f7579cdb30119..999fc07e9dcb2 100644 --- a/docs/docs/debugging-replace-renderer-api.md +++ b/docs/docs/debugging-replace-renderer-api.md @@ -4,15 +4,15 @@ title: Debugging replaceRenderer API ## What is the `replaceRenderer` API? -The `replaceRenderer` API is one of [Gatsby's Server Side Rendering (SSR) hooks](/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. +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? -When using `replaceRenderer` many times in your project, only the last plugin implementing the API can be called - which will break your site builds. +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 your project uses `replaceRenderer` more than once, `gatsby build` will warn you: +If multiple plugins implement `replaceRenderer`, `gatsby build` will warn you: ``` The "replaceRenderer" API is implemented by several enabled plugins. From 00f39e73980e2b6f881eed9adc6a0832a8786f57 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Sat, 17 Feb 2018 09:18:21 +0000 Subject: [PATCH 11/17] More concise 'add internal plugins' --- packages/gatsby/src/bootstrap/load-plugins.js | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js index 3203d144c4d47..7f5df9fc82c1e 100644 --- a/packages/gatsby/src/bootstrap/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins.js @@ -187,30 +187,18 @@ module.exports = async (config = {}) => { } // 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`)) - ) + 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) { From 55b7d3f015b3e098efaaec512cceceedc68d1090 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Sat, 17 Feb 2018 10:45:01 +0000 Subject: [PATCH 12/17] Use work from previous PR's to improve this - remove duplicate error messages - improve message format - only run the last replaceRenderer plugin --- .../cache-dir/__tests__/api-runner-ssr.js | 76 ------------------- packages/gatsby/cache-dir/api-runner-ssr.js | 38 +--------- .../gatsby/cache-dir/develop-static-entry.js | 2 +- packages/gatsby/cache-dir/static-entry.js | 2 +- packages/gatsby/src/bootstrap/index.js | 11 ++- packages/gatsby/src/bootstrap/load-plugins.js | 35 ++++++++- 6 files changed, 44 insertions(+), 120 deletions(-) delete mode 100644 packages/gatsby/cache-dir/__tests__/api-runner-ssr.js diff --git a/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js b/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js deleted file mode 100644 index 89026c4c322d4..0000000000000 --- a/packages/gatsby/cache-dir/__tests__/api-runner-ssr.js +++ /dev/null @@ -1,76 +0,0 @@ -const { duplicatedApis } = require(`../api-runner-ssr`) - -describe(`duplicatedApis`, () => { - it(`identifies duplicate apis`, () => { - const result = duplicatedApis( - [ - { - plugin: { - replaceRenderer: () => {}, - otherApi: () => {}, - }, - path: `/path/to/foo.js`, - }, - { - plugin: { - replaceRenderer: () => {}, - differentApi: () => {}, - }, - path: `/path/to/bar.js`, - }, - ], - `replaceRenderer` - ) - expect(result).toEqual([`/path/to/foo.js`, `/path/to/bar.js`]) - }) - - it(`only identifies the specified duplicate`, () => { - const result = duplicatedApis( - [ - { - plugin: { - replaceRenderer: () => {}, - }, - path: `/path/to/foo.js`, - }, - { - plugin: { - otherDuplicate: () => {}, - replaceRenderer: () => {}, - }, - path: `/path/to/bar.js`, - }, - { - plugin: { - otherDuplicate: () => {}, - replaceRenderer: () => {}, - }, - path: `/path/to/baz.js`, - }, - ], - `otherDuplicate` - ) - expect(result).toEqual([`/path/to/bar.js`, `/path/to/baz.js`]) - }) - - it(`correctly identifies no duplicates`, () => { - const result = duplicatedApis( - [ - { - plugin: { - uniqueApi1: () => {}, - }, - path: `/path/to/foo.js`, - }, - { - plugin: { - uniqueApi2: () => {}, - }, - path: `/path/to/bar.js`, - }, - ], - `uniqueApi1` - ) - expect(result).toEqual([]) - }) -}) diff --git a/packages/gatsby/cache-dir/api-runner-ssr.js b/packages/gatsby/cache-dir/api-runner-ssr.js index 63308dc93ed75..b1c7e3550d0ea 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -6,45 +6,12 @@ const apis = require(`./api-ssr-docs`) -/** - * Some APIs should only be implemented once. Given a list of plugins, and an - * `api` to check, this will return [] for APIs that are implemented 1 or 0 times. - * - * For APIs that have been implemented multiple times, return an array of paths - * pointing to the file implementing `api`. - * - * @param {array} pluginList - * @param {string} api - */ -const duplicatedApis = (pluginList, api) => { - let implementsApi = [] - - pluginList.forEach(p => { - if (p.plugin[api]) implementsApi.push(p.path) - }) - - if (implementsApi.length < 2) return [] // no dupes - return implementsApi // paths to dupes -} - // Run the specified API in any plugins that have implemented it -const apiRunner = (api, args, defaultReturn) => { +module.exports = (api, args, defaultReturn) => { if (!apis[api]) { console.log(`This API doesn't exist`, api) } - if (api === `replaceRenderer`) { - const dupes = duplicatedApis(plugins, api) - if (dupes.length > 0) { - let m = `\nThe "${api}" API is implemented by several enabled plugins.` - let m2 = `This could be an error, see https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/ for details.` - console.log(m) - console.log(m2) - console.log(`Check the following plugins for "${api}" implementations:`) - dupes.map(d => console.log(d)) - } - } - // Run each plugin in series. let results = plugins.map(plugin => { if (plugin.plugin[api]) { @@ -62,6 +29,3 @@ const apiRunner = (api, args, defaultReturn) => { return [defaultReturn] } } - -exports.apiRunner = apiRunner -exports.duplicatedApis = duplicatedApis diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js index 54873fa9374fb..c763978352eac 100644 --- a/packages/gatsby/cache-dir/develop-static-entry.js +++ b/packages/gatsby/cache-dir/develop-static-entry.js @@ -2,7 +2,7 @@ import React from "react" import { renderToStaticMarkup } from "react-dom/server" import { merge } from "lodash" import testRequireError from "./test-require-error" -import { apiRunner } from "./api-runner-ssr" +import apiRunner from "./api-runner-ssr" let HTML try { diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index 3a554e25f4003..3884e46673fdc 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -3,7 +3,7 @@ import { renderToString, renderToStaticMarkup } from "react-dom/server" import { StaticRouter, Route, withRouter } from "react-router-dom" import { kebabCase, get, merge, isArray, isString } from "lodash" -import { apiRunner } from "./api-runner-ssr" +import apiRunner from "./api-runner-ssr" import pages from "./pages.json" import syncRequires from "./sync-requires" import testRequireError from "./test-require-error" diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index 5ec7e6d86abaa..411ac086284dc 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -8,6 +8,7 @@ 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 +176,12 @@ 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) => { + if (plugin[`${env}APIs`].length > 0 ) { + return path.join(plugin.resolve, `gatsby-${env}.js`) + } + return undefined + } const ssrPlugins = _.filter( flattenedPlugins.map(plugin => { @@ -234,7 +238,6 @@ module.exports = async (args: BootstrapArgs) => { plugin => `{ plugin: require('${plugin.resolve}'), - path: "${plugin.resolve}", options: ${JSON.stringify(plugin.options)}, }` ) diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js index 7f5df9fc82c1e..cbd214700a0d4 100644 --- a/packages/gatsby/src/bootstrap/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins.js @@ -309,7 +309,40 @@ module.exports = async (config = {}) => { } }) - if (bad) process.exit() + if (bad) process.exit() // TODO: change to panicOnBuild + + // multiple replaceRenderers may cause problems at build time + if (apiToPlugins.replaceRenderer.length > 1) { + const rendererPlugins = [...apiToPlugins.replaceRenderer] + + if (rendererPlugins.includes(`default-site-plugin`)) { + console.log(`\nreplaceRenderer API found in these plugins:`) + console.log(rendererPlugins.join(`, `)) + console.log(`This might be an error, see: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`) + } else { + console.log(`\nGatsby's replaceRenderer API is implemented by multiple plugins:`) + console.log(rendererPlugins.join(`, `)) + console.log(`This will break your build`) + console.log(`See: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`) + } + + // Now update plugin list so only final replaceRenderer will run + const ignorable = rendererPlugins.slice(0, -1) + + // For each plugin in ignorable, reset its list of ssrAPIs to [] + // 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].ssrAPIs = [] + } + }) + if (messages.length > 1) { + messages.forEach(m => console.log(m)) + console.log(``) + } + } store.dispatch({ type: `SET_SITE_PLUGINS`, From 360677b32350a33c91bdc87b724de5db7fa90259 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Sat, 17 Feb 2018 11:05:46 +0000 Subject: [PATCH 13/17] Remove unused import --- packages/gatsby/src/bootstrap/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gatsby/src/bootstrap/index.js b/packages/gatsby/src/bootstrap/index.js index 411ac086284dc..b377d080ef0a2 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -1,7 +1,6 @@ /* @flow */ const Promise = require(`bluebird`) -const glob = require(`glob`) const _ = require(`lodash`) const slash = require(`slash`) const fs = require(`fs-extra`) From 57375eeba71df3145b4ee12984ad906095c0127b Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Sat, 17 Feb 2018 12:32:55 +0000 Subject: [PATCH 14/17] Use the reporter for nicer error formatting --- packages/gatsby/src/bootstrap/load-plugins.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js index cbd214700a0d4..2dabf7398e99b 100644 --- a/packages/gatsby/src/bootstrap/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins.js @@ -10,6 +10,7 @@ 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`) +const reporter = require(`gatsby-cli/lib/reporter`) // 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. @@ -316,14 +317,15 @@ module.exports = async (config = {}) => { const rendererPlugins = [...apiToPlugins.replaceRenderer] if (rendererPlugins.includes(`default-site-plugin`)) { - console.log(`\nreplaceRenderer API found in these plugins:`) - console.log(rendererPlugins.join(`, `)) - console.log(`This might be an error, see: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`) + 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(`\nGatsby's replaceRenderer API is implemented by multiple plugins:`) - console.log(rendererPlugins.join(`, `)) - console.log(`This will break your build`) - console.log(`See: https://www.gatsbyjs.org/docs/debugging-replace-renderer-api/`) + 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/`) } // Now update plugin list so only final replaceRenderer will run @@ -331,15 +333,16 @@ module.exports = async (config = {}) => { // For each plugin in ignorable, reset its list of ssrAPIs to [] // This prevents apiRunnerSSR() from attempting to run it later - const messages = [``] + 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].ssrAPIs = [] } }) - if (messages.length > 1) { - messages.forEach(m => console.log(m)) + if (messages.length > 0) { + console.log(``) + messages.forEach(m => reporter.warn(m)) console.log(``) } } From 68cfa35eb06926194452b523684b8aca7bb77f50 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Sat, 17 Feb 2018 12:33:18 +0000 Subject: [PATCH 15/17] Exit on error during build --- packages/gatsby/src/bootstrap/load-plugins.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js index 2dabf7398e99b..182f0c7e5c10f 100644 --- a/packages/gatsby/src/bootstrap/load-plugins.js +++ b/packages/gatsby/src/bootstrap/load-plugins.js @@ -326,6 +326,7 @@ module.exports = async (config = {}) => { 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 From a4807ee348866909c350fbfd0877b578956b72f6 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Mon, 26 Feb 2018 17:28:23 +0000 Subject: [PATCH 16/17] Split code out into smaller functions and add tests --- .../__mocks__/resolve-module-exports.js | 19 + packages/gatsby/src/bootstrap/index.js | 7 +- packages/gatsby/src/bootstrap/load-plugins.js | 367 ------------------ .../__snapshots__/load-plugins.js.snap | 24 +- .../__tests__/__snapshots__/validate.js.snap | 259 ++++++++++++ .../__tests__/load-plugins.js | 4 +- .../load-plugins/__tests__/validate.js | 189 +++++++++ .../src/bootstrap/load-plugins/index.js | 82 ++++ .../gatsby/src/bootstrap/load-plugins/load.js | 163 ++++++++ .../src/bootstrap/load-plugins/validate.js | 189 +++++++++ 10 files changed, 921 insertions(+), 382 deletions(-) create mode 100644 packages/gatsby/src/bootstrap/__mocks__/resolve-module-exports.js delete mode 100644 packages/gatsby/src/bootstrap/load-plugins.js rename packages/gatsby/src/bootstrap/{ => load-plugins}/__tests__/__snapshots__/load-plugins.js.snap (98%) create mode 100644 packages/gatsby/src/bootstrap/load-plugins/__tests__/__snapshots__/validate.js.snap rename packages/gatsby/src/bootstrap/{ => load-plugins}/__tests__/load-plugins.js (90%) create mode 100644 packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.js create mode 100644 packages/gatsby/src/bootstrap/load-plugins/index.js create mode 100644 packages/gatsby/src/bootstrap/load-plugins/load.js create mode 100644 packages/gatsby/src/bootstrap/load-plugins/validate.js 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 b377d080ef0a2..2c2b5b9157242 100644 --- a/packages/gatsby/src/bootstrap/index.js +++ b/packages/gatsby/src/bootstrap/index.js @@ -176,7 +176,12 @@ 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) => { - if (plugin[`${env}APIs`].length > 0 ) { + // 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 diff --git a/packages/gatsby/src/bootstrap/load-plugins.js b/packages/gatsby/src/bootstrap/load-plugins.js deleted file mode 100644 index 182f0c7e5c10f..0000000000000 --- a/packages/gatsby/src/bootstrap/load-plugins.js +++ /dev/null @@ -1,367 +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`) -const reporter = require(`gatsby-cli/lib/reporter`) - -// 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 - 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: [], - }, - }) - - // 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() // TODO: change to panicOnBuild - - // 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, reset its list of ssrAPIs to [] - // 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].ssrAPIs = [] - } - }) - if (messages.length > 0) { - console.log(``) - messages.forEach(m => reporter.warn(m)) - console.log(``) - } - } - - 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, +} From db3e56d6c28809e2f588a2fc3337abb7bbbb9e35 Mon Sep 17 00:00:00 2001 From: Mike Allanson Date: Tue, 27 Feb 2018 10:38:02 +0000 Subject: [PATCH 17/17] Update comments --- packages/gatsby/cache-dir/api-runner-browser.js | 10 ++++++++-- packages/gatsby/cache-dir/api-runner-ssr.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) 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 b1c7e3550d0ea..f5891ff43eaa2 100644 --- a/packages/gatsby/cache-dir/api-runner-ssr.js +++ b/packages/gatsby/cache-dir/api-runner-ssr.js @@ -1,7 +1,13 @@ // 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`)