diff --git a/benchmarks/plugin-manifest/README.md b/benchmarks/plugin-manifest/README.md new file mode 100644 index 0000000000000..0d05c2a530687 --- /dev/null +++ b/benchmarks/plugin-manifest/README.md @@ -0,0 +1,21 @@ +# gatsby-plugin-manifest performance tests + +- Setup: `yarn` +- Run: `yarn test` + +Benchmarks the current production version of the plugin unless you use `gatsby-dev`. + +## To benchmark the current branch: + +```sh +# In the root of the Gatsby repository +$ yarn run watch --scope=gatsby-plugin-manifest . +``` + +```sh +# In ./benchmarks/plugin-manifest +# You'll need 'gatsby-dev' installed and configured globally. +$ gatsby-dev --packages gatsby-plugin-manifest +``` + +You may now switch branches using `git checkout` and edit code on the current branch. Changes will be compiled into the local `node_modules` for the benchmark. diff --git a/benchmarks/plugin-manifest/index.js b/benchmarks/plugin-manifest/index.js new file mode 100644 index 0000000000000..cd883bc387432 --- /dev/null +++ b/benchmarks/plugin-manifest/index.js @@ -0,0 +1,77 @@ +const { onPostBootstrap } = require(`gatsby-plugin-manifest/gatsby-node`) +const reporter = require(`gatsby-cli/lib/reporter`) +const fs = require(`fs-extra`) + +//Config for executing onPostBootstrap +const pluginOptions = { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#ffffff`, + theme_color: `#663399`, + display: `minimal-ui`, + icon: `../../www/src/assets/gatsby-icon.png`, + cache_busting_mode: `none`, +} + +//Global Variables +let results = { seconds: [], nanoseconds: [] } +let sum = [0, 0] +let rounds = process.env.TOTAL_ROUNDS || 20 + +//Execute a single test of onPostBootstrap +async function executeBootstrap() { + const timeStart = process.hrtime() + await onPostBootstrap({ reporter }, pluginOptions) + const timeEnd = process.hrtime(timeStart) + + // console.info( + // "Execution time (hr): %ds %dms", + // timeEnd[0], + // timeEnd[1] / 1000000 + // ) + + results.seconds.push(timeEnd[0]) + results.nanoseconds.push(timeEnd[1]) + sum[0] += timeEnd[0] + sum[1] += timeEnd[1] + + fs.removeSync(`public/icons`) + fs.removeSync(`public/manifest.webmanifest`) +} + +//Loop test to run multiple times and calculate results +async function runTest() { + console.info(`Timing 'onPostBootstrap' running ${rounds} times.`) + + for (let i = 0; i < rounds; i++) { + // process.stdout.write(`Round ${i + 1}: `) + await executeBootstrap(i) + } + + let averageSum = [0, 0] + averageSum[0] = sum[0] / rounds + averageSum[1] = sum[1] / rounds + + console.info( + `\nAverage execution time (hr): %ds %dms`, + averageSum[0], + averageSum[1] / 1000000 + ) + + console.info( + `\nMax execution time (hr): ${Math.max(...results.nanoseconds) / 1000000}ms` + ) + console.info( + `\nMin execution time (hr): ${Math.min(...results.nanoseconds) / 1000000}ms` + ) + + console.info( + `\nRange execution time (hr): ${(Math.max(...results.nanoseconds) - + Math.min(...results.nanoseconds)) / + 1000000}ms` + ) +} + +//execute +runTest() diff --git a/benchmarks/plugin-manifest/package.json b/benchmarks/plugin-manifest/package.json new file mode 100644 index 0000000000000..1679e1d59140c --- /dev/null +++ b/benchmarks/plugin-manifest/package.json @@ -0,0 +1,15 @@ +{ + "name": "benchmark-gatsby-plugin-manifest", + "version": "1.0.0", + "description": "Run manifest lots of times", + "main": "index.js", + "scripts": { + "test": "node index.js" + }, + "author": "Alex Moon ` is needed. This plugin creates them by default. If you don't want those icons to be generated you can set the `legacy` option to `false` in plugin configuration: +iOS 11.3 added support for the web app manifest spec. Previous iOS versions won't recognize the icons defined in the webmanifest and the creation of `apple-touch-icon` links in `` is needed. This plugin creates them by default. If you don't want those icons to be generated you can set the `legacy` option to `false` in plugin configuration: ```js // in gatsby-config.js @@ -237,7 +181,7 @@ module.exports = { background_color: `#f7f0eb`, theme_color: `#a2466c`, display: `standalone`, - icon: `src/images/icon.png`, // This path is relative to the root of the site. + icon: `src/images/icon.png`, legacy: false, // this will not add apple-touch-icon links to }, }, @@ -245,9 +189,9 @@ module.exports = { } ``` -## Removing `theme-color` meta tag +#### Disable favicon -By default `gatsby-plugin-manifest` inserts `` tag to html output. This can be problematic if you want to programatically control that tag - for example when implementing light/dark themes in your project. You can set `theme_color_in_head` plugin option to `false` to opt-out of this behavior. +Excludes `` link tag to html output. You can set `include_favicon` plugin option to `false` to opt-out of this behavior. ```js // in gatsby-config.js @@ -263,16 +207,26 @@ module.exports = { theme_color: `#a2466c`, display: `standalone`, icon: `src/images/icon.png`, // This path is relative to the root of the site. - theme_color_in_head: false, // This will avoid adding theme-color meta tag. + include_favicon: false, // This will exclude favicon link tag }, }, ], } ``` -## Exclude `favicon` link tag +#### Disable or configure "[cache busting](https://www.keycdn.com/support/what-is-cache-busting)" + +Cache Busting allows your updated icon to be quickly/easily visible to your sites visitors. HTTP caches could otherwise keep an old icon around for days and weeks. Cache busting can only done in 'automatic' and 'hybrid' modes. + +Cache busting works by calculating a unique "digest" of the provided icon and modifying links or file names of generated images with that unique digest. If you ever update your icon, the digest will change and caches will be busted. + +**Options:** + +- **\`query\`** - This is the default mode. File names are unmodified but a URL query is appended to all links. e.g. `icons/icon-48x48.png?digest=abc123` -Excludes `` link tag to html output. You can set `include_favicon` plugin option to `false` to opt-out of this behaviour. +- **\`name\`** - Changes the cache busting mode to be done by file name. File names and links are modified with the icon digest. e.g. `icons/icon-48x48-abc123.png` (only needed if your CDN does not support URL query based cache busting) + +- **\`none\`** - Disables cache busting. File names and links remain unmodified. ```js // in gatsby-config.js @@ -287,27 +241,17 @@ module.exports = { background_color: `#f7f0eb`, theme_color: `#a2466c`, display: `standalone`, - icon: `src/images/icon.png`, // This path is relative to the root of the site. - include_favicon: false, // This will exclude favicon link tag + icon: `src/images/icon.png`, + cache_busting_mode: `none`, // `query`(default), `name`, or `none` }, }, ], } ``` -## Disabling or changing "[Cache Busting](https://www.keycdn.com/support/what-is-cache-busting)" Mode - -Cache Busting allows your updated icon to be quickly/easily visible to your sites visitors. HTTP caches could otherwise keep an old Icon around for days and weeks. Cache busting is only done in 'automatic' and 'hybrid' modes. +#### Remove `theme-color` meta tag -Cache busting works by calculating a unique "digest" or "hash" of the provided icon and modifying links and file names of generated images with that unique digest. If you ever update your icon, the digest will change and caches will be busted. - -**Options:** - -- **\`query\`** - This is the default mode. File names are unmodified but a URL query is appended to all links. e.g. `icons/icon-48x48.png?digest=abc123` - -- **\`name\`** - Changes the cache busting mode to be done by file name. File names and links are modified with the icon digest. e.g. `icons/icon-48x48-abc123.png` (only needed if your CDN does not support URL query based cache busting) - -- **\`none\`** - Disables cache busting. File names and links remain unmodified. +By default a `` tag is inserted into the html output. This can be problematic if you want to programmatically control that tag (e.g. when implementing light/dark themes in your project). You can set `theme_color_in_head` plugin option to `false` to opt-out of this behavior. ```js // in gatsby-config.js @@ -322,23 +266,21 @@ module.exports = { background_color: `#f7f0eb`, theme_color: `#a2466c`, display: `standalone`, - icon: `src/images/icon.png`, // This path is relative to the root of the site. - cache_busting_mode: `none`, // `none`, `name` or `query` + icon: `src/images/icon.png`, + theme_color_in_head: false, // This will avoid adding theme-color meta tag. }, }, ], } ``` -## Enable CORS using `crossorigin` attribute +#### Enable CORS using `crossorigin` attribute Add a `crossorigin` attribute to the manifest `` link tag. You can set `crossOrigin` plugin option to `'use-credentials'` to enable sharing resources via cookies. Any invalid keyword or empty string will fallback to `'anonymous'`. -You can find more information about `crossorigin` on MDN. - -[https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) +You can find more information about `crossorigin` on [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes). ```js // in gatsby-config.js @@ -353,10 +295,76 @@ module.exports = { background_color: `#f7f0eb`, theme_color: `#a2466c`, display: `standalone`, - icon: `src/images/icon.png`, // This path is relative to the root of the site. - crossOrigin: `use-credentials`, + icon: `src/images/icon.png`, + crossOrigin: `use-credentials`, // `use-credentials` or `anonymous` }, }, ], } ``` + +## Appendices + +Additional information that may be interesting or valuable. + +### Default icon config + +When in automatic mode the following json array is injected into the manifest configuration you provide and the icons are generated from it. + +```json +[ + { + "src": "icons/icon-48x48.png", + "sizes": "48x48", + "type": "image/png" + }, + { + "src": "icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/icon-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } +] +``` + +### Legacy icon support coverage + +Currently this feature only covers older versions of [iOS Safari](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html). + +Internet Explorer is the only other major browser that doesn't support the web app manifest, and it's market share is so small no one has contributed support. + +### Additional resources + +This article from the Chrome DevRel team is a good intro to the web app +manifest—https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ + +For more information see the w3 spec https://www.w3.org/TR/appmanifest/ or Mozilla docs https://developer.mozilla.org/en-US/docs/Web/Manifest. diff --git a/packages/gatsby-plugin-manifest/package.json b/packages/gatsby-plugin-manifest/package.json index b17d2d6a4c359..3fc329e3c97b0 100644 --- a/packages/gatsby-plugin-manifest/package.json +++ b/packages/gatsby-plugin-manifest/package.json @@ -8,8 +8,7 @@ }, "dependencies": { "@babel/runtime": "^7.0.0", - "bluebird": "^3.5.0", - "sharp": "^0.21.3" + "sharp": "^0.22.0" }, "devDependencies": { "@babel/cli": "^7.0.0", diff --git a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap index 5e1f435516377..7512d688892f4 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap +++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap @@ -1,222 +1,197 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`gatsby-plugin-manifest Add a "theme color" meta tag if "theme_color_in_head" is set to true 1`] = ` +exports[`gatsby-plugin-manifest CORS Generation Add crossOrigin when 'crossOrigin' is anonymous 1`] = ` Array [ , - , - , - , - , - , - , - , +] +`; + +exports[`gatsby-plugin-manifest CORS Generation Adds a crossOrigin attribute to manifest link tag if provided 1`] = ` +Array [ , +] +`; + +exports[`gatsby-plugin-manifest CORS Generation Does not add crossOrigin when 'crossOrigin' is blank 1`] = ` +Array [ , ] `; -exports[`gatsby-plugin-manifest Adds "shortcut icon" and "manifest" links and "theme_color" meta tag to head 1`] = ` +exports[`gatsby-plugin-manifest Cache Busting Does file name cache busting if "cache_busting_mode" option is set to name 1`] = ` Array [ , , - , , , , , , , , , ] `; -exports[`gatsby-plugin-manifest Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided 1`] = ` +exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to query 1`] = ` Array [ + , , - , , , , , , , , , ] `; -exports[`gatsby-plugin-manifest Adds a crossorigin attribute to manifest link tag if provided 1`] = ` +exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to undefined 1`] = ` Array [ , + , , , , , , , , , ] `; -exports[`gatsby-plugin-manifest Adds link favicon tag if "include_favicon" is set to true 1`] = ` +exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting if "cache_busting_mode" option is set to none 1`] = ` Array [ , , , , , , , , , , ] `; -exports[`gatsby-plugin-manifest Creates legacy apple touch links Using default set of icons 1`] = ` +exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting in manual mode 1`] = ` Array [ - , , - , , , - , - , - , - , - , - , ] `; -exports[`gatsby-plugin-manifest Creates legacy apple touch links Using user specified list of icons 1`] = ` +exports[`gatsby-plugin-manifest Favicon Adds link favicon tag if "include_favicon" is set to true 1`] = ` Array [ , , - , - , +] +`; + +exports[`gatsby-plugin-manifest Favicon Does not add a link favicon if "include_favicon" option is set to false 1`] = ` +Array [ , ] `; -exports[`gatsby-plugin-manifest Does file name cache busting if "cache_busting_mode" option is set to name 1`] = ` +exports[`gatsby-plugin-manifest Favicon Does not add a link favicon if in manual mode 1`] = ` Array [ , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in automatic mode 1`] = ` +Array [ , , , , , , , , , ] `; -exports[`gatsby-plugin-manifest Does not add a "theme color" meta tag if "theme_color_in_head" is set to false 1`] = ` +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in hybrid mode. 1`] = ` Array [ , , , - , - , - , - , - , - , ] `; -exports[`gatsby-plugin-manifest Does not add a "theme_color" meta tag to head if "theme_color" option is not provided or is an empty string, Adds link favicon if "include_favicon" option is not provided 1`] = ` +exports[`gatsby-plugin-manifest Legacy Icons Does create legacy links if "legacy" not specified in manual mode. 1`] = ` Array [ - , , , , - , - , - , - , - , - , ] `; -exports[`gatsby-plugin-manifest Does not add a link favicon if "include_favicon" option is set to false 1`] = ` +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in automatic 1`] = ` Array [ , - , - , - , - , - , - , - , - , ] `; -exports[`gatsby-plugin-manifest Does not create legacy apple touch links If "legacy" options is false and using default set of icons 1`] = ` +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in hybrid mode 1`] = ` Array [ , +] +`; + +exports[`gatsby-plugin-manifest Legacy Icons Does not create legacy links If "legacy" options is false and in manual mode 1`] = ` +Array [ , , , + , , , , , , , , , ] `; + +exports[`gatsby-plugin-manifest Manifest Link Generation Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided 1`] = ` +Array [ + , + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Does not add a "theme color" meta tag if "theme_color_in_head" is set to false 1`] = ` +Array [ + , +] +`; + +exports[`gatsby-plugin-manifest Manifest Link Generation Does not add a "theme_color" meta tag to head if "theme_color" option is not provided. 1`] = ` +Array [ + , +] +`; diff --git a/packages/gatsby-plugin-manifest/src/__tests__/common.js b/packages/gatsby-plugin-manifest/src/__tests__/common.js index 218202b9dab45..3c7893266ce73 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/common.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/common.js @@ -1,10 +1,5 @@ const path = require(`path`) -const { - defaultIcons, - doesIconExist, - createContentDigest, - addDigestToPath, -} = require(`../common`) +const { defaultIcons, doesIconExist, addDigestToPath } = require(`../common`) describe(`gatsby-plugin-manifest`, () => { describe(`defaultIcons`, () => { @@ -25,15 +20,6 @@ describe(`gatsby-plugin-manifest`, () => { }) }) - describe(`createContentDigest`, () => { - it(`returns valid digest`, () => { - const iconSrc = `thisIsSomethingToHash` - expect(createContentDigest(iconSrc)).toBe( - `24ac9308d3adace282339005aff676bd1576f061` - ) - }) - }) - describe(`addDigestToPath`, () => { it(`returns unmodified path`, () => { expect( diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js index 995c92427ed43..c7f7b0dad3aca 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js @@ -29,12 +29,17 @@ jest.mock(`sharp`, () => { } }() ) + sharp.simd = jest.fn() sharp.concurrency = jest.fn() return sharp }) +jest.mock(`gatsby/dist/utils/create-content-digest`, () => + jest.fn(() => `contentDigest`) +) + const fs = require(`fs`) const path = require(`path`) const sharp = require(`sharp`) @@ -48,6 +53,10 @@ const reporter = { } const { onPostBootstrap } = require(`../gatsby-node`) +const apiArgs = { + reporter, +} + const manifestOptions = { name: `GatsbyJS`, short_name: `GatsbyJS`, @@ -82,17 +91,14 @@ describe(`Test plugin manifest options`, () => { }) it(`correctly works with default parameters`, async () => { - await onPostBootstrap( - { reporter }, - { - name: `GatsbyJS`, - short_name: `GatsbyJS`, - start_url: `/`, - background_color: `#f7f0eb`, - theme_color: `#a2466c`, - display: `standalone`, - } - ) + await onPostBootstrap(apiArgs, { + name: `GatsbyJS`, + short_name: `GatsbyJS`, + start_url: `/`, + background_color: `#f7f0eb`, + theme_color: `#a2466c`, + display: `standalone`, + }) const [filePath, contents] = fs.writeFileSync.mock.calls[0] expect(filePath).toEqual(path.join(`public`, `manifest.webmanifest`)) expect(sharp).toHaveBeenCalledTimes(0) @@ -116,13 +122,10 @@ describe(`Test plugin manifest options`, () => { ], } - await onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ) + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) expect(sharp).toHaveBeenCalledWith(icon, { density: size }) expect(sharp).toHaveBeenCalledTimes(2) @@ -135,13 +138,10 @@ describe(`Test plugin manifest options`, () => { icon: `non/existing/path`, } - return onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ).catch(err => { + return onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }).catch(err => { expect(sharp).toHaveBeenCalledTimes(0) expect(err).toBe( `icon (non/existing/path) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` @@ -156,15 +156,14 @@ describe(`Test plugin manifest options`, () => { plugins: [], theme_color_in_head: false, cache_busting_mode: `name`, + include_favicon: true, + crossOrigin: `anonymous`, icon_options: {}, } - await onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ) + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) expect(sharp).toHaveBeenCalledTimes(0) const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) @@ -179,13 +178,10 @@ describe(`Test plugin manifest options`, () => { legacy: true, cache_busting_mode: `name`, } - await onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ) + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) expect(sharp).toHaveBeenCalledTimes(3) const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) @@ -200,13 +196,10 @@ describe(`Test plugin manifest options`, () => { legacy: true, cache_busting_mode: `none`, } - await onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ) + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) expect(sharp).toHaveBeenCalledTimes(3) const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) @@ -222,13 +215,10 @@ describe(`Test plugin manifest options`, () => { purpose: `maskable`, }, } - await onPostBootstrap( - { reporter }, - { - ...manifestOptions, - ...pluginSpecificOptions, - } - ) + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) expect(sharp).toHaveBeenCalledTimes(3) const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js index 61f2ae02bbfdb..6738e50e3593d 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js @@ -4,6 +4,10 @@ jest.mock(`fs`, () => { } }) +jest.mock(`gatsby/dist/utils/create-content-digest`, () => + jest.fn(() => `contentDigest`) +) + const { onRenderBody } = require(`../gatsby-ssr`) let headComponents @@ -19,78 +23,191 @@ describe(`gatsby-plugin-manifest`, () => { headComponents = [] }) - it(`Adds a crossorigin attribute to manifest link tag if provided`, () => { - onRenderBody(ssrArgs, { crossOrigin: `use-credentials` }) - expect(headComponents).toMatchSnapshot() - }) - - it(`Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided`, () => { - onRenderBody(ssrArgs, { theme_color: `#000000` }) - expect(headComponents).toMatchSnapshot() - }) - - it(`Does not add a "theme color" meta tag if "theme_color_in_head" is set to false`, () => { - onRenderBody(ssrArgs, { - theme_color: `#000000`, - theme_color_in_head: false, - }) - expect(headComponents).toMatchSnapshot() - }) - - it(`Add a "theme color" meta tag if "theme_color_in_head" is set to true`, () => { - onRenderBody(ssrArgs, { - theme_color: `#000000`, - theme_color_in_head: true, - }) - expect(headComponents).toMatchSnapshot() - }) + it(`Creates href attributes using pathPrefix`, () => { + global.__PATH_PREFIX__ = `/path-prefix` - it(`Adds "shortcut icon" and "manifest" links and "theme_color" meta tag to head`, () => { onRenderBody(ssrArgs, { icon: true, theme_color: `#000000`, }) - expect(headComponents).toMatchSnapshot() - }) - - it(`Adds link favicon tag if "include_favicon" is set to true`, () => { - onRenderBody(ssrArgs, { icon: true, include_favicon: true }) - expect(headComponents).toMatchSnapshot() - }) - it(`Does not add a "theme_color" meta tag to head if "theme_color" option is not provided or is an empty string, Adds link favicon if "include_favicon" option is not provided`, () => { - onRenderBody(ssrArgs, { icon: true }) - expect(headComponents).toMatchSnapshot() + headComponents + .filter(component => component.type === `link`) + .forEach(component => { + expect(component.props.href).toEqual( + expect.stringMatching(/^\/path-prefix\//) + ) + }) }) - it(`Does not add a link favicon if "include_favicon" option is set to false`, () => { - onRenderBody(ssrArgs, { icon: true, include_favicon: false }) - expect(headComponents).toMatchSnapshot() - }) + describe(`Manifest Link Generation`, () => { + it(`Adds a "theme color" meta tag to head if "theme_color_in_head" is not provided`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) - it(`doesn't add cache busting if "cache_busting_mode" option is set to none`, () => { - onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `none` }) - expect(headComponents).toMatchSnapshot() - }) + it(`Add a "theme color" meta tag if "theme_color_in_head" is set to true`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + theme_color_in_head: true, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) - it(`Does file name cache busting if "cache_busting_mode" option is set to name`, () => { - onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `name` }) - expect(headComponents).toMatchSnapshot() - }) + it(`Does not add a "theme color" meta tag if "theme_color_in_head" is set to false`, () => { + onRenderBody(ssrArgs, { + theme_color: `#000000`, + theme_color_in_head: false, + include_favicon: false, + cache_busting_mode: false, + legacy: false, + }) + expect(headComponents).toMatchSnapshot() + }) - describe(`Creates legacy apple touch links`, () => { - it(`Using default set of icons`, () => { + it(`Does not add a "theme_color" meta tag to head if "theme_color" option is not provided.`, () => { onRenderBody(ssrArgs, { icon: true, - theme_color: `#000000`, + include_favicon: false, + cache_busting_mode: false, + legacy: false, }) expect(headComponents).toMatchSnapshot() }) - it(`Using user specified list of icons`, () => { + it(`Adds "shortcut icon" and "manifest" links and "theme_color" meta tag to head`, () => { onRenderBody(ssrArgs, { icon: true, theme_color: `#000000`, + }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`Legacy Icons`, () => { + describe(`Does create legacy links`, () => { + it(`if "legacy" not specified in automatic mode`, () => { + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + include_favicon: false, + cache_busting_mode: `none`, + theme_color_in_head: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`if "legacy" not specified in hybrid mode.`, () => { + onRenderBody(ssrArgs, { + icon: true, + theme_color: `#000000`, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + include_favicon: false, + cache_busting_mode: `none`, + theme_color_in_head: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`if "legacy" not specified in manual mode.`, () => { + onRenderBody(ssrArgs, { + icon: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + }) + expect(headComponents).toMatchSnapshot() + }) + }) + + describe(`Does not create legacy links`, () => { + it(`If "legacy" options is false and in automatic`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + include_favicon: false, + cache_busting_mode: false, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`If "legacy" options is false and in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, + theme_color: `#000000`, + legacy: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`If "legacy" options is false and in hybrid mode`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + icons: [ + { + src: `/favicons/android-chrome-48x48.png`, + sizes: `48x48`, + type: `image/png`, + }, + { + src: `/favicons/android-chrome-512x512.png`, + sizes: `512x512`, + type: `image/png`, + }, + ], + include_favicon: false, + cache_busting_mode: false, + }) + expect(headComponents).toMatchSnapshot() + }) + }) + }) + + describe(`Cache Busting`, () => { + it(`doesn't add cache busting in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, icons: [ { src: `/favicons/android-chrome-48x48.png`, @@ -103,26 +220,56 @@ describe(`gatsby-plugin-manifest`, () => { type: `image/png`, }, ], + cache_busting_mode: `name`, }) expect(headComponents).toMatchSnapshot() }) + + it(`doesn't add cache busting if "cache_busting_mode" option is set to none`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `none` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does file name cache busting if "cache_busting_mode" option is set to name`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `name` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does query cache busting if "cache_busting_mode" option is set to query`, () => { + onRenderBody(ssrArgs, { icon: true, cache_busting_mode: `query` }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does query cache busting if "cache_busting_mode" option is set to undefined`, () => { + onRenderBody(ssrArgs, { icon: true }) + expect(headComponents).toMatchSnapshot() + }) }) - describe(`Does not create legacy apple touch links`, () => { - it(`If "legacy" options is false and using default set of icons`, () => { + describe(`Favicon`, () => { + it(`Adds link favicon tag if "include_favicon" is set to true`, () => { onRenderBody(ssrArgs, { icon: true, - theme_color: `#000000`, + include_favicon: true, legacy: false, + cache_busting_mode: `none`, }) expect(headComponents).toMatchSnapshot() }) - it(`If "legacy" options is false and using user specified list of icons`, () => { + it(`Does not add a link favicon if "include_favicon" option is set to false`, () => { onRenderBody(ssrArgs, { icon: true, - theme_color: `#000000`, + include_favicon: false, legacy: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) + + it(`Does not add a link favicon if in manual mode`, () => { + onRenderBody(ssrArgs, { + icon: false, icons: [ { src: `/favicons/android-chrome-48x48.png`, @@ -135,25 +282,43 @@ describe(`gatsby-plugin-manifest`, () => { type: `image/png`, }, ], + legacy: false, + cache_busting_mode: `none`, }) expect(headComponents).toMatchSnapshot() }) }) - it(`Creates href attributes using pathPrefix`, () => { - global.__PATH_PREFIX__ = `/path-prefix` + describe(`CORS Generation`, () => { + it(`Adds a crossOrigin attribute to manifest link tag if provided`, () => { + onRenderBody(ssrArgs, { + crossOrigin: `use-credentials`, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() + }) - onRenderBody(ssrArgs, { - icon: true, - theme_color: `#000000`, + it(`Add crossOrigin when 'crossOrigin' is anonymous`, () => { + onRenderBody(ssrArgs, { + icon: true, + crossOrigin: `anonymous`, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, + }) + expect(headComponents).toMatchSnapshot() }) - headComponents - .filter(component => component.type === `link`) - .forEach(component => { - expect(component.props.href).toEqual( - expect.stringMatching(/^\/path-prefix\//) - ) + it(`Does not add crossOrigin when 'crossOrigin' is blank`, () => { + onRenderBody(ssrArgs, { + icon: true, + legacy: false, + include_favicon: false, + cache_busting_mode: `none`, }) + expect(headComponents).toMatchSnapshot() + }) }) }) diff --git a/packages/gatsby-plugin-manifest/src/common.js b/packages/gatsby-plugin-manifest/src/common.js index ae4d6b58fe4cf..8b05a0700468c 100644 --- a/packages/gatsby-plugin-manifest/src/common.js +++ b/packages/gatsby-plugin-manifest/src/common.js @@ -1,6 +1,5 @@ import fs from "fs" import sysPath from "path" -import crypto from "crypto" // default icons for generating icons exports.defaultIcons = [ @@ -63,15 +62,6 @@ exports.doesIconExist = function doesIconExist(srcIcon) { } } -exports.createContentDigest = function createContentDigest(content) { - let digest = crypto - .createHash(`sha1`) - .update(content) - .digest(`hex`) - - return digest -} - /** * @param {string} path The generic path to an icon * @param {string} digest The digest of the icon provided in the plugin's options. diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js index c8698726b3677..f735497c28109 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js @@ -1,13 +1,8 @@ -const fs = require(`fs`) -const path = require(`path`) -const Promise = require(`bluebird`) -const sharp = require(`sharp`) -const { - defaultIcons, - doesIconExist, - addDigestToPath, - createContentDigest, -} = require(`./common.js`) +import fs from "fs" +import path from "path" +import sharp from "sharp" +import createContentDigest from "gatsby/dist/utils/create-content-digest" +import { defaultIcons, doesIconExist, addDigestToPath } from "./common" sharp.simd(true) @@ -23,24 +18,29 @@ try { } function generateIcons(icons, srcIcon) { - return Promise.map(icons, icon => { - const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`))) - const imgPath = path.join(`public`, icon.src) - - // For vector graphics, instruct sharp to use a pixel density - // suitable for the resolution we're rasterizing to. - // For pixel graphics sources this has no effect. - // Sharp accept density from 1 to 2400 - const density = Math.min(2400, Math.max(1, size)) - return sharp(srcIcon, { density }) - .resize({ - width: size, - height: size, - fit: `contain`, - background: { r: 255, g: 255, b: 255, alpha: 0 }, - }) - .toFile(imgPath) - }) + return Promise.all( + icons.map(async icon => { + const size = parseInt( + icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`)) + ) + const imgPath = path.join(`public`, icon.src) + + // For vector graphics, instruct sharp to use a pixel density + // suitable for the resolution we're rasterizing to. + // For pixel graphics sources this has no effect. + // Sharp accept density from 1 to 2400 + const density = Math.min(2400, Math.max(1, size)) + + return sharp(srcIcon, { density }) + .resize({ + width: size, + height: size, + fit: `contain`, + background: { r: 255, g: 255, b: 255, alpha: 0 }, + }) + .toFile(imgPath) + }) + ) } exports.onPostBootstrap = async ({ reporter }, pluginOptions) => { @@ -53,6 +53,10 @@ exports.onPostBootstrap = async ({ reporter }, pluginOptions) => { delete manifest.cache_busting_mode delete manifest.crossOrigin delete manifest.icon_options + delete manifest.include_favicon + + const activity = reporter.activityTimer(`Build manifest and related icons`) + activity.start() // If icons are not manually defined, use the default icon set. if (!manifest.icons) { @@ -84,9 +88,9 @@ exports.onPostBootstrap = async ({ reporter }, pluginOptions) => { throw `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` } - let sharpIcon = sharp(icon) + const sharpIcon = sharp(icon) - let metadata = await sharpIcon.metadata() + const metadata = await sharpIcon.metadata() if (metadata.width !== metadata.height) { reporter.warn( @@ -125,4 +129,6 @@ exports.onPostBootstrap = async ({ reporter }, pluginOptions) => { path.join(`public`, `manifest.webmanifest`), JSON.stringify(manifest) ) + + activity.end() } diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js index 30ad8667919dd..c4abba42c8720 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js @@ -1,6 +1,7 @@ import React from "react" import { withPrefix } from "gatsby" -import { defaultIcons, createContentDigest, addDigestToPath } from "./common.js" +import createContentDigest from "gatsby/dist/utils/create-content-digest" +import { defaultIcons, addDigestToPath } from "./common.js" import fs from "fs" let iconDigest = null @@ -9,6 +10,8 @@ exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { // We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody. let headComponents = [] + const srcIconExists = !!pluginOptions.icon + const icons = pluginOptions.icons || defaultIcons const legacy = typeof pluginOptions.legacy !== `undefined` ? pluginOptions.legacy : true @@ -19,8 +22,8 @@ exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { : `query` // If icons were generated, also add a favicon link. - if (pluginOptions.icon) { - let favicon = icons && icons.length ? icons[0].src : null + if (srcIconExists) { + const favicon = icons && icons.length ? icons[0].src : null if (cacheBusting !== `none`) { iconDigest = createContentDigest(fs.readFileSync(pluginOptions.icon)) @@ -54,7 +57,7 @@ exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { // The user has an option to opt out of the theme_color meta tag being inserted into the head. if (pluginOptions.theme_color) { - let insertMetaTag = + const insertMetaTag = typeof pluginOptions.theme_color_in_head !== `undefined` ? pluginOptions.theme_color_in_head : true @@ -76,7 +79,13 @@ exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { key={`gatsby-plugin-manifest-apple-touch-icon-${icon.sizes}`} rel="apple-touch-icon" sizes={icon.sizes} - href={withPrefix(addDigestToPath(icon.src, iconDigest, cacheBusting))} + href={withPrefix( + addDigestToPath( + icon.src, + iconDigest, + srcIconExists ? cacheBusting : `none` + ) + )} /> ))