diff --git a/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap b/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap index af8b6df163e6d..9b6fbb96789ef 100644 --- a/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap +++ b/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap @@ -20,8 +20,6 @@ X-Frame-Options Cache-Control: public, max-age=31536000, immutable /component---src-pages-index-js-0bdd01c77ee09ef0224c.js Cache-Control: public, max-age=31536000, immutable -/pages-manifest-ab11f09e0ca7ecd3b43e.js - Cache-Control: public, max-age=31536000, immutable /webpack-runtime-acaa8994f1f704475e21.js Cache-Control: public, max-age=31536000, immutable /styles.1025963f4f2ec7abbad4.css @@ -39,42 +37,56 @@ X-Frame-Options Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hi-folks/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /my-second-post/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hello-world/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin / Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404.html Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin " `; @@ -96,8 +108,6 @@ exports[`build-headers-program with caching headers 1`] = ` Cache-Control: public, max-age=31536000, immutable /component---src-pages-index-js-0bdd01c77ee09ef0224c.js Cache-Control: public, max-age=31536000, immutable -/pages-manifest-ab11f09e0ca7ecd3b43e.js - Cache-Control: public, max-age=31536000, immutable /webpack-runtime-acaa8994f1f704475e21.js Cache-Control: public, max-age=31536000, immutable /styles.1025963f4f2ec7abbad4.css @@ -115,56 +125,67 @@ exports[`build-headers-program with caching headers 1`] = ` Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hi-folks/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /my-second-post/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hello-world/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin / Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404.html Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin " `; -exports[`build-headers-program with security headers 1`] = ` +exports[`build-headers-program with manifest['pages-manifest'] 1`] = ` "## Created with gatsby-plugin-netlify /* - X-Frame-Options: ALLOW-FROM https://app.storyblok.com/ + X-Frame-Options: DENY X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Referrer-Policy: same-origin - Content-Security-Policy: frame-ancestors 'self' https://*.storyblok.com/ -/hello - X-Frame-Options: SAMEORIGIN /component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js Cache-Control: public, max-age=31536000, immutable /0-0180cd94ef2497ac7db8.js @@ -233,59 +254,98 @@ exports[`build-headers-program with security headers 1`] = ` " `; -exports[`build-headers-program without caching headers 1`] = ` +exports[`build-headers-program with security headers 1`] = ` "## Created with gatsby-plugin-netlify /* - X-Frame-Options: DENY + X-Frame-Options: ALLOW-FROM https://app.storyblok.com/ X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Referrer-Policy: same-origin + Content-Security-Policy: frame-ancestors 'self' https://*.storyblok.com/ +/hello + X-Frame-Options: SAMEORIGIN +/component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js + Cache-Control: public, max-age=31536000, immutable +/0-0180cd94ef2497ac7db8.js + Cache-Control: public, max-age=31536000, immutable +/component---src-templates-blog-post-js-517987eae96e75cddbe7.js + Cache-Control: public, max-age=31536000, immutable +/component---src-pages-404-js-53e6c51a5a7e73090f50.js + Cache-Control: public, max-age=31536000, immutable +/component---src-pages-index-js-0bdd01c77ee09ef0224c.js + Cache-Control: public, max-age=31536000, immutable +/webpack-runtime-acaa8994f1f704475e21.js + Cache-Control: public, max-age=31536000, immutable +/styles.1025963f4f2ec7abbad4.css + Cache-Control: public, max-age=31536000, immutable +/styles-565f081c8374bbda155f.js + Cache-Control: public, max-age=31536000, immutable +/app-f33c13590352da20930f.js + Cache-Control: public, max-age=31536000, immutable +/static/* + Cache-Control: public, max-age=31536000, immutable +/sw.js + Cache-Control: no-cache /offline-plugin-app-shell-fallback/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hi-folks/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /my-second-post/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hello-world/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin / Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404.html Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin " `; -exports[`build-headers-program without manifest['pages-manifest'] 1`] = ` +exports[`build-headers-program without caching headers 1`] = ` "## Created with gatsby-plugin-netlify /* @@ -293,68 +353,60 @@ exports[`build-headers-program without manifest['pages-manifest'] 1`] = ` X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Referrer-Policy: same-origin -/component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js - Cache-Control: public, max-age=31536000, immutable -/0-0180cd94ef2497ac7db8.js - Cache-Control: public, max-age=31536000, immutable -/component---src-templates-blog-post-js-517987eae96e75cddbe7.js - Cache-Control: public, max-age=31536000, immutable -/component---src-pages-404-js-53e6c51a5a7e73090f50.js - Cache-Control: public, max-age=31536000, immutable -/component---src-pages-index-js-0bdd01c77ee09ef0224c.js - Cache-Control: public, max-age=31536000, immutable -/webpack-runtime-acaa8994f1f704475e21.js - Cache-Control: public, max-age=31536000, immutable -/styles.1025963f4f2ec7abbad4.css - Cache-Control: public, max-age=31536000, immutable -/styles-565f081c8374bbda155f.js - Cache-Control: public, max-age=31536000, immutable -/app-f33c13590352da20930f.js - Cache-Control: public, max-age=31536000, immutable -/static/* - Cache-Control: public, max-age=31536000, immutable -/sw.js - Cache-Control: no-cache /offline-plugin-app-shell-fallback/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hi-folks/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /my-second-post/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /hello-world/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404/ Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin / Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin /404.html Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script Link: ; rel=preload; as=script + Link: ; rel=preload; as=fetch; crossorigin + Link: ; rel=preload; as=fetch; crossorigin " `; diff --git a/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js b/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js index d579c5f1c1b0d..6f093ece9b85a 100644 --- a/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js +++ b/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js @@ -1,9 +1,16 @@ import buildHeadersProgram from "../build-headers-program" -import path from "path" -import os from "os" -import fs from "fs-extra" +import * as path from "path" +import * as os from "os" +import * as fs from "fs-extra" import { DEFAULT_OPTIONS } from "../constants" +jest.mock(`fs-extra`, () => { + return { + ...jest.requireActual(`fs-extra`), + existsSync: jest.fn(), + } +}) + describe(`build-headers-program`, () => { let reporter @@ -11,6 +18,8 @@ describe(`build-headers-program`, () => { reporter = { warn: jest.fn(), } + fs.existsSync.mockClear() + fs.existsSync.mockReturnValue(true) }) const createPluginData = async () => { @@ -150,10 +159,9 @@ describe(`build-headers-program`, () => { `0-0180cd94ef2497ac7db8.js`, `component---src-pages-index-js-0bdd01c77ee09ef0224c.js`, ], - "pages-manifest": [`pages-manifest-ab11f09e0ca7ecd3b43e.js`], }, pathPrefix: ``, - publicFolder: fileName => path.join(tmpDir, fileName), + publicFolder: (...files) => path.join(tmpDir, ...files), } } @@ -168,16 +176,32 @@ describe(`build-headers-program`, () => { await buildHeadersProgram(pluginData, pluginOptions, reporter) expect(reporter.warn).not.toHaveBeenCalled() - expect( - await fs.readFile(pluginData.publicFolder(`_headers`), `utf8`) - ).toMatchSnapshot() + const output = await fs.readFile( + pluginData.publicFolder(`_headers`), + `utf8` + ) + expect(output).toMatchSnapshot() + expect(output).toMatch(/app-data\.json/) + expect(output).toMatch(/page-data\.json/) + // we should only check page-data & app-data once which leads to 2 times + expect(fs.existsSync).toBeCalledTimes(2) }) - it(`without manifest['pages-manifest']`, async () => { + it(`with manifest['pages-manifest']`, async () => { const pluginData = await createPluginData() - // gatsby 2.9+ no longer has a pages-manifest key - delete pluginData.manifest[`pages-manifest`] + fs.existsSync.mockImplementation(path => { + if (path.includes(`page-data.json`) || path.includes(`app-data.json`)) { + return false + } + + return true + }) + + // gatsby < 2.9 uses page-manifest + pluginData.manifest[`pages-manifest`] = [ + `pages-manifest-ab11f09e0ca7ecd3b43e.js`, + ] const pluginOptions = { ...DEFAULT_OPTIONS, @@ -192,6 +216,38 @@ describe(`build-headers-program`, () => { `utf8` ) expect(output).toMatchSnapshot() + expect(output).toMatch(/\/pages-manifest-ab11f09e0ca7ecd3b43e\.js/g) + expect(output).not.toMatch(/\/app-data\.json/g) + expect(output).not.toMatch(/\/page-data\.json/g) + expect(output).not.toMatch(/\/undefined/g) + }) + + it(`without app-data file`, async () => { + const pluginData = await createPluginData() + + // gatsby 2.17.0+ adds an app-data file + delete pluginData.manifest[`pages-manifest`] + + const pluginOptions = { + ...DEFAULT_OPTIONS, + mergeCachingHeaders: true, + } + fs.existsSync.mockImplementation(path => { + if (path.includes(`app-data.json`)) { + return false + } + + return true + }) + + await buildHeadersProgram(pluginData, pluginOptions, reporter) + + expect(reporter.warn).not.toHaveBeenCalled() + const output = await fs.readFile( + pluginData.publicFolder(`_headers`), + `utf8` + ) + expect(output).not.toMatch(/app-data\.json/g) expect(output).not.toMatch(/\/undefined/g) }) diff --git a/packages/gatsby-plugin-netlify/src/build-headers-program.js b/packages/gatsby-plugin-netlify/src/build-headers-program.js index 51d4fa57dc40e..e7f3a40820e37 100644 --- a/packages/gatsby-plugin-netlify/src/build-headers-program.js +++ b/packages/gatsby-plugin-netlify/src/build-headers-program.js @@ -1,6 +1,6 @@ import _ from "lodash" import { writeFile, existsSync } from "fs-extra" -import { parse } from "path" +import { parse, posix } from "path" import kebabHash from "kebab-hash" import { HEADER_COMMENT, IMMUTABLE_CACHING_HEADER } from "./constants" @@ -10,6 +10,7 @@ import { CACHING_HEADERS, LINK_REGEX, NETLIFY_HEADERS_FILENAME, + PAGE_DATA_DIR, } from "./constants" function getHeaderName(header) { @@ -44,7 +45,9 @@ function validHeaders(headers, reporter) { } function linkTemplate(assetPath, type = `script`) { - return `Link: <${assetPath}>; rel=preload; as=${type}` + return `Link: <${assetPath}>; rel=preload; as=${type}${ + type === `fetch` ? `; crossorigin` : `` + }` } function pathChunkName(path) { @@ -52,52 +55,82 @@ function pathChunkName(path) { return `path---${name}` } -function createScriptHeaderGenerator(manifest, pathPrefix) { - return script => { - const chunk = manifest[script] - - if (!chunk) { - return null - } +function getPageDataPath(path) { + const fixedPagePath = path === `/` ? `index` : path + return posix.join(`page-data`, fixedPagePath, `page-data.json`) +} - // convert to array if it's not already - const chunks = _.isArray(chunk) ? chunk : [chunk] +function getScriptPath(file, manifest) { + const chunk = manifest[file] - return chunks - .filter(script => { - const parsed = parse(script) - // handle only .js, .css content is inlined already - // and doesn't need to be pushed - return parsed.ext === `.js` - }) - .map(script => linkTemplate(`${pathPrefix}/${script}`)) - .join(`\n `) + if (!chunk) { + return [] } + + // convert to array if it's not already + const chunks = _.isArray(chunk) ? chunk : [chunk] + + return chunks.filter(script => { + const parsed = parse(script) + // handle only .js, .css content is inlined already + // and doesn't need to be pushed + return parsed.ext === `.js` + }) } -function linkHeaders(scripts, manifest, pathPrefix) { - return _.compact( - scripts.map(createScriptHeaderGenerator(manifest, pathPrefix)) - ) +function linkHeaders(files, pathPrefix) { + const linkHeaders = [] + for (const resourceType in files) { + files[resourceType].forEach(file => { + linkHeaders.push(linkTemplate(`${pathPrefix}/${file}`, resourceType)) + }) + } + + return linkHeaders } function headersPath(pathPrefix, path) { return `${pathPrefix}${path}` } -function preloadHeadersByPage(pages, manifest, pathPrefix) { +function preloadHeadersByPage({ pages, manifest, pathPrefix, publicFolder }) { let linksByPage = {} + const appDataPath = publicFolder(PAGE_DATA_DIR, `app-data.json`) + const hasAppData = existsSync(appDataPath) + + let hasPageData = false + if (pages.size) { + // test if 1 page-data file exists, if it does we know we're on a gatsby version that supports page-data + const pageDataPath = publicFolder( + getPageDataPath(pages.get(pages.keys().next().value).path) + ) + hasPageData = existsSync(pageDataPath) + } + pages.forEach(page => { - const scripts = [ - ...COMMON_BUNDLES, - pathChunkName(page.path), - page.componentChunkName, - ] + const scripts = _.flatMap(COMMON_BUNDLES, file => + getScriptPath(file, manifest) + ) + scripts.push(...getScriptPath(pathChunkName(page.path), manifest)) + scripts.push(...getScriptPath(page.componentChunkName, manifest)) - const pathKey = headersPath(pathPrefix, page.path) + const json = [] + if (hasAppData) { + json.push(posix.join(PAGE_DATA_DIR, `app-data.json`)) + } - linksByPage[pathKey] = linkHeaders(scripts, manifest, pathPrefix) + if (hasPageData) { + json.push(getPageDataPath(page.path)) + } + + const filesByResourceType = { + script: scripts.filter(Boolean), + fetch: json, + } + + const pathKey = headersPath(pathPrefix, page.path) + linksByPage[pathKey] = linkHeaders(filesByResourceType, pathPrefix) }) return linksByPage @@ -247,8 +280,13 @@ const applyLinkHeaders = (pluginData, { mergeLinkHeaders }) => headers => { return headers } - const { pages, manifest, pathPrefix } = pluginData - const perPageHeaders = preloadHeadersByPage(pages, manifest, pathPrefix) + const { pages, manifest, pathPrefix, publicFolder } = pluginData + const perPageHeaders = preloadHeadersByPage({ + pages, + manifest, + pathPrefix, + publicFolder, + }) return defaultMerge(headers, perPageHeaders) } diff --git a/packages/gatsby-plugin-netlify/src/constants.js b/packages/gatsby-plugin-netlify/src/constants.js index 9352923d19006..ecd9765a49e98 100644 --- a/packages/gatsby-plugin-netlify/src/constants.js +++ b/packages/gatsby-plugin-netlify/src/constants.js @@ -38,3 +38,5 @@ export const ROOT_WILDCARD = `/*` export const COMMON_BUNDLES = [`commons`, `app`] export const HEADER_COMMENT = `## Created with gatsby-plugin-netlify` + +export const PAGE_DATA_DIR = `page-data/`