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

fix(hmr): accept hot updates for modules above page templates #29752

Merged
merged 10 commits into from
Feb 27, 2021
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const TEST_ID = `gatsby-browser-hmr`

describe(`hot reloading above page template (gatsby-browser)`, () => {
beforeEach(() => {
cy.visit(`/`).waitForRouteChange()
})
it(`displays placeholder content on launch`, () => {
cy.getTestElement(TEST_ID).should(
`contain.text`,
`%TEST_HMR_IN_GATSBY_BROWSER%`
)
})

it(`hot reloads with new content`, () => {
const text = `HMR_IN_GATSBY_BROWSER_WORKS`
cy.exec(
`npm run update -- --file src/wrap-root-element.js --replacements "TEST_HMR_IN_GATSBY_BROWSER:${text}"`
)

cy.waitForHmr()

cy.getTestElement(TEST_ID).should(`contain.text`, text)
})
})
14 changes: 8 additions & 6 deletions e2e-tests/development-runtime/gatsby-browser.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Wrapper = require(`./src/wrap-root-element`).default
import WrapRootElement from "./src/wrap-root-element"
import * as React from "react"

if (typeof window !== `undefined`) {
window.___PageComponentLifecycleCallsLog = []
Expand All @@ -11,16 +12,17 @@ const addLogEntry = (action, location) => {
})
}

