From 8a263b0bef2f9beba53788a39aa2446cf96142a3 Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Tue, 17 Nov 2020 17:35:31 +0700 Subject: [PATCH] fix(plugin-manifest): Allow for all valid WebAppManifest properties (#27951) (#28099) Co-authored-by: Max Stoiber Co-authored-by: Alex Moon (cherry picked from commit 88b990ab44c1cd9acc9e1b7367991a3c194dd308) --- packages/gatsby-plugin-manifest/README.md | 6 + .../gatsby-plugin-manifest/src/gatsby-node.js | 40 +-- .../src/pluginOptionsSchema.js | 266 ++++++++++++++++++ 3 files changed, 275 insertions(+), 37 deletions(-) create mode 100644 packages/gatsby-plugin-manifest/src/pluginOptionsSchema.js diff --git a/packages/gatsby-plugin-manifest/README.md b/packages/gatsby-plugin-manifest/README.md index 3d396d096331a..880a19723e9e8 100644 --- a/packages/gatsby-plugin-manifest/README.md +++ b/packages/gatsby-plugin-manifest/README.md @@ -446,6 +446,12 @@ manifest — https://developers.google.com/web/fundamentals/engage-and-retain/we For more information, see the [W3C specification](https://www.w3.org/TR/appmanifest/), [Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/Manifest) or [Web.Dev guide](https://web.dev/add-manifest/). +### Plugin options validation + +This plugin validates plugin options set in the `gatsby-config.js`. It validates the options used by the plugin and the entire WebAppManifest spec. To see the exact implementation of the validator see [src/pluginOptionsSchema.js](src/pluginOptionsSchema.js). + +The WebAppManifest spec is not stable at the time of writing. This version of the validator adheres the [most recent](https://www.w3.org/TR/2020/WD-appmanifest-20201019/) version of the specification available. + ## Troubleshooting ### Incompatible library version: sharp.node requires version X or later, but Z provides version Y diff --git a/packages/gatsby-plugin-manifest/src/gatsby-node.js b/packages/gatsby-plugin-manifest/src/gatsby-node.js index 1df0107bd3767..6e147a2968491 100644 --- a/packages/gatsby-plugin-manifest/src/gatsby-node.js +++ b/packages/gatsby-plugin-manifest/src/gatsby-node.js @@ -9,6 +9,8 @@ import { favicons, } from "./common" +import pluginOptionsSchema from "./pluginOptionsSchema" + sharp.simd(true) // Handle Sharp's concurrency based on the Gatsby CPU count @@ -58,43 +60,7 @@ async function checkCache(cache, icon, srcIcon, srcIconDigest, callback) { } } -exports.pluginOptionsSchema = ({ Joi }) => - Joi.object({ - name: Joi.string(), - short_name: Joi.string(), - description: Joi.string(), - lang: Joi.string(), - localize: Joi.array().items( - Joi.object({ - start_url: Joi.string(), - name: Joi.string(), - short_name: Joi.string(), - description: Joi.string(), - lang: Joi.string(), - }) - ), - start_url: Joi.string(), - background_color: Joi.string(), - theme_color: Joi.string(), - display: Joi.string(), - legacy: Joi.boolean(), - include_favicon: Joi.boolean(), - icon: Joi.string(), - theme_color_in_head: Joi.boolean(), - crossOrigin: Joi.string().valid(`use-credentials`, `anonymous`), - cache_busting_mode: Joi.string().valid(`query`, `name`, `none`), - icons: Joi.array().items( - Joi.object({ - src: Joi.string(), - sizes: Joi.string(), - type: Joi.string(), - purpose: Joi.string(), - }) - ), - icon_options: Joi.object({ - purpose: Joi.string(), - }), - }) +exports.pluginOptionsSchema = pluginOptionsSchema /** * Setup pluginOption defaults diff --git a/packages/gatsby-plugin-manifest/src/pluginOptionsSchema.js b/packages/gatsby-plugin-manifest/src/pluginOptionsSchema.js new file mode 100644 index 0000000000000..82b1445e6802f --- /dev/null +++ b/packages/gatsby-plugin-manifest/src/pluginOptionsSchema.js @@ -0,0 +1,266 @@ +export default function pluginOptionSchema({ Joi }) { + /* Descriptions copied from or based on documentation at https://developer.mozilla.org/en-US/docs/Web/Manifest + * + * Currently based on https://www.w3.org/TR/2020/WD-appmanifest-20201019/ + * and the August 4th 2020 version of https://w3c.github.io/manifest-app-info/ + */ + + const platform = Joi.string() + .optional() + .empty(``) + .valid( + `narrow`, + `wide`, + `chromeos`, + `ios`, + `kaios`, + `macos`, + `windows`, + `windows10x`, + `xbox`, + `chrome_web_store`, + `play`, + `itunes`, + `microsoft` + ) //https://w3c.github.io/manifest-app-info/#platform-member + .description(`The platform on which the application can be found.`) + + const FingerPrint = Joi.object().keys({ + type: Joi.string() + .required() + .description(`syntax and semantics are platform-defined`), + value: Joi.string() + .required() + .description(`syntax and semantics are platform-defined`), + }) + + const ImageResource = Joi.object().keys({ + sizes: Joi.string() + .optional() + .description(`A string containing space-separated image dimensions`), + src: Joi.string() + .required() + .description( + `The path to the image file. If src is a relative URL, the base URL will be the URL of the manifest.` + ), + type: Joi.string() + .optional() + .description( + `A hint as to the media type of the image. The purpose of this member is to allow a user agent to quickly ignore images with media types it does not support.` + ), + }) + + const ManifestImageResource = ImageResource.keys({ + purpose: Joi.string() + .optional() + .description( + `Defines the purpose of the image, for example if the image is intended to serve some special purpose in the context of the host OS.` + ), + }) + + const ShortcutItem = Joi.object().keys({ + name: Joi.string() + .required() + .description( + `The name member of a ShortcutItem is a string that represents the name of the shortcut as it is usually displayed to the user in a context menu. ` + ), + short_name: Joi.string() + .optional() + .description( + `The short_name member of a ShortcutItem is a string that represents a short version of the name of the shortcut. ` + ), + description: Joi.string() + .optional() + .description( + `The description member of a ShortcutItem is a string that allows the developer to describe the purpose of the shortcut. ` + ), + url: Joi.string() + .required() + .description( + `The url member of a ShortcutItem is a URL within scope of a processed manifest that opens when the associated shortcut is activated. ` + ), + icons: Joi.array() + .optional() + .items(ManifestImageResource) + .description( + `The icons member of an ShortcutItem member serve as iconic representations of the shortcut in various contexts. ` + ), + }) + + const ExternalApplicationResource = Joi.object() + .keys({ + platform: platform.required(), + url: Joi.string() + .uri() + .required() + .description(`The URL at which the application can be found.`), + id: Joi.string() + .required() + .description( + `The ID used to represent the application on the specified platform.` + ), + min_version: Joi.string() + .optional() + .description( + `The minimum version of the application that is considered related to this web app.` + ), + fingerprints: Joi.array() + .optional() + .items(FingerPrint) + .description( + `Each Fingerprints represents a set of cryptographic fingerprints used for verifying the application.` + ), + }) + .or(`url`, `id`) + + const WebAppManifest = Joi.object().keys({ + background_color: Joi.string() + .optional() + .description( + `The background_color member defines a placeholder background color for the application page to display before its stylesheet is loaded.` + ), + categories: Joi.array() + .items(Joi.string().empty(``)) + .optional() + .description( + `The categories member is an array of strings defining the names of categories that the application supposedly belongs to.` + ), + description: Joi.string() + .optional() + .description( + `The description member is a string in which developers can explain what the application does. ` + ), + dir: Joi.string() + .optional() + .valid(`auto`, `ltr`, `rtl`) + .description( + `The base direction in which to display direction-capable members of the manifest.` + ), + display: Joi.string() + .optional() + .valid(`fullscreen`, `standalone`, `minimal-ui`, `browser`) + .description( + `The display member is a string that determines the developers’ preferred display mode for the website` + ), + iarc_rating_id: Joi.string() + .guid() + .optional() + .description( + `The iarc_rating_id member is a string that represents the International Age Rating Coalition (IARC) certification code of the web application.` + ), + icons: Joi.array() + .optional() + .items(ManifestImageResource) + .description( + `The icons member specifies an array of objects representing image files that can serve as application icons for different contexts.` + ), + lang: Joi.string() + .optional() + .description( + `The lang member is a string containing a single language tag.` + ), + name: Joi.string() + .optional() + .description( + `The name member is a string that represents the name of the web application as it is usually displayed to the user.` + ), + orientation: Joi.string() + .optional() + .valid( + `any`, + `natural`, + `landscape`, + `landscape-primary`, + `landscape-secondary`, + `portrait`, + `portrait-primary`, + `portrait-secondary` + ) // From: https://www.w3.org/TR/screen-orientation/#screenorientation-interface + .description( + `The orientation member defines the default orientation for all the website's top-level browsing contexts` + ), + prefer_related_applications: Joi.boolean() + .optional() + .description( + `The prefer_related_applications member is a boolean value that specifies that applications listed in related_applications should be preferred over the web application.` + ), + related_applications: Joi.array() + .optional() + .items(ExternalApplicationResource) + .description( + `The related_applications field is an array of objects specifying native applications that are installable by, or accessible to, the underlying platform.` + ), + scope: Joi.string() + .optional() + .description( + `The scope member is a string that defines the navigation scope of this web application's application context.` + ), + screenshots: Joi.array() + .optional() + .items( + ManifestImageResource.keys({ + label: Joi.string() + .optional() + .description( + `The label member is a string that serves as the accessible name of that screenshots object.` + ), + platform: platform, + }) + ) + .description( + `The screenshots member defines an array of screenshots intended to showcase the application.` + ), + short_name: Joi.string() + .optional() + .description( + `The short_name member is a string that represents the name of the web application displayed to the user if there is not enough space to display name.` + ), + shortcuts: Joi.array() + .optional() + .items(ShortcutItem) + .description( + `Each ShortcutItem represents a link to a key task or page within a web app. ` + ), + start_url: Joi.string() + .optional() + .description( + `The start_url member is a string that represents the start URL of the web application.` + ), + theme_color: Joi.string() + .optional() + .description( + `The theme_color member is a string that defines the default theme color for the application.` + ), + }) + + const GatsbyPluginOptions = Joi.object() + .keys({ + icon: Joi.string(), + legacy: Joi.boolean().default(true), + theme_color_in_head: Joi.boolean().default(true), + cache_busting_mode: Joi.string() + .valid(`none`, `query`, `name`) + .default(`query`), + crossOrigin: Joi.string() + .valid(`anonymous`, `use-credentials`) + .default(`anonymous`), + include_favicon: Joi.boolean().default(true), + icon_options: ManifestImageResource.keys({ + src: Joi.string().forbidden(), + sizes: Joi.string().forbidden(), + }), + + localize: Joi.array() + .items( + WebAppManifest.keys({ + lang: Joi.required(), + start_url: Joi.required(), + }) + ) + .description(`Used for localizing your WebAppManifest`), + }) + .or(`icon`, `icons`) + .with(`localize`, `lang`) + + return WebAppManifest.concat(GatsbyPluginOptions) +}