diff --git a/LICENSE b/LICENSE index 35a8e5b..9826a80 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Travis Reynolds +Copyright (c) 2021 Travis Reynolds Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e14938d..0b0d71c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This plugin supports the Storefront API's [`transformedSrc` image field](#transf 3. [Routes & Templates](#routes--templates) 4. [Page Query](#page-query) 5. [Metafields](#metafields) +5. [Translations](#translations) 6. [Additional Resolvers](#additional-resolvers) 7. [Helpful Snippets](#helpful-snippets) @@ -110,7 +111,189 @@ Now this product will be available at `this.$page.shopifyProduct`: ## Metafields To make metafields available to query in the Storefront API, you should follow this guide: [Retrieve metafields with the Storefront API](https://shopify.dev/tutorials/retrieve-metafields-with-storefront-api). -Then metafields will be available in your product query. + +## Translations + +To fetch translations for relevant types, you need to an array of locales to the plugin options. This plugin will fetch the default Shopify locale by default, so this array should only include extra locales you want to fetch: +```js +options: { + // ... + locales: ['es', 'fr'] +} +``` + +These translations are then available a couple of different ways, depending on how your sites internationalization is setup. + +### Translatable Fields + +When the locales config has been added, a `locale` argument is added to relevant fields: `title(locale: "es")`. This can then be used to get a specific translation for that field - it fits well with when you have a site that uses a locale URL prefix, so you have different pages for each locale version. For example: + +`gridsome.server.js` +```js +module.exports = api => { + api.createPages(async ({ graphql, createPage }) => { + const { data } = await graphql(` + { + allShopifyProduct { + edges { + node { + id + handle + } + } + } + } + `) + + const locales = ['en', 'es', 'fr'] + + for (const { node: product } of data.allShopifyProduct.edges) { + for (const locale of locales) { + createPage({ + path: `/${locale}/products/${product.handle}`, + component: './src/templates/Product.vue', + context: { + id: product.id, + locale + } + }) + } + } + }) +} +``` +`Product.vue` +```vue + + + + + +query Product ($id: ID!, $locale: String!) { + shopifyProduct (id: $id) { + id + title(locale: $locale) + descriptionHtml(locale: $locale) + variants { + id + title(locale: $locale) + price { + amount + } + selectedOptions (locale: $locale) { + name + value + } + } + } +} + +``` + +#### Translations Collections + +A translations collection is added for each relevant type, each with an array of nodes containing the translatable fields for that type. You can add filters to these queries to get a specific locale if needed - for example: + +```graphql +allShopifyProductTranslations (filter: { locale: {_eq: "es" }}) { + edges { + node { + id + originalId + title + description + variant { + title + } + } + } +} +``` +> Note: As these collections contain multiple copies of a node (for each translation), the ID is changed to keep it unique. The original Shopify ID can be found under `originalId`. + +This could be used when you have a site setup so there is one page that includes every translation, and a select input is used to switch between locales: + +```vue + + + + + +query Product ($id: ID!) { + shopifyProduct (id: $id) { + id + title + descriptionHtml + variants { + id + title + price { + amount + } + } + } + allShopifyProductTranslation (filter: { id: { eq: $id }}) { + edges { + node { + id + title + locale + descriptionHtml + variants { + title + selectedOptions { + name + value + } + } + } + } + } +} + +``` ## Additional Resolvers diff --git a/package.json b/package.json index 6dd61e7..d1b42f9 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "gridsome-source-shopify", "description": "Shopify source plugin for Gridsome", - "version": "0.2.7", + "version": "0.3.0-beta.2", "main": "lib/index.js", "engines": { - "node": ">=10.x" + "node": ">=12.x" }, "author": { "email": "travis@travisreynolds.dev", @@ -23,14 +23,15 @@ ], "scripts": { "develop": "rollup -c --watch", - "build": "rm -rf lib && rollup -c" + "build": "rm -rf lib && rollup -c", + "lint": "eslint src --fix" }, "dependencies": { - "camelcase": "^6.0.0", - "got": "^11.6.2" + "camelcase": "^6.2.0", + "got": "^11.8.2" }, "devDependencies": { "eslint-config-travisreynolds-node": "^1.2.0", - "rollup": "^2.26.11" + "rollup": "^2.55.1" } } diff --git a/src/client.js b/src/client.js index 49ac073..f3b500b 100644 --- a/src/client.js +++ b/src/client.js @@ -19,10 +19,11 @@ export const createClient = ({ storeUrl, storefrontToken, timeout }) => got.exte * Get all paginated data from a query. Will execute multiple requests as * needed. */ -export const queryAll = async (client, query, variables) => { +export const queryAll = async (client, query, variables, headers) => { const items = client.paginate.each('graphql.json', { method: 'POST', json: { query, variables }, + headers: headers || {}, pagination: { backoff: 1000, transform: ({ body: { data, errors } }) => { @@ -54,7 +55,7 @@ export const queryAll = async (client, query, variables) => { } // Currently setup for Collection.products field, but can extend this method in future, if needed - if (!node.products.pageInfo.hasNextPage) { + if (!node.products || !node.products.pageInfo.hasNextPage) { allNodes.push(node) continue } diff --git a/src/index.js b/src/index.js index 08968bc..19e927c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import camelCase from 'camelcase' import { createClient, queryAll } from './client' import { createSchema } from './schema' -import { COLLECTIONS_QUERY, PRODUCTS_QUERY, PRODUCT_TYPES_QUERY, ARTICLES_QUERY, BLOGS_QUERY, PAGES_QUERY, PRODUCT_TAGS_QUERY } from './queries' +import { COLLECTIONS_QUERY, PRODUCTS_QUERY, PRODUCT_TYPES_QUERY, ARTICLES_QUERY, BLOGS_QUERY, PAGES_QUERY, PRODUCT_TAGS_QUERY, PRODUCTS_TRANSLATIONS_QUERY, COLLECTIONS_TRANSLATIONS_QUERY, BLOGS_TRANSLATIONS_QUERY, ARTICLES_TRANSLATIONS_QUERY, PAGES__TRANSLATIONS_QUERY } from './queries' class ShopifySource { static defaultOptions () { @@ -12,7 +12,8 @@ class ShopifySource { typeName: 'Shopify', types: [], perPage: 100, - timeout: 60000 + timeout: 60000, + locales: [] } } @@ -22,15 +23,28 @@ class ShopifySource { if (!options.storeUrl && !options.storeName) throw new Error('Missing store name or url.') if (!options.storefrontToken) throw new Error('Missing storefront access token.') if (options.storeName) this.options.storeUrl = `https://${options.storeName}.myshopify.com` + if (options.locales) { + if (!Array.isArray(options.locales)) throw new Error('The locales option must be an array of strings.') + if (options.locales.length) { + this.options.hasLocales = true + this.options.locales = options.locales.map(l => l.toLowerCase().trim()) + } + } // Node Types this.TYPENAMES = { ARTICLE: this.createTypeName('Article'), + ARTICLE_TRANSLATION: this.createTypeName('ArticleTranslation'), BLOG: this.createTypeName('Blog'), + BLOG_TRANSLATION: this.createTypeName('BlogTranslation'), COLLECTION: this.createTypeName('Collection'), + COLLECTION_TRANSLATION: this.createTypeName('CollectionTranslation'), PRODUCT: this.createTypeName('Product'), + PRODUCT_TRANSLATION: this.createTypeName('ProductTranslation'), PRODUCT_VARIANT: this.createTypeName('ProductVariant'), + PRODUCT_VARIANT_TRANSLATION: this.createTypeName('ProductVariantTranslation'), PAGE: this.createTypeName('Page'), + PAGE_TRANSLATION: this.createTypeName('PageTranslation'), PRODUCT_TYPE: this.createTypeName('ProductType'), PRODUCT_TAG: this.createTypeName('ProductTag'), IMAGE: 'ShopifyImage', @@ -44,7 +58,7 @@ class ShopifySource { // Create custom schema type for ShopifyImage api.loadSource(actions => { - createSchema(actions, { TYPENAMES: this.TYPENAMES }) + createSchema(actions, { TYPENAMES: this.TYPENAMES, hasLocales: options.hasLocales, createShopifyId: this.createShopifyId }) }) // Load data into store @@ -55,10 +69,15 @@ class ShopifySource { await this.getProductTypes(actions) await this.getProductTags(actions) await this.getCollections(actions) + await this.getCollectionsTranslations(actions) await this.getProducts(actions) + await this.getProductTranslations(actions) await this.getBlogs(actions) + await this.getBlogsTranslations(actions) await this.getArticles(actions) + await this.getArticlesTranslations(actions) await this.getPages(actions) + await this.getPagesTranslations(actions) }) } @@ -111,7 +130,29 @@ class ShopifySource { } } - async getProducts (actions) { + async getCollectionsTranslations (actions) { + if (!this.typesToInclude.includes(this.TYPENAMES.COLLECTION_TRANSLATION) || !this.options.hasLocales) return + + const collectionsTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.COLLECTION_TRANSLATION }) + + for (const locale of this.options.locales) { + const collectionsTranslations = await queryAll(this.shopify, COLLECTIONS_TRANSLATIONS_QUERY, { first: this.options.perPage }, { 'Accept-Language': locale }) + + for (const collection of collectionsTranslations) { + const originalId = collection.id + const id = this.createShopifyId(collection.id, `Locale${locale.toUpperCase()}`) + + collectionsTranslationsStore.addNode({ + ...collection, + id, + locale, + originalId + }) + } + } + } + + async getProducts (actions, locale) { if (!this.typesToInclude.includes(this.TYPENAMES.PRODUCT)) return const productStore = actions.addCollection({ typeName: this.TYPENAMES.PRODUCT }) @@ -170,6 +211,38 @@ class ShopifySource { return { minVariantPrice: actions.createReference(minVariantPrice), maxVariantPrice: actions.createReference(maxVariantPrice) } } + async getProductTranslations (actions) { + if (!this.typesToInclude.includes(this.TYPENAMES.PRODUCT_TRANSLATION) || !this.options.hasLocales) return + + const productTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.PRODUCT_TRANSLATION }) + const productVariantTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.PRODUCT_VARIANT_TRANSLATION }) + + for (const locale of this.options.locales) { + const productsTranslations = await queryAll(this.shopify, PRODUCTS_TRANSLATIONS_QUERY, { first: this.options.perPage }, { 'Accept-Language': locale }) + + for (const product of productsTranslations) { + const originalId = product.id + const id = this.createShopifyId(product.id, `Locale${locale.toUpperCase()}`) + + const variants = product.variants.edges.map(({ node: variant }) => { + const originalId = variant.id + const id = this.createShopifyId(variant.id, `Locale${locale.toUpperCase()}`) + + const variantNode = productVariantTranslationsStore.addNode({ ...variant, id, originalId, locale }) + return actions.createReference(variantNode) + }) + + productTranslationsStore.addNode({ + ...product, + id, + locale, + originalId, + variants + }) + } + } + } + async getBlogs (actions) { if (!this.typesToInclude.includes(this.TYPENAMES.BLOG)) return @@ -182,6 +255,28 @@ class ShopifySource { } } + async getBlogsTranslations (actions) { + if (!this.typesToInclude.includes(this.TYPENAMES.BLOG_TRANSLATION) || !this.options.hasLocales) return + + const blogsTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.BLOG_TRANSLATION }) + + for (const locale of this.options.locales) { + const blogsTranslations = await queryAll(this.shopify, BLOGS_TRANSLATIONS_QUERY, { first: this.options.perPage }, { 'Accept-Language': locale }) + + for (const blog of blogsTranslations) { + const originalId = blog.id + const id = this.createShopifyId(blog.id, `Locale${locale.toUpperCase()}`) + + blogsTranslationsStore.addNode({ + ...blog, + id, + locale, + originalId + }) + } + } + } + async getArticles (actions) { if (!this.typesToInclude.includes(this.TYPENAMES.ARTICLE)) return @@ -204,6 +299,28 @@ class ShopifySource { } } + async getArticlesTranslations (actions) { + if (!this.typesToInclude.includes(this.TYPENAMES.ARTICLE_TRANSLATION) || !this.options.hasLocales) return + + const articlesTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.ARTICLE_TRANSLATION }) + + for (const locale of this.options.locales) { + const articlesTranslations = await queryAll(this.shopify, ARTICLES_TRANSLATIONS_QUERY, { first: this.options.perPage }, { 'Accept-Language': locale }) + + for (const article of articlesTranslations) { + const originalId = article.id + const id = this.createShopifyId(article.id, `Locale${locale.toUpperCase()}`) + + articlesTranslationsStore.addNode({ + ...article, + id, + locale, + originalId + }) + } + } + } + async getPages (actions) { if (!this.typesToInclude.includes(this.TYPENAMES.PAGE)) return @@ -216,6 +333,28 @@ class ShopifySource { } } + async getPagesTranslations (actions) { + if (!this.typesToInclude.includes(this.TYPENAMES.PAGE_TRANSLATION) || !this.options.hasLocales) return + + const pagesTranslationsStore = actions.addCollection({ typeName: this.TYPENAMES.PAGE_TRANSLATION }) + + for (const locale of this.options.locales) { + const pagesTranslations = await queryAll(this.shopify, PAGES__TRANSLATIONS_QUERY, { first: this.options.perPage }, { 'Accept-Language': locale }) + + for (const page of pagesTranslations) { + const originalId = page.id + const id = this.createShopifyId(page.id, `Locale${locale.toUpperCase()}`) + + pagesTranslationsStore.addNode({ + ...page, + id, + locale, + originalId + }) + } + } + } + createTypeName (name) { let typeName = this.options.typeName // If typeName is blank, we need to add a prefix to these types anyway, as on their own they conflict with internal Gridsome types. diff --git a/src/queries.js b/src/queries.js index b92d04d..a7fd4b5 100644 --- a/src/queries.js +++ b/src/queries.js @@ -54,6 +54,26 @@ export const ARTICLES_QUERY = ` } } ` +export const ARTICLES_TRANSLATIONS_QUERY = ` + query Articles ($first: Int!, $after: String) { + data: articles (first: $first, after: $after) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + title + content + contentHtml + excerpt + excerptHtml + } + } + } + } +` export const BLOGS_QUERY = ` query Blogs ($first: Int!, $after: String) { @@ -76,6 +96,22 @@ export const BLOGS_QUERY = ` } } ` +export const BLOGS_TRANSLATIONS_QUERY = ` + query Blogs ($first: Int!, $after: String) { + data: blogs(first: $first, after: $after) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + title + } + } + } + } +` export const COLLECTIONS_QUERY = ` query Collections ($first: Int!, $after: String) { @@ -114,6 +150,25 @@ export const COLLECTIONS_QUERY = ` } } ` +export const COLLECTIONS_TRANSLATIONS_QUERY = ` + query Collections ($first: Int!, $after: String) { + data: collections (first: $first, after: $after) { + pageInfo { + hasNextPage + } + edges { + typeName: __typename + cursor + node { + id + title + description + descriptionHtml + } + } + } + } +` export const COLLECTION_QUERY = `query SingleCollection ($handle: String!, $first: Int!, $after: String) { collection: collectionByHandle (handle: $handle) { @@ -250,6 +305,37 @@ export const PRODUCTS_QUERY = ` } ` +export const PRODUCTS_TRANSLATIONS_QUERY = ` + query Products ($first: Int!, $after: String) { + data: products (first: $first, after: $after) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + title + description + descriptionHtml + variants(first: 250) { + edges { + node { + id + title + selectedOptions { + name + value + } + } + } + } + } + } + } + } +` + export const SHOP_QUERY = ` query Shop { shop { @@ -330,3 +416,21 @@ export const PAGES_QUERY = ` } } ` + +export const PAGES__TRANSLATIONS_QUERY = ` + query Pages ($first: Int!) { + data: pages (first: $first) { + pageInfo { + hasNextPage + } + edges { + cursor + node { + id + title + body + } + } + } + } +` diff --git a/src/schema.js b/src/schema.js index 54a21dd..5d9fd17 100644 --- a/src/schema.js +++ b/src/schema.js @@ -1,4 +1,4 @@ -export const createSchema = ({ addSchemaTypes, schema, addSchemaResolvers }, { TYPENAMES }) => { +export const createSchema = ({ addSchemaTypes, schema, addSchemaResolvers }, { TYPENAMES, hasLocales, createShopifyId }) => { addSchemaTypes([ schema.createEnumType({ name: `${TYPENAMES.IMAGE}CropMode`, @@ -70,4 +70,46 @@ export const createSchema = ({ addSchemaTypes, schema, addSchemaResolvers }, { T } } }) + + if (hasLocales) { + addSchemaTypes(` + type ${TYPENAMES.PRODUCT_VARIANT}_SelectedOptions @infer { + name: String + value: String + } + `) + + const translatableTypes = [ + [TYPENAMES.PRODUCT, TYPENAMES.PRODUCT_TRANSLATION, ['title', 'description', 'descriptionHtml']], + [TYPENAMES.PRODUCT_VARIANT, TYPENAMES.PRODUCT_VARIANT_TRANSLATION, ['title', 'selectedOptions']], + [TYPENAMES.COLLECTION, TYPENAMES.COLLECTION_TRANSLATION, ['title', 'description', 'descriptionHtml']], + [TYPENAMES.ARTICLE, TYPENAMES.ARTICLE_TRANSLATION, ['title', 'content', 'contentHtml', 'excerpt', 'excerptHtml']], + [TYPENAMES.BLOG, TYPENAMES.BLOG_TRANSLATION, ['title']], + [TYPENAMES.PAGE, TYPENAMES.PAGE_TRANSLATION, ['title', 'body']] + ] + + const resolvers = translatableTypes.map(([typeName, translationTypeName, fields]) => { + const resolvers = fields.map(field => { + return [field, { + type: field === 'selectedOptions' ? `[${TYPENAMES.PRODUCT_VARIANT}_SelectedOptions]` : 'String', + args: { + locale: 'String' + }, + resolve: (parent, { locale }, ctx) => { + if (!locale) return Reflect.get(parent, field) + const translationId = createShopifyId(parent.id, `Locale${locale.toUpperCase()}`) + const translationsStore = ctx.store.getCollection(translationTypeName) + + const translation = translationsStore.getNode(translationId) + if (!translation) return Reflect.get(parent, field) + return Reflect.get(translation, field) + } + }] + }) + + return [typeName, Object.fromEntries(resolvers)] + }) + + addSchemaResolvers(Object.fromEntries(resolvers)) + } } diff --git a/yarn.lock b/yarn.lock index 2f3f04d..b203fc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -109,10 +109,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@sindresorhus/is@^3.1.1": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8" - integrity sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ== +"@sindresorhus/is@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.1.tgz#d26729db850fa327b7cacc5522252194404226f5" + integrity sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g== "@szmarczak/http-timer@^4.0.5": version "4.0.5" @@ -292,10 +292,10 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" - integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== +camelcase@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== chalk@^2.0.0: version "2.4.2" @@ -729,10 +729,10 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" @@ -782,12 +782,12 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" -got@^11.6.2: - version "11.6.2" - resolved "https://registry.yarnpkg.com/got/-/got-11.6.2.tgz#79d7bb8c11df212b97f25565407a1f4ae73210ec" - integrity sha512-/21qgUePCeus29Jk7MEti8cgQUNXFSWfIevNIk4H7u1wmXNDrGPKPY6YsPY+o9CIT/a2DjCjRz0x1nM9FtS2/A== +got@^11.8.2: + version "11.8.2" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" + integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== dependencies: - "@sindresorhus/is" "^3.1.1" + "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" @@ -1295,12 +1295,12 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rollup@^2.26.11: - version "2.26.11" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.11.tgz#4fc31de9c7b83d50916fc8395f8c3d24730cdaae" - integrity sha512-xyfxxhsE6hW57xhfL1I+ixH8l2bdoIMaAecdQiWF3N7IgJEMu99JG+daBiSZQjnBpzFxa0/xZm+3pbCdAQehHw== +rollup@^2.55.1: + version "2.55.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.55.1.tgz#66a444648e2fb603d8e329e77a61c608a6510fda" + integrity sha512-1P9w5fpb6b4qroePh8vHKGIvPNxwoCQhjJpIqfZGHLKpZ0xcU2/XBmFxFbc9697/6bmHpmFTLk5R1dAQhFSo0g== optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.2" "semver@2 || 3 || 4 || 5": version "5.7.1"