exports.onPreRouteUpdate = ({ location }) => {
export const onPreRouteUpdate = ({ location }) => {
addLogEntry(`onPreRouteUpdate`, location)
}

exports.onRouteUpdate = ({ location }) => {
export const onRouteUpdate = ({ location }) => {
addLogEntry(`onRouteUpdate`, location)
}

exports.onPrefetchPathname = ({ pathname }) => {
export const onPrefetchPathname = ({ pathname }) => {
addLogEntry(`onPrefetchPathname`, pathname)
}

exports.wrapRootElement = Wrapper
export const wrapRootElement = ({ element }) => (
<WrapRootElement element={element} />
)
Comment on lines +26 to +28
Copy link
Contributor Author

@pieh pieh Feb 27, 2021

Choose a reason for hiding this comment

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

This is actually important - whatever we pass to wrapRootElement (or wrapPageElement) is NOT treated as component. We just straight call this API (and not use React.createElement) - that's because this is gatsby-browser API hook and we also pass pluginOptions as second parameter. Because of above this is actually not represented as React Component in react tree and react-refresh won't actually apply HMR to it (even tho webpack plugin would treat Wrapper as react component and register it as such). On top of problems there are multiple exports in gatsby-browser and that is also no-no for react-refresh.

Above is also reason why we can't use hooks in there etc

This might need some additional notes in https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#wrapPageElement (and maybe migration guide?)

3 changes: 3 additions & 0 deletions e2e-tests/development-runtime/src/wrap-root-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const WrapRootElement = ({ element }) => (
<div>
StaticQuery in wrapRootElement test (should show site title):
<span data-testid="wrap-root-element-result">{title}</span>
<div data-testid="gatsby-browser-hmr">
%TEST_HMR_IN_GATSBY_BROWSER%
</div>
</div>
</>
)}
Expand Down
13 changes: 6 additions & 7 deletions packages/gatsby/cache-dir/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ import asyncRequires from "$virtual/async-requires"
// Generated during bootstrap
import matchPaths from "$virtual/match-paths.json"
import { LoadingIndicatorEventHandler } from "./loading-indicator"

import Root from "./root"
import { init as navigationInit } from "./navigation"
// ensure in develop we have at least some .css (even if it's empty).
// this is so there is no warning about not matching content-type when site doesn't include any regular css (for example when css-in-js is used)
// this also make sure that if all css is removed in develop we are not left with stale commons.css that have stale content
import "./blank.css"

// Enable fast-refresh for virtual sync-requires
module.hot.accept(`$virtual/async-requires`, () => {
// Manually reload
})
// Enable fast-refresh for virtual sync-requires and gatsby-browser
module.hot.accept([`$virtual/async-requires`, `./api-runner-browser`])

window.___emitter = emitter

Expand Down Expand Up @@ -160,8 +159,8 @@ apiRunnerAsync(`onClientEntry`).then(() => {
loader.loadPage(`/404.html`),
loader.loadPage(window.location.pathname),
]).then(() => {
const preferDefault = m => (m && m.default) || m
const Root = preferDefault(require(`./root`))
Comment on lines -163 to -164
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was causing some weird module dependency chains and it wasn't exactly playing nice with HMR - hot updates for gatsby-browser modules were declined even tho I was accepting them on line 23 (for ./api-runner-browser).

I'm not exactly sure why it was done this way (delayed commonjs import) :/

navigationInit()

domReady(() => {
if (dismissLoadingIndicator) {
dismissLoadingIndicator()
Expand Down
24 changes: 11 additions & 13 deletions packages/gatsby/cache-dir/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@ import React from "react"
import { Router, Location, BaseContext } from "@reach/router"
import { ScrollContext } from "gatsby-react-router-scroll"

import {
shouldUpdateScroll,
init as navigationInit,
RouteUpdates,
} from "./navigation"
import { shouldUpdateScroll, RouteUpdates } from "./navigation"
import { apiRunner } from "./api-runner-browser"
import loader from "./loader"
import { PageQueryStore, StaticQueryStore } from "./query-result-store"
import EnsureResources from "./ensure-resources"
import FastRefreshOverlay from "./fast-refresh-overlay"

navigationInit()

// In gatsby v2 if Router is used in page using matchPaths
// paths need to contain full path.
// For example:
Expand Down Expand Up @@ -103,7 +97,7 @@ const Root = () => (
)

// Let site, plugins wrap the site e.g. for Redux.
const WrappedRoot = apiRunner(
const rootWrappedWithWrapRootElement = apiRunner(
`wrapRootElement`,
{ element: <Root /> },
<Root />,
Expand All @@ -112,8 +106,12 @@ const WrappedRoot = apiRunner(
}
).pop()

export default () => (
<FastRefreshOverlay>
<StaticQueryStore>{WrappedRoot}</StaticQueryStore>
</FastRefreshOverlay>
)
function RootWrappedWithOverlayAndProvider() {
return (
<FastRefreshOverlay>
<StaticQueryStore>{rootWrappedWithWrapRootElement}</StaticQueryStore>
</FastRefreshOverlay>
)
}

export default RootWrappedWithOverlayAndProvider
Comment on lines +109 to +117
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Name our Root Component :)

23 changes: 21 additions & 2 deletions packages/gatsby/src/utils/webpack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,13 +731,32 @@ export const createWebpackUtils = (
}
): CssMinimizerPlugin => new CssMinimizerPlugin(options)

plugins.fastRefresh = (): Plugin =>
new ReactRefreshWebpackPlugin({
plugins.fastRefresh = ({ modulesThatUseGatsby }): Plugin => {
const regExpToHack = /node_modules/
regExpToHack.test = (modulePath: string): boolean => {
// when it's not coming from node_modules we treat it as a source file.
if (!vendorRegex.test(modulePath)) {
return false
}

// If the module uses Gatsby as a dependency
// we want to treat it as src because of shadowing
return !modulesThatUseGatsby.some(module =>
modulePath.includes(module.path)
)
}

return new ReactRefreshWebpackPlugin({
overlay: {
sockIntegration: `whm`,
module: path.join(__dirname, `fast-refresh-module`),
},
// this is a bit hacky - exclude expect string or regexp or array of those
// so this is tricking ReactRefreshWebpackPlugin with providing regexp with
// overwritten .test method
exclude: regExpToHack,
})
}

plugins.extractText = (options: any): Plugin =>
new MiniCssExtractPlugin({
Expand Down
2 changes: 1 addition & 1 deletion packages/gatsby/src/utils/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ module.exports = async (
case `develop`: {
configPlugins = configPlugins
.concat([
plugins.fastRefresh(),
plugins.fastRefresh({ modulesThatUseGatsby }),
plugins.hotModuleReplacement(),
plugins.noEmitOnErrors(),
plugins.eslintGraphqlSchemaReload(),
Expand Down