diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d48c1329..99522e57d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,56 @@ All notable changes to this project will be documented in this file. The format This project does NOT adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and major versions of this project denote compatibility with Sitecore Platform versions. Refer to the "Headless Services" section in the [Sitecore modules compatibility table](https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1000576) or the [Headless Rendering download page](https://dev.sitecore.net/Downloads/Sitecore_Headless_Rendering.aspx) for more details on versioning. +## 20.3.2 + +### ๐Ÿ› Bug Fixes + +* `[sitecore-jss]` Handle null items in graphql layout service ([#1771](https://github.com/Sitecore/jss/pull/1771)) + +## 20.3.1 + +### ๐Ÿ› Bug Fixes + +* `[sitecore-jss]` Export _ClientError_ type ([#1738](https://github.com/Sitecore/jss/pull/1738)) +* `[templates/nextjs]` `[sitecore-jss-nextjs]` Better error handling for component-level data fetching ([#1586](https://github.com/Sitecore/jss/pull/1586)) +* `[sitecore-jss]` Enable the Layout, dictionary and Error Page service to use custom `retryStrategy`. ([#1749](https://github.com/Sitecore/jss/pull/1749)) + + +## 20.3.0 + +### ๐ŸŽ‰ New Features & Improvements + +* `[sitecore-jss]` Retry policy to handle transient network errors. Users can pass `retryStrategy` to configure custom retry config to the services. They can customize the error codes and the number of retries. It consist of two functions shouldRetry and getDelay. To determine the back-off time, we employ an exponential strategy with a default factor of 2.([#1731](https://github.com/Sitecore/jss/pull/1731)) ([#1733](https://github.com/Sitecore/jss/pull/1733)) + +## 20.2.3 + +### ๐Ÿ› Bug Fixes + +* `[sitecore-jss-react]` `[sitecore-jss-nextjs]` Link component does not add anchor to the internal links ([#1226](https://github.com/Sitecore/jss/pull/1226)) + +## 20.2.2 + +### ๐Ÿงน Chores + +* `[create-sitecore-jss]` This is a maintenance release to fix package versioning in JSS templates. + +## 20.2.1 + +### ๐Ÿงน Chores + +* `[create-sitecore-jss]` This is a maintenance release to fix package versioning in JSS templates. + +## 20.2.0 + +### ๐ŸŽ‰ New Features & Improvements + +* `[sitecore-jss]` `[templates/nextjs]` GraphQL Layout and Dictionary services can handle endpoint rate limits through retryer functionality in GraphQLClient. To prevent SSG builds from failing and enable multiple retries, set retry amount in lib/dictionary-service-factory and lib/layout-service-factory ([commit](https://github.com/Sitecore/jss/pull/1631/commits/d39d74ad7bbeddcb66b7de4377070e178851abc5))([#1631](https://github.com/Sitecore/jss/pull/1631)) +* `[sitecore-jss-nextjs]` Reduce the amount of Edge API calls during fetch getStaticPaths ([commit](https://github.com/Sitecore/jss/pull/1631/commits/cd2771b256ac7c38818ee6bea48278958ac455ca))([#1631](https://github.com/Sitecore/jss/pull/1631)) + +### ๐Ÿ› Bug Fixes + +* `[sitecore-jss-proxy]` Setting "followRedirects" to "true" breaks HEAD requests ([#1630](https://github.com/Sitecore/jss/pull/1635)) + ## 20.1.0 ### ๐ŸŽ‰ New Features & Improvements diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b0be29309d..a52f636ff6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -83,7 +83,7 @@ steps: # - script: | echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} > .npmrc - $(npm bin)/lerna publish from-git --canary --pre-dist-tag ver20 --preid canary --no-verify-access --yes + $(npm bin)/lerna publish from-git --canary --pre-dist-tag ver20-canary --preid canary --no-verify-access --yes yarn install --no-immutable --mode=update-lockfile git add yarn.lock git commit -m "update yarn.lock [skip ci]" diff --git a/lerna.json b/lerna.json index d6416fe528..885d7df546 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ "packages/*", "samples/*" ], - "version": "20.1.0", + "version": "20.3.3", "npmClient": "yarn", "useWorkspaces": true } diff --git a/packages/create-sitecore-jss/package.json b/packages/create-sitecore-jss/package.json index fa2577721e..b28305184b 100644 --- a/packages/create-sitecore-jss/package.json +++ b/packages/create-sitecore-jss/package.json @@ -1,6 +1,6 @@ { "name": "create-sitecore-jss", - "version": "20.1.0", + "version": "20.3.3", "description": "Sitecore JSS initializer", "bin": "./dist/index.js", "scripts": { diff --git a/packages/create-sitecore-jss/src/initializers/nextjs/index.ts b/packages/create-sitecore-jss/src/initializers/nextjs/index.ts index 84efdb8749..c9f945008d 100644 --- a/packages/create-sitecore-jss/src/initializers/nextjs/index.ts +++ b/packages/create-sitecore-jss/src/initializers/nextjs/index.ts @@ -43,7 +43,8 @@ export default class NextjsInitializer implements Initializer { value: 'nextjs-styleguide', }, { - name: 'nextjs-sxa - Includes example components and setup for working using SXA', + name: + 'nextjs-sxa - Includes example components and setup for SXA Headless (only compatible with Sitecore XM Cloud)', value: 'nextjs-sxa', }, ], diff --git a/packages/create-sitecore-jss/src/templates/angular/package.json b/packages/create-sitecore-jss/src/templates/angular/package.json index 39a22916b3..7b6ac8867c 100644 --- a/packages/create-sitecore-jss/src/templates/angular/package.json +++ b/packages/create-sitecore-jss/src/templates/angular/package.json @@ -1,6 +1,6 @@ { "name": "<%- appName %>", - "version": "20.1.0", + "version": "20.3.3", "description": "Application utilizing Sitecore JavaScript Services and Angular (angular-cli).", "config": { "appName": "<%- appName %>", @@ -63,8 +63,8 @@ "@apollo/client": "^3.3.12", "@ngx-translate/core": "~13.0.0", "@ngx-translate/http-loader": "~6.0.0", - "@sitecore-jss/sitecore-jss": "^20.1.0", - "@sitecore-jss/sitecore-jss-angular": "^20.1.0", + "@sitecore-jss/sitecore-jss": "^20.3.3", + "@sitecore-jss/sitecore-jss-angular": "^20.3.3", "apollo-angular": "~2.4.0", "bootstrap": "^4.3.1", "core-js": "~3.9.1", @@ -86,9 +86,9 @@ "@angular/cli": "~11.2.5", "@angular/compiler-cli": "~11.2.6", "@angular/language-service": "~11.2.6", - "@sitecore-jss/sitecore-jss-angular-schematics": "^20.1.0", - "@sitecore-jss/sitecore-jss-cli": "^20.1.0", - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", + "@sitecore-jss/sitecore-jss-angular-schematics": "^20.3.3", + "@sitecore-jss/sitecore-jss-cli": "^20.3.3", + "@sitecore-jss/sitecore-jss-dev-tools": "^20.3.3", "@types/isomorphic-fetch": "0.0.35", "@types/jasmine": "~3.6.7", "@types/jasminewd2": "~2.0.8", diff --git a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/package.json b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/package.json index 57dab68990..13e9ddeef9 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/package.json +++ b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/package.json @@ -4,11 +4,11 @@ "nprogress": "~0.2.0" }, "devDependencies": { - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", + "@sitecore-jss/sitecore-jss-dev-tools": "^20.3.3", "@types/nprogress": "^0.2.0" }, "scripts": { "start": "cross-env-shell JSS_MODE=disconnected \"npm-run-all --serial bootstrap --parallel next:dev start:disconnected-proxy start:watch-components\"", "start:disconnected-proxy": "ts-node --project tsconfig.scripts.json ./scripts/disconnected-mode-proxy.ts" } -} \ No newline at end of file +} diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/page-props-factory/plugins/error-pages.ts b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/page-props-factory/plugins/error-pages.ts index ffe06eb737..af84245991 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/page-props-factory/plugins/error-pages.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/page-props-factory/plugins/error-pages.ts @@ -14,6 +14,10 @@ class ErrorPagesPlugin implements Plugin { apiKey: config.sitecoreApiKey, siteName: config.jssAppName, language: props.locale, + retries: + (process.env.GRAPH_QL_SERVICE_RETRIES && + parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) || + 0, }); if (props.notFound) { diff --git a/packages/create-sitecore-jss/src/templates/nextjs/.env b/packages/create-sitecore-jss/src/templates/nextjs/.env index ccc7ecd3da..81bd93b76b 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/.env +++ b/packages/create-sitecore-jss/src/templates/nextjs/.env @@ -36,6 +36,9 @@ SITECORE_API_HOST= # the resolved Sitecore API hostname + the `graphQLEndpointPath` defined in your `package.json`. GRAPH_QL_ENDPOINT= +# How many times should GraphQL Layout, Dictionary and ErrorPages services retry a fetch when endpoint rate limit is reached +GRAPH_QL_SERVICE_RETRIES=0 + # The way in which layout and dictionary data is fetched from Sitecore FETCH_WITH=<%- fetchWith %> diff --git a/packages/create-sitecore-jss/src/templates/nextjs/package.json b/packages/create-sitecore-jss/src/templates/nextjs/package.json index 52e58752ee..1f9d695034 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/package.json +++ b/packages/create-sitecore-jss/src/templates/nextjs/package.json @@ -1,7 +1,7 @@ { "name": "<%- appName %>", "description": "Application utilizing Sitecore JavaScript Services and Next.js", - "version": "20.1.0", + "version": "20.3.3", "private": true, "config": { "appName": "<%- appName %>", @@ -29,7 +29,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@sitecore-jss/sitecore-jss-nextjs": "ver20", + "@sitecore-jss/sitecore-jss-nextjs": "^20.3.3", "graphql": "~15.8.0", "graphql-tag": "^2.11.0", "next": "^12.2.4", @@ -45,7 +45,7 @@ "@graphql-codegen/typescript-operations": "^1.17.9", "@graphql-codegen/typescript-resolvers": "^1.17.10", "@graphql-typed-document-node/core": "^3.1.0", - "@sitecore-jss/sitecore-jss-cli": "^20.1.0", + "@sitecore-jss/sitecore-jss-cli": "^20.3.3", "@types/node": "^14.6.4", "@types/react": "^17.0.15", "@types/react-dom": "^17.0.9", diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts index 25616ccafb..8bd65f4ef0 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts @@ -18,6 +18,20 @@ export class DictionaryServiceFactory { otherwise, the service will attempt to figure out the root item for the current JSS App using GraphQL and app name. rootItemId: '{GUID}' */ + /* + GraphQL endpoint may reach its rate limit with the amount of requests it receives and throw a rate limit error. + GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. + For this, specify the number of 'retries' the GraphQL client will attempt. + It will only try the request once by default. + + Additionally, you have the flexibility to customize the retry strategy by passing a 'retryStrategy'. + By default it uses the `DefaultRetryStrategy` with exponential back-off factor of 2 for error codes 429, + 502, 503, 504, 520, 521, 522, 523, and 524. You can use this class or your own implementation of `RetryStrategy`. + */ + retries: + (process.env.GRAPH_QL_SERVICE_RETRIES && + parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) || + 0, }) : new RestDictionaryService({ apiHost: config.sitecoreApiHost, diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts index a3e466ae31..584b2a37eb 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts @@ -12,6 +12,20 @@ export class LayoutServiceFactory { endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, siteName: config.jssAppName, + /* + GraphQL endpoint may reach its rate limit with the amount of requests it receives and throw a rate limit error. + GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. + For this, specify the number of 'retries' the GraphQL client will attempt. + It will only try the request once by default. + + Additionally, you have the flexibility to customize the retry strategy by passing a 'retryStrategy'. + By default it uses the `DefaultRetryStrategy` with exponential back-off factor of 2 for error codes 429, + 502, 503, 504, 520, 521, 522, 523, and 524. You can use this class or your own implementation of `RetryStrategy`. + */ + retries: + (process.env.GRAPH_QL_SERVICE_RETRIES && + parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) || + 0, }) : new RestLayoutService({ apiHost: config.sitecoreApiHost, diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/component-props.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/component-props.ts index e5b91ec8e7..c66f61c226 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/component-props.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/component-props.ts @@ -1,4 +1,4 @@ -import { ComponentPropsService } from '@sitecore-jss/sitecore-jss-nextjs'; +import { ComponentPropsService, ComponentPropsError } from '@sitecore-jss/sitecore-jss-nextjs'; import { SitecorePageProps } from 'lib/page-props'; import { GetServerSidePropsContext, GetStaticPropsContext } from 'next'; import { componentModule } from 'temp/componentFactory'; @@ -31,6 +31,20 @@ class ComponentPropsPlugin implements Plugin { }); } + const errors = Object.keys(props.componentProps) + .map(id => { + const component = props.componentProps[id] as ComponentPropsError; + + return component.error + ? `\nUnable to get component props for ${component.componentName} (${id}): ${component.error}` + : ''; + }) + .join(''); + + if (errors.length) { + throw new Error(errors); + } + return props; } } diff --git a/packages/create-sitecore-jss/src/templates/node-headless-ssr-experience-edge/package.json b/packages/create-sitecore-jss/src/templates/node-headless-ssr-experience-edge/package.json index 73ed9142be..8c0af8f09c 100644 --- a/packages/create-sitecore-jss/src/templates/node-headless-ssr-experience-edge/package.json +++ b/packages/create-sitecore-jss/src/templates/node-headless-ssr-experience-edge/package.json @@ -1,6 +1,6 @@ { "name": "node-headless-ssr-experience-edge-sample", - "version": "20.1.0", + "version": "20.3.3", "description": "Node server-side-rendering sample for running JSS apps under Node hosting using Experience Edge", "main": "index.js", "scripts": { @@ -24,7 +24,7 @@ "homepage": "https://jss.sitecore.com", "license": "Apache-2.0", "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", + "@sitecore-jss/sitecore-jss": "^20.3.3", "compression": "^1.7.4", "express": "^4.17.1" }, diff --git a/packages/create-sitecore-jss/src/templates/node-headless-ssr-proxy/package.json b/packages/create-sitecore-jss/src/templates/node-headless-ssr-proxy/package.json index 0d88f21323..5bbff09872 100644 --- a/packages/create-sitecore-jss/src/templates/node-headless-ssr-proxy/package.json +++ b/packages/create-sitecore-jss/src/templates/node-headless-ssr-proxy/package.json @@ -1,6 +1,6 @@ { "name": "node-headless-ssr-proxy-sample", - "version": "20.1.0", + "version": "20.3.3", "description": "Node server-side-rendering proxy sample for running JSS apps under Node hosting", "main": "dist/index.js", "scripts": { @@ -24,8 +24,8 @@ "homepage": "https://jss.sitecore.com", "license": "Apache-2.0", "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", - "@sitecore-jss/sitecore-jss-proxy": "^20.1.0", + "@sitecore-jss/sitecore-jss": "^20.3.3", + "@sitecore-jss/sitecore-jss-proxy": "^20.3.3", "agentkeepalive": "^4.1.3", "compression": "~1.7.3", "express": "~4.16.4", diff --git a/packages/create-sitecore-jss/src/templates/react-native/package.json b/packages/create-sitecore-jss/src/templates/react-native/package.json index 7a53aad6f1..fcb1289beb 100644 --- a/packages/create-sitecore-jss/src/templates/react-native/package.json +++ b/packages/create-sitecore-jss/src/templates/react-native/package.json @@ -1,6 +1,6 @@ { "name": "<%- appName %>", - "version": "20.1.0", + "version": "20.3.3", "description": "A basic React Native app utilizing Sitecore JavaScript Services", "config": { "appName": "<%- appName %>", @@ -27,7 +27,7 @@ }, "dependencies": { "@react-native-community/masked-view": "^0.1.10", - "@sitecore-jss/sitecore-jss-react-native": "^20.1.0", + "@sitecore-jss/sitecore-jss-react-native": "^20.3.3", "prop-types": "^15.6.0", "react": "16.13.1", "react-native": "^0.63.4", @@ -41,8 +41,8 @@ }, "private": true, "devDependencies": { - "@sitecore-jss/sitecore-jss-cli": "^20.1.0", - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", + "@sitecore-jss/sitecore-jss-cli": "^20.3.3", + "@sitecore-jss/sitecore-jss-dev-tools": "^20.3.3", "babel-core": "^6.26.0", "babel-eslint": "^8.2.1", "babel-plugin-inline-replace-variables": "^1.3.1", diff --git a/packages/create-sitecore-jss/src/templates/react/package.json b/packages/create-sitecore-jss/src/templates/react/package.json index fdccaa4790..c323ca6352 100644 --- a/packages/create-sitecore-jss/src/templates/react/package.json +++ b/packages/create-sitecore-jss/src/templates/react/package.json @@ -1,7 +1,7 @@ { "name": "<%- appName %>", "description": "Application utilizing Sitecore JavaScript Services and React (create-react-app).", - "version": "20.1.0", + "version": "20.3.3", "private": true, "config": { "appName": "<%- appName %>", @@ -32,7 +32,7 @@ "license": "Apache-2.0", "dependencies": { "@apollo/client": "^3.5.6", - "@sitecore-jss/sitecore-jss-react": "^20.1.0", + "@sitecore-jss/sitecore-jss-react": "^20.3.3", "axios": "^0.21.1", "bootstrap": "^4.3.1", "cross-fetch": "^3.0.6", @@ -54,9 +54,9 @@ "@babel/core": "^7.16.0", "@babel/preset-env": "^7.10.4", "@babel/register": "~7.6.2", - "@sitecore-jss/sitecore-jss-cli": "^20.1.0", - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", - "@sitecore-jss/sitecore-jss-rendering-host": "^20.1.0", + "@sitecore-jss/sitecore-jss-cli": "^20.3.3", + "@sitecore-jss/sitecore-jss-dev-tools": "^20.3.3", + "@sitecore-jss/sitecore-jss-rendering-host": "^20.3.3", "babel-eslint": "^10.1.0", "babel-loader": "8.1.0", "babel-preset-react-app": "~9.0.2", diff --git a/packages/create-sitecore-jss/src/templates/vue/.eslintrc.js b/packages/create-sitecore-jss/src/templates/vue/.eslintrc.js index 7b987d45ca..2fa21774d6 100644 --- a/packages/create-sitecore-jss/src/templates/vue/.eslintrc.js +++ b/packages/create-sitecore-jss/src/templates/vue/.eslintrc.js @@ -8,8 +8,11 @@ module.exports = { rules: { 'no-console': 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'vue/multi-word-component-names': 'off', }, parserOptions: { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', + ecmaVersion: 8, + requireConfigFile: false, }, }; diff --git a/packages/create-sitecore-jss/src/templates/vue/package.json b/packages/create-sitecore-jss/src/templates/vue/package.json index 3bd5f9674a..2e5dbf1248 100644 --- a/packages/create-sitecore-jss/src/templates/vue/package.json +++ b/packages/create-sitecore-jss/src/templates/vue/package.json @@ -1,6 +1,6 @@ { "name": "<%- appName %>", - "version": "20.1.0", + "version": "20.3.3", "description": "Application utilizing Sitecore JavaScript Services and Vue (vue-cli).", "private": true, "config": { @@ -48,43 +48,46 @@ "dependencies": { "@apollo/client": "^3.5.6", "@panter/vue-i18next": "~0.15.1", - "@sitecore-jss/sitecore-jss-vue": "^20.1.0", - "@vue/apollo-composable": "^4.0.0-alpha.12", + "@sitecore-jss/sitecore-jss-vue": "^20.3.3", + "@vue/apollo-composable": "4.0.0-beta.2", "@vue/apollo-option": "^4.0.0-alpha.11", "@vue/apollo-ssr": "^4.0.0-alpha.11", "@vue/server-renderer": "^3.0.11", "axios": "^0.21.1", "bootstrap": "^4.3.1", - "cross-fetch": "~2.2.3", + "cross-fetch": "~3.1.5", "graphql": "^15.5.0", "js-sha256": "^0.9.0", "register-service-worker": "~1.6.2", "serialize-javascript": "^5.0.1", "vue": "^v3.1.0-beta.6", - "vue-i18n": "^9.1.6", - "vue-meta": "3.0.0-alpha.6", - "vue-router": "^4.0.8" + "vue-i18n": "9.1.10", + "vue-meta": "3.0.0-alpha.10", + "vue-router": "4.0.16" }, "devDependencies": { - "@babel/register": "7.6.2", - "@sitecore-jss/sitecore-jss-cli": "^20.1.0", - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", - "@vue/cli-plugin-babel": "~4.5.13", - "@vue/cli-plugin-eslint": "~4.5.13", - "@vue/cli-service": "~4.5.13", + "@babel/register": "7.18.9", + "@babel/eslint-parser": "^7.19.1", + "@sitecore-jss/sitecore-jss-cli": "^20.3.3", + "@sitecore-jss/sitecore-jss-dev-tools": "^20.3.3", + "@vue/cli-plugin-babel": "~5.0.8", + "@vue/cli-plugin-eslint": "~5.0.8", + "@vue/cli-service": "~5.0.8", "@vue/compiler-sfc": "^3.0.11", - "@vue/eslint-config-prettier": "~5.0.0", + "@vue/eslint-config-prettier": "~7.0.0", "babel-eslint": "~10.0.3", "chokidar": "~3.1.1", "constant-case": "^3.0.4", "cross-env": "~6.0.0", - "eslint": "^6.8.0", - "eslint-plugin-prettier": "^3.1.3", - "eslint-plugin-vue": "~7.9.0", + "eslint": "^8.32.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-vue": "~9.9.0", "eslint-plugin-yaml": "^0.2.0", "graphql-tag": "^2.12.4", "html-loader": "~0.5.5", "npm-run-all": "~4.1.5", - "null-loader": "~3.0.0" + "null-loader": "~4.0.0", + "prettier": "^3.0.0", + "tslib": "2.5.1" } } diff --git a/packages/create-sitecore-jss/src/templates/vue/src/components/GraphQL/GraphQL-SSRDemo.vue b/packages/create-sitecore-jss/src/templates/vue/src/components/GraphQL/GraphQL-SSRDemo.vue index 5c94bb1532..2f1372ffb3 100644 --- a/packages/create-sitecore-jss/src/templates/vue/src/components/GraphQL/GraphQL-SSRDemo.vue +++ b/packages/create-sitecore-jss/src/templates/vue/src/components/GraphQL/GraphQL-SSRDemo.vue @@ -13,22 +13,23 @@ NOTE: when using the useQuery prefetch option, GraphQL queries are executed prior to app rendering.

-

Expected behavior for this component:

-

GraphQL query is executing...

GraphQL query error: {{ error.toString() }}

diff --git a/packages/create-sitecore-jss/src/templates/vue/vue.config.js b/packages/create-sitecore-jss/src/templates/vue/vue.config.js index 5e869cead8..c8559295fa 100644 --- a/packages/create-sitecore-jss/src/templates/vue/vue.config.js +++ b/packages/create-sitecore-jss/src/templates/vue/vue.config.js @@ -24,6 +24,7 @@ if (process.env.BUILD_TARGET_ENV === 'server') { config.plugin('html').init((Plugin, args) => { const newArgs = { ...args[0], + minify: args[0].minify || {}, }; newArgs.minify.removeAttributeQuotes = false; return new Plugin(newArgs); diff --git a/packages/sitecore-jss-angular-schematics/package.json b/packages/sitecore-jss-angular-schematics/package.json index bf1cbe3070..f5a42951ce 100644 --- a/packages/sitecore-jss-angular-schematics/package.json +++ b/packages/sitecore-jss-angular-schematics/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-angular-schematics", - "version": "20.1.0", + "version": "20.3.3", "description": "Scaffolding schematics for Sitecore JSS Angular apps", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/packages/sitecore-jss-angular/package.json b/packages/sitecore-jss-angular/package.json index 9768f3d99e..39c907816c 100644 --- a/packages/sitecore-jss-angular/package.json +++ b/packages/sitecore-jss-angular/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-angular", - "version": "20.1.0", + "version": "20.3.3", "description": "", "scripts": { "build": "ng-packagr -p ng-package.json", @@ -58,7 +58,7 @@ "rxjs": "~6.6.6" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0" + "@sitecore-jss/sitecore-jss": "20.3.3" }, "main": "dist/bundles/sitecore-jss-sitecore-jss-angular.umd.js", "module": "dist/fesm2015/sitecore-jss-sitecore-jss-angular.js", diff --git a/packages/sitecore-jss-angular/src/public_api.ts b/packages/sitecore-jss-angular/src/public_api.ts index d2890b25f5..4ca72ded32 100644 --- a/packages/sitecore-jss-angular/src/public_api.ts +++ b/packages/sitecore-jss-angular/src/public_api.ts @@ -40,7 +40,8 @@ export { ComponentFields, ComponentParams, } from '@sitecore-jss/sitecore-jss/layout'; -export { constants, HttpDataFetcher, HttpResponse } from '@sitecore-jss/sitecore-jss'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; +export { constants, HttpDataFetcher, HttpResponse, ClientError } from '@sitecore-jss/sitecore-jss'; export { isServer, isExperienceEditorActive, diff --git a/packages/sitecore-jss-cli/package.json b/packages/sitecore-jss-cli/package.json index d3a34d6bf6..029c19243e 100644 --- a/packages/sitecore-jss-cli/package.json +++ b/packages/sitecore-jss-cli/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-cli", - "version": "20.1.0", + "version": "20.3.3", "description": "Sitecore JSS command-line", "main": "dist/cjs/cli.js", "module": "dist/esm/cli.js", @@ -33,7 +33,7 @@ "url": "https://github.com/sitecore/jss/issues" }, "dependencies": { - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", + "@sitecore-jss/sitecore-jss-dev-tools": "20.3.3", "chalk": "^2.4.2", "cross-spawn": "^7.0.0", "dotenv": "^8.2.0", diff --git a/packages/sitecore-jss-dev-tools/package.json b/packages/sitecore-jss-dev-tools/package.json index 819461ab74..db91a68a8c 100644 --- a/packages/sitecore-jss-dev-tools/package.json +++ b/packages/sitecore-jss-dev-tools/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-dev-tools", - "version": "20.1.0", + "version": "20.3.3", "description": "Utilities to assist in the development and deployment of Sitecore JSS apps.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -33,7 +33,7 @@ "url": "https://github.com/sitecore/jss/issues" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", + "@sitecore-jss/sitecore-jss": "20.3.3", "axios": "^0.21.1", "chalk": "^2.4.1", "chokidar": "^3.2.1", diff --git a/packages/sitecore-jss-forms/package.json b/packages/sitecore-jss-forms/package.json index 13156ef9fa..053f748e23 100644 --- a/packages/sitecore-jss-forms/package.json +++ b/packages/sitecore-jss-forms/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-forms", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -44,7 +44,7 @@ "typescript": "~4.3.5" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0" + "@sitecore-jss/sitecore-jss": "20.3.3" }, "description": "", "types": "types/index.d.ts", diff --git a/packages/sitecore-jss-forms/src/serializeForm.ts b/packages/sitecore-jss-forms/src/serializeForm.ts index d4d21fd66e..5f7456ab50 100644 --- a/packages/sitecore-jss-forms/src/serializeForm.ts +++ b/packages/sitecore-jss-forms/src/serializeForm.ts @@ -1,10 +1,10 @@ import { FormField, + ValueFormField, instanceOfButtonFormField, instanceOfFormFieldSection, instanceOfValueFormField, } from './FormField'; -import { TrackableValueFormField } from './FormTracker'; import { getFieldValueFromModel } from './getFieldValueFromModel'; import { HtmlFormField } from './HtmlFormField'; import { JssFormData } from './JssFormData'; @@ -47,11 +47,7 @@ export function serializeForm(form: SitecoreForm, options?: SerializeFormOptions * @param {Array} fields * @param {SerializeFormOptions} options */ -function pushFields( - result: JssFormData, - fields: ((TrackableValueFormField & FormField) | FormField)[], - options: SerializeFormOptions -) { +function pushFields(result: JssFormData, fields: FormField[], options: SerializeFormOptions) { fields.forEach((field) => { if ( instanceOfButtonFormField(field) && @@ -61,16 +57,15 @@ function pushFields( pushField(result, field.navigationButtonsField); pushField(result, field.navigationStepField); } else if (instanceOfValueFormField(field)) { + pushField(result, field.indexField); + pushField(result, field.fieldIdField); if (field.valueField.name.endsWith('.Files')) { - const fileUploadField: TrackableValueFormField & FormField = field; - - if (!fileUploadField.originalValue && !fileUploadField.model.files) { + const fileUploadField = field as ValueFormField; + if (!fileUploadField.model.files) { return; } } - pushField(result, field.indexField); - pushField(result, field.fieldIdField); // get stored value (i.e. if a multistep form) if (instanceOfInputViewModel(field.model) && options.fieldValueParser) { const fieldValue = options.fieldValueParser(field); diff --git a/packages/sitecore-jss-nextjs/package.json b/packages/sitecore-jss-nextjs/package.json index 6ab5ad09fa..e90f3a7abf 100644 --- a/packages/sitecore-jss-nextjs/package.json +++ b/packages/sitecore-jss-nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-nextjs", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -69,9 +69,9 @@ "react-dom": "^17.0.2" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", - "@sitecore-jss/sitecore-jss-dev-tools": "^20.1.0", - "@sitecore-jss/sitecore-jss-react": "^20.1.0", + "@sitecore-jss/sitecore-jss": "20.3.3", + "@sitecore-jss/sitecore-jss-dev-tools": "20.3.3", + "@sitecore-jss/sitecore-jss-react": "20.3.3", "prop-types": "^15.7.2", "regex-parser": "^2.2.11", "sync-disk-cache": "^2.1.0" diff --git a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx index f36affbc9c..5e6ee5181b 100644 --- a/packages/sitecore-jss-nextjs/src/components/Link.test.tsx +++ b/packages/sitecore-jss-nextjs/src/components/Link.test.tsx @@ -42,6 +42,7 @@ describe('', () => { title: 'My Link', target: '_blank', querystring: 'foo=bar', + anchor: 'foo', }, }; @@ -53,7 +54,9 @@ describe('', () => { const link = c.find('a'); - expect(link.html()).to.contain(`href="${field.value.href}?${field.value.querystring}"`); + expect(link.html()).to.contain( + `href="${field.value.href}?${field.value.querystring}#${field.value.anchor}"` + ); expect(link.html()).to.contain(`class="${field.value.class}"`); expect(link.html()).to.contain(`title="${field.value.title}"`); expect(link.html()).to.contain(`target="${field.value.target}"`); diff --git a/packages/sitecore-jss-nextjs/src/components/Link.tsx b/packages/sitecore-jss-nextjs/src/components/Link.tsx index 334acfa552..ec715069d0 100644 --- a/packages/sitecore-jss-nextjs/src/components/Link.tsx +++ b/packages/sitecore-jss-nextjs/src/components/Link.tsx @@ -30,7 +30,7 @@ export const Link = (props: LinkProps): JSX.Element => { const value = ((field as LinkFieldValue).href ? field : (field as LinkField).value) as LinkFieldValue; - const { href, querystring } = value; + const { href, querystring, anchor } = value; const isEditing = editable && (field as LinkFieldValue).editable; if (href && !isEditing) { @@ -39,7 +39,11 @@ export const Link = (props: LinkProps): JSX.Element => { // determine if a link is a route or not. if (internalLinkMatcher.test(href)) { return ( - + {text} {children} diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index ab5f273e44..8a6fe89be7 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -5,6 +5,7 @@ export { HttpResponse, AxiosDataFetcher, AxiosDataFetcherConfig, + ClientError, } from '@sitecore-jss/sitecore-jss'; export { isEditorActive, @@ -56,10 +57,12 @@ export { GraphQLRobotsService, GraphQLRobotsServiceConfig, } from '@sitecore-jss/sitecore-jss/site'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { GraphQLRequestClient } from '@sitecore-jss/sitecore-jss'; export { ComponentPropsCollection, + ComponentPropsError, GetStaticComponentProps, GetServerSideComponentProps, } from './sharedTypes/component-props'; diff --git a/packages/sitecore-jss-nextjs/src/services/component-props-service.test.ts b/packages/sitecore-jss-nextjs/src/services/component-props-service.test.ts index 9530978de7..40e2555ee8 100644 --- a/packages/sitecore-jss-nextjs/src/services/component-props-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/component-props-service.test.ts @@ -105,7 +105,8 @@ describe('ComponentPropsService', () => { expect(result).to.deep.equal({ x11: 'x11SSRData', x14: { - error: 'Error during preload data for component x14: whoops', + error: 'Error during preload data for component namex14 (x14): whoops', + componentName: 'namex14', }, x16: 'myCustomComponentSSRData', x161: 'myCustomComponentSSRData', @@ -156,7 +157,8 @@ describe('ComponentPropsService', () => { expect(result).to.deep.equal({ x11: 'x11SSRData', x14: { - error: 'Error during preload data for component x14: whoops', + error: 'Error during preload data for component namex14 (x14): whoops', + componentName: 'namex14', }, x16: 'myCustomComponentSSRData', x161: 'myCustomComponentSSRData', @@ -198,7 +200,8 @@ describe('ComponentPropsService', () => { expect(result).to.deep.equal({ x11: 'x11StaticData', x14: { - error: 'Error during preload data for component x14: whoops', + error: 'Error during preload data for component namex14 (x14): whoops', + componentName: 'namex14', }, x16: 'myCustomComponentStaticData', x161: 'myCustomComponentStaticData', @@ -234,7 +237,8 @@ describe('ComponentPropsService', () => { expect(result).to.deep.equal({ x11: 'x11StaticData', x14: { - error: 'Error during preload data for component x14: whoops', + error: 'Error during preload data for component namex14 (x14): whoops', + componentName: 'namex14', }, x16: 'myCustomComponentStaticData', x161: 'myCustomComponentStaticData', diff --git a/packages/sitecore-jss-nextjs/src/services/component-props-service.ts b/packages/sitecore-jss-nextjs/src/services/component-props-service.ts index f980dd93e6..62995e7178 100644 --- a/packages/sitecore-jss-nextjs/src/services/component-props-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/component-props-service.ts @@ -182,13 +182,15 @@ export class ComponentPropsService { componentProps[uid] = result; }) .catch((error) => { - const errLog = `Error during preload data for component ${uid}: ${error.message || - error}`; + const errLog = `Error during preload data for component ${ + req.rendering.componentName + } (${uid}): ${error.message || error}`; console.error(chalk.red(errLog)); componentProps[uid] = { error: error.message || errLog, + componentName: req.rendering.componentName, }; }); }); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 3abccb0142..1367b6b882 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -232,6 +232,17 @@ describe('GraphQLSitemapService', () => { }); }); + it('should throw error if empty language is provided', async () => { + mockPathsRequest(); + + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + await service.fetchSSGSitemap(['']).catch((error: RangeError) => { + expect(error.message).to.equal('The language must be a non-empty string'); + }); + + return expect(nock.isDone()).to.be.false; + }); + it('should use a custom pageSize, if provided', async () => { const customPageSize = 20; @@ -257,7 +268,7 @@ describe('GraphQLSitemapService', () => { .post( '/', (body) => - body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined + body.query.indexOf('$pageSize: Int = 100') > 0 && body.variables.pageSize === undefined ) .reply(200, sitemapQueryResult); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 5461f9af21..1182a36c9a 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -14,12 +14,15 @@ export const queryError = /** @private */ export const languageError = 'The list of languages cannot be empty'; +/** @private */ +const languageEmptyError = 'The language must be a non-empty string'; + // Even though _hasLayout should always be "true" in this query, using a variable is necessary for compatibility with Edge const defaultQuery = /* GraphQL */ ` query SitemapQuery( $rootItemId: String! $language: String! - $pageSize: Int = 10 + $pageSize: Int = 100 $hasLayout: String = "true" $after: String ) { @@ -173,23 +176,29 @@ export class GraphQLSitemapService { throw new Error(queryError); } - // Fetch paths using all locales - const paths = await Promise.all( - languages.map((language) => { - debug.sitemap('fetching sitemap data for %s', language); - return this.searchService - .fetch(this.query, { - rootItemId, - language, - pageSize: this.options.pageSize, - }) - .then((results) => { - return results.map((item) => - formatStaticPath(item.url.path.replace(/^\/|\/$/g, '').split('/'), language) - ); - }); - }) - ); + const paths: StaticPath[] = []; + + for (const language of languages) { + if (language === '') { + throw new RangeError(languageEmptyError); + } + + debug.sitemap('fetching sitemap data for %s', language); + + const languagePaths = await this.searchService + .fetch(this.query, { + rootItemId, + language, + pageSize: this.options.pageSize, + }) + .then((results) => { + return results.map((item) => + formatStaticPath(item.url.path.replace(/^\/|\/$/g, '').split('/'), language) + ); + }); + + paths.push(...languagePaths); + } // merge promises results into single result return ([] as StaticPath[]).concat(...paths); diff --git a/packages/sitecore-jss-nextjs/src/sharedTypes/component-props.ts b/packages/sitecore-jss-nextjs/src/sharedTypes/component-props.ts index 60aaef6aed..1084cd1c5e 100644 --- a/packages/sitecore-jss-nextjs/src/sharedTypes/component-props.ts +++ b/packages/sitecore-jss-nextjs/src/sharedTypes/component-props.ts @@ -1,11 +1,13 @@ import { GetServerSidePropsContext, GetStaticPropsContext } from 'next'; import { ComponentRendering, LayoutServiceData } from '@sitecore-jss/sitecore-jss/layout'; +export type ComponentPropsError = { error: string; componentName: string }; + /** * Shape of component props storage */ export type ComponentPropsCollection = { - [componentUid: string]: unknown; + [componentUid: string]: unknown | ComponentPropsError; }; /** diff --git a/packages/sitecore-jss-proxy/package-lock.json b/packages/sitecore-jss-proxy/package-lock.json index 95dd7c0b62..f9a537e322 100644 --- a/packages/sitecore-jss-proxy/package-lock.json +++ b/packages/sitecore-jss-proxy/package-lock.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-proxy", - "version": "20.1.0", + "version": "20.3.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/sitecore-jss-proxy/package.json b/packages/sitecore-jss-proxy/package.json index 7a3855e771..44471ffc46 100644 --- a/packages/sitecore-jss-proxy/package.json +++ b/packages/sitecore-jss-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-proxy", - "version": "20.1.0", + "version": "20.3.3", "description": "Proxy middleware for express.js server.", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -34,6 +34,7 @@ }, "devDependencies": { "@types/chai": "^4.1.6", + "@types/express": "^4.17.17", "@types/http-proxy-middleware": "^0.19.3", "@types/mocha": "^5.2.5", "@types/node": "^12.7.11", diff --git a/packages/sitecore-jss-proxy/src/index.ts b/packages/sitecore-jss-proxy/src/index.ts index 7ce54741b8..26ecc6fb20 100644 --- a/packages/sitecore-jss-proxy/src/index.ts +++ b/packages/sitecore-jss-proxy/src/index.ts @@ -8,6 +8,7 @@ import { ProxyConfig } from './ProxyConfig'; import { RenderResponse } from './RenderResponse'; import { RouteUrlParser } from './RouteUrlParser'; import { buildQueryString, tryParseJson } from './util'; +import { Request, RequestHandler } from 'express'; /** * Extends IncomingMessage as it should contain these properties but they are not provided in types @@ -15,13 +16,12 @@ import { buildQueryString, tryParseJson } from './util'; export interface ProxyIncomingMessage extends IncomingMessage { originalUrl: string; query: { [key: string]: string | number | boolean }; + originalMethod?: string; } -/** - * Extends ClientRequest as it should contain `method` but it's not provided in types - */ -interface ExtendedClientRequest extends ClientRequest { - method: string; +interface ExtendedRequest extends Request { + // Custom property we set to keep an original method when we swap 'HEAD' with 'GET' + originalMethod?: string; } // For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string. @@ -300,7 +300,7 @@ async function renderAppToResponse( viewBag ); } catch (error) { - return replyWithError(error); + return replyWithError(error as Error); } }; } @@ -500,7 +500,7 @@ function isUrlIgnored(originalUrl: string, config: ProxyConfig, noDebug = false) */ function handleProxyRequest( proxyReq: ClientRequest, - req: IncomingMessage, + req: ProxyIncomingMessage, res: ServerResponse, config: ProxyConfig, customOnProxyReq: @@ -508,14 +508,24 @@ function handleProxyRequest( | undefined ) { // if a HEAD request, we still need to issue a GET so we can return accurate headers - if ( - (proxyReq as ExtendedClientRequest).method === 'HEAD' && - !isUrlIgnored((req as ProxyIncomingMessage).originalUrl, config, true) - ) { - if (config.debug) { - console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); + if (!isUrlIgnored(req.originalUrl, config, true)) { + // In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod' + // now we need to set req.method back to original one, since proxyReq is already initialized. + // See more info in 'preProxyHandler' + if (config.proxyOptions?.followRedirects && req.originalMethod === 'HEAD') { + req.method = req.originalMethod; + delete req.originalMethod; + + if (config.debug) { + console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); + } + } else if (proxyReq.method === 'HEAD') { + if (config.debug) { + console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers'); + } + // if a HEAD request, we still need to issue a GET so we can return accurate headers + proxyReq.method = 'GET'; } - (proxyReq as ExtendedClientRequest).method = 'GET'; } // invoke custom onProxyReq if (customOnProxyReq) { @@ -557,7 +567,7 @@ function createOptions( pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res) => - handleProxyRequest(proxyReq, req, res, config, customOnProxyReq), + handleProxyRequest(proxyReq, req as ProxyIncomingMessage, res, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config), }; } @@ -573,5 +583,24 @@ export default function scProxy( parseRouteUrl: RouteUrlParser ) { const options = createOptions(renderer, config, parseRouteUrl); - return proxy(options); + + const preProxyHandler: RequestHandler = (req, _res, next) => { + // When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req' + // and there are no public properties/methods to modify Redirectable 'proxyReq'. + // so, we need to set 'HEAD' req as 'GET' before the proxy is initialized. + // During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'. + // if a HEAD request, we need to issue a GET so we can return accurate headers + if ( + req.method === 'HEAD' && + options.followRedirects && + !isUrlIgnored(req.originalUrl, config, true) + ) { + req.method = 'GET'; + (req as ExtendedRequest).originalMethod = 'HEAD'; + } + + next(); + }; + + return [preProxyHandler, proxy(options)]; } diff --git a/packages/sitecore-jss-react-forms/package.json b/packages/sitecore-jss-react-forms/package.json index 28e7e5aaaf..ffac989eaf 100644 --- a/packages/sitecore-jss-react-forms/package.json +++ b/packages/sitecore-jss-react-forms/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-react-forms", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -55,7 +55,7 @@ "react-dom": "^17.0.2" }, "dependencies": { - "@sitecore-jss/sitecore-jss-forms": "^20.1.0", + "@sitecore-jss/sitecore-jss-forms": "20.3.3", "prop-types": "^15.7.2" }, "description": "", diff --git a/packages/sitecore-jss-react-native/package.json b/packages/sitecore-jss-react-native/package.json index 1df8fbd6b3..0c6ceef3a9 100644 --- a/packages/sitecore-jss-react-native/package.json +++ b/packages/sitecore-jss-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-react-native", - "version": "20.1.0", + "version": "20.3.3", "description": "", "main": "dist/index.js", "scripts": { @@ -28,7 +28,7 @@ "url": "https://github.com/sitecore/jss/issues" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", + "@sitecore-jss/sitecore-jss": "20.3.3", "prop-types": "^15.7.2", "react-native-htmlview": "^0.15.0", "react-native-svg": "^5.3.0", diff --git a/packages/sitecore-jss-react/package.json b/packages/sitecore-jss-react/package.json index 36b0458abf..7123ccb40e 100644 --- a/packages/sitecore-jss-react/package.json +++ b/packages/sitecore-jss-react/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-react", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -61,7 +61,7 @@ "react-dom": "^17.0.2" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", + "@sitecore-jss/sitecore-jss": "20.3.3", "deep-equal": "^2.0.5", "prop-types": "^15.7.2", "style-attr": "^1.3.0" diff --git a/packages/sitecore-jss-react/src/components/Link.test.tsx b/packages/sitecore-jss-react/src/components/Link.test.tsx index 8eef32bb9b..fb27f2aa29 100644 --- a/packages/sitecore-jss-react/src/components/Link.test.tsx +++ b/packages/sitecore-jss-react/src/components/Link.test.tsx @@ -64,6 +64,7 @@ describe('', () => { const field = { value: { href: '/lorem', + anchor: 'foo', text: 'ipsum', class: 'my-link', title: 'My Link', @@ -72,7 +73,9 @@ describe('', () => { }, }; const rendered = mount().find('a'); - expect(rendered.html()).to.contain(`href="${field.value.href}?${field.value.querystring}"`); + expect(rendered.html()).to.contain( + `href="${field.value.href}?${field.value.querystring}#${field.value.anchor}"` + ); expect(rendered.html()).to.contain(`class="${field.value.class}"`); expect(rendered.html()).to.contain(`title="${field.value.title}"`); expect(rendered.html()).to.contain(`target="${field.value.target}"`); diff --git a/packages/sitecore-jss-react/src/components/Link.tsx b/packages/sitecore-jss-react/src/components/Link.tsx index 5412fb9b88..4ecefe2cf9 100644 --- a/packages/sitecore-jss-react/src/components/Link.tsx +++ b/packages/sitecore-jss-react/src/components/Link.tsx @@ -9,6 +9,7 @@ export interface LinkFieldValue { title?: string; target?: string; text?: string; + anchor?: string; querystring?: string; } @@ -94,8 +95,11 @@ export const Link: React.SFC = ({ return null; } + const anchor = link.anchor ? `#${link.anchor}` : ''; + const querystring = link.querystring ? `?${link.querystring}` : ''; + const anchorAttrs: { [attr: string]: unknown } = { - href: link.querystring ? `${link.href}?${link.querystring}` : link.href, + href: link.href ? `${link.href}${querystring}${anchor}` : undefined, className: link.class, title: link.title, target: link.target, diff --git a/packages/sitecore-jss-react/src/index.ts b/packages/sitecore-jss-react/src/index.ts index 65830aafac..58c4827187 100644 --- a/packages/sitecore-jss-react/src/index.ts +++ b/packages/sitecore-jss-react/src/index.ts @@ -1,4 +1,4 @@ -export { constants } from '@sitecore-jss/sitecore-jss'; +export { constants, ClientError } from '@sitecore-jss/sitecore-jss'; export { isExperienceEditorActive, resetExperienceEditorChromes, @@ -38,6 +38,7 @@ export { GraphQLDictionaryService, RestDictionaryService, } from '@sitecore-jss/sitecore-jss/i18n'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { mediaApi } from '@sitecore-jss/sitecore-jss/media'; export { ComponentFactory } from './components/sharedTypes'; export { Placeholder, PlaceholderComponentProps } from './components/Placeholder'; diff --git a/packages/sitecore-jss-rendering-host/package.json b/packages/sitecore-jss-rendering-host/package.json index c1c10fb1b6..28bbab8846 100644 --- a/packages/sitecore-jss-rendering-host/package.json +++ b/packages/sitecore-jss-rendering-host/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-rendering-host", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, diff --git a/packages/sitecore-jss-vue/package.json b/packages/sitecore-jss-vue/package.json index 001d6fb941..11b4fb5c5d 100644 --- a/packages/sitecore-jss-vue/package.json +++ b/packages/sitecore-jss-vue/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss-vue", - "version": "20.1.0", + "version": "20.3.3", "description": "A library for building Sitecore JSS apps using Vue.js", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -49,7 +49,7 @@ "vue": "^v3.1.0-beta.6" }, "dependencies": { - "@sitecore-jss/sitecore-jss": "^20.1.0", + "@sitecore-jss/sitecore-jss": "20.3.3", "@vue/compiler-sfc": "^3.0.11" }, "types": "./types/index.d.ts", diff --git a/packages/sitecore-jss-vue/src/index.ts b/packages/sitecore-jss-vue/src/index.ts index de96d45e8f..86b297dd96 100644 --- a/packages/sitecore-jss-vue/src/index.ts +++ b/packages/sitecore-jss-vue/src/index.ts @@ -5,7 +5,7 @@ export { resetExperienceEditorChromes, handleEditorAnchors, } from '@sitecore-jss/sitecore-jss/utils'; -export { constants } from '@sitecore-jss/sitecore-jss'; +export { constants, ClientError } from '@sitecore-jss/sitecore-jss'; export { trackingApi, TrackingRequestOptions, @@ -35,6 +35,7 @@ export { GraphQLDictionaryService, RestDictionaryService, } from '@sitecore-jss/sitecore-jss/i18n'; +export { RetryStrategy, DefaultRetryStrategy } from '@sitecore-jss/sitecore-jss/graphql'; export { mediaApi } from '@sitecore-jss/sitecore-jss/media'; export { Placeholder } from './components/Placeholder'; export { Image } from './components/Image'; diff --git a/packages/sitecore-jss/package.json b/packages/sitecore-jss/package.json index ec1777921b..3393df4b5d 100644 --- a/packages/sitecore-jss/package.json +++ b/packages/sitecore-jss/package.json @@ -1,6 +1,6 @@ { "name": "@sitecore-jss/sitecore-jss", - "version": "20.1.0", + "version": "20.3.3", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "sideEffects": false, @@ -39,7 +39,8 @@ "@types/memory-cache": "^0.2.1", "@types/mocha": "^5.2.7", "@types/node": "^16.11.6", - "@types/url-parse": "1.4.3", + "@types/sinon": "^17.0.3", + "@types/url-parse": "1.4.8", "chai": "^4.2.0", "chai-spies": "^1.0.0", "chai-string": "^1.5.0", @@ -48,6 +49,7 @@ "mocha": "^8.1.3", "nock": "^13.0.5", "nyc": "^15.1.0", + "sinon": "^17.0.1", "ts-node": "^8.4.1", "tslib": "^1.10.0", "typescript": "~4.3.5" diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 26e73abdef..ae07e2c908 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -1,8 +1,11 @@ +/* eslint-disable dot-notation */ /* eslint-disable no-unused-expressions */ import { expect, use, spy } from 'chai'; +import sinon from 'sinon'; import spies from 'chai-spies'; import nock from 'nock'; -import { GraphQLRequestClient } from './graphql-request-client'; +import { GraphQLRequestClient, DefaultRetryStrategy } from './graphql-request-client'; +import { ClientError } from 'graphql-request'; import debugApi from 'debug'; import debug from './debug'; @@ -117,4 +120,328 @@ describe('GraphQLRequestClient', () => { ); } }); + + it('should throw error when request is aborted with default timeout value', async () => { + nock('http://jssnextweb') + .post('/graphql') + .delay(100) + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint); + await graphQLClient.request('test').catch((error) => { + expect(error.name).to.equal('AbortError'); + }); + }); + + it('should use retry and throw error when retries specified', async function() { + this.timeout(8000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 2, + }); + spy.on(graphQLClient['client'], 'request'); + await graphQLClient.request('test').catch((error) => { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.be.called.exactly(3); + spy.restore(graphQLClient); + }); + }); + + it('should use retry and resolve if one of the requests resolves', async function() { + this.timeout(8000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 2 }); + spy.on(graphQLClient['client'], 'request'); + + const data = await graphQLClient.request('test'); + + expect(data).to.not.be.null; + expect(graphQLClient['client'].request).to.be.called.exactly(3); + spy.restore(graphQLClient); + }); + + it('should use [retry-after] header value when response is 429', async function() { + this.timeout(7000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429, {}, { 'Retry-After': '2' }); + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 1 }); + spy.on(graphQLClient, 'debug'); + + await graphQLClient.request('test').catch(() => { + expect(graphQLClient['debug']).to.have.been.called.with( + 'Error: %d. Retrying in %dms (attempt %d).', + 429, + 2000, + 1 + ); + spy.restore(graphQLClient); + }); + }); + + it('should throw error when request is aborted value after retry', async function() { + this.timeout(3000); + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .delay(100) + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { retries: 1, timeout: 50 }); + spy.on(graphQLClient['client'], 'request'); + try { + await graphQLClient.request('test'); + // If the request does not throw an error, fail the test + expect.fail('Expected request to throw an error'); + } catch (error) { + expect(graphQLClient['client'].request).to.be.called.exactly(2); + expect(error.name).to.equal('AssertionError'); + } finally { + spy.restore(graphQLClient); + } + }); + + it('should throw error upon request timeout using provided timeout value', async () => { + nock('http://jssnextweb') + .post('/graphql') + .delay(30) + .reply(408, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { timeout: 10 }); + await graphQLClient.request('test').catch((error) => { + expect(error.name).to.equal('Error'); + }); + }); + + describe('Retrayable status codes', () => { + const retryableStatusCodeThrowError = async (statusCode: number) => { + nock('http://jssnextweb') + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 2, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + } catch (error) { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.have.been.called.exactly(3); + spy.restore(graphQLClient); + } + }; + + // Test cases for each retryable status code + for (const statusCode of [429, 502, 503, 504, 520, 521, 522, 523, 524]) { + it(`should retry and throw error for ${statusCode} when retries specified`, async function() { + this.timeout(8000); + await retryableStatusCodeThrowError(statusCode); + }); + } + + const retryableStatusCodeResolve = async (statusCode: number) => { + nock('http://jssnextweb') + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(statusCode) + .post('/graphql') + .reply(200, { + data: { + result: 'Hello world...', + }, + }); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 3, + }); + + spy.on(graphQLClient['client'], 'request'); + + const data = await graphQLClient.request('test'); + + try { + await graphQLClient.request('test'); + expect(data).to.not.be.null; + } catch (error) { + expect(graphQLClient['client'].request).to.have.been.called.exactly(4); + spy.restore(graphQLClient); + } + }; + + // Test cases for each retryable status code + for (const statusCode of [429, 502, 503, 504, 520, 521, 522, 523, 524]) { + it(`should retry and resolve for ${statusCode} if one of the request resolves`, async function() { + this.timeout(16000); + await retryableStatusCodeResolve(statusCode); + }); + } + + it('should retry based on custom retryStrategy', async function() { + this.timeout(8000); + + nock('http://jssnextweb') + .post('/graphql') + .reply(502, { + data: { + result: 'Hello world...', + }, + }); + + const customRetryStrategy = { + shouldRetry: (_: any, attempt: number) => attempt <= 3, + getDelay: () => 1000, + }; + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 4, + retryStrategy: customRetryStrategy, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + } catch (error) { + expect(error).to.not.be.undefined; + expect(graphQLClient['client'].request).to.be.called.exactly(4); + spy.restore(graphQLClient); + } + }); + + it('should delay before retrying based on exponential backoff', async function() { + this.timeout(32000); + + nock('http://jssnextweb') + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429) + .post('/graphql') + .reply(429); + + const graphQLClient = new GraphQLRequestClient(endpoint, { + retries: 4, + }); + + spy.on(graphQLClient['client'], 'request'); + + try { + await graphQLClient.request('test'); + + expect(graphQLClient['client'].request).to.have.been.called.exactly(1); + + const clock = sinon.useFakeTimers(); + clock.tick(1000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(2); + + clock.tick(2000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(3); + + clock.tick(4000); + + await graphQLClient.request('test'); + expect(graphQLClient['client'].request).to.have.been.called.exactly(4); + + clock.restore(); + } catch (error) { + console.log('error'); + } + }); + }); + + describe('DefaultRetryStrategy', () => { + const mockClientError = new ClientError( + { + data: undefined, + errors: [{ message: 'GaphqlError' }], + extensions: undefined, + status: 429, + }, + { + query: 'query', + } + ); + it('should return true from shouldRetry and use default values from constructor', () => { + const retryStrategy = new DefaultRetryStrategy(); + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 1, 3); + expect(shouldRetry).to.equal(true); + }); + + it('should return false when attempt exceeds retries', () => { + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [503] }); + mockClientError.response.status = 503; + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 2, 1); + expect(shouldRetry).to.equal(false); + }); + + it('should return false when retries is 0', () => { + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [503] }); + mockClientError.response.status = 503; + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 1, 0); + expect(shouldRetry).to.equal(false); + }); + + it('should return delay using exponential backoff when Retry-After header is not present', () => { + const retryStrategy = new DefaultRetryStrategy(); + const delay = retryStrategy.getDelay(mockClientError, 3); + const expectedDelay = Math.pow(retryStrategy['factor'], 3 - 1) * 1000; + expect(delay).to.equal(expectedDelay); + }); + + it('should use custom exponential factor', () => { + const customFactor = 3; + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [429], factor: customFactor }); + + const delay = retryStrategy.getDelay(mockClientError, 3); + const expectedDelay = Math.pow(customFactor, 3 - 1) * 1000; + expect(delay).to.equal(expectedDelay); + }); + }); }); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index f6ffb928e9..e154a6a481 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -2,6 +2,7 @@ import { GraphQLClient as Client, ClientError } from 'graphql-request'; import parse from 'url-parse'; import { DocumentNode } from 'graphql'; import debuggers, { Debugger } from './debug'; +import TimeoutPromise from './utils/timeout-promise'; /** * An interface for GraphQL clients for Sitecore APIs @@ -14,6 +15,26 @@ export interface GraphQLClient { */ request(query: string | DocumentNode, variables?: { [key: string]: unknown }): Promise; } +/** + * Defines the strategy for retrying GraphQL requests based on errors and attempts. + */ +export interface RetryStrategy { + /** + * Determines whether a request should be retried based on the given error and attempt count. + * @param error - The error received from the GraphQL request. + * @param attempt - The current attempt number. + * @param retries - The number of retries configured. + * @returns A boolean indicating whether to retry the request. + */ + shouldRetry(error: ClientError, attempt: number, retries: number): boolean; + /** + * Calculates the delay (in milliseconds) before the next retry based on the given error and attempt count. + * @param error - The error received from the GraphQL request. + * @param attempt - The current attempt number. + * @returns The delay in milliseconds before the next retry. + */ + getDelay(error: ClientError, attempt: number): number; +} /** * Minimum configuration options for classes that implement @see GraphQLClient @@ -31,8 +52,59 @@ export type GraphQLRequestClientConfig = { * Override fetch method. Uses 'graphql-request' library default otherwise ('cross-fetch'). */ fetch?: typeof fetch; + /** + * GraphQLClient request timeout (in milliseconds). + */ + timeout?: number; + /** + * Number of retries for client. Will use the specified `retryStrategy`. + */ + retries?: number; + /** + * Retry strategy for the client. Uses `DefaultRetryStrategy` by default with exponential + * back-off factor of 2 for codes 429, 502, 503, 504, 520, 521, 522, 523, 524. + */ + retryStrategy?: RetryStrategy; }; +/** + * Represents a default retry strategy for handling retry attempts in case of specific HTTP status codes. + * This class implements the RetryStrategy interface and provides methods to determine whether a request + * should be retried and calculates the delay before the next retry attempt. + */ +export class DefaultRetryStrategy implements RetryStrategy { + private statusCodes: number[]; + private factor: number; + + /** + * @param {Object} options Configurable options for retry mechanism. + * @param {number[]} options.statusCodes HTTP status codes to trigger retries on + * @param {number} options.factor Factor by which the delay increases with each retry attempt + */ + constructor(options: { statusCodes?: number[]; factor?: number } = {}) { + this.statusCodes = options.statusCodes || [429]; + this.factor = options.factor || 2; + } + + shouldRetry(error: ClientError, attempt: number, retries: number): boolean { + return ( + retries > 0 && + attempt <= retries && + error.response?.status !== undefined && + this.statusCodes.includes(error.response.status) + ); + } + + getDelay(error: ClientError, attempt: number): number { + const rawHeaders = error.response?.headers; + const delaySeconds = rawHeaders?.get('Retry-After') + ? Number.parseInt(rawHeaders?.get('Retry-After'), 10) + : Math.pow(this.factor, attempt - 1); + + return delaySeconds * 1000; + } +} + /** * A GraphQL client for Sitecore APIs that uses the 'graphql-request' library. * https://github.com/prisma-labs/graphql-request @@ -41,6 +113,10 @@ export class GraphQLRequestClient implements GraphQLClient { private client: Client; private headers: Record = {}; private debug: Debugger; + private abortTimeout?: TimeoutPromise; + private timeout?: number; + private retries: number; + private retryStrategy: RetryStrategy; /** * Provides ability to execute graphql query using given `endpoint` @@ -58,7 +134,14 @@ export class GraphQLRequestClient implements GraphQLClient { ); } - this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch }); + this.retries = clientConfig.retries || 0; + this.retryStrategy = + clientConfig.retryStrategy || + new DefaultRetryStrategy({ statusCodes: [429, 502, 503, 504, 520, 521, 522, 523, 524] }); + this.client = new Client(endpoint, { + headers: this.headers, + fetch: clientConfig.fetch, + }); this.debug = clientConfig.debugger || debuggers.http; } @@ -71,7 +154,9 @@ export class GraphQLRequestClient implements GraphQLClient { query: string | DocumentNode, variables?: { [key: string]: unknown } ): Promise { - return new Promise((resolve, reject) => { + let attempt = 1; + + const retryer = async (): Promise => { // Note we don't have access to raw request/response with graphql-request // (or nice hooks like we have with Axios), but we should log whatever we have. this.debug('request: %o', { @@ -80,17 +165,38 @@ export class GraphQLRequestClient implements GraphQLClient { query, variables, }); + const startTimestamp = Date.now(); + const fetchWithOptionalTimeout = [this.client.request(query, variables)]; + if (this.timeout) { + this.abortTimeout = new TimeoutPromise(this.timeout); + fetchWithOptionalTimeout.push(this.abortTimeout.start); + } - this.client - .request(query, variables) - .then((data: T) => { - this.debug('response: %o', data); - resolve(data); - }) - .catch((error: ClientError) => { - this.debug('response error: %o', error.response); - return reject(error); - }); - }); + return Promise.race(fetchWithOptionalTimeout).then( + (data: T) => { + this.abortTimeout?.clear(); + this.debug('response in %dms: %o', Date.now() - startTimestamp, data); + return Promise.resolve(data); + }, + async (error: ClientError) => { + this.abortTimeout?.clear(); + this.debug('response error: %o', error.response || error.message || error); + const status = error.response?.status; + const shouldRetry = this.retryStrategy.shouldRetry(error, attempt, this.retries); + + if (shouldRetry) { + const delayMs = this.retryStrategy.getDelay(error, attempt); + this.debug('Error: %d. Retrying in %dms (attempt %d).', status, delayMs, attempt); + + attempt++; + return new Promise((resolve) => setTimeout(resolve, delayMs)).then(retryer); + } else { + return Promise.reject(error); + } + } + ); + }; + + return retryer(); } } diff --git a/packages/sitecore-jss/src/graphql/index.ts b/packages/sitecore-jss/src/graphql/index.ts index 46772b1e84..ad00fb202e 100644 --- a/packages/sitecore-jss/src/graphql/index.ts +++ b/packages/sitecore-jss/src/graphql/index.ts @@ -1,5 +1,7 @@ export { getAppRootId, AppRootQueryResult } from './app-root-query'; export { + RetryStrategy, + DefaultRetryStrategy, GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index fd3dab86f4..260b8c9688 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -1,4 +1,8 @@ -import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLRequestClientConfig, +} from '../graphql-request-client'; import { SitecoreTemplateId } from '../constants'; import { DictionaryPhrases, DictionaryServiceBase } from './dictionary-service'; import { CacheOptions } from '../cache-client'; @@ -48,7 +52,10 @@ const query = /* GraphQL */ ` /** * Configuration options for @see GraphQLDictionaryService instances */ -export interface GraphQLDictionaryServiceConfig extends SearchServiceConfig, CacheOptions { +export interface GraphQLDictionaryServiceConfig + extends SearchServiceConfig, + CacheOptions, + Pick { /** * The URL of the graphQL endpoint. */ @@ -155,6 +162,8 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.dictionary, + retries: this.options.retries, + retryStrategy: this.options.retryStrategy, }); } } diff --git a/packages/sitecore-jss/src/index.ts b/packages/sitecore-jss/src/index.ts index 729e1d4c11..40588c76a8 100644 --- a/packages/sitecore-jss/src/index.ts +++ b/packages/sitecore-jss/src/index.ts @@ -5,9 +5,12 @@ import * as constants from './constants'; export { default as debug, Debugger } from './debug'; export { HttpDataFetcher, HttpResponse, fetchData } from './data-fetcher'; export { + RetryStrategy, + DefaultRetryStrategy, GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, } from './graphql-request-client'; export { AxiosDataFetcher, AxiosDataFetcherConfig } from './axios-fetcher'; +export { ClientError } from 'graphql-request'; export { constants }; diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 4adaeac927..418e38f32f 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -1,9 +1,14 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; -import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLRequestClientConfig, +} from '../graphql-request-client'; import debug from '../debug'; -export type GraphQLLayoutServiceConfig = { +export interface GraphQLLayoutServiceConfig + extends Pick { /** * Your Graphql endpoint */ @@ -28,7 +33,7 @@ export type GraphQLLayoutServiceConfig = { * layout(site:"${siteName}", routePath:"${itemPath}", language:"${language}") */ formatLayoutQuery?: (siteName: string, itemPath: string, locale?: string) => string; -}; +} export class GraphQLLayoutService extends LayoutServiceBase { private graphQLClient: GraphQLClient; @@ -63,7 +68,7 @@ export class GraphQLLayoutService extends LayoutServiceBase { // If `rendered` is empty -> not found return ( - data?.layout?.item.rendered || { + data?.layout?.item?.rendered || { sitecore: { context: { pageEditing: false, language }, route: null }, } ); @@ -79,6 +84,8 @@ export class GraphQLLayoutService extends LayoutServiceBase { return new GraphQLRequestClient(this.serviceConfig.endpoint, { apiKey: this.serviceConfig.apiKey, debugger: debug.layout, + retries: this.serviceConfig.retries, + retryStrategy: this.serviceConfig.retryStrategy, }); } diff --git a/packages/sitecore-jss/src/site/graphql-error-pages-service.ts b/packages/sitecore-jss/src/site/graphql-error-pages-service.ts index 9d12d363e7..4e62ed8966 100644 --- a/packages/sitecore-jss/src/site/graphql-error-pages-service.ts +++ b/packages/sitecore-jss/src/site/graphql-error-pages-service.ts @@ -1,4 +1,4 @@ -import { GraphQLClient, GraphQLRequestClient } from '../graphql'; +import { GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig } from '../graphql'; import { siteNameError } from '../constants'; import debug from '../debug'; @@ -16,7 +16,8 @@ const defaultQuery = /* GraphQL */ ` } `; -export type GraphQLErrorPagesServiceConfig = { +export interface GraphQLErrorPagesServiceConfig + extends Pick { /** * Your Graphql endpoint */ @@ -33,7 +34,7 @@ export type GraphQLErrorPagesServiceConfig = { * The language */ language: string; -}; +} /** * Object model of Error Pages result @@ -98,6 +99,8 @@ export class GraphQLErrorPagesService { return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.errorpages, + retries: this.options.retries, + retryStrategy: this.options.retryStrategy, }); } } diff --git a/packages/sitecore-jss/src/utils/timeout-promise.ts b/packages/sitecore-jss/src/utils/timeout-promise.ts new file mode 100644 index 0000000000..b14750bf04 --- /dev/null +++ b/packages/sitecore-jss/src/utils/timeout-promise.ts @@ -0,0 +1,31 @@ +๏ปฟ/** + * A helper to assign timeouts to fetch or other promises + * Useful in nextjs middleware until fetch.signal is fully supported by Vercel edge functions + */ +export default class TimeoutPromise { + timeoutId: NodeJS.Timeout | undefined; + + constructor(private timeout: number) { + this.timeoutId = undefined; + } + + /** + * Creates a timeout promise + */ + get start(): Promise { + return new Promise((_, reject) => { + this.timeoutId = setTimeout(() => { + const abortError = new Error(`Request timed out, timeout of ${this.timeout}ms is exceeded`); + abortError.name = 'AbortError'; + reject(abortError); + }, this.timeout); + }); + } + + /** + * Clears the timeout from timeout promise + */ + clear() { + this.timeoutId && clearTimeout(this.timeoutId); + } +} diff --git a/yarn.lock b/yarn.lock index 8fb62b5f92..71de413635 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3645,6 +3645,33 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^2.0.0": + version: 2.0.0 + resolution: "@sinonjs/commons@npm:2.0.0" + dependencies: + type-detect: 4.0.8 + checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" + dependencies: + type-detect: 4.0.8 + checksum: a7c3e7cc612352f4004873747d9d8b2d4d90b13a6d483f685598c945a70e734e255f1ca5dc49702515533c403b32725defff148177453b3f3915bcb60e9d4601 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^11.2.2": + version: 11.2.2 + resolution: "@sinonjs/fake-timers@npm:11.2.2" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 68c29b0e1856fdc280df03ddbf57c726420b78e9f943a241b471edc018fb14ff36fdc1daafd6026cba08c3c7f50c976fb7ae11b88ff44cd7f609692ca7d25158 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^6.0.0, @sinonjs/fake-timers@npm:^6.0.1": version: 6.0.1 resolution: "@sinonjs/fake-timers@npm:6.0.1" @@ -3715,6 +3742,17 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^8.0.0": + version: 8.0.0 + resolution: "@sinonjs/samsam@npm:8.0.0" + dependencies: + "@sinonjs/commons": ^2.0.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: 95e40d0bb9f7288e27c379bee1b03c3dc51e7e78b9d5ea6aef66a690da7e81efc4715145b561b449cefc5361a171791e3ce30fb1a46ab247d4c0766024c60a60 + languageName: node + linkType: hard + "@sinonjs/text-encoding@npm:^0.7.1": version: 0.7.1 resolution: "@sinonjs/text-encoding@npm:0.7.1" @@ -3722,6 +3760,13 @@ __metadata: languageName: node linkType: hard +"@sinonjs/text-encoding@npm:^0.7.2": + version: 0.7.2 + resolution: "@sinonjs/text-encoding@npm:0.7.2" + checksum: fe690002a32ba06906cf87e2e8fe84d1590294586f2a7fd180a65355b53660c155c3273d8011a5f2b77209b819aa7306678ae6e4aea0df014bd7ffd4bbbcf1ab + languageName: node + linkType: hard + "@sitecore-jss/sitecore-jss-angular-schematics@workspace:packages/sitecore-jss-angular-schematics": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-angular-schematics@workspace:packages/sitecore-jss-angular-schematics" @@ -3750,7 +3795,7 @@ __metadata: "@angular/platform-browser": ~11.2.6 "@angular/platform-browser-dynamic": ~11.2.6 "@angular/router": ~11.2.6 - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/jasmine": ^3.4.1 "@types/node": ^14.14.35 codelyzer: ^6.0.1 @@ -3781,7 +3826,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-cli@workspace:packages/sitecore-jss-cli" dependencies: - "@sitecore-jss/sitecore-jss-dev-tools": ^20.1.0 + "@sitecore-jss/sitecore-jss-dev-tools": 20.3.3 "@types/chai": ^4.2.3 "@types/cross-spawn": ^6.0.0 "@types/mocha": ^5.2.7 @@ -3813,11 +3858,11 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-dev-tools@^20.1.0, @sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools": +"@sitecore-jss/sitecore-jss-dev-tools@20.3.3, @sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-dev-tools@workspace:packages/sitecore-jss-dev-tools" dependencies: - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/chai": ^4.2.3 "@types/chokidar": ^2.1.3 "@types/del": ^4.0.0 @@ -3871,11 +3916,11 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-forms@^20.1.0, @sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms": +"@sitecore-jss/sitecore-jss-forms@20.3.3, @sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-forms@workspace:packages/sitecore-jss-forms" dependencies: - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/chai": ^4.1.6 "@types/chai-string": ^1.4.1 "@types/lodash.unescape": ^4.0.4 @@ -3898,9 +3943,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-nextjs@workspace:packages/sitecore-jss-nextjs" dependencies: - "@sitecore-jss/sitecore-jss": ^20.1.0 - "@sitecore-jss/sitecore-jss-dev-tools": ^20.1.0 - "@sitecore-jss/sitecore-jss-react": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 + "@sitecore-jss/sitecore-jss-dev-tools": 20.3.3 + "@sitecore-jss/sitecore-jss-react": 20.3.3 "@types/chai": ^4.2.2 "@types/chai-as-promised": ^7.1.3 "@types/chai-string": ^1.4.2 @@ -3948,6 +3993,7 @@ __metadata: resolution: "@sitecore-jss/sitecore-jss-proxy@workspace:packages/sitecore-jss-proxy" dependencies: "@types/chai": ^4.1.6 + "@types/express": ^4.17.17 "@types/http-proxy-middleware": ^0.19.3 "@types/mocha": ^5.2.5 "@types/node": ^12.7.11 @@ -3969,7 +4015,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-react-forms@workspace:packages/sitecore-jss-react-forms" dependencies: - "@sitecore-jss/sitecore-jss-forms": ^20.1.0 + "@sitecore-jss/sitecore-jss-forms": 20.3.3 "@types/chai": ^4.2.11 "@types/enzyme": ^3.10.5 "@types/mocha": ^7.0.2 @@ -4009,7 +4055,7 @@ __metadata: "@babel/plugin-proposal-export-default-from": ^7.5.2 "@babel/preset-env": ^7.6.2 "@babel/preset-typescript": ^7.6.0 - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/jest": ^24.0.18 "@types/prop-types": ^15.7.3 "@types/react": ^16.9.5 @@ -4039,11 +4085,11 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss-react@^20.1.0, @sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react": +"@sitecore-jss/sitecore-jss-react@20.3.3, @sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss-react@workspace:packages/sitecore-jss-react" dependencies: - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/chai": ^4.2.2 "@types/chai-string": ^1.4.2 "@types/deep-equal": ^1.0.1 @@ -4116,7 +4162,7 @@ __metadata: resolution: "@sitecore-jss/sitecore-jss-vue@workspace:packages/sitecore-jss-vue" dependencies: "@babel/core": ^7.16.0 - "@sitecore-jss/sitecore-jss": ^20.1.0 + "@sitecore-jss/sitecore-jss": 20.3.3 "@types/jest": ^26.0.23 "@vue/compiler-dom": ^3.2.21 "@vue/compiler-sfc": ^3.0.11 @@ -4138,7 +4184,7 @@ __metadata: languageName: unknown linkType: soft -"@sitecore-jss/sitecore-jss@^20.1.0, @sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss": +"@sitecore-jss/sitecore-jss@20.3.3, @sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss": version: 0.0.0-use.local resolution: "@sitecore-jss/sitecore-jss@workspace:packages/sitecore-jss" dependencies: @@ -4150,7 +4196,8 @@ __metadata: "@types/memory-cache": ^0.2.1 "@types/mocha": ^5.2.7 "@types/node": ^16.11.6 - "@types/url-parse": 1.4.3 + "@types/sinon": ^17.0.3 + "@types/url-parse": 1.4.8 axios: ^0.21.1 chai: ^4.2.0 chai-spies: ^1.0.0 @@ -4166,6 +4213,7 @@ __metadata: mocha: ^8.1.3 nock: ^13.0.5 nyc: ^15.1.0 + sinon: ^17.0.1 ts-node: ^8.4.1 tslib: ^1.10.0 typescript: ~4.3.5 @@ -4500,6 +4548,18 @@ __metadata: languageName: node linkType: hard +"@types/express-serve-static-core@npm:^4.17.33": + version: 4.17.37 + resolution: "@types/express-serve-static-core@npm:4.17.37" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + "@types/send": "*" + checksum: 2dab1380e45eb44e56ecc1be1c42c4b897364d2f2a08e03ca28fbcb1e6866e390217385435813711c046f9acd684424d088855dc32825d5cbecf72c60ecd037f + languageName: node + linkType: hard + "@types/express@npm:*, @types/express@npm:^4.17.1": version: 4.17.13 resolution: "@types/express@npm:4.17.13" @@ -4524,6 +4584,18 @@ __metadata: languageName: node linkType: hard +"@types/express@npm:^4.17.17": + version: 4.17.20 + resolution: "@types/express@npm:4.17.20" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.33 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: bf8a97d283128e5129f9ccabbeef728ff3f0484465e0ae74a304bd0588fa6cb715ae68845650caba9a641944b7791ba125d02ddbd47a7e62aaefdd036570c6c5 + languageName: node + linkType: hard + "@types/fs-extra@npm:^8.0.0": version: 8.1.2 resolution: "@types/fs-extra@npm:8.1.2" @@ -4989,6 +5061,16 @@ __metadata: languageName: node linkType: hard +"@types/send@npm:*": + version: 0.17.2 + resolution: "@types/send@npm:0.17.2" + dependencies: + "@types/mime": ^1 + "@types/node": "*" + checksum: 1ff5b1bd6a4f6fdc6402c7024781ff5dbd0e1f51a43c69529fb67c710943c7416d2f0d77c57c70fccf6616f25f838f32f960284526e408d4edae2e91e1fce95a + languageName: node + linkType: hard + "@types/serve-static@npm:*": version: 1.13.10 resolution: "@types/serve-static@npm:1.13.10" @@ -5037,6 +5119,15 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^17.0.3": + version: 17.0.3 + resolution: "@types/sinon@npm:17.0.3" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: c8e9956d9c90fe1ec1cc43085ae48897f93f9ea86e909ab47f255ea71f5229651faa070393950fb6923aef426c84e92b375503f9f8886ef44668b82a8ee49e9a + languageName: node + linkType: hard + "@types/sinon@npm:^7.5.0": version: 7.5.2 resolution: "@types/sinon@npm:7.5.2" @@ -5136,10 +5227,10 @@ __metadata: languageName: node linkType: hard -"@types/url-parse@npm:1.4.3": - version: 1.4.3 - resolution: "@types/url-parse@npm:1.4.3" - checksum: d8e8c3219cb7ce5d275b2b93751494ddce9ee9038021f98c1e8e24fe5523b553faae5de2bcc3523d586d8dadc5d259c8e53a74f004f517de32fb53c1280426ff +"@types/url-parse@npm:1.4.8": + version: 1.4.8 + resolution: "@types/url-parse@npm:1.4.8" + checksum: 44a5e96ed4b579c43750f3578bfa9165f97a359c3b2a85ee126e9c16db964f6ea105e152afd3d1adbd15850a8b812043215f3820112177bb4255a60b432dbd85 languageName: node linkType: hard @@ -10681,6 +10772,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.1.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "diffable-html@npm:^4.1.0": version: 4.1.0 resolution: "diffable-html@npm:4.1.0" @@ -16443,6 +16541,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^6.2.0": + version: 6.2.0 + resolution: "just-extend@npm:6.2.0" + checksum: 022024d6f687c807963b97a24728a378799f7e4af7357d1c1f90dedb402943d5c12be99a5136654bed8362c37a358b1793feaad3366896f239a44e17c5032d86 + languageName: node + linkType: hard + "karma-chrome-launcher@npm:^3.1.0": version: 3.1.0 resolution: "karma-chrome-launcher@npm:3.1.0" @@ -18667,6 +18772,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.5": + version: 5.1.9 + resolution: "nise@npm:5.1.9" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sinonjs/text-encoding": ^0.7.2 + just-extend: ^6.2.0 + path-to-regexp: ^6.2.1 + checksum: ab9fd6eabc98170f18aef6c9567983145c1dc62c7aef46eda0fea754083316c1f0f9b2c32e9b4bfdd25122276d670293596ed672b54dd1ffa8eb58b56a30ea95 + languageName: node + linkType: hard + "nocache@npm:^2.1.0": version: 2.1.0 resolution: "nocache@npm:2.1.0" @@ -20010,6 +20128,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^6.2.1": + version: 6.2.1 + resolution: "path-to-regexp@npm:6.2.1" + checksum: f0227af8284ea13300f4293ba111e3635142f976d4197f14d5ad1f124aebd9118783dd2e5f1fe16f7273743cc3dbeddfb7493f237bb27c10fdae07020cc9b698 + languageName: node + linkType: hard + "path-type@npm:^1.0.0": version: 1.1.0 resolution: "path-type@npm:1.1.0" @@ -22680,6 +22805,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^17.0.1": + version: 17.0.1 + resolution: "sinon@npm:17.0.1" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^11.2.2 + "@sinonjs/samsam": ^8.0.0 + diff: ^5.1.0 + nise: ^5.1.5 + supports-color: ^7.2.0 + checksum: a807c2997d6eabdcaa4409df9fd9816a3e839f96d7e5d76610a33f5e1b60cf37616c6288f0f580262da17ea4ee626c6d1600325bf423e30c5a7f0d9a203e26c0 + languageName: node + linkType: hard + "sinon@npm:^7.5.0": version: 7.5.0 resolution: "sinon@npm:7.5.0"