diff --git a/packages/gatsby-plugin-manifest/README.md b/packages/gatsby-plugin-manifest/README.md index cf4e862e981d4..9fc2d621dba26 100644 --- a/packages/gatsby-plugin-manifest/README.md +++ b/packages/gatsby-plugin-manifest/README.md @@ -238,7 +238,9 @@ module.exports = { #### Disable favicon -Excludes `` link tag to html output. You can set `include_favicon` plugin option to `false` to opt-out of this behavior. +A favicon is generated by default in automatic and hybrid modes (a 32x32 PNG, included via a `` tag in the document head). + +You can set the `include_favicon` plugin option to `false` to opt-out of this behavior. ```js // in gatsby-config.js diff --git a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap index 1580eba43d9f7..ca2789e272e64 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap +++ b/packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-node.js.snap @@ -9,7 +9,7 @@ exports[`Test plugin manifest options does file name based cache busting 1`] = ` "calls": Array [ Array [ "public/manifest.webmanifest", - "{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48-contentDigest.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\",\\"purpose\\":\\"all\\"},{\\"src\\":\\"icons/icon-128x128-contentDigest.png\\",\\"sizes\\":\\"128x128\\",\\"type\\":\\"image/png\\"}]}", + "{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48-00913339321ee5a854812aea11f8a5d4.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\",\\"purpose\\":\\"all\\"},{\\"src\\":\\"icons/icon-128x128-00913339321ee5a854812aea11f8a5d4.png\\",\\"sizes\\":\\"128x128\\",\\"type\\":\\"image/png\\"}]}", ], ], "results": Array [ 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 9b9d68e552d14..f7b95fedf987e 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 @@ -32,7 +32,7 @@ Array [ exports[`gatsby-plugin-manifest Cache Busting Does file name cache busting if "cache_busting_mode" option is set to name 1`] = ` Array [ , , , , , , , , , , @@ -85,7 +85,7 @@ Array [ exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to query 1`] = ` Array [ , , , , , , , , , , @@ -138,7 +138,7 @@ Array [ exports[`gatsby-plugin-manifest Cache Busting Does query cache busting if "cache_busting_mode" option is set to undefined 1`] = ` Array [ , , , , , , , , , , @@ -191,7 +191,7 @@ Array [ exports[`gatsby-plugin-manifest Cache Busting doesn't add cache busting if "cache_busting_mode" option is set to none 1`] = ` Array [ , , , , , , , , , , , , diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js index 48cca31da2f4d..1c750c871197e 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js @@ -43,7 +43,7 @@ jest.mock(`gatsby-core-utils`, () => { return { slash: originalCoreUtils.slash, cpuCoreCount: jest.fn(() => `1`), - createContentDigest: jest.fn(() => `contentDigest`), + createContentDigest: originalCoreUtils.createContentDigest, } }) @@ -86,6 +86,13 @@ const manifestOptions = { ], } +// Some of these tests check the number of sharp calls to assert that the +// correct number of images are created. +// +// As long as an `icon` is defined in the config, there's always an extra +// call to sharp to check the source icon is square. Therefore the assertions +// check for N + 1 sharp calls, where N is the expected number of icons +// generated. describe(`Test plugin manifest options`, () => { beforeEach(() => { fs.writeFileSync.mockReset() @@ -151,6 +158,9 @@ describe(`Test plugin manifest options`, () => { path.dirname(`other-icons/icon-48x48.png`) ) + // No sharp calls because this is manual mode: user provides all icon sizes + // rather than the plugin generating them + expect(sharp).toHaveBeenCalledTimes(0) expect(fs.mkdirSync).toHaveBeenNthCalledWith(1, firstIconPath) expect(fs.mkdirSync).toHaveBeenNthCalledWith(2, secondIconPath) }) @@ -177,8 +187,44 @@ describe(`Test plugin manifest options`, () => { ...pluginSpecificOptions, }) + // One call to sharp to check the source icon is square + // + another for the favicon (enabled by default) + // + another for the single icon in the `icons` config + // => 3 total calls + expect(sharp).toHaveBeenCalledTimes(3) + expect(sharp).toHaveBeenCalledWith(icon, { density: 32 }) // the default favicon expect(sharp).toHaveBeenCalledWith(icon, { density: size }) + }) + + it(`skips favicon generation if "include_favicon" option is set to false`, async () => { + fs.statSync.mockReturnValueOnce({ isFile: () => true }) + + const icon = `pretend/this/exists.png` + const size = 48 + + const pluginSpecificOptions = { + icon: icon, + icons: [ + { + src: `icons/icon-48x48.png`, + sizes: `${size}x${size}`, + type: `image/png`, + }, + ], + include_favicon: false, + } + + await onPostBootstrap(apiArgs, { + ...manifestOptions, + ...pluginSpecificOptions, + }) + + // Only two sharp calls here: one to check the source icon size, + // and another to generate the single icon in the config. + // By default, there would be a 3rd call for the favicon, but that's + // disabled by the `include_favicon` option. expect(sharp).toHaveBeenCalledTimes(2) + expect(sharp).toHaveBeenCalledWith(icon, { density: size }) }) it(`fails on non existing icon`, async () => { @@ -236,7 +282,11 @@ describe(`Test plugin manifest options`, () => { ...pluginSpecificOptions, }) - expect(sharp).toHaveBeenCalledTimes(2) + // Two icons in the config, plus a favicon, plus one call to check the + // source icon size => 4 total calls to sharp. + expect(sharp).toHaveBeenCalledTimes(4) + + // Filenames in the manifest should be suffixed with the content digest expect(fs.writeFileSync).toMatchSnapshot() }) @@ -253,7 +303,9 @@ describe(`Test plugin manifest options`, () => { ...pluginSpecificOptions, }) - expect(sharp).toHaveBeenCalledTimes(2) + // Two icons in the config, plus a favicon, plus one call to check the + // source icon size => 4 total calls to sharp. + expect(sharp).toHaveBeenCalledTimes(4) expect(fs.writeFileSync).toHaveBeenCalledWith( expect.anything(), JSON.stringify(manifestOptions) @@ -274,7 +326,9 @@ describe(`Test plugin manifest options`, () => { ...pluginSpecificOptions, }) - expect(sharp).toHaveBeenCalledTimes(2) + // Two icons in the config, plus a favicon, plus one call to check the + // source icon size => 4 total calls to sharp. + expect(sharp).toHaveBeenCalledTimes(4) const content = JSON.parse(fs.writeFileSync.mock.calls[0][1]) expect(content.icons[0].purpose).toEqual(`all`) expect(content.icons[1].purpose).toEqual(`maskable`) @@ -338,6 +392,7 @@ describe(`Test plugin manifest options`, () => { JSON.stringify(expectedResults[2]) ) }) + it(`generates all language versions with pathPrefix`, async () => { fs.statSync.mockReturnValueOnce({ isFile: () => true }) const pluginSpecificOptions = { diff --git a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js index 7eb4142a00569..24d3e32bbc70f 100644 --- a/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js @@ -4,12 +4,6 @@ jest.mock(`fs`, () => { } }) -jest.mock(`gatsby-core-utils`, () => { - return { - createContentDigest: jest.fn(() => `contentDigest`), - } -}) - const { onRenderBody } = require(`../gatsby-ssr`) let headComponents diff --git a/packages/gatsby-plugin-manifest/src/common.js b/packages/gatsby-plugin-manifest/src/common.js index 8b05a0700468c..df5e5e692057a 100644 --- a/packages/gatsby-plugin-manifest/src/common.js +++ b/packages/gatsby-plugin-manifest/src/common.js @@ -1,6 +1,14 @@ import fs from "fs" import sysPath from "path" +exports.favicons = [ + { + src: `favicon-32x32.png`, + sizes: `32x32`, + type: `image/png`, + }, +] + // default icons for generating icons exports.defaultIcons = [ { diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js index 30cc7072e07d9..db7f45d94c6bf 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js @@ -2,7 +2,12 @@ import fs from "fs" import path from "path" import sharp from "./safe-sharp" import { createContentDigest, cpuCoreCount, slash } from "gatsby-core-utils" -import { defaultIcons, doesIconExist, addDigestToPath } from "./common" +import { + defaultIcons, + doesIconExist, + addDigestToPath, + favicons, +} from "./common" sharp.simd(true) @@ -126,6 +131,8 @@ const makeManifest = async ({ const suffix = shouldLocalize && pluginOptions.lang ? `_${pluginOptions.lang}` : `` + const faviconIsEnabled = pluginOptions.include_favicon ?? true + // Delete options we won't pass to the manifest.webmanifest. delete manifest.plugins delete manifest.legacy @@ -192,30 +199,46 @@ const makeManifest = async ({ const iconDigest = createContentDigest(fs.readFileSync(icon)) - //if cacheBusting is being done via url query icons must be generated before cache busting runs - if (cacheMode === `query`) { - await Promise.all( - manifest.icons.map(dstIcon => - checkCache(cache, dstIcon, icon, iconDigest, generateIcon) + /** + * Given an array of icon configs, generate the various output sizes from + * the source icon image. + */ + async function processIconSet(iconSet) { + //if cacheBusting is being done via url query icons must be generated before cache busting runs + if (cacheMode === `query`) { + await Promise.all( + iconSet.map(dstIcon => + checkCache(cache, dstIcon, icon, iconDigest, generateIcon) + ) ) - ) - } + } - if (cacheMode !== `none`) { - manifest.icons = manifest.icons.map(icon => { - let newIcon = { ...icon } - newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode) - return newIcon - }) - } + if (cacheMode !== `none`) { + iconSet = iconSet.map(icon => { + let newIcon = { ...icon } + newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode) + return newIcon + }) + } - //if file names are being modified by cacheBusting icons must be generated after cache busting runs - if (cacheMode !== `query`) { - await Promise.all( - manifest.icons.map(dstIcon => - checkCache(cache, dstIcon, icon, iconDigest, generateIcon) + //if file names are being modified by cacheBusting icons must be generated after cache busting runs + if (cacheMode !== `query`) { + await Promise.all( + iconSet.map(dstIcon => + checkCache(cache, dstIcon, icon, iconDigest, generateIcon) + ) ) - ) + } + + return iconSet + } + + manifest.icons = await processIconSet(manifest.icons) + + // If favicon is enabled, apply the same caching policy and generate + // the resized image(s) + if (faviconIsEnabled) { + await processIconSet(favicons) } } diff --git a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js index 17f5a15321a4e..ea655902c4422 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-ssr.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-ssr.js @@ -2,7 +2,7 @@ import React from "react" import { withPrefix as fallbackWithPrefix, withAssetPrefix } from "gatsby" import fs from "fs" import { createContentDigest } from "gatsby-core-utils" -import { defaultIcons, addDigestToPath } from "./common.js" +import { defaultIcons, addDigestToPath, favicons } from "./common.js" import getManifestForPathname from "./get-manifest-pathname" // TODO: remove for v3 @@ -30,8 +30,6 @@ exports.onRenderBody = ( // If icons were generated, also add a favicon link. if (srcIconExists) { - const favicon = icons && icons.length ? icons[0].src : null - if (cacheBusting !== `none`) { iconDigest = createContentDigest(fs.readFileSync(pluginOptions.icon)) } @@ -41,14 +39,18 @@ exports.onRenderBody = ( ? pluginOptions.include_favicon : true - if (favicon && insertFaviconLinkTag) { - headComponents.push( - - ) + if (insertFaviconLinkTag) { + favicons.forEach(favicon => { + headComponents.push( + + ) + }) } }