From e4ca79a810ca89f609082b994a9ad069e9d64579 Mon Sep 17 00:00:00 2001 From: Tiago Gimenes Date: Fri, 30 Jul 2021 10:56:33 -0300 Subject: [PATCH] feat(collections): Source collections from category tree (#871) ] --- packages/gatsby-plugin-cms/README.md | 45 +++++ packages/gatsby-plugin-cms/package.json | 5 +- packages/gatsby-plugin-cms/src/gatsby-node.ts | 131 +++++++++++++-- packages/gatsby-plugin-cms/src/index.ts | 10 +- .../src/native-types/blocks/collection.ts | 86 ++++++++++ .../src/native-types/blocks/seo.ts | 38 +++++ .../src/native-types/blocks/sort.ts | 38 +++++ .../src/native-types/contentTypes/plp.ts | 21 +++ .../src/node-api/catalog/index.ts | 67 ++++++++ .../src/node-api/cms/fetchNodes.ts | 119 +++++++++++++ .../src/node-api/cms/sourceLocalNodes.ts | 38 +++++ .../src/node-api/{ => cms}/sourceNode.ts | 7 +- .../src/node-api/{ => cms}/types.ts | 1 + .../src/node-api/fetchNodes.ts | 114 ------------- .../src/node-api/sourceLocalNodes.ts | 81 --------- .../gatsby-plugin-cms/src/utils/barrier.ts | 24 +++ packages/gatsby-plugin-cms/src/utils/fetch.ts | 10 ++ packages/gatsby-source-vtex/index.js | 1 + packages/gatsby-source-vtex/src/constants.ts | 1 + .../gatsby-source-vtex/src/gatsby-node.ts | 135 ++++++++------- .../src/graphql/types/collection/index.ts | 158 ++++++++++++++++++ .../graphql/types/collection/typeDefs.graphql | 19 +++ packages/gatsby-source-vtex/src/index.ts | 6 + packages/gatsby-source-vtex/src/types.ts | 18 +- yarn.lock | 97 ++++++----- 25 files changed, 952 insertions(+), 318 deletions(-) create mode 100644 packages/gatsby-plugin-cms/src/native-types/blocks/collection.ts create mode 100644 packages/gatsby-plugin-cms/src/native-types/blocks/seo.ts create mode 100644 packages/gatsby-plugin-cms/src/native-types/blocks/sort.ts create mode 100644 packages/gatsby-plugin-cms/src/native-types/contentTypes/plp.ts create mode 100644 packages/gatsby-plugin-cms/src/node-api/catalog/index.ts create mode 100644 packages/gatsby-plugin-cms/src/node-api/cms/fetchNodes.ts create mode 100644 packages/gatsby-plugin-cms/src/node-api/cms/sourceLocalNodes.ts rename packages/gatsby-plugin-cms/src/node-api/{ => cms}/sourceNode.ts (90%) rename packages/gatsby-plugin-cms/src/node-api/{ => cms}/types.ts (97%) delete mode 100644 packages/gatsby-plugin-cms/src/node-api/fetchNodes.ts delete mode 100644 packages/gatsby-plugin-cms/src/node-api/sourceLocalNodes.ts create mode 100644 packages/gatsby-plugin-cms/src/utils/barrier.ts create mode 100644 packages/gatsby-plugin-cms/src/utils/fetch.ts create mode 100644 packages/gatsby-source-vtex/index.js create mode 100644 packages/gatsby-source-vtex/src/constants.ts create mode 100644 packages/gatsby-source-vtex/src/graphql/types/collection/index.ts create mode 100644 packages/gatsby-source-vtex/src/graphql/types/collection/typeDefs.graphql diff --git a/packages/gatsby-plugin-cms/README.md b/packages/gatsby-plugin-cms/README.md index 8af590d2f3..56f9be5466 100644 --- a/packages/gatsby-plugin-cms/README.md +++ b/packages/gatsby-plugin-cms/README.md @@ -200,3 +200,48 @@ which would return the follwing json: } } ``` + +## Native Types +CMS plugin has pre-built blocks that speed up your content types creation. Think of this like a component library that you can import and stitch together to create the content type you desire. +These types include Carousel, Seo, and much more. To use it on your project, just: +```ts +import { Carousel } from '@vtex/gatsby-plugin-cms' + +... + +export default { + ... + blocks: { + myBlock: { + Carousel, + ... + } + } + ... +} +``` + +Check all available blocks, and their definition, at [`gatsby-plugin-cms/native-types`](https://github.com/vtex/faststore/tree/master/packages/gatsby-plugin-cms/src/native-types) + +### VTEX modules and Native Types +Some VTEX modules have first-class support in our CMS. To enable this support, you need to create your contentTypes with our native types for that specific module. +Below you can find the doc and how these integrations work for each module. + +#### Catalog +Sourcing Brands/Categories can be achieved by using `@vtex/gatsby-source-vtex` plugin. This plugin sources a `StoreCollection` node into the Gatsby's data layer containing basic info about a category and brand. Although being handy for creating pages using the [File System Route API](https://www.gatsbyjs.com/docs/reference/routing/file-system-route-api/), `StoreCollection` does not have enough data to create a rich PLP, with banners and much more. For this, you need to extend `StoreCollection` with more data. +To help you extend `StoreCollection` for your business users, we created a native type called `PLP` for the Product List Page. + +Whenever the CMS finds a node with the `PLP` signature, it will create a customization on the corresponding `StoreCollection` node adding this `PLP` as a foreign key on the `StoreCollection` node. This way, you can easily fetch all sections of the `PLP` when rendering the `StoreCollection` page, thus allowing you to add any information you want to the `PLP`. + +To use it, just add this to your cms config: +```ts +import { PLP } from '@vtex/gatsby-plugin-cms' + +export default { + ... + contentTypes: { + ...PLP() + }, + ... +} +``` diff --git a/packages/gatsby-plugin-cms/package.json b/packages/gatsby-plugin-cms/package.json index f3487e82e5..af71221913 100644 --- a/packages/gatsby-plugin-cms/package.json +++ b/packages/gatsby-plugin-cms/package.json @@ -34,10 +34,13 @@ "dependencies": { "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.1", + "@vtex/gatsby-source-vtex": "^0.372.18", "camelcase": "^6.2.0", "chokidar": "^3.5.0", + "fetch-retry": "^4.1.1", "fs-extra": "^9.0.1", - "gatsby-graphql-source-toolkit": "^2.0.1", + "globby": "^11.0.0", + "isomorphic-unfetch": "^3.1.0", "p-map": "^4.0.0" } } diff --git a/packages/gatsby-plugin-cms/src/gatsby-node.ts b/packages/gatsby-plugin-cms/src/gatsby-node.ts index 034f096d31..45d28782db 100644 --- a/packages/gatsby-plugin-cms/src/gatsby-node.ts +++ b/packages/gatsby-plugin-cms/src/gatsby-node.ts @@ -1,18 +1,34 @@ import { join } from 'path' import { outputJSON, pathExists } from 'fs-extra' +import { sourceStoreCollectionNode } from '@vtex/gatsby-source-vtex' import type { JSONSchema6 } from 'json-schema' import type { CreatePagesArgs, SourceNodesArgs, PluginOptionsSchemaArgs, + CreateNodeArgs, } from 'gatsby' +import type { StoreCollection } from '@vtex/gatsby-source-vtex' -import { PLUGIN } from './constants' -import { fetchAllNodes } from './node-api/fetchNodes' -import { createSchemaCustomization, sourceNode } from './node-api/sourceNode' -import { sourceAllLocalNodes } from './node-api/sourceLocalNodes' +import { Barrier } from './utils/barrier' +import { fetchAllNodes as fetchAllRemoteNodes } from './node-api/cms/fetchNodes' +import { + createSchemaCustomization as createCmsSchemaCustomization, + sourceNode as sourceCmsNode, +} from './node-api/cms/sourceNode' +import { fetchAllNodes as fetchAllLocalNodes } from './node-api/cms/sourceLocalNodes' +import { + getCollectionsFromPageContent, + splitCollections, +} from './node-api/catalog' +import type { WithPLP } from './node-api/catalog/index' import type { BuilderConfig } from './index' +import type { + ICategoryCollection, + IClusterCollection, + IBrandCollection, +} from './native-types/blocks/collection' interface CMSContentType { id: string @@ -47,28 +63,123 @@ const SHADOWED_INDEX_PATH = join(root, 'src', name, 'index.ts') export interface Options { tenant: string - workspace?: string + workspace: string + environment: 'vtexcommercestable' | 'vtexcommercebeta' } export const pluginOptionsSchema = ({ Joi }: PluginOptionsSchemaArgs) => Joi.object({ tenant: Joi.string().required(), - workspace: Joi.string(), + workspace: Joi.string().required(), + environment: Joi.string() + .required() + .valid('vtexcommercestable', 'vtexcommercebeta'), }) +interface CollectionsByType { + Category: Record> + Department: Record> + Cluster: Record> + Brand: Record> +} + +const overridesBarrier = new Barrier() + export const sourceNodes = async ( gatsbyApi: SourceNodesArgs, options: Options ) => { - const nodes = await fetchAllNodes(gatsbyApi, options) + // Warning: Do not source remote and local nodes in a different order since this + // is important for the local nodes not to overrider remote ones + const nodes = await Promise.all([ + fetchAllRemoteNodes(gatsbyApi, options), + fetchAllLocalNodes(gatsbyApi), + ]).then(([x, y]) => [...x, ...y]) - createSchemaCustomization(gatsbyApi, nodes) + createCmsSchemaCustomization(gatsbyApi, nodes) for (const node of nodes) { - sourceNode(gatsbyApi, node) + sourceCmsNode(gatsbyApi, node) + } + + /** + * Add CMS overrides to StoreCollection Nodes + */ + const collections = getCollectionsFromPageContent(gatsbyApi, nodes) + const splitted = splitCollections(collections) + + overridesBarrier.set({ + Category: splitted.categories, + Department: splitted.categories, + Brand: splitted.brands, + Cluster: splitted.clusters, + }) + + /** + * Source StoreCollection from clusters. This part isn't done in + * gatsby-source-vtex because collections do not have a defined + * path on the store + */ + for (const cluster of Object.values(splitted.clusters)) { + const node: StoreCollection = { + id: `${cluster.clusterId}:${cluster.seo.slug}`, + remoteId: cluster.clusterId, + slug: cluster.seo.slug, + seo: { + title: cluster.seo.title, + description: cluster.seo.description, + }, + type: 'Cluster', + } + + sourceStoreCollectionNode(gatsbyApi, node) + } +} + +const TypeKeyMap = { + Cluster: 'productClusterIds', + Brand: 'b', + Category: 'c', + Department: 'c', +} + +/** + * @description + * Create custom fields on StoreCollection when this collection is defined on the CMS + */ +export const onCreateNode = async (gatsbyApi: CreateNodeArgs) => { + const { node } = gatsbyApi + + if (node.internal.type !== 'StoreCollection') { + return } - await sourceAllLocalNodes(gatsbyApi, process.cwd(), PLUGIN) + const collection = (node as unknown) as StoreCollection + const overrides = await overridesBarrier.get() + + const override = overrides[collection.type][collection.remoteId] + + gatsbyApi.actions.createNodeField({ + node, + name: 'searchParams', + value: { + sort: override?.sort ?? '""', + itemsPerPage: 12, + selectedFacets: + collection.type === 'Cluster' + ? [{ key: TypeKeyMap.Cluster, value: collection.remoteId }] + : collection.slug.split('/').map((segment) => ({ + key: TypeKeyMap[collection.type], + value: segment, + })), + }, + }) + + gatsbyApi.actions.createNodeField({ + node, + name: `plp___NODE`, + value: override?.plp ?? null, + }) } export const createPages = async ({ graphql, reporter }: CreatePagesArgs) => { diff --git a/packages/gatsby-plugin-cms/src/index.ts b/packages/gatsby-plugin-cms/src/index.ts index 4690c5d70c..cb33fa3939 100644 --- a/packages/gatsby-plugin-cms/src/index.ts +++ b/packages/gatsby-plugin-cms/src/index.ts @@ -1,5 +1,13 @@ import type { JSONSchema6 } from 'json-schema' +export { PLP } from './native-types/contentTypes/plp' + +export { Seo } from './native-types/blocks/seo' +export type { ISeo } from './native-types/blocks/seo' + +export { Sort } from './native-types/blocks/sort' +export type { ISort } from './native-types/blocks/sort' + export interface Schema extends JSONSchema6 { title: string description?: string @@ -7,7 +15,7 @@ export interface Schema extends JSONSchema6 { export type Schemas = Record -interface ContentType { +export interface ContentType { name: string extraBlocks: Record beforeBlocks: Schemas diff --git a/packages/gatsby-plugin-cms/src/native-types/blocks/collection.ts b/packages/gatsby-plugin-cms/src/native-types/blocks/collection.ts new file mode 100644 index 0000000000..ba007d932b --- /dev/null +++ b/packages/gatsby-plugin-cms/src/native-types/blocks/collection.ts @@ -0,0 +1,86 @@ +import { Seo } from './seo' +import { Sort } from './sort' +import type { ISeo } from './seo' +import type { ISort } from './sort' +import type { Schema } from '../../index' + +export interface ICategoryCollection { + sort: keyof ISort + categoryId: string +} + +export interface IBrandCollection { + sort: keyof ISort + brandId: string +} + +export interface IClusterCollection { + seo: ISeo + sort: keyof ISort + clusterId: string +} + +export const isCategoryCollection = ( + x: ICollection +): x is ICategoryCollection => typeof (x as any).categoryId === 'string' + +export const isBrandCollection = (x: ICollection): x is IBrandCollection => + typeof (x as any).brandId === 'string' + +export const isClusterCollection = (x: ICollection): x is IClusterCollection => + typeof (x as any).clusterId === 'string' + +/** + * Definition of a Collection in the CMS + */ +export type ICollection = + | ICategoryCollection + | IBrandCollection + | IClusterCollection + +export const Collection = { + title: 'Collection', + description: 'Definition of a Collection for the CMS', + oneOf: [ + { + title: 'Category', + description: 'Configure a Category', + type: 'object', + required: ['categoryId', 'sort'], + properties: { + categoryId: { + title: 'Category ID', + type: 'string', + }, + sort: Sort, + }, + }, + { + title: 'Brand', + description: 'Configure a Brand', + type: 'object', + required: ['brandId', 'sort'], + properties: { + brandId: { + title: 'Brand ID', + type: 'string', + }, + sort: Sort, + }, + }, + { + title: 'Collection', + description: 'Configure a Collection', + type: 'object', + required: ['clusterId', 'sort', 'seo'], + properties: { + clusterId: { + title: 'Collection ID', + type: 'string', + }, + sort: Sort, + seo: Seo, + }, + }, + ], +} as Schema diff --git a/packages/gatsby-plugin-cms/src/native-types/blocks/seo.ts b/packages/gatsby-plugin-cms/src/native-types/blocks/seo.ts new file mode 100644 index 0000000000..dd42672bdd --- /dev/null +++ b/packages/gatsby-plugin-cms/src/native-types/blocks/seo.ts @@ -0,0 +1,38 @@ +import type { Schema } from '../..' + +export interface ISeo { + title: string + slug: string + description: string +} + +export const Seo = { + type: 'object', + title: 'Seo', + widget: { + 'ui:ObjectFieldTemplate': 'GoogleSeoPreview', + }, + required: ['title', 'description', 'slug'], + properties: { + title: { + type: 'string', + title: 'Title', + description: + 'Appears in the browser tab and is suggested for search engines', + default: 'Page title', + }, + slug: { + type: 'string', + title: 'URL slug', + description: "Final part of the page's address. No spaces allowed.", + default: '/path/to/page', + pattern: '^/([a-zA-Z0-9]|-|/|_)*', + }, + description: { + type: 'string', + title: 'Description (Meta description)', + description: 'Suggested for search engines', + default: 'Page description', + }, + }, +} as Schema diff --git a/packages/gatsby-plugin-cms/src/native-types/blocks/sort.ts b/packages/gatsby-plugin-cms/src/native-types/blocks/sort.ts new file mode 100644 index 0000000000..945b6b0c94 --- /dev/null +++ b/packages/gatsby-plugin-cms/src/native-types/blocks/sort.ts @@ -0,0 +1,38 @@ +import type { Schema } from '../../index' + +export interface ISort { + '""': 'Relevance' + 'discount:desc': 'Discount' + 'release:desc': 'Release date' + 'name:asc': 'Name, ascending' + 'name:desc': 'Name, descending' + 'orders:desc': 'Sales' + 'price:asc': 'Price: Low to High' + 'price:desc': 'Price: High to Low' +} + +export const Sort = { + title: 'Default ordering', + type: 'string', + default: '""', + enum: [ + '""', + 'discount:desc', + 'release:desc', + 'name:asc', + 'name:desc', + 'orders:desc', + 'price:asc', + 'price:desc', + ], + enumNames: [ + 'Relevance', + 'Discount', + 'Release date', + 'Name, ascending', + 'Name, descending', + 'Sales', + 'Price: Low to High', + 'Price: High to Low', + ], +} as Schema diff --git a/packages/gatsby-plugin-cms/src/native-types/contentTypes/plp.ts b/packages/gatsby-plugin-cms/src/native-types/contentTypes/plp.ts new file mode 100644 index 0000000000..abebf9a3a2 --- /dev/null +++ b/packages/gatsby-plugin-cms/src/native-types/contentTypes/plp.ts @@ -0,0 +1,21 @@ +import { Collection } from '../blocks/collection' +import type { ContentType } from '../../index' + +export const PLP = ({ + extraBlocks = {}, + beforeBlocks = {}, + afterBlocks = {}, +}: Partial>) => ({ + plp: { + name: 'PLP', + extraBlocks: { + ...extraBlocks, + Parameters: { + ...extraBlocks.Parameters, + Collection, + }, + }, + beforeBlocks, + afterBlocks, + }, +}) diff --git a/packages/gatsby-plugin-cms/src/node-api/catalog/index.ts b/packages/gatsby-plugin-cms/src/node-api/catalog/index.ts new file mode 100644 index 0000000000..d5670e3db1 --- /dev/null +++ b/packages/gatsby-plugin-cms/src/node-api/catalog/index.ts @@ -0,0 +1,67 @@ +import type { ParentSpanPluginArgs } from 'gatsby' + +import { nodeId } from '../cms/sourceNode' +import type { RemotePageContent } from '../cms/types' +import type { + ICollection, + ICategoryCollection, + IBrandCollection, + IClusterCollection, +} from '../../native-types/blocks/collection' +import { + isCategoryCollection, + isBrandCollection, + isClusterCollection, +} from '../../native-types/blocks/collection' + +export type WithPLP = T & { plp: string } + +export const getCollectionsFromPageContent = ( + gatsbyApi: ParentSpanPluginArgs, + nodes: RemotePageContent[] +) => { + const collectionBlocks: Array> = [] + + for (const node of nodes) { + // We only allow plp content types + if (node.type !== 'plp') { + continue + } + + for (const extraBlock of node.extraBlocks) { + const block = extraBlock.blocks.find((x) => x.name === 'Collection') + + if (block) { + const props = (block.props as unknown) as ICollection + + collectionBlocks.push({ + ...props, + plp: gatsbyApi.createNodeId(nodeId(node)), + }) + } + } + } + + return collectionBlocks +} + +export const splitCollections = (collections: Array>) => ({ + categories: collections + .filter((x): x is WithPLP => isCategoryCollection(x)) + .reduce( + (acc, curr) => ({ ...acc, [curr.categoryId]: curr }), + {} as Record> + ), + brands: collections + .filter((x): x is WithPLP => isBrandCollection(x)) + .reduce( + (acc, curr) => ({ ...acc, [curr.brandId]: curr }), + {} as Record> + ), + clusters: collections + .filter((x): x is WithPLP => isClusterCollection(x)) + .reduce( + (acc, curr) => ({ ...acc, [curr.clusterId]: curr }), + {} as Record> + ), +}) diff --git a/packages/gatsby-plugin-cms/src/node-api/cms/fetchNodes.ts b/packages/gatsby-plugin-cms/src/node-api/cms/fetchNodes.ts new file mode 100644 index 0000000000..3e78c721ca --- /dev/null +++ b/packages/gatsby-plugin-cms/src/node-api/cms/fetchNodes.ts @@ -0,0 +1,119 @@ +import type { ParentSpanPluginArgs } from 'gatsby' + +import fetch from '../../utils/fetch' +import { PLUGIN } from '../../constants' +import type { Options } from '../../gatsby-node' +import type { RemotePageContent } from './types' + +const LIST_PAGES_QUERY = ` +query LIST_PAGES ($first: Int!, $after: String ) { + vtex { + pages (first: $first, after: $after, builderId: "faststore") { + pageInfo { + hasNextPage + } + edges { + cursor + node { + ...PageContentFragment + } + } + } + } +} +fragment PageContentFragment on PageContent { + __typename + id + remoteId: id + name + type + builderId + lastUpdatedAt + author { + id + email + } + blocks { + name + props + } + beforeBlocks { + name + props + } + afterBlocks { + name + props + } + extraBlocks { + name + blocks { + name + props + } + } +} +` + +export const fetchAllNodes = async ( + gatsbyApi: ParentSpanPluginArgs, + options: Options +): Promise => { + const { tenant, workspace } = options + + const activity = gatsbyApi.reporter.activityTimer( + `[${PLUGIN}]: fetching PageContents from remote` + ) + + activity.start() + + const executor = ({ + query, + variables, + operationName, + }: { + query: string + variables: any + operationName: string + }) => + fetch(`https://${workspace}--${tenant}.myvtex.com/graphql`, { + body: JSON.stringify({ + query, + variables, + operationName, + }), + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + }).then((x) => x.json()) as Promise<{ data: T | null; errors: any[] }> + + let hasNextPage = true + let data: RemotePageContent[] = [] + let after: string | undefined + + while (hasNextPage === true) { + hasNextPage = false + + const response = await executor({ + query: LIST_PAGES_QUERY, + variables: { first: 90, after }, + operationName: 'LIST_PAGES', + }) + + const pages = response.data?.vtex.pages + + if (pages) { + const { edges, pageInfo } = pages + const nodes = edges.map((x: any) => x.node) + + after = edges[edges.length - 1].cursor + hasNextPage = pageInfo.hasNextPage + data = [...data, ...nodes] + } + } + + activity.end() + + return data +} diff --git a/packages/gatsby-plugin-cms/src/node-api/cms/sourceLocalNodes.ts b/packages/gatsby-plugin-cms/src/node-api/cms/sourceLocalNodes.ts new file mode 100644 index 0000000000..f865bb9547 --- /dev/null +++ b/packages/gatsby-plugin-cms/src/node-api/cms/sourceLocalNodes.ts @@ -0,0 +1,38 @@ +import { join } from 'path' + +import globby from 'globby' +import { readJSON } from 'fs-extra' +import type { ParentSpanPluginArgs } from 'gatsby' + +import { PLUGIN } from '../../constants' +import type { RemotePageContent } from './types' + +const localNodeKey = (path: string) => `${PLUGIN}:fixture:${path}` + +/** + * @description Fetch Nodes from fixtures folder + */ +export const fetchAllNodes = async (gatsbyApi: ParentSpanPluginArgs) => { + const activity = gatsbyApi.reporter.activityTimer( + `[${PLUGIN}]: fetching PageContents from fixtures` + ) + + activity.start() + + const root = join(process.cwd(), 'src', PLUGIN, 'fixtures') + + const files = await globby('*.json', { cwd: root, deep: 1, onlyFiles: true }) + + const nodes = await Promise.all( + files.map(async (file) => { + const json = await readJSON(join(root, file)) + const id = localNodeKey(file) + + return { ...json, remoteId: id, id } as RemotePageContent + }) + ) + + activity.end() + + return nodes +} diff --git a/packages/gatsby-plugin-cms/src/node-api/sourceNode.ts b/packages/gatsby-plugin-cms/src/node-api/cms/sourceNode.ts similarity index 90% rename from packages/gatsby-plugin-cms/src/node-api/sourceNode.ts rename to packages/gatsby-plugin-cms/src/node-api/cms/sourceNode.ts index c2fa350008..50e4f244a8 100644 --- a/packages/gatsby-plugin-cms/src/node-api/sourceNode.ts +++ b/packages/gatsby-plugin-cms/src/node-api/cms/sourceNode.ts @@ -1,10 +1,11 @@ import camelcase from 'camelcase' import type { ParentSpanPluginArgs } from 'gatsby' -import { PLUGIN } from '../constants' +import { PLUGIN } from '../../constants' import type { RemotePageContent, Block, PageContent } from './types' -export const getTypeName = (name: string) => camelcase(['cms', name]) +export const getTypeName = (name: string) => + camelcase(['cms', name], { pascalCase: true }) const baseSchema = ` scalar JSONPropsCmsObject @@ -34,7 +35,7 @@ export const createSchemaCustomization = ( gatsbyApi.actions.createTypes(typeDefs) } -const nodeId = (node: RemotePageContent): string => +export const nodeId = (node: RemotePageContent): string => `${getTypeName(node.type)}:${node.remoteId}` export const sourceNode = ( diff --git a/packages/gatsby-plugin-cms/src/node-api/types.ts b/packages/gatsby-plugin-cms/src/node-api/cms/types.ts similarity index 97% rename from packages/gatsby-plugin-cms/src/node-api/types.ts rename to packages/gatsby-plugin-cms/src/node-api/cms/types.ts index dfcf42125b..00de6b7b83 100644 --- a/packages/gatsby-plugin-cms/src/node-api/types.ts +++ b/packages/gatsby-plugin-cms/src/node-api/cms/types.ts @@ -5,6 +5,7 @@ export interface Block { export interface RemotePageContent { remoteId: string + id: string name: string type: string builderId: 'faststore' diff --git a/packages/gatsby-plugin-cms/src/node-api/fetchNodes.ts b/packages/gatsby-plugin-cms/src/node-api/fetchNodes.ts deleted file mode 100644 index 4f97bac5d8..0000000000 --- a/packages/gatsby-plugin-cms/src/node-api/fetchNodes.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { ParentSpanPluginArgs } from 'gatsby' -import { - compileNodeQueries, - createDefaultQueryExecutor, - generateDefaultFragments, - loadSchema, -} from 'gatsby-graphql-source-toolkit' -import { print } from 'graphql' - -import { PLUGIN } from '../constants' -import type { Options } from '../gatsby-node' -import type { RemotePageContent } from './types' - -export const fetchAllNodes = async ( - gatsbyApi: ParentSpanPluginArgs, - options: Options -): Promise => { - const activity1 = gatsbyApi.reporter.activityTimer( - `[${PLUGIN}]: Fetching CMS schema and creating queries` - ) - - activity1.start() - - const { tenant, workspace } = options - - // Step1. Set up remote schema: - const executor = createDefaultQueryExecutor( - `https://${workspace}--${tenant}.myvtex.com/graphql`, - { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - } - ) - - const schema = await loadSchema(executor) - - // Step2. Configure Gatsby node types - const gatsbyNodeTypes = [ - { - remoteTypeName: `PageContent`, - queries: ` - query LIST_PAGES ($first: Int!, $after: String ) { - vtex { - pages (first: $first, after: $after, builderId: "faststore") { - pageInfo { - hasNextPage - } - edges { - cursor - node { - ...PageContentFragment - } - } - } - } - } - fragment PageContentFragment on PageContent { __typename id } - `, - }, - ] - - // Step3. Generate fragments with fields to be fetched - const fragments = generateDefaultFragments({ schema, gatsbyNodeTypes }) - - // Step4. Compile sourcing queries - const documents = compileNodeQueries({ - schema, - gatsbyNodeTypes, - customFragments: fragments, - }) - - const pageContentQuery = documents.get(gatsbyNodeTypes[0].remoteTypeName)! - - activity1.end() - - // Step5. Fetch all remote nodes - let hasNextPage = true - let data: RemotePageContent[] = [] - let after: string | undefined - - const activity2 = gatsbyApi.reporter.activityTimer( - `[${PLUGIN}]: Fetching PageContents` - ) - - activity2.start() - - while (hasNextPage === true) { - hasNextPage = false - - const response = await executor({ - query: print(pageContentQuery), - document: pageContentQuery, - variables: { first: 90, after }, - operationName: 'LIST_PAGES', - }) - - const pages = response.data?.vtex.pages - - if (pages) { - const { edges, pageInfo } = pages - const nodes = edges.map((x: any) => x.node) - - after = edges[edges.length - 1].cursor - hasNextPage = pageInfo.hasNextPage - data = [...data, ...nodes] - } - } - - activity2.end() - - return data -} diff --git a/packages/gatsby-plugin-cms/src/node-api/sourceLocalNodes.ts b/packages/gatsby-plugin-cms/src/node-api/sourceLocalNodes.ts deleted file mode 100644 index a43672f656..0000000000 --- a/packages/gatsby-plugin-cms/src/node-api/sourceLocalNodes.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { join } from 'path' - -import chokidar from 'chokidar' -import { readJSON } from 'fs-extra' -import type { ParentSpanPluginArgs } from 'gatsby' - -import { PLUGIN } from '../constants' -import { createSchemaCustomization, deleteNode, sourceNode } from './sourceNode' -import type { RemotePageContent } from './types' - -const localNodeKey = (path: string) => `${PLUGIN}:fixture:${path}` - -const sourceLocalNode = async ( - gatsbyApi: ParentSpanPluginArgs, - path: string -) => { - if (!path.endsWith('.json')) { - return - } - - const node: RemotePageContent = await readJSON(path) - - // TODO: We should add a verification if the json complies with the exported JSON schema - if (!node || Object.keys(node).length === 0) { - return - } - - node.remoteId = node.remoteId ?? path - - await gatsbyApi.cache.set(localNodeKey(path), node) - - // Must create schema customization before sourcing nodes - createSchemaCustomization(gatsbyApi, [node]) - - sourceNode(gatsbyApi, node) -} - -const deleteLocalNode = async ( - gatsbyApi: ParentSpanPluginArgs, - path: string -) => { - if (!path.endsWith('.json')) { - return - } - - const localNode = await gatsbyApi.cache.get(localNodeKey(path)) - - deleteNode(gatsbyApi, localNode) -} - -/** Source Nodes from fixtures folder */ -export const sourceAllLocalNodes = async ( - gatsbyApi: ParentSpanPluginArgs, - root: string, - name: string -) => { - const watcher = chokidar.watch(join(root, 'src', name, 'fixtures'), { - ignoreInitial: false, - depth: 1, - }) - - watcher.on('add', (path) => sourceLocalNode(gatsbyApi, path)) - watcher.on('change', (path) => sourceLocalNode(gatsbyApi, path)) - watcher.on('unlink', (path) => deleteLocalNode(gatsbyApi, path)) - - // Wait for chokidar ready event. - // - // If we don't wait for this event, gatsby will think this plugin didn't source any - // node and will ask to the developer to remove it. Since we are `ignoreInitial: false` - // we are sure chokidar will fire the ready event and not stall the build process - const onReady = new Promise((resolve) => { - watcher.on('ready', resolve) - }) - - await onReady - - // Do not keep the watcher in `gatsby build` - if (process.env.NODE_ENV === 'production') { - watcher.close() - } -} diff --git a/packages/gatsby-plugin-cms/src/utils/barrier.ts b/packages/gatsby-plugin-cms/src/utils/barrier.ts new file mode 100644 index 0000000000..73dbda6d15 --- /dev/null +++ b/packages/gatsby-plugin-cms/src/utils/barrier.ts @@ -0,0 +1,24 @@ +/** + * @description + * A simple barrier that waits for the data until it's available. + * This is handy when you want to coordinate data processing between asynchronous functions + */ +export class Barrier { + private promise: Promise + private resolve: ((value: T) => void) | undefined + + constructor() { + this.resolve = undefined + this.promise = new Promise((resolve) => { + this.resolve = resolve + }) + } + + public set(data: T) { + this.resolve?.(data) + } + + public get() { + return this.promise + } +} diff --git a/packages/gatsby-plugin-cms/src/utils/fetch.ts b/packages/gatsby-plugin-cms/src/utils/fetch.ts new file mode 100644 index 0000000000..b5d963279f --- /dev/null +++ b/packages/gatsby-plugin-cms/src/utils/fetch.ts @@ -0,0 +1,10 @@ +import unfetch from 'isomorphic-unfetch' +import retry from 'fetch-retry' + +const fetch = (input: RequestInfo, init?: RequestInit) => + retry(unfetch, { + retries: 3, + retryDelay: 500, + })(input, init) + +export default fetch diff --git a/packages/gatsby-source-vtex/index.js b/packages/gatsby-source-vtex/index.js new file mode 100644 index 0000000000..2d2f3c0bcb --- /dev/null +++ b/packages/gatsby-source-vtex/index.js @@ -0,0 +1 @@ +module.exports = require('./dist') diff --git a/packages/gatsby-source-vtex/src/constants.ts b/packages/gatsby-source-vtex/src/constants.ts new file mode 100644 index 0000000000..bf6304c6e4 --- /dev/null +++ b/packages/gatsby-source-vtex/src/constants.ts @@ -0,0 +1 @@ +export const PLUGIN = '@vtex/gatsby-source-vtex' diff --git a/packages/gatsby-source-vtex/src/gatsby-node.ts b/packages/gatsby-source-vtex/src/gatsby-node.ts index 2d0fdacb17..fc64bb72b7 100644 --- a/packages/gatsby-source-vtex/src/gatsby-node.ts +++ b/packages/gatsby-source-vtex/src/gatsby-node.ts @@ -32,6 +32,12 @@ import type { NodeEvent, } from 'gatsby-graphql-source-toolkit/dist/types' +// import type { NodeEvent as CollectionNodeEvent } from './graphql/types/collection' +import { + // sourceNodeChanges as sourceCollectionNodeChanges, + sourceAllNodes as sourceAllCollectionNodes, + typeDefs as StoreCollectionTypeDefs, +} from './graphql/types/collection' import { api } from './api' import { fetchVTEX } from './fetch' import { ProductPaginationAdapter } from './graphql/pagination/product' @@ -63,6 +69,7 @@ export interface Options extends PluginOptions, VTEXOptions { * @description minimum number of products to fetch from catalog * */ minProducts?: number + sourceCollections?: boolean } const DEFAULT_PAGE_TYPES_WHITELIST = [ @@ -86,42 +93,48 @@ const DEFAULT_PAGE_TYPES_WHITELIST = [ * data model for incremental site generation */ export const createSchemaCustomization = async ( - args: CreateSchemaCustomizationArgs, + gatsbyApi: CreateSchemaCustomizationArgs, options: Options ) => { - const { - reporter, - actions: { addThirdPartySchema }, - } = args - - const activity = reporter.activityTimer( - '[gatsby-source-vtex]: adding VTEX Gateway GraphQL Schema' - ) - - activity.start() - - // Create executor to run queries against schema - const executor = getExecutor(options) - - const schema = wrapSchema({ - schema: await introspectSchema(executor), - executor, - transforms: [ - // Filter CMS fields so people use the VTEX CMS plugin instead of this one - new FilterObjectFields( - (typeName, fieldName) => - !( - typeName === 'VTEX' && - (fieldName === 'pages' || fieldName === 'product') - ) - ), - new PruneSchema({}), - ], - }) + const addThirdPartySchema = async () => { + const activity = gatsbyApi.reporter.activityTimer( + '[gatsby-source-vtex]: adding VTEX Gateway GraphQL Schema' + ) + + activity.start() + + // Create executor to run queries against schema + const executor = getExecutor(options) - addThirdPartySchema({ schema }, { name: 'gatsby-source-vtex' }) + const schema = wrapSchema({ + schema: await introspectSchema(executor), + executor, + transforms: [ + // Filter CMS fields so people use the VTEX CMS plugin instead of this one + new FilterObjectFields( + (typeName, fieldName) => + !( + typeName === 'VTEX' && + (fieldName === 'pages' || fieldName === 'product') + ) + ), + new PruneSchema({}), + ], + }) + + gatsbyApi.actions.addThirdPartySchema( + { schema }, + { name: 'gatsby-source-vtex' } + ) - activity.end() + activity.end() + } + + const addStoreCollection = async () => { + gatsbyApi.actions.createTypes(StoreCollectionTypeDefs) + } + + await Promise.all([addThirdPartySchema(), addStoreCollection()]) } /** @@ -150,7 +163,7 @@ export const createResolvers = ({ } export const sourceNodes: GatsbyNode['sourceNodes'] = async ( - args: SourceNodesArgs, + gatsbyApi: SourceNodesArgs, options: Options ) => { const { @@ -161,12 +174,22 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( ignorePaths = [], } = options - const { reporter } = args + const { reporter } = gatsbyApi /** Reset last build time on this machine */ - const lastBuildTime = await args.cache.get(`LAST_BUILD_TIME`) + const lastBuildTime = await gatsbyApi.cache.get(`LAST_BUILD_TIME`) - await args.cache.set(`LAST_BUILD_TIME`, Date.now()) + await gatsbyApi.cache.set(`LAST_BUILD_TIME`, Date.now()) + + if (lastBuildTime) { + reporter.info( + '[gatsby-source-vtex]: CACHE FOUND! We are about to go FAST! Skipping FETCH' + ) + } else { + reporter.info( + '[gatsby-source-vtex]: No cache found. Sourcing all data from scratch' + ) + } const promisses = [] as Array<() => Promise> @@ -186,7 +209,7 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( ) for (const binding of bindings) { - createChannelNode(args, binding) + createChannelNode(gatsbyApi, binding) } activity.end() @@ -208,7 +231,7 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( ) for (const department of departments) { - createDepartmentNode(args, department) + createDepartmentNode(gatsbyApi, department) } activity.end() @@ -265,7 +288,7 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( continue } - createStaticPathNode(args, pageType, staticPath) + createStaticPathNode(gatsbyApi, pageType, staticPath) } activity.end() @@ -353,7 +376,7 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( ) const config: ISourcingConfig = { - gatsbyApi: args, + gatsbyApi, schema, execute: run, gatsbyTypePrefix: `Store`, @@ -366,20 +389,23 @@ export const sourceNodes: GatsbyNode['sourceNodes'] = async ( // Step6. Source nodes either from delta changes or scratch if (lastBuildTime) { - reporter.info( - '[gatsby-source-vtex]: CACHE FOUND! We are about to go FAST! Skipping FETCH' - ) const nodeEvents: NodeEvent[] = [] await sourceNodeChanges(config, { nodeEvents }) } else { - reporter.info( - '[gatsby-source-vtex]: No cache found. Sourcing all data from scratch' - ) await sourceAllNodes(config) } }) + if (options.sourceCollections !== false) { + promisses.push(async () => { + const config = { gatsbyApi, options } + + // TODO: Implement incremental build + await sourceAllCollectionNodes(config) + }) + } + await Promise.all(promisses.map((x) => x())) } @@ -392,21 +418,13 @@ export const createPages = async ( */ const { data: { - searches: { nodes: searches }, + allStoreCollection: { totalCount: plps }, allStoreProduct: { totalCount: pdps }, }, } = await graphql(` query GetAllStaticPaths { - searches: allStaticPath( - filter: { - pageType: { in: ["Department", "Category", "Brand", "SubCategory"] } - } - ) { - nodes { - id - path - pageType - } + allStoreCollection { + totalCount } allStoreProduct { totalCount @@ -415,7 +433,7 @@ export const createPages = async ( `) reporter.info(`[gatsby-source-vtex]: Available pdps: ${pdps}`) - reporter.info(`[gatsby-source-vtex]: Available plps: ${searches.length}`) + reporter.info(`[gatsby-source-vtex]: Available plps: ${plps}`) /** * Create all proxy rules for VTEX Store @@ -570,4 +588,5 @@ export const pluginOptionsSchema = ({ Joi }: PluginOptionsSchemaArgs) => getRedirects: Joi.function().arity(0), pageTypes: Joi.array().items(Joi.string()), minProducts: Joi.number(), + sourceCollections: Joi.boolean(), }) diff --git a/packages/gatsby-source-vtex/src/graphql/types/collection/index.ts b/packages/gatsby-source-vtex/src/graphql/types/collection/index.ts new file mode 100644 index 0000000000..9d50414540 --- /dev/null +++ b/packages/gatsby-source-vtex/src/graphql/types/collection/index.ts @@ -0,0 +1,158 @@ +import { readFileSync } from 'fs' +import { join } from 'path' + +import slugify from 'slugify' +import type { ParentSpanPluginArgs } from 'gatsby' + +import { PLUGIN } from '../../../constants' +import { fetchVTEX } from '../../../fetch' +import { api } from '../../../api' +import type { Brand, Category } from '../../../types' +import type { VTEXOptions as Options } from '../../../fetch' + +type CollectionType = 'Department' | 'Category' | 'Brand' | 'Cluster' + +export interface StoreCollection { + id: string + remoteId: string + seo: { + title: string + description: string + } + slug: string + parent?: string + children?: string[] + type: CollectionType +} + +interface Config { + gatsbyApi: ParentSpanPluginArgs + options: Options +} + +export const typeDefs = readFileSync( + join(__dirname, '../src/graphql/types/collection/typeDefs.graphql') +).toString() + +const typeName = `StoreCollection` + +const createNodeId = ( + id: string, + type: CollectionType, + gatsbyApi: ParentSpanPluginArgs +) => gatsbyApi.createNodeId(`${typeName}:${type}:${id}`) + +export const createNode = ( + gatsbyApi: ParentSpanPluginArgs, + node: StoreCollection +) => { + const id = createNodeId(node.id, node.type, gatsbyApi) + const parentType = node.type === 'Category' ? 'Department' : node.type + const childType = node.type === 'Department' ? 'Category' : node.type + + const data = { + ...node, + id, + parent: node.parent && createNodeId(node.parent, parentType, gatsbyApi), + children: node.children?.map((child) => + createNodeId(child, childType, gatsbyApi) + ), + } + + gatsbyApi.actions.createNode( + { + ...data, + internal: { + type: typeName, + content: JSON.stringify(data), + contentDigest: gatsbyApi.createContentDigest(data), + }, + }, + { name: PLUGIN } + ) + + return id +} + +const brandToStoreCollection = (node: Brand): StoreCollection => ({ + id: `${node.id}`, + remoteId: `${node.id}`, + seo: { + title: node.title ?? '', + description: node.metaTagDescription ?? '', + }, + type: 'Brand', + slug: slugify(node.name, { replacement: '-', lower: true }), +}) + +const categoryToStoreCollection = ( + node: Category, + parent: string | undefined +): StoreCollection => ({ + id: `${node.id}`, + remoteId: `${node.id}`, + parent, + children: node.children.map((child) => `${child.id}`), + seo: { + title: node.Title ?? '', + description: node.MetaTagDescription ?? '', + }, + type: parent === undefined ? 'Department' : 'Category', + slug: new URL(node.url).pathname.slice(1), +}) + +export const fetchAllNodes = async ({ + gatsbyApi, + options, +}: Config): Promise => { + const activity = gatsbyApi.reporter.activityTimer( + `[gatsby-source-vtex]: fetching Categories/Brands` + ) + + activity.start() + + const [tree, brands] = await Promise.all([ + fetchVTEX(api.catalog.category.tree(4), options), + fetchVTEX( + api.catalog.brand.list({ page: 0, pageSize: 1000 }), + options + ), + ]) + + const collectionCategories: StoreCollection[] = [] + const dfs = ( + node: Category, + collections: StoreCollection[], + parent: string | undefined + ) => { + const collection = categoryToStoreCollection(node, parent) + + collections.push(collection) + + for (const child of node.children) { + dfs(child, collections, `${node.id}`) + } + } + + for (const node of tree) { + dfs(node, collectionCategories, undefined) + } + + const collectionBrands = brands + .filter((x) => x.isActive) + .map(brandToStoreCollection) + + activity.end() + + return [...collectionCategories, ...collectionBrands] +} + +export const sourceAllNodes = async (config: Config) => { + const { gatsbyApi } = config + + const nodes = await fetchAllNodes(config) + + for (const node of nodes) { + createNode(gatsbyApi, node) + } +} diff --git a/packages/gatsby-source-vtex/src/graphql/types/collection/typeDefs.graphql b/packages/gatsby-source-vtex/src/graphql/types/collection/typeDefs.graphql new file mode 100644 index 0000000000..e5e100482a --- /dev/null +++ b/packages/gatsby-source-vtex/src/graphql/types/collection/typeDefs.graphql @@ -0,0 +1,19 @@ +type StoreCollectionSeo { + title: String! + description: String! +} + +enum StoreCollectionType { + Department + Category + Brand + Cluster +} + +type StoreCollection implements Node @childOf(types: ["StoreCollection"]) { + id: ID! + remoteId: ID! + slug: String! + seo: StoreCollectionSeo! + type: StoreCollectionType! +} diff --git a/packages/gatsby-source-vtex/src/index.ts b/packages/gatsby-source-vtex/src/index.ts index 513770aba9..d151a25cff 100644 --- a/packages/gatsby-source-vtex/src/index.ts +++ b/packages/gatsby-source-vtex/src/index.ts @@ -1,2 +1,8 @@ export * as GatsbyNode from './gatsby-node' export { config as GatsbyConfig } from './gatsby-config' + +export { + createNode as sourceStoreCollectionNode, + fetchAllNodes as fetchAllStoreCollectionNodes, +} from './graphql/types/collection' +export type { StoreCollection } from './graphql/types/collection' diff --git a/packages/gatsby-source-vtex/src/types.ts b/packages/gatsby-source-vtex/src/types.ts index f6f6bfe192..8bbfb08c6b 100644 --- a/packages/gatsby-source-vtex/src/types.ts +++ b/packages/gatsby-source-vtex/src/types.ts @@ -79,16 +79,14 @@ export interface PageType { } export type Sort = - | 'OrderByPriceDESC' - | 'OrderByPriceASC' - | 'OrderByTopSaleDESC' - | 'OrderByReviewRateDESC' - | 'OrderByNameASC' - | 'OrderByNameDESC' - | 'OrderByReleaseDateDESC' - | 'OrderByBestDiscountDESC' - | 'OrderByScoreDESC' - | '' + | '' // 'Relevance', + | 'price:desc' // 'Price: High to Low', + | 'price:asc' // 'Price: Low to High', + | 'orders:desc' // 'Sales', + | 'name:desc' // 'Name, descending', + | 'name:asc' // 'Name, ascending', + | 'release:desc' // 'Release date', + | 'discount:desc' // 'Discount', export interface Redirect { [key: string]: unknown diff --git a/yarn.lock b/yarn.lock index 8881cb049c..14fff070f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5640,27 +5640,27 @@ integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw== "@typescript-eslint/eslint-plugin@^2.10.0", "@typescript-eslint/eslint-plugin@^2.12.0", "@typescript-eslint/eslint-plugin@^4", "@typescript-eslint/eslint-plugin@^4.15.2", "@typescript-eslint/eslint-plugin@^4.18.0": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz#e73c8cabbf3f08dee0e1bda65ed4e622ae8f8921" - integrity sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw== + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz#8197f1473e7da8218c6a37ff308d695707835684" + integrity sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q== dependencies: - "@typescript-eslint/experimental-utils" "4.28.4" - "@typescript-eslint/scope-manager" "4.28.4" + "@typescript-eslint/experimental-utils" "4.28.5" + "@typescript-eslint/scope-manager" "4.28.5" debug "^4.3.1" functional-red-black-tree "^1.0.1" regexpp "^3.1.0" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz#9c70c35ebed087a5c70fb0ecd90979547b7fec96" - integrity sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA== +"@typescript-eslint/experimental-utils@4.28.5": + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz#66c28bef115b417cf9d80812a713e0e46bb42a64" + integrity sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA== dependencies: "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.28.4" - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/typescript-estree" "4.28.4" + "@typescript-eslint/scope-manager" "4.28.5" + "@typescript-eslint/types" "4.28.5" + "@typescript-eslint/typescript-estree" "4.28.5" eslint-scope "^5.1.1" eslint-utils "^3.0.0" @@ -5689,13 +5689,13 @@ eslint-utils "^2.0.0" "@typescript-eslint/parser@^2.10.0", "@typescript-eslint/parser@^2.12.0", "@typescript-eslint/parser@^4", "@typescript-eslint/parser@^4.15.2", "@typescript-eslint/parser@^4.18.0": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.4.tgz#bc462dc2779afeefdcf49082516afdc3e7b96fab" - integrity sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA== + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.5.tgz#9c971668f86d1b5c552266c47788a87488a47d1c" + integrity sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw== dependencies: - "@typescript-eslint/scope-manager" "4.28.4" - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/typescript-estree" "4.28.4" + "@typescript-eslint/scope-manager" "4.28.5" + "@typescript-eslint/types" "4.28.5" + "@typescript-eslint/typescript-estree" "4.28.5" debug "^4.3.1" "@typescript-eslint/scope-manager@4.19.0": @@ -5714,13 +5714,13 @@ "@typescript-eslint/types" "4.22.0" "@typescript-eslint/visitor-keys" "4.22.0" -"@typescript-eslint/scope-manager@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz#bdbce9b6a644e34f767bd68bc17bb14353b9fe7f" - integrity sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w== +"@typescript-eslint/scope-manager@4.28.5": + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz#3a1b70c50c1535ac33322786ea99ebe403d3b923" + integrity sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ== dependencies: - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/visitor-keys" "4.28.4" + "@typescript-eslint/types" "4.28.5" + "@typescript-eslint/visitor-keys" "4.28.5" "@typescript-eslint/types@4.19.0": version "4.19.0" @@ -5732,10 +5732,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.22.0.tgz#0ca6fde5b68daf6dba133f30959cc0688c8dd0b6" integrity sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA== -"@typescript-eslint/types@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.4.tgz#41acbd79b5816b7c0dd7530a43d97d020d3aeb42" - integrity sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww== +"@typescript-eslint/types@4.28.5": + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.5.tgz#d33edf8e429f0c0930a7c3d44e9b010354c422e9" + integrity sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA== "@typescript-eslint/typescript-estree@4.19.0": version "4.19.0" @@ -5763,13 +5763,13 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz#252e6863278dc0727244be9e371eb35241c46d00" - integrity sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ== +"@typescript-eslint/typescript-estree@4.28.5": + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz#4906d343de693cf3d8dcc301383ed638e0441cd1" + integrity sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw== dependencies: - "@typescript-eslint/types" "4.28.4" - "@typescript-eslint/visitor-keys" "4.28.4" + "@typescript-eslint/types" "4.28.5" + "@typescript-eslint/visitor-keys" "4.28.5" debug "^4.3.1" globby "^11.0.3" is-glob "^4.0.1" @@ -5792,12 +5792,12 @@ "@typescript-eslint/types" "4.22.0" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@4.28.4": - version "4.28.4" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz#92dacfefccd6751cbb0a964f06683bfd72d0c4d3" - integrity sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg== +"@typescript-eslint/visitor-keys@4.28.5": + version "4.28.5" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz#ffee2c602762ed6893405ee7c1144d9cc0a29675" + integrity sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg== dependencies: - "@typescript-eslint/types" "4.28.4" + "@typescript-eslint/types" "4.28.5" eslint-visitor-keys "^2.0.0" "@vtex-components/accordion@^0.2.4": @@ -11729,6 +11729,11 @@ fd@~0.0.2: resolved "https://registry.yarnpkg.com/fd/-/fd-0.0.3.tgz#b3240de86dbf5a345baae7382a07d4713566ff0c" integrity sha512-iAHrIslQb3U68OcMSP0kkNWabp7sSN6d2TBSb2JO3gcLJVDd4owr/hKM4SFJovFOUeeXeItjYgouEDTMWiVAnA== +fetch-retry@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3" + integrity sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA== + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -13567,6 +13572,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^11.0.1, globby@^11.0.2, globby@^11.0.3: version "11.0.3" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" @@ -15354,7 +15371,7 @@ isomorphic-fetch@^2.1.1: node-fetch "^1.0.1" whatwg-fetch ">=0.10.0" -isomorphic-unfetch@^3.0.0: +isomorphic-unfetch@^3.0.0, isomorphic-unfetch@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f" integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==