diff --git a/e2e-tests/production-runtime/cypress/integration/1-production.js b/e2e-tests/production-runtime/cypress/integration/1-production.js index 3e1da2d76876b..6277ff008a3b1 100644 --- a/e2e-tests/production-runtime/cypress/integration/1-production.js +++ b/e2e-tests/production-runtime/cypress/integration/1-production.js @@ -1,5 +1,14 @@ /* global Cypress, cy */ +// NOTE: This needs to be run before any other integration tests as it +// sets up the service worker in offline mode. Therefore, if you want +// to test an individual integration test, you must run this +// first. E.g to run `compilation-hash.js` test, run +// +// cypress run -s \ +// "cypress/integration/1-production.js,cypress/integration/compilation-hash.js" \ +// -b chrome + describe(`Production build tests`, () => { it(`should render properly`, () => { cy.visit(`/`).waitForRouteChange() @@ -74,6 +83,13 @@ describe(`Production build tests`, () => { .should(`exist`) }) + it(`should pass pathContext to props`, () => { + cy.visit(`/path-context`).waitForRouteChange() + + // `bar` is set in gatsby-node createPages + cy.getTestElement(`path-context-foo`).contains(`bar`) + }) + it(`Uses env vars`, () => { cy.visit(`/env-vars`).waitForRouteChange() diff --git a/e2e-tests/production-runtime/cypress/integration/compilation-hash.js b/e2e-tests/production-runtime/cypress/integration/compilation-hash.js new file mode 100644 index 0000000000000..84fb3213fad29 --- /dev/null +++ b/e2e-tests/production-runtime/cypress/integration/compilation-hash.js @@ -0,0 +1,64 @@ +/* global cy */ + +const getRandomInt = (min, max) => { + min = Math.ceil(min) + max = Math.floor(max) + return Math.floor(Math.random() * (max - min)) + min +} + +const createMockCompilationHash = () => + [...Array(20)] + .map(a => getRandomInt(0, 16)) + .map(k => k.toString(16)) + .join(``) + +describe(`Webpack Compilation Hash tests`, () => { + it(`should render properly`, () => { + cy.visit(`/`).waitForRouteChange() + }) + + // This covers the case where a user loads a gatsby site and then + // the site is changed resulting in a webpack recompile and a + // redeploy. This could result in a mismatch between the page-data + // and the component. To protect against this, when gatsby loads a + // new page-data.json, it refreshes the page if it's webpack + // compilation hash differs from the one on on the window object + // (which was set on initial page load) + // + // Since initial page load results in all links being prefetched, we + // have to navigate to a non-prefetched page to test this. Thus the + // `deep-link-page`. + // + // We simulate a rebuild by updating all page-data.jsons and page + // htmls with the new hash. It's not pretty, but it's easier than + // figuring out how to perform an actual rebuild while cypress is + // running. See ../plugins/compilation-hash.js for the + // implementation + it(`should reload page if build occurs in background`, () => { + cy.window().then(window => { + const oldHash = window.___webpackCompilationHash + expect(oldHash).to.not.eq(undefined) + + const mockHash = createMockCompilationHash() + + // Simulate a new webpack build + cy.task(`overwriteWebpackCompilationHash`, mockHash).then(() => { + cy.getTestElement(`compilation-hash`).click() + cy.waitForAPIorTimeout(`onRouteUpdate`, { timeout: 3000 }) + + // Navigate into a non-prefetched page + cy.getTestElement(`deep-link-page`).click() + cy.waitForAPIorTimeout(`onRouteUpdate`, { timeout: 3000 }) + + // If the window compilation hash has changed, we know the + // page was refreshed + cy.window() + .its(`___webpackCompilationHash`) + .should(`equal`, mockHash) + }) + + // Cleanup + cy.task(`overwriteWebpackCompilationHash`, oldHash) + }) + }) +}) diff --git a/e2e-tests/production-runtime/cypress/integration/resource-loading-resilience.js b/e2e-tests/production-runtime/cypress/integration/resource-loading-resilience.js index 2e1093969b355..e65af9cc8e3c7 100644 --- a/e2e-tests/production-runtime/cypress/integration/resource-loading-resilience.js +++ b/e2e-tests/production-runtime/cypress/integration/resource-loading-resilience.js @@ -49,58 +49,52 @@ const runTests = () => { describe(`Every resources available`, () => { it(`Restore resources`, () => { - cy.exec(`npm run chunks -- restore`) + cy.task(`restoreAllBlockedResources`) }) runTests() }) -describe(`Missing top level resources`, () => { - describe(`Deleted pages manifest`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block pages-manifest`) +const runBlockedScenario = (scenario, args) => { + it(`Block resources`, () => { + cy.task(`restoreAllBlockedResources`).then(() => { + cy.task(scenario, args).then(() => { + runTests() + }) }) - runTests() }) +} +describe(`Missing top level resources`, () => { describe(`Deleted app chunk assets`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block app`) - }) - runTests() + runBlockedScenario(`blockAssetsForChunk`, { chunk: `app` }) }) }) -const runSuiteForPage = (label, path) => { +const runSuiteForPage = (label, pagePath) => { describe(`Missing "${label}" resources`, () => { describe(`Missing "${label}" page query results`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block-page ${path} query-result`) + runBlockedScenario(`blockAssetsForPage`, { + pagePath, + filter: `page-data`, }) - runTests() }) describe(`Missing "${label}" page page-template asset`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block-page ${path} page-template`) + runBlockedScenario(`blockAssetsForPage`, { + pagePath, + filter: `page-template`, }) - runTests() }) describe(`Missing "${label}" page extra assets`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block-page ${path} extra`) + runBlockedScenario(`blockAssetsForPage`, { + pagePath, + filter: `extra`, }) - runTests() }) describe(`Missing all "${label}" page assets`, () => { - it(`Block resources`, () => { - cy.exec(`npm run chunks -- restore`) - cy.exec(`npm run chunks -- block-page ${path} all`) + runBlockedScenario(`blockAssetsForPage`, { + pagePath, + filter: `all`, }) - runTests() }) }) } @@ -111,6 +105,6 @@ runSuiteForPage(`404`, `/404.html`) describe(`Cleanup`, () => { it(`Restore resources`, () => { - cy.exec(`npm run chunks -- restore`) + cy.task(`restoreAllBlockedResources`) }) }) diff --git a/e2e-tests/production-runtime/cypress/plugins/block-resources.js b/e2e-tests/production-runtime/cypress/plugins/block-resources.js new file mode 100644 index 0000000000000..a335d068016e0 --- /dev/null +++ b/e2e-tests/production-runtime/cypress/plugins/block-resources.js @@ -0,0 +1,120 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const glob = require(`glob`) + +const publicDir = path.join(__dirname, `..`, `..`, `public`) + +const getAssetManifest = () => { + const { assetsByChunkName } = require(`${publicDir}/webpack.stats.json`) + return assetsByChunkName +} + +const moveAsset = (from, to) => { + const fromExists = fs.existsSync(from) + const toExists = fs.existsSync(to) + + if (fromExists && !toExists) { + fs.moveSync(from, to, { + overwrite: true, + }) + } +} + +const getAssetPath = assetFileName => path.join(publicDir, assetFileName) +const getHiddenAssetPath = assetFileName => getAssetPath(`_${assetFileName}`) + +const restoreAsset = assetFileName => { + moveAsset(getHiddenAssetPath(assetFileName), getAssetPath(assetFileName)) +} + +const blockAsset = assetFileName => { + moveAsset(getAssetPath(assetFileName), getHiddenAssetPath(assetFileName)) +} + +const blockAssetsForChunk = ({ chunk, filter }) => { + const assetManifest = getAssetManifest() + assetManifest[chunk].forEach(blockAsset) + console.log(`Blocked assets for chunk "${chunk}"`) + return null +} + +const restorePageData = hiddenPath => { + if (path.basename(hiddenPath).charAt(0) !== `_`) { + throw new Error(`hiddenPath should have _ prefix`) + } + const restoredPath = path.join( + path.dirname(hiddenPath), + path.basename(hiddenPath).slice(1) + ) + moveAsset(hiddenPath, restoredPath) +} + +const getPageDataPath = pagePath => { + const fixedPagePath = pagePath === `/` ? `index` : pagePath + return path.join(publicDir, `page-data`, fixedPagePath, `page-data.json`) +} + +const getHiddenPageDataPath = pagePath => { + const fixedPagePath = pagePath === `/` ? `index` : pagePath + return path.join(publicDir, `page-data`, fixedPagePath, `_page-data.json`) +} + +const blockPageData = pagePath => + moveAsset(getPageDataPath(pagePath), getHiddenPageDataPath(pagePath)) + +const filterAssets = (assetsForPath, filter) => + assetsForPath.filter(asset => { + if (filter === `all`) { + return true + } else if (filter === `page-data`) { + return false + } + + const isMain = asset.startsWith(`component---`) + if (filter === `page-template`) { + return isMain + } else if (filter === `extra`) { + return !isMain + } + return false + }) + +const blockAssetsForPage = ({ pagePath, filter }) => { + const assetManifest = getAssetManifest() + + const pageData = JSON.parse(fs.readFileSync(getPageDataPath(pagePath))) + const { componentChunkName } = pageData + const assetsForPath = assetManifest[componentChunkName] + + const assets = filterAssets(assetsForPath, filter) + assets.forEach(blockAsset) + + if (filter === `all` || filter === `page-data`) { + blockPageData(pagePath) + } + + console.log(`Blocked assets for path "${pagePath}" [${filter}]`) + return null +} + +const restore = () => { + const allAssets = Object.values(getAssetManifest()).reduce((acc, assets) => { + assets.forEach(asset => acc.add(asset)) + return acc + }, new Set()) + + allAssets.forEach(restoreAsset) + + const globPattern = path.join(publicDir, `/page-data/**`, `_page-data.json`) + const hiddenPageDatas = glob.sync(globPattern) + hiddenPageDatas.forEach(restorePageData) + + console.log(`Restored resources`) + return null +} + +module.exports = { + restoreAllBlockedResources: restore, + blockAssetsForChunk, + blockAssetsForPage, +} diff --git a/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js b/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js new file mode 100644 index 0000000000000..61e38d642cc9a --- /dev/null +++ b/e2e-tests/production-runtime/cypress/plugins/compilation-hash.js @@ -0,0 +1,32 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const glob = require(`glob`) + +const replaceHtmlCompilationHash = (filename, newHash) => { + const html = fs.readFileSync(filename, `utf-8`) + const regex = /window\.webpackCompilationHash="\w*"/ + const replace = `window.webpackCompilationHash="${newHash}"` + fs.writeFileSync(filename, html.replace(regex, replace), `utf-8`) +} + +const replacePageDataCompilationHash = (filename, newHash) => { + const pageData = JSON.parse(fs.readFileSync(filename, `utf-8`)) + pageData.webpackCompilationHash = newHash + fs.writeFileSync(filename, JSON.stringify(pageData), `utf-8`) +} + +const overwriteWebpackCompilationHash = newHash => { + glob + .sync(path.join(__dirname, `../../public/page-data/**/page-data.json`)) + .forEach(filename => replacePageDataCompilationHash(filename, newHash)) + glob + .sync(path.join(__dirname, `../../public/**/index.html`)) + .forEach(filename => replaceHtmlCompilationHash(filename, newHash)) + + // cypress requires that null be returned instead of undefined + return null +} + +module.exports = { + overwriteWebpackCompilationHash, +} diff --git a/e2e-tests/production-runtime/cypress/plugins/index.js b/e2e-tests/production-runtime/cypress/plugins/index.js index 871f0fb6a83c5..933dcc078b175 100644 --- a/e2e-tests/production-runtime/cypress/plugins/index.js +++ b/e2e-tests/production-runtime/cypress/plugins/index.js @@ -1,15 +1,5 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) +const compilationHash = require(`./compilation-hash`) +const blockResources = require(`./block-resources`) module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits @@ -27,4 +17,6 @@ module.exports = (on, config) => { return args }) } + + on(`task`, Object.assign({}, compilationHash, blockResources)) } diff --git a/e2e-tests/production-runtime/gatsby-node.js b/e2e-tests/production-runtime/gatsby-node.js index 1b7aa08513b2e..465407b7f593f 100644 --- a/e2e-tests/production-runtime/gatsby-node.js +++ b/e2e-tests/production-runtime/gatsby-node.js @@ -1,17 +1,30 @@ exports.onCreatePage = ({ page, actions }) => { - if (page.path === `/client-only-paths/`) { - // create client-only-paths - page.matchPath = `/client-only-paths/*` - actions.createPage(page) - } else if (page.path === `/`) { - // use index page as template - // (mimics) - actions.createPage({ - ...page, - path: `/duplicated`, - context: { - DOMMarker: `duplicated`, - }, - }) + switch (page.path) { + case `/client-only-paths/`: + // create client-only-paths + page.matchPath = `/client-only-paths/*` + actions.createPage(page) + break + + case `/path-context/`: + actions.createPage({ + ...page, + context: { + foo: `bar`, + }, + }) + break + + case `/`: + // use index page as template + // (mimics) + actions.createPage({ + ...page, + path: `/duplicated`, + context: { + DOMMarker: `duplicated`, + }, + }) + break } } diff --git a/e2e-tests/production-runtime/package.json b/e2e-tests/production-runtime/package.json index 662a8baed0781..89c90bca24b6b 100644 --- a/e2e-tests/production-runtime/package.json +++ b/e2e-tests/production-runtime/package.json @@ -9,6 +9,7 @@ "gatsby-plugin-manifest": "^2.0.17", "gatsby-plugin-offline": "^2.0.23", "gatsby-plugin-react-helmet": "^3.0.6", + "glob": "^7.1.3", "react": "^16.8.0", "react-dom": "^16.8.0", "react-helmet": "^5.2.0" @@ -34,14 +35,12 @@ "cy:run": "npm run cy:run:normal && npm run cy:run:slow", "cy:run:offline": "npm run cy:run:normal -- --env TEST_PLUGIN_OFFLINE=y && npm run cy:run:slow -- --env TEST_PLUGIN_OFFLINE=y", "cy:run:normal": "cypress run --browser chrome", - "cy:run:slow": "CYPRESS_CONNECTION_TYPE=slow cypress run --browser chrome --config testFiles=prefetching.js", - "chunks": "node scripts/chunks.js" + "cy:run:slow": "CYPRESS_CONNECTION_TYPE=slow cypress run --browser chrome --config testFiles=prefetching.js" }, "devDependencies": { "fs-extra": "^7.0.1", "prettier": "^1.14.3", - "start-server-and-test": "^1.7.1", - "yargs": "^12.0.5" + "start-server-and-test": "^1.7.1" }, "repository": { "type": "git", diff --git a/e2e-tests/production-runtime/scripts/chunks.js b/e2e-tests/production-runtime/scripts/chunks.js deleted file mode 100644 index 0c5e8520e9f8f..0000000000000 --- a/e2e-tests/production-runtime/scripts/chunks.js +++ /dev/null @@ -1,154 +0,0 @@ -const fs = require(`fs-extra`) -const path = require(`path`) -const yargs = require(`yargs`) - -const getAssetManifest = () => { - const { assetsByChunkName } = require(`../public/webpack.stats.json`) - return assetsByChunkName -} - -const getPagesManifest = () => { - const { pages, dataPaths } = require(`../.cache/data.json`) - - return pages.reduce((result, page) => { - result[page.path] = { - componentChunkName: page.componentChunkName, - queryResult: dataPaths[page.jsonName], - } - return result - }, {}) -} - -const getAssetPath = assetFileName => - path.join(__dirname, `..`, `public`, assetFileName) -const getHiddenAssetPath = assetFileName => getAssetPath(`_${assetFileName}`) - -const getQueryResultPath = queryResultFileName => - path.join( - __dirname, - `..`, - `public`, - `static`, - `d`, - `${queryResultFileName}.json` - ) -const getHiddenQueryResultPath = queryResultFileName => - getQueryResultPath(`_${queryResultFileName}`) - -const moveAsset = (from, to) => { - const fromExists = fs.existsSync(from) - const toExists = fs.existsSync(to) - - if (fromExists && !toExists) { - fs.moveSync(from, to, { - overwrite: true, - }) - } -} - -const blockQueryResult = queryResultFileName => { - moveAsset( - getQueryResultPath(queryResultFileName), - getHiddenQueryResultPath(queryResultFileName) - ) -} - -const restoreQueryResult = queryResultFileName => { - moveAsset( - getHiddenQueryResultPath(queryResultFileName), - getQueryResultPath(queryResultFileName) - ) -} - -const blockAsset = assetFileName => { - moveAsset(getAssetPath(assetFileName), getHiddenAssetPath(assetFileName)) -} - -const restoreAsset = assetFileName => { - moveAsset(getHiddenAssetPath(assetFileName), getAssetPath(assetFileName)) -} - -const blockAssetsForChunk = ({ chunk, filter }) => { - const assetManifest = getAssetManifest() - assetManifest[chunk].forEach(blockAsset) - console.log(`Blocked assets for chunk "${chunk}"`) -} - -const filterAssets = (assetsForPath, filter) => - assetsForPath.filter(asset => { - if (filter === `all`) { - return true - } else if (filter === `query-result`) { - return false - } - - const isMain = asset.startsWith(`component---`) - if (filter === `page-template`) { - return isMain - } else if (filter === `extra`) { - return !isMain - } - return false - }) - -const blockAssetsForPath = ({ page, filter }) => { - const pagesManifest = getPagesManifest() - const assetManifest = getAssetManifest() - - const { componentChunkName, queryResult } = pagesManifest[page] - const assetsForPath = assetManifest[componentChunkName] - - const assets = filterAssets(assetsForPath, filter) - assets.forEach(blockAsset) - - if (filter === `all` || filter === `query-result`) { - blockQueryResult(queryResult) - } - - console.log(`Blocked assets for path "${page}" [${filter}]`) -} - -const restore = () => { - const allAssets = Object.values(getAssetManifest()).reduce((acc, assets) => { - assets.forEach(asset => acc.add(asset)) - return acc - }, new Set()) - - allAssets.forEach(restoreAsset) - - Object.values(getPagesManifest()) - .map(page => page.queryResult) - .forEach(restoreQueryResult) - - console.log(`Restored resources`) -} -// eslint-disable-next-line no-unused-expressions -yargs - .command( - `block `, - `Block assets for given chunk`, - yargs => - yargs.positional(`chunk`, { - describe: `Name of the chunk`, - type: `string`, - }), - blockAssetsForChunk - ) - .command( - `block-page []`, - `Block assets for a template page used by given path`, - yargs => - yargs - .positional(`page`, { - describe: `Path to a page`, - type: `string`, - }) - .positional(`filter`, { - describe: `Type of chunks associated with page`, - type: `string`, - choices: [`all`, `page-template`, `extra`, `query-result`], - default: `all`, - }), - blockAssetsForPath - ) - .command(`restore`, `Restore all the assets`, restore).argv diff --git a/e2e-tests/production-runtime/src/pages/compilation-hash.js b/e2e-tests/production-runtime/src/pages/compilation-hash.js new file mode 100644 index 0000000000000..0fccb86d27ae7 --- /dev/null +++ b/e2e-tests/production-runtime/src/pages/compilation-hash.js @@ -0,0 +1,19 @@ +import React from 'react' +import { Link } from 'gatsby' + +import Layout from '../components/layout' +import InstrumentPage from '../utils/instrument-page' + +const CompilationHashPage = () => ( + +

