Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Warn when implementing apis twice #3889

Merged
merged 17 commits into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions docs/docs/debugging-replace-renderer-api.md
Original file line number Diff line number Diff line change
@@ -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 = () => <Provider store={store}>{bodyComponent}</Provider>
replaceBodyHTMLString(renderToString(<ConnectedBody />))
}
```

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 = () => (
<Provider store={store}>
<StyleSheetManager sheet={sheet.instance}>
{bodyComponent}
</StyleSheetManager>
</Provider>
)
replaceBodyHTMLString(renderToString(<app />))
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).
10 changes: 8 additions & 2 deletions packages/gatsby/cache-dir/api-runner-browser.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
12 changes: 10 additions & 2 deletions packages/gatsby/cache-dir/api-runner-ssr.js
Original file line number Diff line number Diff line change
@@ -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]) {
Expand Down
3 changes: 1 addition & 2 deletions packages/gatsby/cache-dir/develop-static-entry.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -17,7 +17,6 @@ try {
}

module.exports = (locals, callback) => {
// const apiRunner = require(`${directory}/.cache/api-runner-ssr`)
let headComponents = []
let htmlAttributes = {}
let bodyAttributes = {}
Expand Down
19 changes: 19 additions & 0 deletions packages/gatsby/src/bootstrap/__mocks__/resolve-module-exports.js
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 12 additions & 4 deletions packages/gatsby/src/bootstrap/index.js
Original file line number Diff line number Diff line change
@@ -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`)
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we disable plugins for individual APIs not all SSR APIs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented disabling individual APIs on my checkout, but then had a thought - is there any reason for a plugin to implement replaceRenderer and onRenderBody? I can't find any official or community plugins that do this.

Disabling all SSR APIs keeps things simpler as it means api-runner-ssr.js doesn't have to check whether it should run an API or not. I'd be tempted to roll with this implementation and then review if it causes problems.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does seem unlikely. If we add more APIs in the future, this could potentially become not true — that a plugin would implement multiple ssr APIs. I did a query (this one was fun ag -l replaceRenderer | xargs -L 1 ag onRenderBody -l) and there isn't any plugin currently that implements both. So let's go with it and see what happens. It'd be easy to change later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. Nice querying :)


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 => {
Expand Down
Loading