Hi from Compilation Hash page

+

Used by integration/compilation-hash.js test

+

+ + To deeply linked page + +

+
+) + +export default InstrumentPage(CompilationHashPage) diff --git a/e2e-tests/production-runtime/src/pages/deep-link-page.js b/e2e-tests/production-runtime/src/pages/deep-link-page.js new file mode 100644 index 0000000000000..6d1b881f8fb0e --- /dev/null +++ b/e2e-tests/production-runtime/src/pages/deep-link-page.js @@ -0,0 +1,16 @@ +import React from 'react' + +import Layout from '../components/layout' +import InstrumentPage from '../utils/instrument-page' + +const DeepLinkPage = () => ( + +

Hi from a deeply linked page

+

+ Used to navigate to a non prefetched page by + integrations/compilation-hash.js tests +

+
+) + +export default InstrumentPage(DeepLinkPage) diff --git a/e2e-tests/production-runtime/src/pages/index.js b/e2e-tests/production-runtime/src/pages/index.js index 6e211b8981b69..cfea7e41748c6 100644 --- a/e2e-tests/production-runtime/src/pages/index.js +++ b/e2e-tests/production-runtime/src/pages/index.js @@ -46,6 +46,16 @@ const IndexPage = ({ pageContext }) => ( StaticQuery and useStaticQuery +
  • + + Compilation Hash Page + +
  • +
  • + + Path Context + +
  • ) diff --git a/e2e-tests/production-runtime/src/pages/path-context.js b/e2e-tests/production-runtime/src/pages/path-context.js new file mode 100644 index 0000000000000..f6abb380ef029 --- /dev/null +++ b/e2e-tests/production-runtime/src/pages/path-context.js @@ -0,0 +1,17 @@ +import React from 'react' + +import Layout from '../components/layout' +import InstrumentPage from '../utils/instrument-page' + +const PathContextPage = ({ pathContext }) => ( + +

    Hello from a page that uses the old pathContext

    +

    It was deprecated in favor of pageContext, but is still supported

    +

    + page.pathContext.foo = + {pathContext.foo} +

    +
    +) + +export default InstrumentPage(PathContextPage) diff --git a/packages/gatsby-plugin-guess-js/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-guess-js/src/__tests__/gatsby-ssr.js new file mode 100644 index 0000000000000..8d4536f83dda8 --- /dev/null +++ b/packages/gatsby-plugin-guess-js/src/__tests__/gatsby-ssr.js @@ -0,0 +1,53 @@ +const gatsbySsr = require(`../gatsby-ssr`) +const guessWebpack = require(`guess-webpack/api`) +const fs = require(`fs`) + +jest.mock(`guess-webpack/api`) +jest.mock(`fs`) + +describe(`gatsby-plugin-guess-js`, () => { + describe(`gatsby-ssr`, () => { + it(`should add prefetch links to head components`, () => { + const path = `some/path` + const componentChunkName = `chunkName` + const chunks = [`chunk1`, `chunk2`] + const pageData = { path, componentChunkName } + + // Mock out webpack.stats.json + const stats = { + assetsByChunkName: { + [componentChunkName]: chunks, + }, + } + fs.readFileSync.mockImplementation(file => + file === `${process.cwd()}/public/webpack.stats.json` + ? JSON.stringify(stats) + : undefined + ) + + // Mock out guess plugin + guessWebpack.guess.mockImplementation(() => { + return { + [path]: `ignored`, + } + }) + + // mock out static-render.js/loadPageDataSync + const loadPageDataSync = () => pageData + const setHeadComponents = jest.fn() + const pluginOptions = { + minimumThreshold: 1000, + } + + gatsbySsr.onRenderBody( + { loadPageDataSync, setHeadComponents }, + pluginOptions + ) + + expect(setHeadComponents.mock.calls[0][0]).toMatchObject([ + { props: { href: `/chunk1` } }, + { props: { href: `/chunk2` } }, + ]) + }) + }) +}) diff --git a/packages/gatsby-plugin-guess-js/src/gatsby-browser.js b/packages/gatsby-plugin-guess-js/src/gatsby-browser.js index 1e16ce0079505..102c8a164246f 100644 --- a/packages/gatsby-plugin-guess-js/src/gatsby-browser.js +++ b/packages/gatsby-plugin-guess-js/src/gatsby-browser.js @@ -12,7 +12,7 @@ exports.onRouteUpdate = ({ location }) => { initialPath = location.pathname } -exports.onPrefetchPathname = ({ getResourcesForPathname }, pluginOptions) => { +exports.onPrefetchPathname = ({ loadPage }, pluginOptions) => { if (process.env.NODE_ENV !== `production`) return const matchedPaths = Object.keys( @@ -24,6 +24,7 @@ exports.onPrefetchPathname = ({ getResourcesForPathname }, pluginOptions) => { // Don't prefetch from client for the initial path as we did that // during SSR - if (!(notNavigated && initialPath === window.location.pathname)) - matchedPaths.forEach(getResourcesForPathname) + if (!(notNavigated && initialPath === window.location.pathname)) { + matchedPaths.forEach(loadPage) + } } diff --git a/packages/gatsby-plugin-guess-js/src/gatsby-ssr.js b/packages/gatsby-plugin-guess-js/src/gatsby-ssr.js index d0c0e96b1ddaf..bb32ba5feccb7 100644 --- a/packages/gatsby-plugin-guess-js/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-guess-js/src/gatsby-ssr.js @@ -1,5 +1,4 @@ const _ = require(`lodash`) -const nodePath = require(`path`) const fs = require(`fs`) const React = require(`react`) @@ -12,18 +11,6 @@ function urlJoin(...parts) { }, ``) } -let pd = [] -const readPageData = () => { - if (pd.length > 0) { - return pd - } else { - pd = JSON.parse( - fs.readFileSync(nodePath.join(process.cwd(), `.cache`, `data.json`)) - ) - return pd - } -} - let s const readStats = () => { if (s) { @@ -37,19 +24,19 @@ const readStats = () => { } exports.onRenderBody = ( - { setHeadComponents, pathname, pathPrefix }, + { setHeadComponents, pathname, pathPrefix, loadPageDataSync }, pluginOptions ) => { - if (process.env.NODE_ENV === `production`) { - const pagesData = readPageData() + if ( + process.env.NODE_ENV === `production` || + process.env.NODE_ENV === `test` + ) { const stats = readStats() const matchedPaths = Object.keys( guess({ path: pathname, threshold: pluginOptions.minimumThreshold }) ) if (!_.isEmpty(matchedPaths)) { - const matchedPages = matchedPaths.map(match => - _.find(pagesData.pages, page => page.path === match) - ) + const matchedPages = matchedPaths.map(loadPageDataSync) let componentUrls = [] matchedPages.forEach(p => { if (p && p.componentChunkName) { @@ -68,7 +55,6 @@ exports.onRenderBody = ( href: urlJoin(pathPrefix, c), }) ) - setHeadComponents(components) } diff --git a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap index 2137dd0c59a58..9b381e0865bc3 100644 --- a/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap +++ b/packages/gatsby/cache-dir/__tests__/__snapshots__/static-entry.js.snap @@ -6,8 +6,8 @@ exports[`develop-static-entry onPreRenderHTML can be used to replace postBodyCom exports[`develop-static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; -exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"
    "`; +exports[`static-entry onPreRenderHTML can be used to replace headComponents 1`] = `"
    "`; -exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`static-entry onPreRenderHTML can be used to replace postBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; -exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; +exports[`static-entry onPreRenderHTML can be used to replace preBodyComponents 1`] = `"
    div3
    div2
    div1
    "`; diff --git a/packages/gatsby/cache-dir/__tests__/find-page.js b/packages/gatsby/cache-dir/__tests__/find-page.js deleted file mode 100644 index c998b8e813c9a..0000000000000 --- a/packages/gatsby/cache-dir/__tests__/find-page.js +++ /dev/null @@ -1,67 +0,0 @@ -const pageFinderFactory = require(`../find-page`).default - -let findPage - -describe(`find-page`, () => { - beforeEach(() => { - const newPages = [ - { - path: `/about/`, - componentChunkName: `page-component---src-pages-test-js`, - jsonName: `about.json`, - }, - { - path: `/about/me/`, - componentChunkName: `page-component---src-pages-test-js`, - jsonName: `about-me.json`, - }, - { - path: `/about/the best/`, - componentChunkName: `page-component---src-pages-test-js`, - jsonName: `the-best.json`, - }, - { - path: `/app/`, - matchPath: `/app/*`, - componentChunkName: `page-component---src-pages-app-js`, - jsonName: `app.json`, - }, - ] - findPage = pageFinderFactory(newPages) - }) - - it(`can find a page`, () => { - expect(findPage(`/about/`).path).toBe(`/about/`) - expect(findPage(`/about/me/`).path).toBe(`/about/me/`) - }) - - it(`can find a page with space in its path`, () => { - expect(findPage(`/about/the best/`).path).toBe(`/about/the best/`) - expect(findPage(`/about/the%20best/`).path).toBe(`/about/the best/`) - }) - - it(`can find a client only path`, () => { - expect(findPage(`/about/super-duper/`)).toBeUndefined() - expect(findPage(`/app/client/only/path`).path).toBe(`/app/`) - }) - - it(`can find links with hashes`, () => { - expect(findPage(`/about/me/#hashtagawesome`).path).toBe(`/about/me/`) - }) - - it(`can find links with search query`, () => { - expect(findPage(`/about/me/?query=awesome`).path).toBe(`/about/me/`) - }) - - it(`handles finding prefixed links`, () => { - const newPages = [ - { - path: `/about/`, - componentChunkName: `page-component---src-pages-test-js`, - jsonName: `about.json`, - }, - ] - const findPage2 = pageFinderFactory(newPages, `/my-test-prefix`) - expect(findPage2(`/my-test-prefix/about/`).path).toBe(`/about/`) - }) -}) diff --git a/packages/gatsby/cache-dir/__tests__/static-entry.js b/packages/gatsby/cache-dir/__tests__/static-entry.js index 8cb6487dd496b..ffe5db5797cf3 100644 --- a/packages/gatsby/cache-dir/__tests__/static-entry.js +++ b/packages/gatsby/cache-dir/__tests__/static-entry.js @@ -1,5 +1,6 @@ import React from "react" import fs from "fs" +const { join } = require(`path`) import DevelopStaticEntry from "../develop-static-entry" @@ -30,32 +31,17 @@ jest.mock( } ) -jest.mock( - `../data.json`, - () => { - return { - dataPaths: [ - { - [`about.json`]: `/400/about`, - }, - ], - pages: [ - { - path: `/about/`, - componentChunkName: `page-component---src-pages-test-js`, - jsonName: `about.json`, - }, - ], - } - }, - { - virtual: true, - } -) - const MOCK_FILE_INFO = { [`${process.cwd()}/public/webpack.stats.json`]: `{}`, [`${process.cwd()}/public/chunk-map.json`]: `{}`, + [join( + process.cwd(), + `/public/page-data/about/page-data.json` + )]: JSON.stringify({ + componentChunkName: `page-component---src-pages-test-js`, + path: `/about/`, + webpackCompilationHash: `1234567890abcdef1234`, + }), } let StaticEntry diff --git a/packages/gatsby/cache-dir/api-runner-browser.js b/packages/gatsby/cache-dir/api-runner-browser.js index 0a4ff8b1168f4..2dd5981615854 100644 --- a/packages/gatsby/cache-dir/api-runner-browser.js +++ b/packages/gatsby/cache-dir/api-runner-browser.js @@ -3,6 +3,8 @@ const { getResourcesForPathname, getResourcesForPathnameSync, getResourceURLsForPathname, + loadPage, + loadPageSync, } = require(`./loader`).publicLoader exports.apiRunner = (api, args = {}, defaultReturn, argTransform) => { @@ -22,9 +24,13 @@ exports.apiRunner = (api, args = {}, defaultReturn, argTransform) => { return undefined } + // Deprecated April 2019. Use `loadPageSync` instead args.getResourcesForPathnameSync = getResourcesForPathnameSync + // Deprecated April 2019. Use `loadPage` instead args.getResourcesForPathname = getResourcesForPathname args.getResourceURLsForPathname = getResourceURLsForPathname + args.loadPage = loadPage + args.loadPageSync = loadPageSync const result = plugin.plugin[api](args, plugin.options) if (result && argTransform) { diff --git a/packages/gatsby/cache-dir/app.js b/packages/gatsby/cache-dir/app.js index 4f502082ec8d9..c09e618b2b48a 100644 --- a/packages/gatsby/cache-dir/app.js +++ b/packages/gatsby/cache-dir/app.js @@ -5,9 +5,8 @@ import domReady from "@mikaelkristiansson/domready" import socketIo from "./socketIo" import emitter from "./emitter" import { apiRunner, apiRunnerAsync } from "./api-runner-browser" -import loader, { setApiRunnerForLoader, postInitialRenderWork } from "./loader" +import loader, { setApiRunnerForLoader } from "./loader" import syncRequires from "./sync-requires" -import pages from "./pages.json" window.___emitter = emitter setApiRunnerForLoader(apiRunner) @@ -49,18 +48,16 @@ apiRunnerAsync(`onClientEntry`).then(() => { ReactDOM.render )[0] - loader.addPagesArray(pages) loader.addDevRequires(syncRequires) Promise.all([ - loader.getResourcesForPathname(`/dev-404-page/`), - loader.getResourcesForPathname(`/404.html`), - loader.getResourcesForPathname(window.location.pathname), + loader.loadPage(`/dev-404-page/`), + loader.loadPage(`/404.html`), + loader.loadPage(window.location.pathname), ]).then(() => { const preferDefault = m => (m && m.default) || m let Root = preferDefault(require(`./root`)) domReady(() => { renderer(, rootElement, () => { - postInitialRenderWork() apiRunner(`onInitialClientRender`) }) }) diff --git a/packages/gatsby/cache-dir/ensure-resources.js b/packages/gatsby/cache-dir/ensure-resources.js index 0c37dd50333d6..4ef5da0cb927e 100644 --- a/packages/gatsby/cache-dir/ensure-resources.js +++ b/packages/gatsby/cache-dir/ensure-resources.js @@ -5,19 +5,55 @@ import shallowCompare from "shallow-compare" let isInitialRender = true -// Pass pathname in as prop. -// component will try fetching resources. If they exist, -// will just render, else will render null. -// It will also wait for pageResources -// before propagating location change to children. +// ensure-resources.js is the top-level component called by the +// Router. It is therefore rendered on every page navigation. It's job +// is to make sure that we have all the resources required to render +// the page for the props.location. And to fall back to a 404 page if +// necessary. Once these resources are ensured, then they are passed +// to props.children, which is actually a function that takes those +// resources and returns React components (see production-app.js). +// +// On the initial render, production-app.js will have already called +// loader.loadPage. If no resources were found, then ensure-resources +// will throw an exception, which will end the running js and result +// in only the pure HTML of the page being displayed. +// +// ## Happy Path: +// +// When a navigation occurs, navigate.js will call +// loader.loadPage. `getDerivedStateFromProps` will retrieve that data +// and return it as props. `shouldComponentUpdate` will check if those +// resources have changed, and if so, will call render. +// +// ## Page data or component blocked: +// +// This can happen for e.g if an ad blocker blocks json or js, or +// internet connection cutoff halfway through request. In this case, +// when navigation occurs, navigate.js calls loader.loadPage, which +// returns null. `getDerivedStateFromProps` updates props with null +// pageResources. `shouldComponentUpdate` sees no resources, so calls +// `retryResources`, which performs a request for the page html. If +// the html page exists, then we have a fallback, so we reload the +// entire page, resulting in the pure HTML being rendered. +// +// ## Page doesn't exist at all (404) +// +// As above, except that `retryResources` will not find html, and will +// instead call loader.load404Page(). If 404 resources exist, they are +// set on the component state, resulting in a `render` call. If not, +// then the whole above sequence will except it will try and fallback +// ot the pure 404 HTML page. class EnsureResources extends React.Component { constructor(props) { super() let location = props.location - + let pageResources = loader.loadPageSync(location.pathname) + if (!pageResources && !loader.doesPageHtmlExistSync(location.pathname)) { + pageResources = loader.loadPageSync(`/404.html`) + } this.state = { location: { ...location }, - pageResources: loader.getResourcesForPathnameSync(location.pathname), + pageResources, } } @@ -33,10 +69,8 @@ class EnsureResources extends React.Component { } static getDerivedStateFromProps({ location }, prevState) { - if (prevState.location !== location) { - const pageResources = loader.getResourcesForPathnameSync( - location.pathname - ) + if (prevState.location.href !== location.href) { + const pageResources = loader.loadPageSync(location.pathname) return { pageResources, @@ -62,15 +96,19 @@ class EnsureResources extends React.Component { retryResources(nextProps) { const { pathname } = nextProps.location - if (!loader.getResourcesForPathnameSync(pathname)) { + if (!loader.loadPageSync(pathname)) { // Store the previous and next location before resolving resources const prevLocation = this.props.location this.nextLocation = nextProps.location - // Page resources won't be set in cases where the browser back button - // or forward button is pushed as we can't wait as normal for resources - // to load before changing the page. - loader.getResourcesForPathname(pathname).then(pageResources => { + if (loader.doesPageHtmlExistSync(pathname)) { + this.reloadPage(prevLocation.href) + return + } + + // If we can't find the page resources, or its HTML, then this + // page doesn't exist. Load the /404.html page + loader.loadPageOr404(pathname).then(pageResources => { // The page may have changed since we started this, in which case doesn't update if (this.nextLocation !== nextProps.location) { return diff --git a/packages/gatsby/cache-dir/find-page.js b/packages/gatsby/cache-dir/find-page.js deleted file mode 100644 index f25078f4f6b7b..0000000000000 --- a/packages/gatsby/cache-dir/find-page.js +++ /dev/null @@ -1,55 +0,0 @@ -// TODO add tests especially for handling prefixed links. -import { match as matchPath } from "@reach/router/lib/utils" -import stripPrefix from "./strip-prefix" - -const pageCache = {} - -export default (pages, pathPrefix = ``) => rawPathname => { - let pathname = decodeURIComponent(rawPathname) - - // Remove the pathPrefix from the pathname. - let trimmedPathname = stripPrefix(pathname, pathPrefix) - - // Remove any hashfragment - if (trimmedPathname.split(`#`).length > 1) { - trimmedPathname = trimmedPathname - .split(`#`) - .slice(0, -1) - .join(``) - } - - // Remove search query - if (trimmedPathname.split(`?`).length > 1) { - trimmedPathname = trimmedPathname - .split(`?`) - .slice(0, -1) - .join(``) - } - - if (pageCache[trimmedPathname]) { - return pageCache[trimmedPathname] - } - - let foundPage - // Array.prototype.find is not supported in IE so we use this somewhat odd - // work around. - pages.some(page => { - let pathToMatch = page.matchPath ? page.matchPath : page.path - if (matchPath(pathToMatch, trimmedPathname)) { - foundPage = page - pageCache[trimmedPathname] = page - return true - } - - // Finally, try and match request with default document. - if (matchPath(`${page.path}index.html`, trimmedPathname)) { - foundPage = page - pageCache[trimmedPathname] = page - return true - } - - return false - }) - - return foundPage -} diff --git a/packages/gatsby/cache-dir/json-store.js b/packages/gatsby/cache-dir/json-store.js index 8bec0d5b40c51..147198b193313 100644 --- a/packages/gatsby/cache-dir/json-store.js +++ b/packages/gatsby/cache-dir/json-store.js @@ -1,6 +1,7 @@ import React from "react" import PageRenderer from "./page-renderer" +import normalizePagePath from "./normalize-page-path" import { StaticQueryContext } from "gatsby" import { getStaticQueryData, @@ -22,7 +23,7 @@ if (process.env.NODE_ENV === `production`) { const getPathFromProps = props => props.pageResources && props.pageResources.page - ? props.pageResources.page.path + ? normalizePagePath(props.pageResources.page.path) : undefined class JSONStore extends React.Component { @@ -74,8 +75,8 @@ class JSONStore extends React.Component { return ( this.props.location !== nextProps.location || this.state.path !== nextState.path || - this.state.pageQueryData[nextState.path] !== - nextState.pageQueryData[nextState.path] || + this.state.pageQueryData[normalizePagePath(nextState.path)] !== + nextState.pageQueryData[normalizePagePath(nextState.path)] || this.state.staticQueryData !== nextState.staticQueryData ) } diff --git a/packages/gatsby/cache-dir/loader.js b/packages/gatsby/cache-dir/loader.js index 5a9dec232489e..f53d8195f3be6 100644 --- a/packages/gatsby/cache-dir/loader.js +++ b/packages/gatsby/cache-dir/loader.js @@ -1,166 +1,163 @@ -import pageFinderFactory from "./find-page" import emitter from "./emitter" import prefetchHelper from "./prefetch" +import { match } from "@reach/router/lib/utils" +import normalizePagePath from "./normalize-page-path" +import stripPrefix from "./strip-prefix" +// Generated during bootstrap +import matchPaths from "./match-paths.json" const preferDefault = m => (m && m.default) || m -let devGetPageData -let inInitialRender = true -let hasFetched = Object.create(null) +const pageNotFoundPaths = new Set() + +let apiRunner let syncRequires = {} let asyncRequires = {} -let jsonDataPaths = {} -let fetchHistory = [] -let fetchingPageResourceMapPromise = null -let fetchedPageResourceMap = false -/** - * Indicate if pages manifest is loaded - * - in production it is split to separate "pages-manifest" chunk that need to be lazy loaded, - * - in development it is part of single "common" chunk and is available from the start. - */ -let hasPageResourceMap = process.env.NODE_ENV !== `production` -let apiRunner -const failedPaths = {} -const MAX_HISTORY = 5 -const jsonPromiseStore = {} +const fetchedPageData = {} +const pageDatas = {} +const fetchPromiseStore = {} +const pageHtmlExistsResults = {} +let devGetPageData if (process.env.NODE_ENV !== `production`) { devGetPageData = require(`./socketIo`).getPageData } -/** - * Fetch resource map (pages data and paths to json files with results of - * queries) - */ -const fetchPageResourceMap = () => { - if (!fetchingPageResourceMapPromise) { - fetchingPageResourceMapPromise = new Promise(resolve => { - asyncRequires - .data() - .then(({ pages, dataPaths }) => { - // TODO — expose proper way to access this data from plugins. - // Need to come up with an API for plugins to access - // site info. - window.___dataPaths = dataPaths - queue.addPagesArray(pages) - queue.addDataPaths(dataPaths) - hasPageResourceMap = true - resolve((fetchedPageResourceMap = true)) - }) - .catch(e => { - console.warn( - `Failed to fetch pages manifest. Gatsby will reload on next navigation.` - ) - // failed to grab pages metadata - // for now let's just resolve this - on navigation this will cause missing resources - // and will trigger page reload and then it will retry - // this can happen with service worker updates when webpack manifest points to old - // chunk that no longer exists on server - resolve((fetchedPageResourceMap = true)) - }) - }) +// Cache for `cleanAndFindPath()`. In case `match-paths.json` is large +const cleanAndFindPathCache = {} + +const findMatchPath = (matchPaths, trimmedPathname) => { + for (const { matchPath, path } of matchPaths) { + if (match(matchPath, trimmedPathname)) { + return path + } } - return fetchingPageResourceMapPromise + return null } -const createJsonURL = jsonName => `${__PATH_PREFIX__}/static/d/${jsonName}.json` -const createComponentUrls = componentChunkName => - window.___chunkMapping[componentChunkName].map( - chunk => __PATH_PREFIX__ + chunk - ) +// Given a raw URL path, returns the cleaned version of it (trim off +// `#` and query params), or if it matches an entry in +// `match-paths.json`, its matched path is returned +// +// E.g `/foo?bar=far` => `/foo` +// +// Or if `match-paths.json` contains `{ "/foo*": "/page1", ...}`, then +// `/foo?bar=far` => `/page1` +const cleanAndFindPath = rawPathname => { + let pathname = decodeURIComponent(rawPathname) + // Remove the pathPrefix from the pathname. + let trimmedPathname = stripPrefix(pathname, __BASE_PATH__) + // Remove any hashfragment + if (trimmedPathname.split(`#`).length > 1) { + trimmedPathname = trimmedPathname + .split(`#`) + .slice(0, -1) + .join(``) + } -const fetchResource = resourceName => { - // Find resource - let resourceFunction - if (resourceName.slice(0, 12) === `component---`) { - resourceFunction = asyncRequires.components[resourceName] - } else { - if (resourceName in jsonPromiseStore) { - resourceFunction = () => jsonPromiseStore[resourceName] + // Remove search query + if (trimmedPathname.split(`?`).length > 1) { + trimmedPathname = trimmedPathname + .split(`?`) + .slice(0, -1) + .join(``) + } + if (cleanAndFindPathCache[trimmedPathname]) { + return cleanAndFindPathCache[trimmedPathname] + } + + let foundPath = findMatchPath(matchPaths, trimmedPathname) + if (!foundPath) { + if (trimmedPathname === `/index.html`) { + foundPath = `/` } else { - resourceFunction = () => { - const fetchPromise = new Promise((resolve, reject) => { - const url = createJsonURL(jsonDataPaths[resourceName]) - const req = new XMLHttpRequest() - req.open(`GET`, url, true) - req.withCredentials = true - req.onreadystatechange = () => { - if (req.readyState == 4) { - if (req.status === 200) { - resolve(JSON.parse(req.responseText)) - } else { - delete jsonPromiseStore[resourceName] - reject() - } - } - } - req.send(null) - }) - jsonPromiseStore[resourceName] = fetchPromise - return fetchPromise - } + foundPath = trimmedPathname } } + foundPath = normalizePagePath(foundPath) + cleanAndFindPathCache[trimmedPathname] = foundPath + return foundPath +} - // Download the resource - hasFetched[resourceName] = true - return new Promise(resolve => { - const fetchPromise = resourceFunction() - let failed = false - return fetchPromise - .catch(() => { - failed = true - }) - .then(component => { - fetchHistory.push({ - resource: resourceName, - succeeded: !failed, - }) - - fetchHistory = fetchHistory.slice(-MAX_HISTORY) - - resolve(component) - }) +const cachedFetch = (resourceName, fetchFn) => { + if (resourceName in fetchPromiseStore) { + return fetchPromiseStore[resourceName] + } + const promise = fetchFn(resourceName) + fetchPromiseStore[resourceName] = promise + return promise.catch(err => { + delete fetchPromiseStore[resourceName] + return err }) } -const prefetchResource = resourceName => { - if (resourceName.slice(0, 12) === `component---`) { - return Promise.all( - createComponentUrls(resourceName).map(url => prefetchHelper(url)) - ) +const doFetch = (url, method = `GET`) => + new Promise((resolve, reject) => { + const req = new XMLHttpRequest() + req.open(method, url, true) + req.withCredentials = true + req.onreadystatechange = () => { + if (req.readyState == 4) { + resolve(req) + } + } + req.send(null) + }) + +const handlePageDataResponse = (path, req) => { + fetchedPageData[path] = true + if (req.status === 200) { + const contentType = req.getResponseHeader(`content-type`) + // Since we don't know if a URL is a page or not until we make a + // request to the server, the response could be anything. E.g an + // index.html. So we have to double check that response is + // actually a proper JSON file. If it isn't, then it's not a page + // and we can infer that the requested page doesn't exist + if (!contentType || !contentType.startsWith(`application/json`)) { + pageNotFoundPaths.add(path) + // null signifies "page doesn't exist" + return null + } else { + const pageData = JSON.parse(req.responseText) + pageDatas[path] = pageData + return pageData + } + } else if (req.status === 404) { + pageNotFoundPaths.add(path) + return null } else { - const url = createJsonURL(jsonDataPaths[resourceName]) - return prefetchHelper(url) + // TODO At the moment, if a 500 error occurs, we act as if the + // page doesn't exist at all. We should perform retry logic + // instead + pageNotFoundPaths.add(path) + return null } } -const getResourceModule = resourceName => - fetchResource(resourceName).then(preferDefault) +const fetchPageData = path => { + const url = createPageDataUrl(path) + return cachedFetch(url, doFetch).then(req => + handlePageDataResponse(path, req) + ) +} -const appearsOnLine = () => { - const isOnLine = navigator.onLine - if (typeof isOnLine === `boolean`) { - return isOnLine - } +const createComponentUrls = componentChunkName => + window.___chunkMapping[componentChunkName].map( + chunk => __PATH_PREFIX__ + chunk + ) - // If no navigator.onLine support assume onLine if any of last N fetches succeeded - const succeededFetch = fetchHistory.find(entry => entry.succeeded) - return !!succeededFetch -} +const fetchComponent = chunkName => asyncRequires.components[chunkName]() -const handleResourceLoadError = (path, message) => { - if (!failedPaths[path]) { - failedPaths[path] = message - } +const stripSurroundingSlashes = s => { + s = s[0] === `/` ? s.slice(1) : s + s = s.endsWith(`/`) ? s.slice(0, -1) : s + return s +} - if ( - appearsOnLine() && - window.location.pathname.replace(/\/$/g, ``) !== path.replace(/\/$/g, ``) - ) { - window.location.pathname = path - } +const createPageDataUrl = path => { + const fixedPath = path === `/` ? `index` : stripSurroundingSlashes(path) + return `${__PATH_PREFIX__}/page-data/${fixedPath}/page-data.json` } const onPrefetchPathname = pathname => { @@ -170,57 +167,44 @@ const onPrefetchPathname = pathname => { } } -const onPostPrefetchPathname = pathname => { - if (!prefetchCompleted[pathname]) { - apiRunner(`onPostPrefetchPathname`, { pathname }) - prefetchCompleted[pathname] = true - } -} - -/** - * Check if we should fallback to resources for 404 page if resources for a page are not found - * - * We can't do that when we don't have full pages manifest - we don't know if page exist or not if we don't have it. - * We also can't do that on initial render / mount in case we just can't load resources needed for first page. - * Not falling back to 404 resources will cause "EnsureResources" component to handle scenarios like this with - * potential reload - * @param {string} path Path to a page - */ -const shouldFallbackTo404Resources = path => - (hasPageResourceMap || inInitialRender) && path !== `/404.html` - // Note we're not actively using the path data atm. There // could be future optimizations however around trying to ensure // we load all resources for likely-to-be-visited paths. // let pathArray = [] // let pathCount = {} -let findPage let pathScriptsCache = {} let prefetchTriggered = {} let prefetchCompleted = {} let disableCorePrefetching = false +const onPostPrefetchPathname = pathname => { + if (!prefetchCompleted[pathname]) { + apiRunner(`onPostPrefetchPathname`, { pathname }) + prefetchCompleted[pathname] = true + } +} + +const loadComponent = componentChunkName => { + if (process.env.NODE_ENV !== `production`) { + return Promise.resolve(syncRequires.components[componentChunkName]) + } else { + return cachedFetch(componentChunkName, fetchComponent).then(preferDefault) + } +} + const queue = { - addPagesArray: newPages => { - findPage = pageFinderFactory(newPages, __BASE_PATH__) - }, addDevRequires: devRequires => { syncRequires = devRequires }, addProdRequires: prodRequires => { asyncRequires = prodRequires }, - addDataPaths: dataPaths => { - jsonDataPaths = dataPaths - }, // Hovering on a link is a very strong indication the user is going to // click on it soon so let's start prefetching resources for this // pathname. - hovering: path => { - queue.getResourcesForPathname(path) - }, - enqueue: path => { + hovering: path => queue.loadPage(path), + enqueue: rawPath => { if (!apiRunner) console.error(`Run setApiRunnerForLoader() before enqueing paths`) @@ -236,7 +220,7 @@ const queue = { // Tell plugins with custom prefetching logic that they should start // prefetching this path. - onPrefetchPathname(path) + onPrefetchPathname(rawPath) // If a plugin has disabled core prefetching, stop now. if (disableCorePrefetching.some(a => a)) { @@ -244,193 +228,145 @@ const queue = { } // Check if the page exists. - let page = findPage(path) + let realPath = cleanAndFindPath(rawPath) - // In production, we lazy load page metadata. If that - // hasn't been fetched yet, start fetching it now. - if ( - process.env.NODE_ENV === `production` && - !page && - !fetchedPageResourceMap - ) { - // If page wasn't found check and we didn't fetch resources map for - // all pages, wait for fetch to complete and try find page again - return fetchPageResourceMap().then(() => queue.enqueue(path)) - } - - if (!page) { - return false + if (pageDatas[realPath]) { + return true } if ( process.env.NODE_ENV !== `production` && process.env.NODE_ENV !== `test` ) { - devGetPageData(page.path) + // Ensure latest version of page data is in the JSON store + devGetPageData(realPath) } - // Prefetch resources. if (process.env.NODE_ENV === `production`) { - Promise.all([ - prefetchResource(page.jsonName), - prefetchResource(page.componentChunkName), - ]).then(() => { - // Tell plugins the path has been successfully prefetched - onPostPrefetchPathname(path) - }) + const pageDataUrl = createPageDataUrl(realPath) + prefetchHelper(pageDataUrl) + .then(() => + // This was just prefetched, so will return a response from + // the cache instead of making another request to the server + fetchPageData(realPath) + ) + .then(pageData => { + if (pageData === null) { + return Promise.resolve() + } + // Tell plugins the path has been successfully prefetched + const chunkName = pageData.componentChunkName + const componentUrls = createComponentUrls(chunkName) + return Promise.all(componentUrls.map(prefetchHelper)).then(() => { + onPostPrefetchPathname(rawPath) + }) + }) } return true }, - getPage: pathname => findPage(pathname), + isPageNotFound: pathname => pageNotFoundPaths.has(cleanAndFindPath(pathname)), - getResourceURLsForPathname: path => { - const page = findPage(path) - if (page) { - return [ - ...createComponentUrls(page.componentChunkName), - createJsonURL(jsonDataPaths[page.jsonName]), - ] - } else { - return null + loadPageData: rawPath => { + const realPath = cleanAndFindPath(rawPath) + if (queue.isPageNotFound(realPath)) { + return Promise.resolve(null) } - }, - - getResourcesForPathnameSync: path => { - const page = findPage(path) - if (page) { - return pathScriptsCache[page.path] - } else if (shouldFallbackTo404Resources(path)) { - return queue.getResourcesForPathnameSync(`/404.html`) - } else { - return null - } - }, - - // Get resources (code/data) for a path. Fetches metdata first - // if necessary and then the code/data bundles. Used for prefetching - // and getting resources for page changes. - getResourcesForPathname: path => - new Promise((resolve, reject) => { - // Production code path - if (failedPaths[path]) { - handleResourceLoadError( - path, - `Previously detected load failure for "${path}"` - ) - reject() - return - } - const page = findPage(path) - - // In production, we lazy load page metadata. If that - // hasn't been fetched yet, start fetching it now. - if ( - !page && - !fetchedPageResourceMap && - process.env.NODE_ENV === `production` - ) { - // If page wasn't found check and we didn't fetch resources map for - // all pages, wait for fetch to complete and try to get resources again - fetchPageResourceMap().then(() => - resolve(queue.getResourcesForPathname(path)) - ) - return - } - - if (!page) { - if (shouldFallbackTo404Resources(path)) { - console.log(`A page wasn't found for "${path}"`) - - // Preload the custom 404 page - resolve(queue.getResourcesForPathname(`/404.html`)) - return + if (!fetchedPageData[realPath]) { + return fetchPageData(realPath).then(pageData => { + if (process.env.NODE_ENV !== `production`) { + devGetPageData(realPath) } - - resolve() - return - } - - // Use the path from the page so the pathScriptsCache uses - // the normalized path. - path = page.path - - // Check if it's in the cache already. - if (pathScriptsCache[path]) { - emitter.emit(`onPostLoadPageResources`, { - page, - pageResources: pathScriptsCache[path], - }) - resolve(pathScriptsCache[path]) - return - } - - // Nope, we need to load resource(s) - emitter.emit(`onPreLoadPageResources`, { - path, + return queue.loadPageData(rawPath) }) + } + return Promise.resolve(pageDatas[realPath]) + }, - // In development we know the code is loaded already - // so we just return with it immediately. - if (process.env.NODE_ENV !== `production`) { - const pageResources = { - component: syncRequires.components[page.componentChunkName], - page, + loadPage: rawPath => + queue + .loadPageData(rawPath) + .then(pageData => { + // If no page was found, then preload the 404.html + if (pageData === null && rawPath !== `/404.html`) { + return Promise.all([ + queue.doesPageHtmlExist(rawPath), + queue.loadPage(`/404.html`), + ]).then(() => null) } - - // Add to the cache. - pathScriptsCache[path] = pageResources - devGetPageData(page.path).then(pageData => { - emitter.emit(`onPostLoadPageResources`, { - page, - pageResources, - }) - // Tell plugins the path has been successfully prefetched - onPostPrefetchPathname(path) - - resolve(pageResources) - }) - } else { - Promise.all([ - getResourceModule(page.componentChunkName), - getResourceModule(page.jsonName), - ]).then(([component, json]) => { - if (!(component && json)) { - resolve(null) - return + // Otherwise go ahead and load the page's component + return loadComponent(pageData.componentChunkName).then(component => { + const page = { + componentChunkName: pageData.componentChunkName, + path: pageData.path, + webpackCompilationHash: pageData.webpackCompilationHash, } + const jsonData = pageData.result + const pageResources = { component, - json, + json: jsonData, page, } - pageResources.page.jsonURL = createJsonURL( - jsonDataPaths[page.jsonName] - ) - pathScriptsCache[path] = pageResources - resolve(pageResources) + pathScriptsCache[cleanAndFindPath(rawPath)] = pageResources emitter.emit(`onPostLoadPageResources`, { - page, + page: pageResources, pageResources, }) - // Tell plugins the path has been successfully prefetched - onPostPrefetchPathname(path) + if (process.env.NODE_ENV === `production`) { + onPostPrefetchPathname(rawPath) + } + + return pageResources }) - } - }), + }) + .catch(() => null), + + loadPageOr404: rawPath => + queue + .loadPage(rawPath) + .then(result => + result === null && rawPath !== `/404.html` + ? queue.loadPageSync(`/404.html`) + : null + ), + + loadPageSync: rawPath => pathScriptsCache[cleanAndFindPath(rawPath)], + + getResourceURLsForPathname: rawPath => { + const path = cleanAndFindPath(rawPath) + const pageData = pageDatas[path] + if (pageData) { + return [ + ...createComponentUrls(pageData.componentChunkName), + createPageDataUrl(path), + ] + } else { + return null + } + }, + + doesPageHtmlExist: rawPath => { + const path = cleanAndFindPath(rawPath) + if (pageHtmlExistsResults.hasOwnProperty(path)) { + return pageHtmlExistsResults[path] + } + + return doFetch(path, `HEAD`).then(req => { + pageHtmlExistsResults[path] = req.status === 200 + }) + }, + + doesPageHtmlExistSync: rawPath => + pageHtmlExistsResults[cleanAndFindPath(rawPath)], } export const postInitialRenderWork = () => { - inInitialRender = false - if (process.env.NODE_ENV === `production`) { - // We got all resources needed for first mount, - // we can fetch resources for all pages. - fetchPageResourceMap() - } + console.warn(`Warning: postInitialRenderWork is deprecated. It is now a noop`) } export const setApiRunnerForLoader = runner => { @@ -439,9 +375,26 @@ export const setApiRunnerForLoader = runner => { } export const publicLoader = { - getResourcesForPathname: queue.getResourcesForPathname, + // Deprecated methods. As far as we're aware, these are only used by + // core gatsby and the offline plugin, however there's a very small + // chance they're called by others. + getResourcesForPathname: rawPath => { + console.warn( + `Warning: getResourcesForPathname is deprecated. Use loadPage instead` + ) + return queue.loadPage(rawPath) + }, + getResourcesForPathnameSync: rawPath => { + console.warn( + `Warning: getResourcesForPathnameSync is deprecated. Use loadPageSync instead` + ) + return queue.loadPageSync(rawPath) + }, + + // Real methods getResourceURLsForPathname: queue.getResourceURLsForPathname, - getResourcesForPathnameSync: queue.getResourcesForPathnameSync, + loadPage: queue.loadPage, + loadPageSync: queue.loadPageSync, } export default queue diff --git a/packages/gatsby/cache-dir/navigation.js b/packages/gatsby/cache-dir/navigation.js index 6289cc2d3f8a1..9e26ce50b2e22 100644 --- a/packages/gatsby/cache-dir/navigation.js +++ b/packages/gatsby/cache-dir/navigation.js @@ -18,7 +18,7 @@ function maybeRedirect(pathname) { if (redirect != null) { if (process.env.NODE_ENV !== `production`) { - const pageResources = loader.getResourcesForPathnameSync(pathname) + const pageResources = loader.loadPageSync(pathname) if (pageResources != null) { console.error( @@ -81,7 +81,29 @@ const navigate = (to, options = {}) => { }) }, 1000) - loader.getResourcesForPathname(pathname).then(pageResources => { + loader.loadPage(pathname).then(pageResources => { + // If the loaded page has a different compilation hash to the + // window, then a rebuild has occurred on the server. Reload. + if (process.env.NODE_ENV === `production` && pageResources) { + if ( + pageResources.page.webpackCompilationHash !== + window.___webpackCompilationHash + ) { + // Purge plugin-offline cache + if ( + `serviceWorker` in navigator && + navigator.serviceWorker.controller !== null && + navigator.serviceWorker.controller.state === `activated` + ) { + navigator.serviceWorker.controller.postMessage({ + gatsbyApi: `resetWhitelist`, + }) + } + + console.log(`Site has changed on server. Reloading browser`) + window.location = pathname + } + } reachNavigate(to, options) clearTimeout(timeoutId) }) diff --git a/packages/gatsby/cache-dir/normalize-page-path.js b/packages/gatsby/cache-dir/normalize-page-path.js new file mode 100644 index 0000000000000..e3aa70b2f45c5 --- /dev/null +++ b/packages/gatsby/cache-dir/normalize-page-path.js @@ -0,0 +1,12 @@ +export default path => { + if (path === undefined) { + return path + } + if (path === `/`) { + return `/` + } + if (path.charAt(path.length - 1) === `/`) { + return path.slice(0, -1) + } + return path +} diff --git a/packages/gatsby/cache-dir/production-app.js b/packages/gatsby/cache-dir/production-app.js index 90d8f871289cc..4f4b1efd363b4 100644 --- a/packages/gatsby/cache-dir/production-app.js +++ b/packages/gatsby/cache-dir/production-app.js @@ -2,7 +2,6 @@ import { apiRunner, apiRunnerAsync } from "./api-runner-browser" import React, { createElement } from "react" import ReactDOM from "react-dom" import { Router, navigate } from "@reach/router" -import { match } from "@reach/router/lib/utils" import { ScrollContext } from "gatsby-react-router-scroll" import domReady from "@mikaelkristiansson/domready" import { @@ -13,15 +12,14 @@ import { import emitter from "./emitter" import PageRenderer from "./page-renderer" import asyncRequires from "./async-requires" -import loader, { setApiRunnerForLoader, postInitialRenderWork } from "./loader" +import loader, { setApiRunnerForLoader } from "./loader" import EnsureResources from "./ensure-resources" window.asyncRequires = asyncRequires window.___emitter = emitter window.___loader = loader +window.___webpackCompilationHash = window.webpackCompilationHash -loader.addPagesArray([window.page]) -loader.addDataPaths({ [window.page.jsonName]: window.dataPath }) loader.addProdRequires(asyncRequires) setApiRunnerForLoader(apiRunner) @@ -61,28 +59,25 @@ apiRunnerAsync(`onClientEntry`).then(() => { } } - const { page, location: browserLoc } = window + const { pagePath, location: browserLoc } = window if ( // Make sure the window.page object is defined - page && + pagePath && // The canonical path doesn't match the actual path (i.e. the address bar) - __BASE_PATH__ + page.path !== browserLoc.pathname && - // ...and if matchPage is specified, it also doesn't match the actual path - (!page.matchPath || - !match(__BASE_PATH__ + page.matchPath, browserLoc.pathname)) && + __BASE_PATH__ + pagePath !== browserLoc.pathname && // Ignore 404 pages, since we want to keep the same URL - page.path !== `/404.html` && - !page.path.match(/^\/404\/?$/) && + pagePath !== `/404.html` && + !pagePath.match(/^\/404\/?$/) && // Also ignore the offline shell (since when using the offline plugin, all // pages have this canonical path) - !page.path.match(/^\/offline-plugin-app-shell-fallback\/?$/) + !pagePath.match(/^\/offline-plugin-app-shell-fallback\/?$/) ) { - navigate(__BASE_PATH__ + page.path + browserLoc.search + browserLoc.hash, { + navigate(__BASE_PATH__ + pagePath + browserLoc.search + browserLoc.hash, { replace: true, }) } - loader.getResourcesForPathname(browserLoc.pathname).then(() => { + loader.loadPage(browserLoc.pathname).then(() => { const Root = () => createElement( Router, @@ -116,7 +111,6 @@ apiRunnerAsync(`onClientEntry`).then(() => { ? document.getElementById(`___gatsby`) : void 0, () => { - postInitialRenderWork() apiRunner(`onInitialClientRender`) } ) diff --git a/packages/gatsby/cache-dir/public-page-renderer-dev.js b/packages/gatsby/cache-dir/public-page-renderer-dev.js index 6bcfd16d4bd20..700905bf96a8a 100644 --- a/packages/gatsby/cache-dir/public-page-renderer-dev.js +++ b/packages/gatsby/cache-dir/public-page-renderer-dev.js @@ -5,7 +5,7 @@ import loader from "./loader" import JSONStore from "./json-store" const DevPageRenderer = ({ location }) => { - const pageResources = loader.getResourcesForPathnameSync(location.pathname) + const pageResources = loader.loadPageSync(location.pathname) return React.createElement(JSONStore, { location, pageResources, diff --git a/packages/gatsby/cache-dir/public-page-renderer-prod.js b/packages/gatsby/cache-dir/public-page-renderer-prod.js index 195567cd6241b..2ab693856ddf3 100644 --- a/packages/gatsby/cache-dir/public-page-renderer-prod.js +++ b/packages/gatsby/cache-dir/public-page-renderer-prod.js @@ -2,14 +2,11 @@ import React from "react" import PropTypes from "prop-types" import InternalPageRenderer from "./page-renderer" -import loader from "./loader" -const ProdPageRenderer = ({ location }) => { - const pageResources = loader.getResourcesForPathnameSync(location.pathname) +const ProdPageRenderer = ({ location, pageResources }) => { if (!pageResources) { return null } - return React.createElement(InternalPageRenderer, { location, pageResources, diff --git a/packages/gatsby/cache-dir/root.js b/packages/gatsby/cache-dir/root.js index c19a134f30bcb..0ea2c01c6bbbf 100644 --- a/packages/gatsby/cache-dir/root.js +++ b/packages/gatsby/cache-dir/root.js @@ -37,11 +37,7 @@ class RouteHandler extends React.Component { render() { let { location } = this.props - // check if page exists - in dev pages are sync loaded, it's safe to use - // loader.getPage - let page = loader.getPage(location.pathname) - - if (page) { + if (!loader.isPageNotFound(location.pathname)) { return ( {locationAndPageResources => ( @@ -56,30 +52,26 @@ class RouteHandler extends React.Component { )} ) - } else { - const dev404PageResources = loader.getResourcesForPathnameSync( - `/dev-404-page/` - ) - const real404PageResources = loader.getResourcesForPathnameSync( - `/404.html` - ) - let custom404 - if (real404PageResources) { - custom404 = ( - - ) - } + } - return ( - - - + const dev404PageResources = loader.loadPageSync(`/dev-404-page`) + const real404PageResources = loader.loadPageSync(`/404.html`) + let custom404 + if (real404PageResources) { + custom404 = ( + ) } + + return ( + + + + ) } } diff --git a/packages/gatsby/cache-dir/socketIo.js b/packages/gatsby/cache-dir/socketIo.js index 978f7db8966d1..c645499a31e42 100644 --- a/packages/gatsby/cache-dir/socketIo.js +++ b/packages/gatsby/cache-dir/socketIo.js @@ -1,4 +1,5 @@ import { reportError, clearError } from "./error-overlay-handler" +import normalizePagePath from "./normalize-page-path" let socket = null @@ -18,10 +19,16 @@ export default function socketIo() { // eslint-disable-next-line no-undef socket = io() - const didDataChange = (msg, queryData) => - !(msg.payload.id in queryData) || - JSON.stringify(msg.payload.result) !== - JSON.stringify(queryData[msg.payload.id]) + const didDataChange = (msg, queryData) => { + const id = + msg.type === `staticQueryResult` + ? msg.payload.id + : normalizePagePath(msg.payload.id) + return ( + !(id in queryData) || + JSON.stringify(msg.payload.result) !== JSON.stringify(queryData[id]) + ) + } socket.on(`message`, msg => { if (msg.type === `staticQueryResult`) { @@ -35,7 +42,7 @@ export default function socketIo() { if (didDataChange(msg, pageQueryData)) { pageQueryData = { ...pageQueryData, - [msg.payload.id]: msg.payload.result, + [normalizePagePath(msg.payload.id)]: msg.payload.result, } } } else if (msg.type === `overlayError`) { @@ -61,6 +68,7 @@ export default function socketIo() { const inFlightGetPageDataPromiseCache = {} function getPageData(pathname) { + pathname = normalizePagePath(pathname) if (inFlightGetPageDataPromiseCache[pathname]) { return inFlightGetPageDataPromiseCache[pathname] } else { @@ -70,7 +78,10 @@ function getPageData(pathname) { resolve(pageQueryData[pathname]) } else { const onPageDataCallback = msg => { - if (msg.type === `pageQueryResult` && msg.payload.id === pathname) { + if ( + msg.type === `pageQueryResult` && + normalizePagePath(msg.payload.id) === pathname + ) { socket.off(`message`, onPageDataCallback) delete inFlightGetPageDataPromiseCache[pathname] resolve(pageQueryData[pathname]) diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js index dfb4e8f94a352..3cc4b04c2a05f 100644 --- a/packages/gatsby/cache-dir/static-entry.js +++ b/packages/gatsby/cache-dir/static-entry.js @@ -7,13 +7,8 @@ const { get, merge, isObject, flatten, uniqBy } = require(`lodash`) const apiRunner = require(`./api-runner-ssr`) const syncRequires = require(`./sync-requires`) -const { dataPaths, pages } = require(`./data.json`) const { version: gatsbyVersion } = require(`gatsby/package.json`) -// Speed up looking up pages. -const pagesObjectMap = new Map() -pages.forEach(p => pagesObjectMap.set(p.path, p)) - const stats = JSON.parse( fs.readFileSync(`${process.cwd()}/public/webpack.stats.json`, `utf-8`) ) @@ -45,7 +40,32 @@ try { Html = Html && Html.__esModule ? Html.default : Html -const getPage = path => pagesObjectMap.get(path) +const getPageDataPath = path => { + const fixedPagePath = path === `/` ? `index` : path + return join(`page-data`, fixedPagePath, `page-data.json`) +} + +const getPageDataUrl = pagePath => { + const pageDataPath = getPageDataPath(pagePath) + return `${__PATH_PREFIX__}/${pageDataPath}` +} + +const getPageDataFile = pagePath => { + const pageDataPath = getPageDataPath(pagePath) + return join(process.cwd(), `public`, pageDataPath) +} + +const loadPageDataSync = pagePath => { + const pageDataPath = getPageDataPath(pagePath) + const pageDataFile = join(process.cwd(), `public`, pageDataPath) + try { + const pageDataJson = fs.readFileSync(pageDataFile) + return JSON.parse(pageDataJson) + } catch (error) { + // not an error if file is not found. There's just no page data + return null + } +} const createElement = React.createElement @@ -122,33 +142,22 @@ export default (pagePath, callback) => { postBodyComponents = sanitizeComponents(components) } - const page = getPage(pagePath) - - let dataAndContext = {} - if (page.jsonName in dataPaths) { - const pathToJsonData = join( - process.cwd(), - `/public/static/d`, - `${dataPaths[page.jsonName]}.json` - ) - try { - dataAndContext = JSON.parse(fs.readFileSync(pathToJsonData)) - } catch (e) { - console.log(`error`, pathToJsonData, e) - process.exit() - } - } + const pageDataRaw = fs.readFileSync(getPageDataFile(pagePath)) + const pageData = JSON.parse(pageDataRaw) + const pageDataUrl = getPageDataUrl(pagePath) + const { componentChunkName } = pageData class RouteHandler extends React.Component { render() { const props = { ...this.props, - ...dataAndContext, - pathContext: dataAndContext.pageContext, + ...pageData.result, + // pathContext was deprecated in v2. Renamed to pageContext + pathContext: pageData.result ? pageData.result.pageContext : undefined, } const pageElement = createElement( - syncRequires.components[page.componentChunkName], + syncRequires.components[componentChunkName], props ) @@ -212,7 +221,7 @@ export default (pagePath, callback) => { // Create paths to scripts let scriptsAndStyles = flatten( - [`app`, page.componentChunkName].map(s => { + [`app`, componentChunkName].map(s => { const fetchKey = `assetsByChunkName[${s}]` let chunks = get(stats, fetchKey) @@ -266,6 +275,7 @@ export default (pagePath, callback) => { setPostBodyComponents, setBodyProps, pathname: pagePath, + loadPageDataSync, bodyHtml, scripts, styles, @@ -287,16 +297,13 @@ export default (pagePath, callback) => { ) }) - if (page.jsonName in dataPaths) { - const dataPath = `${__PATH_PREFIX__}/static/d/${ - dataPaths[page.jsonName] - }.json` + if (pageData) { headComponents.push( ) @@ -333,19 +340,17 @@ export default (pagePath, callback) => { } }) + const webpackCompilationHash = pageData.webpackCompilationHash + // Add page metadata for the current page - const windowData = `/**/` + const windowPageData = `/**/` postBodyComponents.push(