diff --git a/.eslintrc.js b/.eslintrc.js index 13d2f3b..9eba935 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,8 @@ module.exports = { 'extends': [ 'eslint:recommended', 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended' + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended' ], 'parser': '@typescript-eslint/parser', 'parserOptions': { @@ -28,7 +29,7 @@ module.exports = { 'eol-last': ['error', 'always'], 'keyword-spacing': 'error', 'no-trailing-spaces': 'error', - 'space-before-function-paren': ['error', {'named': 'never'}], + 'space-before-function-paren': ['error', { 'named': 'never' }], 'react/display-name': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-inferrable-types': 'off', @@ -39,6 +40,11 @@ module.exports = { 'destructuring': 'all' }] }, + 'settings': { + 'react': { + 'version': '16.3.0' // minimum supported version of React + } + }, 'overrides': [{ 'files': ['src/**/*.ts', 'src/**/*.tsx'], 'excludedFiles': ['src/**/__tests__/**'], @@ -52,7 +58,7 @@ module.exports = { }, 'rules': { 'no-restricted-syntax': ['error', 'ForOfStatement', 'ForInStatement'], - 'compat/compat': ['error', 'defaults, not ie < 11'], + 'compat/compat': ['error', 'defaults, ie 11'], 'no-throw-literal': 'error', 'import/no-self-import': 'error', 'import/no-default-export': 'error', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6a4fe1..bc3a8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: npm run test -- --coverage - name: npm build - run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") BUILD_COMMIT=${{ github.sha }} npm run build + run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") npm run build - name: Set VERSION env run: echo "VERSION=$(cat package.json | jq -r .version)" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index 568cd2b..204ddd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ /lib /es /umd +/types /coverage .scannerwork diff --git a/CHANGES.txt b/CHANGES.txt index 03774ac..053cfd6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,19 @@ +1.10.0 (November 16, 2023) + - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added a new `flagSets` prop to the `SplitTreatments` component and `flagSets` option to the `useSplitTreatments` hook options object, to support evaluating flags in given flag set/s. Either `names` or `flagSets` must be provided to the component and hook. If both are provided, `names` will be used. + - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views. + - Added new `useSplitClient`, `useSplitTreatments` and `useSplitManager` hooks as replacements for the now deprecated `useClient`, `useTreatments` and `useManager` hooks. + - These new hooks return the Split context object along with the SDK client, treatments and manager respectively, enabling direct access to status properties like `isReady`, eliminating the need for using the `useContext` hook or the client's `ready` promise. + - `useSplitClient` and `useSplitTreatments` accept an options object as parameter, which support the same arguments as their predecessors, with additional boolean options for controlling re-rendering: `updateOnSdkReady`, `updateOnSdkReadyFromCache`, `updateOnSdkTimedout`, and `updateOnSdkUpdate`. + - `useSplitTreatments` optimizes feature flag evaluations by using the `useMemo` hook to memoize `getTreatmentsWithConfig` method calls from the SDK. This avoids re-evaluating feature flags when the hook is called with the same options and the feature flag definitions have not changed. + - They fixed a bug in the deprecated `useClient` and `useTreatments` hooks, which caused them to not re-render and re-evaluate feature flags when they access a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event). + - Added TypeScript types and interfaces to the library index exports, allowing them to be imported from the library index, e.g., `import type { ISplitFactoryProps } from '@splitsoftware/splitio-react'` (Related to issue https://github.com/splitio/react-client/issues/162). + - Updated type declarations of the library components to not restrict the type of the `children` prop to ReactElement, allowing to pass any valid ReactNode value (Related to issue https://github.com/splitio/react-client/issues/164). + - Updated linter and other dependencies for vulnerability fixes. + - Bugfixing - Removed conditional code within hooks to adhere to the rules of hooks and prevent React warnings. Previously, this code checked for the availability of the hooks API (available in React version 16.8.0 or above) and logged an error message. Now, using hooks with React versions below 16.8.0 will throw an error. + - Bugfixing - Updated `useClient` and `useTreatments` hooks to re-render and re-evaluate feature flags when they consume a different SDK client than the context and its status updates (i.e., when it emits SDK_READY or other event). + 1.9.0 (July 18, 2023) - Updated some transitive dependencies for vulnerability fixes. - Updated @splitsoftware/splitio package to version 10.23.0 that includes: diff --git a/README.md b/README.md index 18ce0c7..a171ed1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w This SDK is compatible with React 16.3.0 and above, since it uses [React Context API](https://reactjs.org/docs/context.html). -Some features, such as `useClient` and `useManager`, use [React Hooks API](https://reactjs.org/docs/hooks-overview.html) that requires React 16.8.0 or later. +Some features, such as `useSplitClient` and `useSplitTreatments`, use [React Hooks API](https://reactjs.org/docs/hooks-overview.html) that requires React 16.8.0 or later. ## Getting started Below is a simple example that describes the instantiation and most basic usage of our SDK: @@ -20,7 +20,7 @@ Below is a simple example that describes the instantiation and most basic usage import React from 'react'; // Import SDK functions -import { SplitFactory, SplitTreatments } from '@splitsoftware/splitio-react'; +import { SplitFactory, useSplitTreatments } from '@splitsoftware/splitio-react'; // Define your config object const CONFIG = { @@ -30,25 +30,27 @@ const CONFIG = { } }; -function MyReactComponent() { +function MyComponent() { + // Evaluate feature flags with useSplitTreatments hook + const { treatments: { FEATURE_FLAG_NAME }, isReady } = useSplitTreatments({ names: ['FEATURE_FLAG_NAME'] }); + + // Check SDK readiness using isReady prop + if (!isReady) return
Loading SDK ...
; + + if (FEATURE_FLAG_NAME.treatment === 'on') { + // return JSX for on treatment + } else if (FEATURE_FLAG_NAME.treatment === 'off') { + // return JSX for off treatment + } else { + // return JSX for control treatment + }; +} + +function MyApp() { return ( - /* Use SplitFactory to instantiate the SDK and makes it available to nested components */ + // Use SplitFactory to instantiate the SDK and makes it available to nested components - {/* Evaluate feature flags with SplitTreatments component */} - - {({ treatments: { FEATURE_FLAG_NAME }, isReady }) => { - // Check SDK readiness using isReady prop - if (!isReady) - return
Loading SDK ...
; - if (FEATURE_FLAG_NAME.treatment === 'on') { - // return JSX for on treatment - } else if (FEATURE_FLAG_NAME.treatment === 'off') { - // return JSX for off treatment - } else { - // return JSX for control treatment - } - }} -
+
); } diff --git a/package-lock.json b/package-lock.json index cd658ee..f1c9717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.0", + "version": "1.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.9.0", + "version": "1.10.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -22,12 +22,13 @@ "@types/react-dom": "^18.0.0", "@types/react-test-renderer": "^18.0.0", "@types/shallowequal": "^1.1.1", - "@typescript-eslint/eslint-plugin": "^5.55.0", - "@typescript-eslint/parser": "^5.55.0", - "eslint": "^8.36.0", - "eslint-plugin-compat": "^4.1.2", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "eslint": "^8.48.0", + "eslint-plugin-compat": "^4.2.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", @@ -46,19 +47,29 @@ "react": ">=16.3.0" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@adobe/css-tools": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -122,28 +133,20 @@ } }, "node_modules/@babel/generator": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", - "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.15.4", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.15.4", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", @@ -171,39 +174,35 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", - "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "dependencies": { - "@babel/helper-get-function-arity": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-get-function-arity": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", - "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/types": "^7.15.4" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", - "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -301,21 +300,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", - "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -345,13 +353,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -359,9 +367,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", - "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -545,32 +553,33 @@ } }, "node_modules/@babel/template": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", - "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", - "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -579,12 +588,13 @@ } }, "node_modules/@babel/types": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", - "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.14.9", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -607,9 +617,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -622,23 +632,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", - "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -660,9 +670,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -736,18 +746,18 @@ } }, "node_modules/@eslint/js": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", - "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -1468,19 +1478,19 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mdn/browser-compat-data": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.3.4.tgz", - "integrity": "sha512-zzSR4lk5i9YSSQtQwZpSYK84xrSCPtrtU6F6WkYcchOYwkOiJXezpiaS4GnKFmafFcxCXNI2wPrsCoj2OO0VrA==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.3.15.tgz", + "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, "node_modules/@nodelib/fs.scandir": { @@ -1537,11 +1547,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "dependencies": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1559,9 +1569,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "dependencies": { "tslib": "^2.3.1" }, @@ -1779,12 +1789,6 @@ "node": ">= 6" } }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -1912,9 +1916,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "node_modules/@types/json5": { @@ -1982,9 +1986,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", "dev": true }, "node_modules/@types/shallowequal": { @@ -2024,32 +2028,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", - "integrity": "sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", + "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/type-utils": "5.55.0", - "@typescript-eslint/utils": "5.55.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/type-utils": "6.6.0", + "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2073,25 +2078,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.55.0.tgz", - "integrity": "sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz", + "integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2100,16 +2106,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz", - "integrity": "sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", + "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0" + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2117,25 +2123,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz", - "integrity": "sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", + "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.55.0", - "@typescript-eslint/utils": "5.55.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/utils": "6.6.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2144,12 +2150,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.55.0.tgz", - "integrity": "sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", + "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2157,21 +2163,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz", - "integrity": "sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", + "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2199,29 +2205,28 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.55.0.tgz", - "integrity": "sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", + "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { @@ -2240,16 +2245,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz", - "integrity": "sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", + "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.55.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.6.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2457,9 +2462,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2982,9 +2987,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "funding": [ { @@ -2994,13 +2999,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" }, "bin": { "browserslist": "cli.js" @@ -3092,9 +3101,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001516", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz", - "integrity": "sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==", + "version": "1.0.30001532", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz", + "integrity": "sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==", "dev": true, "funding": [ { @@ -3545,9 +3554,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.330", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.330.tgz", - "integrity": "sha512-PqyefhybrVdjAJ45HaPLtuVaehiSw7C3ya0aad+rvmV53IVyXmYRk3pwIOb2TxTDTnmgQdn46NjMMaysx79/6Q==", + "version": "1.4.513", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", + "integrity": "sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==", "dev": true }, "node_modules/emittery": { @@ -3846,27 +3855,27 @@ } }, "node_modules/eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", - "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.1", - "@eslint/js": "8.36.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -3874,22 +3883,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -3949,19 +3955,18 @@ } }, "node_modules/eslint-plugin-compat": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-4.1.4.tgz", - "integrity": "sha512-RxySWBmzfIROLFKgeJBJue2BU/6vM2KJWXWAUq+oW4QtrsZXRxbjgxmO1OfF3sHcRuuIenTS/wgo3GyUWZF24w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz", + "integrity": "sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w==", "dev": true, "dependencies": { - "@mdn/browser-compat-data": "^5.2.47", - "@tsconfig/node14": "^1.0.3", + "@mdn/browser-compat-data": "^5.3.13", "ast-metadata-inferer": "^0.8.0", - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001473", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001524", "find-up": "^5.0.0", - "lodash.memoize": "4.1.2", - "semver": "7.3.8" + "lodash.memoize": "^4.1.2", + "semver": "^7.5.4" }, "engines": { "node": ">=14.x" @@ -4032,9 +4037,9 @@ } }, "node_modules/eslint-plugin-compat/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4146,6 +4151,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4219,12 +4236,15 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-styles": { @@ -4309,9 +4329,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4319,6 +4339,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/estraverse": { @@ -4370,22 +4393,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4439,17 +4446,17 @@ } }, "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -4503,15 +4510,6 @@ "node": ">= 0.8.0" } }, - "node_modules/eslint/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eslint/node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4585,14 +4583,14 @@ } }, "node_modules/espree": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", - "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4745,9 +4743,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5118,10 +5116,10 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has": { @@ -7892,16 +7890,6 @@ "node": ">=8" } }, - "node_modules/js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8352,12 +8340,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -8424,9 +8406,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "node_modules/normalize-package-data": { @@ -10038,6 +10020,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "27.0.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.5.tgz", @@ -10155,30 +10149,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type-check": { "version": "0.3.2", @@ -10276,9 +10249,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "funding": [ { @@ -10288,6 +10261,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { @@ -10295,7 +10272,7 @@ "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -10979,19 +10956,26 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@adobe/css-tools": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", - "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", + "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", "dev": true }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -11038,22 +11022,15 @@ } }, "@babel/generator": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", - "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.15.4", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" } }, "@babel/helper-compilation-targets": { @@ -11076,33 +11053,29 @@ } } }, - "@babel/helper-function-name": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", - "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4" - } + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true }, - "@babel/helper-get-function-arity": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", - "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", - "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -11176,18 +11149,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", - "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.22.5" } }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -11208,20 +11187,20 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.15.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.7.tgz", - "integrity": "sha512-rycZXvQ+xS9QyIcJ9HXeDWf1uxqlbVFAUq0Rq0dbc50Zb/+wUe/ehyfzGfm9KZZF0kBejYgxltBXocP+gKdL2g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -11351,40 +11330,42 @@ } }, "@babel/template": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", - "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", - "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", - "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.9", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -11401,29 +11382,29 @@ "dev": true }, "@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" } }, "@eslint-community/regexpp": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", - "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", - "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.0", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -11439,9 +11420,9 @@ "dev": true }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -11490,15 +11471,15 @@ } }, "@eslint/js": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", - "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -12048,19 +12029,19 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@mdn/browser-compat-data": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.3.4.tgz", - "integrity": "sha512-zzSR4lk5i9YSSQtQwZpSYK84xrSCPtrtU6F6WkYcchOYwkOiJXezpiaS4GnKFmafFcxCXNI2wPrsCoj2OO0VrA==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.3.15.tgz", + "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, "@nodelib/fs.scandir": { @@ -12108,11 +12089,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.23.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.23.0.tgz", - "integrity": "sha512-b9mn2B8U1DfpDETsaWH4T1jhkn8XWwlAVsHwhgIRhCgBs0B9wm4SsXx+OWHZ5bl5uvEwtFFIAtCU58j/irnqpw==", + "version": "10.24.0-beta", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.24.0-beta.tgz", + "integrity": "sha512-SpYsWoZKLNXtQjQ5xJLJ2BaLZFZBSH3vRJXuYgf1BpsSv6n0s3Lc1NJ4gDI0zRCvGjWEfLOz6VrdBM0klRao8w==", "requires": { - "@splitsoftware/splitio-commons": "1.9.0", + "@splitsoftware/splitio-commons": "1.10.1-rc.3", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -12124,9 +12105,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.9.0.tgz", - "integrity": "sha512-2QoWvGOk/LB+q2TglqGD0w/hcUKG4DZwBSt5NtmT1ODGiLyCf2wbcfG/eBR9QlUnLisJ62dj6vOQsVUB2kiHOw==", + "version": "1.10.1-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.10.1-rc.3.tgz", + "integrity": "sha512-eqJxAMtqFK7fXFKL8gMGfRsMBdxrYI9tIGUHHpY1NcyeKkn4OWqAOZMhX6z2qLdBArzHi34Li0Lb72o+Bh1Tqg==", "requires": { "tslib": "^2.3.1" } @@ -12283,12 +12264,6 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -12416,9 +12391,9 @@ } }, "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, "@types/json5": { @@ -12486,9 +12461,9 @@ "dev": true }, "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==", "dev": true }, "@types/shallowequal": { @@ -12528,21 +12503,22 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.55.0.tgz", - "integrity": "sha512-IZGc50rtbjk+xp5YQoJvmMPmJEYoC53SiKPXyqWfv15XoD2Y5Kju6zN0DwlmaGJp1Iw33JsWJcQ7nw0lGCGjVg==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.6.0.tgz", + "integrity": "sha512-CW9YDGTQnNYMIo5lMeuiIG08p4E0cXrXTbcZ2saT/ETE7dWUrNxlijsQeU04qAAKkILiLzdQz+cGFxCJjaZUmA==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/type-utils": "5.55.0", - "@typescript-eslint/utils": "5.55.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/type-utils": "6.6.0", + "@typescript-eslint/utils": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { @@ -12557,58 +12533,59 @@ } }, "@typescript-eslint/parser": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.55.0.tgz", - "integrity": "sha512-ppvmeF7hvdhUUZWSd2EEWfzcFkjJzgNQzVST22nzg958CR+sphy8A6K7LXQZd6V75m1VKjp+J4g/PCEfSCmzhw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.6.0.tgz", + "integrity": "sha512-setq5aJgUwtzGrhW177/i+DMLqBaJbdwGj2CPIVFFLE0NCliy5ujIdLHd2D1ysmlmsjdL2GWW+hR85neEfc12w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.55.0.tgz", - "integrity": "sha512-OK+cIO1ZGhJYNCL//a3ROpsd83psf4dUJ4j7pdNVzd5DmIk+ffkuUIX2vcZQbEW/IR41DYsfJTB19tpCboxQuw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.6.0.tgz", + "integrity": "sha512-pT08u5W/GT4KjPUmEtc2kSYvrH8x89cVzkA0Sy2aaOUIw6YxOIjA8ilwLr/1fLjOedX1QAuBpG9XggWqIIfERw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0" + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0" } }, "@typescript-eslint/type-utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.55.0.tgz", - "integrity": "sha512-ObqxBgHIXj8rBNm0yh8oORFrICcJuZPZTqtAFh0oZQyr5DnAHZWfyw54RwpEEH+fD8suZaI0YxvWu5tYE/WswA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.6.0.tgz", + "integrity": "sha512-8m16fwAcEnQc69IpeDyokNO+D5spo0w1jepWWY2Q6y5ZKNuj5EhVQXjtVAeDDqvW6Yg7dhclbsz6rTtOvcwpHg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.55.0", - "@typescript-eslint/utils": "5.55.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "@typescript-eslint/utils": "6.6.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.55.0.tgz", - "integrity": "sha512-M4iRh4AG1ChrOL6Y+mETEKGeDnT7Sparn6fhZ5LtVJF1909D5O4uqK+C5NPbLmpfZ0XIIxCdwzKiijpZUOvOug==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.6.0.tgz", + "integrity": "sha512-CB6QpJQ6BAHlJXdwUmiaXDBmTqIE2bzGTDLADgvqtHWuhfNP3rAOK7kAgRMAET5rDRr9Utt+qAzRBdu3AhR3sg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.55.0.tgz", - "integrity": "sha512-I7X4A9ovA8gdpWMpr7b1BN9eEbvlEtWhQvpxp/yogt48fy9Lj3iE3ild/1H3jKBBIYj5YYJmS2+9ystVhC7eaQ==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.6.0.tgz", + "integrity": "sha512-hMcTQ6Al8MP2E6JKBAaSxSVw5bDhdmbCEhGW/V8QXkb9oNsFkA4SBuOMYVPxD3jbtQ4R/vSODBsr76R6fP3tbA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/visitor-keys": "5.55.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/visitor-keys": "6.6.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "semver": { @@ -12623,19 +12600,18 @@ } }, "@typescript-eslint/utils": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.55.0.tgz", - "integrity": "sha512-FkW+i2pQKcpDC3AY6DU54yl8Lfl14FVGYDgBTyGKB75cCwV3KpkpTMFi9d9j2WAJ4271LR2HeC5SEWF/CZmmfw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.6.0.tgz", + "integrity": "sha512-mPHFoNa2bPIWWglWYdR0QfY9GN0CfvvXX1Sv6DlSTive3jlMTUy+an67//Gysc+0Me9pjitrq0LJp0nGtLgftw==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.55.0", - "@typescript-eslint/types": "5.55.0", - "@typescript-eslint/typescript-estree": "5.55.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.6.0", + "@typescript-eslint/types": "6.6.0", + "@typescript-eslint/typescript-estree": "6.6.0", + "semver": "^7.5.4" }, "dependencies": { "semver": { @@ -12650,13 +12626,13 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.55.0.tgz", - "integrity": "sha512-q2dlHHwWgirKh1D3acnuApXG+VNXpEY5/AwRxDVuEQpxWaB0jCDe0jFMVMALJ3ebSfuOVE8/rMS+9ZOYGg1GWw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.6.0.tgz", + "integrity": "sha512-L61uJT26cMOfFQ+lMZKoJNbAEckLe539VhTxiGHrWl5XSKQgA0RTBZJW2HFPy5T0ZvPVSD93QsrTKDkfNwJGyQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.55.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.6.0", + "eslint-visitor-keys": "^3.4.1" } }, "@webassemblyjs/ast": { @@ -12847,9 +12823,9 @@ "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-globals": { @@ -13245,15 +13221,15 @@ "dev": true }, "browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" } }, "bs-logger": { @@ -13321,9 +13297,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001516", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001516.tgz", - "integrity": "sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==", + "version": "1.0.30001532", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz", + "integrity": "sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==", "dev": true }, "chalk": { @@ -13687,9 +13663,9 @@ } }, "electron-to-chromium": { - "version": "1.4.330", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.330.tgz", - "integrity": "sha512-PqyefhybrVdjAJ45HaPLtuVaehiSw7C3ya0aad+rvmV53IVyXmYRk3pwIOb2TxTDTnmgQdn46NjMMaysx79/6Q==", + "version": "1.4.513", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", + "integrity": "sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==", "dev": true }, "emittery": { @@ -13934,27 +13910,27 @@ } }, "eslint": { - "version": "8.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", - "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.1", - "@eslint/js": "8.36.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -13962,22 +13938,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -14039,9 +14012,9 @@ "dev": true }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -14079,16 +14052,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -14127,17 +14090,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "p-limit": { @@ -14170,12 +14133,6 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -14269,19 +14226,18 @@ } }, "eslint-plugin-compat": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-4.1.4.tgz", - "integrity": "sha512-RxySWBmzfIROLFKgeJBJue2BU/6vM2KJWXWAUq+oW4QtrsZXRxbjgxmO1OfF3sHcRuuIenTS/wgo3GyUWZF24w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-4.2.0.tgz", + "integrity": "sha512-RDKSYD0maWy5r7zb5cWQS+uSPc26mgOzdORJ8hxILmWM7S/Ncwky7BcAtXVY5iRbKjBdHsWU8Yg7hfoZjtkv7w==", "dev": true, "requires": { - "@mdn/browser-compat-data": "^5.2.47", - "@tsconfig/node14": "^1.0.3", + "@mdn/browser-compat-data": "^5.3.13", "ast-metadata-inferer": "^0.8.0", - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001473", + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001524", "find-up": "^5.0.0", - "lodash.memoize": "4.1.2", - "semver": "7.3.8" + "lodash.memoize": "^4.1.2", + "semver": "^7.5.4" }, "dependencies": { "find-up": { @@ -14322,9 +14278,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -14456,6 +14412,13 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -14467,20 +14430,20 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", - "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -14590,9 +14553,9 @@ "dev": true }, "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -14877,10 +14840,10 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "has": { @@ -16898,12 +16861,6 @@ } } }, - "js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17262,12 +17219,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -17322,9 +17273,9 @@ "dev": true }, "node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, "normalize-package-data": { @@ -18545,6 +18496,13 @@ "punycode": "^2.1.1" } }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-jest": { "version": "27.0.5", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.5.tgz", @@ -18623,26 +18581,9 @@ } }, "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "type-check": { "version": "0.3.2", @@ -18715,9 +18656,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "requires": { "escalade": "^3.1.1", diff --git a/package.json b/package.json index e7132ad..4a497b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.9.0", + "version": "1.10.0", "description": "A React library to easily integrate and use Split JS SDK", "main": "lib/index.js", "module": "es/index.js", @@ -19,7 +19,7 @@ "scripts": { "build:cjs": "rimraf lib/* types/* && tsc -m commonjs --outDir lib -d true --declarationDir types", "build:esm": "rimraf es/* && tsc", - "build:umd": "rimraf umd/* && webpack --config webpack.dev.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT", + "build:umd": "rimraf umd/* && webpack --config webpack.dev.js --env branch=$BUILD_BRANCH && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH", "build": "npm run build:cjs && npm run build:esm && npm run build:umd", "postbuild": "replace 'REACT_SDK_VERSION_NUMBER' $npm_package_version ./lib/constants.js ./es/constants.js ./umd -r", "check": "npm run check:lint && npm run check:types", @@ -62,7 +62,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.23.0", + "@splitsoftware/splitio": "10.24.0-beta", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0" }, @@ -75,12 +75,13 @@ "@types/react-dom": "^18.0.0", "@types/react-test-renderer": "^18.0.0", "@types/shallowequal": "^1.1.1", - "@typescript-eslint/eslint-plugin": "^5.55.0", - "@typescript-eslint/parser": "^5.55.0", - "eslint": "^8.36.0", - "eslint-plugin-compat": "^4.1.2", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "eslint": "^8.48.0", + "eslint-plugin-compat": "^4.2.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 7c53172..9e8f88a 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { SplitContext } from './SplitContext'; import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; import { ERROR_SC_NO_FACTORY } from './constants'; -import { getStatus, getSplitSharedClient, initAttributes } from './utils'; +import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils'; +import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; /** * Common component used to handle the status and events of a Split client passed as prop. @@ -11,18 +12,15 @@ import { getStatus, getSplitSharedClient, initAttributes } from './utils'; export class SplitComponent extends React.Component { static defaultProps = { - updateOnSdkUpdate: false, - updateOnSdkTimedout: false, - updateOnSdkReady: true, - updateOnSdkReadyFromCache: true, children: null, factory: null, client: null, + ...DEFAULT_UPDATE_OPTIONS, } // Using `getDerivedStateFromProps` since the state depends on the status of the client in props, which might change over time. // It could be avoided by removing the client and its status from the component state. - // But it implies to have another instance property to use instead of the state, because we need a unique reference value for SplitContext.Producer + // But it implies to have another instance property to use instead of the state, because we need a unique reference value for SplitContext.Provider static getDerivedStateFromProps(props: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }, state: ISplitContextValues) { const { client, factory, attributes } = props; // initAttributes can be called in the `render` method too, but it is better here for separation of concerns @@ -58,7 +56,6 @@ export class SplitComponent extends React.Component { - if (this.props.updateOnSdkReady) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkReady) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setReadyFromCache = () => { - if (this.props.updateOnSdkReadyFromCache) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkReadyFromCache) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setTimedout = () => { - if (this.props.updateOnSdkTimedout) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkTimedout) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } setUpdate = () => { - if (this.props.updateOnSdkUpdate) this.setState({ lastUpdate: Date.now() }); + if (this.props.updateOnSdkUpdate) this.setState({ lastUpdate: (this.state.client as IClientWithContext).lastUpdate }); } componentDidMount() { @@ -112,7 +108,7 @@ export class SplitComponent extends React.Component {(splitContext: ISplitContextValues) => { const { factory } = splitContext; - // getSplitSharedClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT - const client = factory ? getSplitSharedClient(factory, props.splitKey, props.trafficType) : null; + // getSplitClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT + const client = factory ? getSplitClient(factory, props.splitKey, props.trafficType) : null; return ( ); diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx index 6de0019..ba2c12d 100644 --- a/src/SplitFactory.tsx +++ b/src/SplitFactory.tsx @@ -3,7 +3,8 @@ import React from 'react'; import { SplitComponent } from './SplitClient'; import { ISplitFactoryProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients } from './utils'; +import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils'; +import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; /** * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, @@ -18,11 +19,8 @@ import { getSplitFactory, destroySplitFactory, IFactoryWithClients } from './uti export class SplitFactory extends React.Component { static defaultProps: ISplitFactoryProps = { - updateOnSdkUpdate: false, - updateOnSdkTimedout: false, - updateOnSdkReady: true, - updateOnSdkReadyFromCache: true, children: null, + ...DEFAULT_UPDATE_OPTIONS, }; readonly state: Readonly<{ factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }>; @@ -54,7 +52,7 @@ export class SplitFactory extends React.Component { private logWarning?: boolean; - // Attaching a memoized `client.getTreatmentsWithConfig` function to the component instance, to avoid duplicated impressions because - // the function result is the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. - private evaluateFeatureFlags = memoizeOne(evaluateFeatureFlags, argsAreEqual); + // Using a memoized `client.getTreatmentsWithConfig` function to avoid duplicated impressions + private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig(); render() { - const { names, children, attributes } = this.props; + const { names, flagSets, children, attributes } = this.props; return ( {(splitContext: ISplitContextValues) => { - const { client, isReady, isReadyFromCache, isDestroyed, lastUpdate } = splitContext; - let treatments; - const isOperational = !isDestroyed && (isReady || isReadyFromCache); - if (client && isOperational) { - // Cloning `client.getAttributes` result for memoization, because it returns the same reference unless `client.clearAttributes` is called. - // Caveat: same issue happens with `names` and `attributes` props if the user follows the bad practice of mutating the object instead of providing a new one. - treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, { ...client.getAttributes() }); - } else { - treatments = getControlTreatmentsWithConfig(names); - if (!client) { this.logWarning = true; } - } + const { client, lastUpdate } = splitContext; + const treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); + if (!client) { this.logWarning = true; } // SplitTreatments only accepts a function as a child, not a React Element (JSX) return children({ ...splitContext, treatments, diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 91638c2..4bc4a90 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks and test utils */ -import { mockSdk, Event, assertNoListeners, clientListenerCount } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitSdk'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -20,12 +20,10 @@ import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('SplitClient', () => { test('passes no-ready props to the child if client is not ready.', () => { - let clientToCheck; - render( - {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { + {({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { expect(isReady).toBe(false); expect(isReadyFromCache).toBe(false); expect(hasTimedout).toBe(false); @@ -33,17 +31,11 @@ describe('SplitClient', () => { expect(isDestroyed).toBe(false); expect(lastUpdate).toBe(0); - clientToCheck = client; return null; }} ); - - // After component mount, listeners should be attached for all ONCE events when none of them has been emitted yet - expect(clientListenerCount(clientToCheck, Event.SDK_READY)).toBe(1); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_FROM_CACHE)).toBe(1); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_TIMED_OUT)).toBe(1); }); test('passes ready props to the child if client is ready.', async () => { @@ -52,7 +44,6 @@ describe('SplitClient', () => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); await outerFactory.client().ready(); - let clientToCheck; render( @@ -66,17 +57,11 @@ describe('SplitClient', () => { expect(isDestroyed).toBe(false); expect(lastUpdate).toBe(0); - clientToCheck = client; return null; }} ); - - // After component mount, listeners should not be attached for those ONCE events that has been already emitted - expect(clientListenerCount(clientToCheck, Event.SDK_READY)).toBe(0); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_FROM_CACHE)).toBe(0); - expect(clientListenerCount(clientToCheck, Event.SDK_READY_TIMED_OUT)).toBe(0); // this event was not emitted, but will not anyway, since SDK_READY has. }); test('rerender child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events.', async () => { @@ -89,7 +74,7 @@ describe('SplitClient', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { @@ -114,8 +99,7 @@ describe('SplitClient', () => { fail('Child must not be rerendered'); } expect(client).toBe(outerFactory.client('user2')); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -130,10 +114,6 @@ describe('SplitClient', () => { act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); expect(renderTimes).toBe(5); - - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); }); test('rerender child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { @@ -146,7 +126,7 @@ describe('SplitClient', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { @@ -165,8 +145,7 @@ describe('SplitClient', () => { fail('Child must not be rerendered'); } expect(client).toBe(outerFactory.client('user2')); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -179,10 +158,6 @@ describe('SplitClient', () => { act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); expect(renderTimes).toBe(3); - - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); }); test('rerender child only on SDK_READY event, as default behaviour.', async () => { @@ -212,7 +187,6 @@ describe('SplitClient', () => { } expect(client).toBe(outerFactory.client('user2')); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -279,40 +253,18 @@ describe('SplitClient', () => { this.state = { splitKey: 'user1' }; } - componentDidMount() { - setTimeout(() => { - act(() => this.setState({ splitKey: 'user2' })); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - setTimeout(() => { - act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - setTimeout(() => { - act(() => this.setState({ splitKey: 'user3' })); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); - setTimeout(() => { - act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY)); - setTimeout(() => { - act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); - setTimeout(() => { - act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE)); - setTimeout(() => { - expect(renderTimes).toBe(6); - - // check that outerFactory's clients have no event listeners - // eslint-disable-next-line no-use-before-define - wrapper.unmount(); - assertNoListeners(outerFactory); - done(); - }); - }); - }); - }); - }); - }); - }); - }); - }); + async componentDidMount() { + await act(() => this.setState({ splitKey: 'user2' })); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + await act(() => (outerFactory as any).client('user1').__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + await act(() => this.setState({ splitKey: 'user3' })); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE)); + await act(() => (outerFactory as any).client('user3').__emitter__.emit(Event.SDK_UPDATE)); + expect(renderTimes).toBe(6); + + done(); } render() { @@ -359,7 +311,7 @@ describe('SplitClient', () => { } } - const wrapper = render( + render( diff --git a/src/__tests__/SplitFactory.test.tsx b/src/__tests__/SplitFactory.test.tsx index 68bf307..b5a90e6 100644 --- a/src/__tests__/SplitFactory.test.tsx +++ b/src/__tests__/SplitFactory.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event, assertNoListeners } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitSdk'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -67,7 +67,7 @@ describe('SplitFactory', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -91,8 +91,7 @@ describe('SplitFactory', () => { fail('Child must not be rerendered'); } expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -104,11 +103,8 @@ describe('SplitFactory', () => { act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(5); - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); + expect(renderTimes).toBe(5); }); test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { @@ -116,7 +112,7 @@ describe('SplitFactory', () => { let renderTimes = 0; let previousLastUpdate = -1; - const wrapper = render( + render( {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; @@ -134,8 +130,7 @@ describe('SplitFactory', () => { fail('Child must not be rerendered'); } expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThanOrEqual(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; return null; @@ -146,11 +141,8 @@ describe('SplitFactory', () => { act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(3); - // check that outerFactory's clients have no event listeners - wrapper.unmount(); - assertNoListeners(outerFactory); + expect(renderTimes).toBe(3); }); test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour.', async () => { @@ -174,7 +166,6 @@ describe('SplitFactory', () => { } expect(factory).toBe(outerFactory); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - expect(lastUpdate).toBeLessThanOrEqual(Date.now()); renderTimes++; previousLastUpdate = lastUpdate; return null; diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index ce558e8..13d8990 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -8,30 +8,27 @@ jest.mock('@splitsoftware/splitio/client', () => { }); import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -const logSpy = jest.spyOn(console, 'log'); +import { getStatus } from '../utils'; +import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; +import { CONTROL_WITH_CONFIG, WARN_ST_NO_CLIENT } from '../constants'; /** Test target */ import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types'; import { SplitTreatments } from '../SplitTreatments'; import { SplitClient } from '../SplitClient'; import { SplitFactory } from '../SplitFactory'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { getControlTreatmentsWithConfig, WARN_ST_NO_CLIENT } from '../constants'; -import { getStatus } from '../utils'; -import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; +import { useSplitTreatments } from '../useSplitTreatments'; + +const logSpy = jest.spyOn(console, 'log'); describe('SplitTreatments', () => { + const featureFlagNames = ['split1', 'split2']; + const flagSets = ['set1', 'set2']; + afterEach(() => { logSpy.mockClear() }); - it('passes as treatments prop the value returned by the function "getControlTreatmentsWithConfig" if the SDK is not ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes control treatments (empty object if flagSets is provided) if the SDK is not ready.', () => { render( {({ factory }) => { @@ -40,9 +37,16 @@ describe('SplitTreatments', () => { {({ treatments }: ISplitTreatmentsChildProps) => { const clientMock: any = factory?.client('user1'); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(0); - expect(treatments).toEqual(getControlTreatmentsWithConfig(featureFlagNames)); - done(); + expect(clientMock.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client('user1'); + expect(clientMock.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatments).toEqual({}); return null; }} @@ -53,8 +57,7 @@ describe('SplitTreatments', () => { ); }); - it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig" if the SDK is ready.', (done) => { - const featureFlagNames = ['split1', 'split2']; + it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig(ByFlagSets)" if the SDK is ready.', () => { const outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); @@ -64,37 +67,44 @@ describe('SplitTreatments', () => { expect(getStatus(outerFactory.client()).isReady).toBe(isReady); expect(isReady).toBe(true); return ( - - {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client(); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); - expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); - expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); - expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]); - done(); - return null; - }} - + <> + + {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); + expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); + expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, 0]); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.results[0].value); + expect(flagSets).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls[0][0]); + return null; + }} + + ); }} ); }); - it('logs error and passes control treatments ("getControlTreatmentsWithConfig") if rendered outside an SplitProvider component.', () => { - const featureFlagNames = ['split1', 'split2']; - let passedTreatments; + it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { render( {({ treatments }: ISplitTreatmentsChildProps) => { - passedTreatments = treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); return null; }} ); + expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(passedTreatments); }); /** @@ -102,8 +112,6 @@ describe('SplitTreatments', () => { * is not ready doesn't emit errors, and logs meaningful messages instead. */ it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { - const featureFlagNames = ['split1', 'split2']; - render( {() => { @@ -135,26 +143,56 @@ describe('SplitTreatments', () => { }} ); - expect(logSpy).toBeCalledWith('[ERROR] split names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid split name, split name must be a non-empty string.'); + expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); + expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.'); done(); }); + + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + // @ts-expect-error flagSets and names are mutually exclusive + + {({ treatments }) => { + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + ); + + expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); + }); }); +let renderTimes = 0; + /** - * Tests for asserting that client.getTreatmentsWithConfig is not called unnecessarely + * Tests for asserting that client.getTreatmentsWithConfig and client.getTreatmentsWithConfigByFlagSets are not called unnecessarily when using SplitTreatments and useSplitTreatments. */ -describe('SplitTreatments optimization', () => { - - let renderTimes = 0; - +describe.each([ + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => ( + // @ts-expect-error names and flagSets are mutually exclusive + + {() => { + renderTimes++; + return null; + }} + + ), + ({ names, flagSets, attributes }: { names?: string[], flagSets?: string[], attributes?: SplitIO.Attributes }) => { + // @ts-expect-error names and flagSets are mutually exclusive + useSplitTreatments({ names, flagSets, attributes }); + renderTimes++; + return null; + } +])('SplitTreatments & useSplitTreatments optimization', (InnerComponent) => { let outerFactory = SplitSdk(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - function Component({ names, attributes, splitKey, clientAttributes }: { - names: ISplitTreatmentsProps['names'] + function Component({ names, flagSets, attributes, splitKey, clientAttributes }: { + names?: ISplitTreatmentsProps['names'] + flagSets?: ISplitTreatmentsProps['flagSets'] attributes: ISplitTreatmentsProps['attributes'] splitKey: ISplitClientProps['splitKey'] clientAttributes?: ISplitClientProps['attributes'] @@ -162,18 +200,14 @@ describe('SplitTreatments optimization', () => { return ( - - {() => { - renderTimes++; - return null; - }} - + ); } const names = ['split1', 'split2']; + const flagSets = ['set1', 'set2']; const attributes = { att1: 'att1' }; const splitKey = sdkBrowser.core.key; @@ -182,25 +216,27 @@ describe('SplitTreatments optimization', () => { beforeEach(() => { renderTimes = 0; (outerFactory.client().getTreatmentsWithConfig as jest.Mock).mockClear(); - wrapper = render(); + wrapper = render(); }) afterEach(() => { wrapper.unmount(); // unmount to remove event listener from factory }) - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are the same object.', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are the same object.', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); - it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names and attributes are equals (shallow comparison).', () => { - wrapper.rerender(); + it('rerenders but does not re-evaluate feature flags if client, lastUpdate, names, flagSets and attributes are equals (shallow comparison).', () => { + wrapper.rerender(); expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); }); it('rerenders and re-evaluates feature flags if names are not equals (shallow array comparison).', () => { @@ -208,6 +244,19 @@ describe('SplitTreatments optimization', () => { expect(renderTimes).toBe(2); expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(0); + }); + + it('rerenders and re-evaluates feature flags if flag sets are not equals (shallow array comparison).', () => { + wrapper.rerender(); + wrapper.rerender(); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(1); + + wrapper.rerender(); + + expect(renderTimes).toBe(4); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(1); + expect(outerFactory.client().getTreatmentsWithConfigByFlagSets).toBeCalledTimes(2); }); it('rerenders and re-evaluates feature flags if attributes are not equals (shallow object comparison).', () => { @@ -224,7 +273,7 @@ describe('SplitTreatments optimization', () => { expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); }); - it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', (done) => { + it('rerenders and re-evaluates feature flags if lastUpdate timestamp changes (e.g., SDK_UPDATE event).', () => { expect(renderTimes).toBe(1); // State update and split evaluation @@ -234,21 +283,18 @@ describe('SplitTreatments optimization', () => { (outerFactory as any).client().destroy(); wrapper.rerender(); - setTimeout(() => { - // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching - expect(renderTimes).toBe(3); - expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); + // Updates were batched as a single render, due to automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching + expect(renderTimes).toBe(3); + expect(outerFactory.client().getTreatmentsWithConfig).toBeCalledTimes(2); - // Restore the client to be READY - (outerFactory as any).client().__restore(); - (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - done(); - }) + // Restore the client to be READY + (outerFactory as any).client().__restore(); + (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); }); - it('rerenders and re-evaluates feature flags if client changes.', () => { + it('rerenders and re-evaluates feature flags if client changes.', async () => { wrapper.rerender(); - act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY)); + await act(() => (outerFactory as any).client('otherKey').__emitter__.emit(Event.SDK_READY)); // Initial render + 2 renders (in 3 updates) -> automatic batching https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-automatic-batching expect(renderTimes).toBe(3); @@ -256,11 +302,9 @@ describe('SplitTreatments optimization', () => { expect(outerFactory.client('otherKey').getTreatmentsWithConfig).toBeCalledTimes(1); }); - it('rerenders and re-evaluate splfeature flagsits when Split context changes (in both SplitFactory and SplitClient components).', async () => { + it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactory and SplitClient components).', async () => { // changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments const outerFactory = SplitSdk(sdkBrowser); - const names = ['split1', 'split2']; - const attributes = { att1: 'att1' }; let renderTimesComp1 = 0; let renderTimesComp2 = 0; @@ -301,8 +345,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(2); expect(renderTimesComp2).toBe(2); // updateOnSdkReadyFromCache === false, in second component - // delay SDK events to guarantee a different lastUpdate timestamp for SplitTreatments to re-evaluate - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY_TIMED_OUT); @@ -311,7 +353,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(3); expect(renderTimesComp2).toBe(3); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_READY); @@ -320,7 +361,6 @@ describe('SplitTreatments optimization', () => { expect(renderTimesComp1).toBe(3); // updateOnSdkReady === false, in first component expect(renderTimesComp2).toBe(4); - await new Promise(resolve => setTimeout(resolve, 10)); act(() => { (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE); (outerFactory as any).client('user2').__emitter__.emit(Event.SDK_UPDATE); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 647dbd8..c8bd2f3 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { SplitContext as ExportedSplitContext, SplitSdk as ExportedSplitSdk, @@ -11,6 +12,22 @@ import { useManager as exportedUseManager, useTrack as exportedUseTrack, useTreatments as exportedUseTreatments, + useSplitClient as exportedUseSplitClient, + useSplitTreatments as exportedUseSplitTreatments, + useSplitManager as exportedUseSplitManager, + // Checks that types are exported. Otherwise, the test would fail with a TS error. + GetTreatmentsOptions, + ISplitClientChildProps, + ISplitClientProps, + ISplitContextValues, + ISplitFactoryChildProps, + ISplitFactoryProps, + ISplitStatus, + ISplitTreatmentsChildProps, + ISplitTreatmentsProps, + IUpdateProps, + IUseSplitClientOptions, + IUseSplitTreatmentsOptions, } from '../index'; import { SplitContext } from '../SplitContext'; import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client'; @@ -24,6 +41,9 @@ import { useClient } from '../useClient'; import { useManager } from '../useManager'; import { useTrack } from '../useTrack'; import { useTreatments } from '../useTreatments'; +import { useSplitClient } from '../useSplitClient'; +import { useSplitTreatments } from '../useSplitTreatments'; +import { useSplitManager } from '../useSplitManager'; describe('index', () => { @@ -44,6 +64,9 @@ describe('index', () => { expect(exportedUseManager).toBe(useManager); expect(exportedUseTrack).toBe(useTrack); expect(exportedUseTreatments).toBe(useTreatments); + expect(exportedUseSplitClient).toBe(useSplitClient); + expect(exportedUseSplitTreatments).toBe(useSplitTreatments); + expect(exportedUseSplitManager).toBe(useSplitManager); }); it('should export SplitContext', () => { diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index 18cfb21..22d40ae 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -40,35 +40,42 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { __emitter__.on(Event.SDK_READY, () => { __isReady__ = true; }); __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { __isReadyFromCache__ = true; }); __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { __hasTimedout__ = true; }); - const __internalListenersCount__ = { - [Event.SDK_READY]: 1, - [Event.SDK_READY_FROM_CACHE]: 1, - [Event.SDK_READY_TIMED_OUT]: 1, - [Event.SDK_UPDATE]: 0, - }; + + let attributesCache = {}; // Client methods const track: jest.Mock = jest.fn(() => { return true; }); - const getTreatmentsWithConfig: jest.Mock = jest.fn(() => { - return 'getTreatmentsWithConfig'; + const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => { + return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => { + result[featureName] = { treatment: 'on', config: null }; + return result; + }, {}); + }); + const getTreatmentsWithConfigByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => { + return flagSets.reduce((result: SplitIO.TreatmentsWithConfig, flagSet: string) => { + result[flagSet + '_feature_flag'] = { treatment: 'on', config: null }; + return result; + }, {}); }); - const setAttributes: jest.Mock = jest.fn(() => { + const setAttributes: jest.Mock = jest.fn((attributes) => { + attributesCache = Object.assign(attributesCache, attributes); return true; }); const clearAttributes: jest.Mock = jest.fn(() => { + attributesCache = {}; return true; }); const getAttributes: jest.Mock = jest.fn(() => { - return true; + return attributesCache; }); const ready: jest.Mock = jest.fn(() => { return new Promise((res, rej) => { if (__isReady__) res(); - else { __internalListenersCount__[Event.SDK_READY]++; __emitter__.on(Event.SDK_READY, res); } + else { __emitter__.on(Event.SDK_READY, res); } if (__hasTimedout__) rej(); - else { __internalListenersCount__[Event.SDK_READY_TIMED_OUT]++; __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } + else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } }); }); const __getStatus = () => ({ @@ -80,11 +87,13 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { }); const destroy: jest.Mock = jest.fn(() => { __isDestroyed__ = true; + // __emitter__.removeAllListeners(); return Promise.resolve(); }); return Object.assign(Object.create(__emitter__), { getTreatmentsWithConfig, + getTreatmentsWithConfigByFlagSets, track, ready, destroy, @@ -94,8 +103,6 @@ function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { getAttributes, // EventEmitter exposed to trigger events manually __emitter__, - // Count of internal listeners set by the client mock, used to assert how many external listeners were attached - __internalListenersCount__, // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) __getStatus, // Restore the mock client to its initial NO-READY status. @@ -140,18 +147,3 @@ export function mockSdk() { }); } - -export function assertNoListeners(factory: any) { - Object.values((factory as any).__clients__).forEach((client) => assertNoListenersOnClient(client)); -} - -function assertNoListenersOnClient(client: any) { - expect(client.__emitter__.listenerCount(Event.SDK_READY)).toBe(client.__internalListenersCount__[Event.SDK_READY]); - expect(client.__emitter__.listenerCount(Event.SDK_READY_FROM_CACHE)).toBe(client.__internalListenersCount__[Event.SDK_READY_FROM_CACHE]); - expect(client.__emitter__.listenerCount(Event.SDK_READY_TIMED_OUT)).toBe(client.__internalListenersCount__[Event.SDK_READY_TIMED_OUT]); - expect(client.__emitter__.listenerCount(Event.SDK_UPDATE)).toBe(client.__internalListenersCount__[Event.SDK_UPDATE]); -} - -export function clientListenerCount(client: any, event: string) { - return client.__emitter__.listenerCount(event) - client.__internalListenersCount__[event]; -} diff --git a/src/__tests__/useClient.test.tsx b/src/__tests__/useClient.test.tsx index 72e3b47..544e843 100644 --- a/src/__tests__/useClient.test.tsx +++ b/src/__tests__/useClient.test.tsx @@ -1,97 +1,23 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; +const useSplitClientMock = jest.fn(); +jest.mock('../useSplitClient', () => ({ + useSplitClient: useSplitClientMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; -import { SplitClient } from '../SplitClient'; import { useClient } from '../useClient'; -import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; describe('useClient', () => { - test('returns the main client from the context updated by SplitFactory.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - {React.createElement(() => { - client = useClient(); - return null; - })} - - ); - expect(client).toBe(outerFactory.client()); - }); - - test('returns the client from the context updated by SplitClient.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - - {React.createElement(() => { - client = useClient(); - return null; - })} - - - ); - expect(client).toBe(outerFactory.client('user2')); - }); - - test('returns a new client from the factory at Split context given a splitKey.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - {React.createElement(() => { - (outerFactory.client as jest.Mock).mockClear(); - client = useClient('user2', 'user'); - return null; - })} - - ); - expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); - expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); - }); - - test('returns null if invoked outside Split context.', () => { - let client; - let sharedClient; - render( - React.createElement(() => { - client = useClient(); - sharedClient = useClient('user2', 'user'); - return null; - }) - ); - expect(client).toBe(null); - expect(sharedClient).toBe(null); - }); - - test('attributes binding test with utility', (done) => { + test('calls useSplitClient with the correct arguments and returns the client.', () => { + const attributes = { someAttribute: 'someValue' }; + const client = 'client'; + useSplitClientMock.mockReturnValue({ client, isReady: false }); - function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { - return ( - - {React.createElement(() => { - useClient(splitKey, 'user', attributesClient); - testSwitch(done, splitKey); - return null; - })} - - ); - } + expect(useClient('someKey', 'someTrafficType', attributes)).toBe(client); - testAttributesBinding(Component); + expect(useSplitClientMock).toHaveBeenCalledTimes(1); + expect(useSplitClientMock).toHaveBeenCalledWith({ splitKey: 'someKey', trafficType: 'someTrafficType', attributes }); }); }); diff --git a/src/__tests__/useManager.test.tsx b/src/__tests__/useManager.test.tsx index 041c7b4..ba4c36c 100644 --- a/src/__tests__/useManager.test.tsx +++ b/src/__tests__/useManager.test.tsx @@ -1,43 +1,21 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; +const useSplitManagerMock = jest.fn(); +jest.mock('../useSplitManager', () => ({ + useSplitManager: useSplitManagerMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; import { useManager } from '../useManager'; describe('useManager', () => { - test('returns the factory manager from the Split context.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let manager; - render( - - {React.createElement(() => { - manager = useManager(); - return null; - })} - - ); - expect(manager).toBe(outerFactory.manager()); - }); + test('calls useSplitManager with the correct arguments and returns the manager.', () => { + const manager = 'manager'; + useSplitManagerMock.mockReturnValue({ manager, isReady: false }); + + expect(useManager()).toBe(manager); - test('returns null if invoked outside Split context.', () => { - let manager; - render( - React.createElement(() => { - manager = useManager(); - return null; - }) - ); - expect(manager).toBe(null); + expect(useSplitManagerMock).toHaveBeenCalledTimes(1); }); }); diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx new file mode 100644 index 0000000..3897182 --- /dev/null +++ b/src/__tests__/useSplitClient.test.tsx @@ -0,0 +1,259 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; + +/** Mocks */ +import { mockSdk, Event } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; + +/** Test target */ +import { useSplitClient } from '../useSplitClient'; +import { SplitFactory } from '../SplitFactory'; +import { SplitClient } from '../SplitClient'; +import { SplitContext } from '../SplitContext'; +import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; + +describe('useSplitClient', () => { + + test('returns the main client from the context updated by SplitFactory.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + + {React.createElement(() => { + client = useSplitClient().client; + return null; + })} + + ); + expect(client).toBe(outerFactory.client()); + }); + + test('returns the client from the context updated by SplitClient.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + + + {React.createElement(() => { + client = useSplitClient().client; + return null; + })} + + + ); + expect(client).toBe(outerFactory.client('user2')); + }); + + test('returns a new client from the factory at Split context given a splitKey.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let client; + render( + + {React.createElement(() => { + (outerFactory.client as jest.Mock).mockClear(); + client = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; + return null; + })} + + ); + expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); + expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); + }); + + test('returns null if invoked outside Split context.', () => { + let client; + let sharedClient; + render( + React.createElement(() => { + client = useSplitClient().client; + sharedClient = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; + return null; + }) + ); + expect(client).toBe(null); + expect(sharedClient).toBe(null); + }); + + test('attributes binding test with utility', (done) => { + + // eslint-disable-next-line react/prop-types + const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => { + useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient}); + testSwitch(done, splitKey); + return null; + }; + + function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { + return ( + + + + ); + } + + testAttributesBinding(Component); + }); + + test('useSplitClient must update on SDK events', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; + let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithUpdate = 0; + let countNestedComponent = 0; + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitClient++; return null }} + + {React.createElement(() => { + // Equivalent to + // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` + // - Disabling update props, since the wrapping SplitFactory has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` + const { client } = useSplitClient({ attributes: { att1: 'att1' } }); + expect(client).toBe(mainClient); // Assert that the main client was retrieved. + expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. + countUseSplitClient++; + return null; + })} + + {() => { countSplitClientUser2++; return null }} + + {React.createElement(() => { + const { client } = useSplitClient({ splitKey: 'user_2' }); + expect(client).toBe(user2Client); + countUseSplitClientUser2++; + return null; + })} + + {() => { countSplitClientWithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; + countUseSplitClientWithUpdate++; + return null; + })} + + {() => { countSplitClientUser2WithUpdate++; return null }} + + {React.createElement(() => { + useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); + countUseSplitClientUser2WithUpdate++; + return null; + })} + + {React.createElement(() => { + const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); + expect(status.client).toBe(user2Client); + + // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event + countNestedComponent++; + switch (countNestedComponent) { + case 1: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(false); + break; + case 2: + expect(status.isReady).toBe(false); + expect(status.isReadyFromCache).toBe(true); + break; + case 3: + expect(status.isReady).toBe(true); + expect(status.isReadyFromCache).toBe(true); + break; + case 4: + break; + default: + throw new Error('Unexpected render'); + } + return null; + })} + + + + ); + + act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); + + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); + + // If SplitClient and useSplitClient retrieve the same client than the context and have default update options, + // they render when the context renders. + expect(countSplitClient).toEqual(countSplitContext); + expect(countUseSplitClient).toEqual(countSplitContext); + + // If SplitClient and useSplitClient retrieve a different client than the context and have default update options, + // they render when the context renders and when the new client is ready and ready from cache. + expect(countSplitClientUser2).toEqual(countSplitContext + 2); + expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); + + // If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the client updates. + expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1); + expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1); + + // If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true, + // they render when the context renders and when the new client is ready, ready from cache and updates. + expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + expect(countUseSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); + + expect(countNestedComponent).toEqual(4); + }); + + test('useSplitClient must support changes in update props', () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + + let rendersCount = 0; + + function InnerComponent(updateOptions) { + useSplitClient(updateOptions); + rendersCount++; + return null; + } + + function Component(updateOptions) { + return ( + + + + ) + } + + const wrapper = render(); + + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default + expect(rendersCount).toBe(2); + + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // trigger re-render because updateOnSdkUpdate is true now + + expect(rendersCount).toBe(4); + + wrapper.rerender(); // trigger re-render + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false now + + expect(rendersCount).toBe(5); + }); + +}); diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx new file mode 100644 index 0000000..9ec5367 --- /dev/null +++ b/src/__tests__/useSplitManager.test.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; + +/** Mocks */ +import { Event, mockSdk } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; +import { getStatus } from '../utils'; + +/** Test target */ +import { SplitFactory } from '../SplitFactory'; +import { useSplitManager } from '../useSplitManager'; + +describe('useSplitManager', () => { + + test('returns the factory manager from the Split context, and updates when the context changes.', () => { + const outerFactory = SplitSdk(sdkBrowser); + let hookResult; + render( + + {React.createElement(() => { + hookResult = useSplitManager(); + return null; + })} + + ); + + expect(hookResult).toStrictEqual({ + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, + hasTimedout: false, + isDestroyed: false, + isReady: false, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: 0, + }); + + act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); + + expect(hookResult).toStrictEqual({ + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, + hasTimedout: false, + isDestroyed: false, + isReady: true, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: getStatus(outerFactory.client()).lastUpdate, + }); + }); + + test('returns null if invoked outside Split context.', () => { + let hookResult; + render( + React.createElement(() => { + hookResult = useSplitManager(); + return null; + }) + ); + + expect(hookResult).toStrictEqual({ + manager: null, + client: null, + factory: null, + hasTimedout: false, + isDestroyed: false, + isReady: false, + isReadyFromCache: false, + isTimedout: false, + lastUpdate: 0, + }); + }); + +}); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx new file mode 100644 index 0000000..cbdc62d --- /dev/null +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -0,0 +1,262 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; + +/** Mocks */ +import { mockSdk, Event } from './testUtils/mockSplitSdk'; +jest.mock('@splitsoftware/splitio/client', () => { + return { SplitFactory: mockSdk() }; +}); +import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { sdkBrowser } from './testUtils/sdkConfigs'; +import { CONTROL_WITH_CONFIG } from '../constants'; + +/** Test target */ +import { SplitFactory } from '../SplitFactory'; +import { SplitClient } from '../SplitClient'; +import { useSplitTreatments } from '../useSplitTreatments'; +import { SplitTreatments } from '../SplitTreatments'; +import { SplitContext } from '../SplitContext'; +import { ISplitTreatmentsChildProps } from '../types'; + +const logSpy = jest.spyOn(console, 'log'); + +describe('useSplitTreatments', () => { + + const featureFlagNames = ['split1']; + const flagSets = ['set1']; + const attributes = { att1: 'att1' }; + + test('returns the treatments evaluated by the client at Split context, or control if the client is not operational.', () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client(); + let treatments: SplitIO.TreatmentsWithConfig; + let treatmentsByFlagSets: SplitIO.TreatmentsWithConfig; + + render( + + {React.createElement(() => { + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + treatmentsByFlagSets = useSplitTreatments({ flagSets, attributes }).treatments; + + // @ts-expect-error Options object must provide either names or flagSets + expect(useSplitTreatments({}).treatments).toEqual({}); + return null; + })} + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + + // returns empty treatments object if not operational, without calling `getTreatmentsWithConfigByFlagSets` method + expect(client.getTreatmentsWithConfigByFlagSets).not.toBeCalled(); + expect(treatmentsByFlagSets!).toEqual({}); + + // once operational (SDK_READY), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY)); + + expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + + expect(client.getTreatmentsWithConfigByFlagSets).toBeCalledWith(flagSets, attributes); + expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets); + }); + + test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client('user2'); + let treatments: SplitIO.TreatmentsWithConfig; + + render( + + + {React.createElement(() => { + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + return null; + })} + + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); + + // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + + expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + }); + + test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { + const outerFactory = SplitSdk(sdkBrowser); + const client: any = outerFactory.client('user2'); + let renderTimes = 0; + + render( + + {React.createElement(() => { + const treatments = useSplitTreatments({ names: featureFlagNames, attributes, splitKey: 'user2' }).treatments; + + renderTimes++; + switch (renderTimes) { + case 1: + // returns control if not operational (SDK not ready), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + break; + case 2: + case 3: + // once operational (SDK_READY or SDK_READY_FROM_CACHE), it evaluates feature flags + expect(client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveLastReturnedWith(treatments); + break; + default: + throw new Error('Unexpected render'); + } + + return null; + })} + + ); + + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => client.__emitter__.emit(Event.SDK_READY)); + act(() => client.__emitter__.emit(Event.SDK_UPDATE)); // should not trigger a re-render by default + expect(client.getTreatmentsWithConfig).toBeCalledTimes(2); + }); + + // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' + test('returns control treatments (empty object if flagSets is provided) if invoked outside Split context.', () => { + render( + React.createElement(() => { + const treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + + const treatmentsByFlagSets = useSplitTreatments({ flagSets: featureFlagNames }).treatments; + expect(treatmentsByFlagSets).toEqual({}); + return null; + }) + ); + }); + + /** + * Input validation. Passing invalid feature flag names or attributes while the Sdk + * is not ready doesn't emit errors, and logs meaningful messages instead. + */ + test('Input validation: invalid "names" and "attributes" params in useSplitTreatments.', () => { + render( + React.createElement(() => { + // @ts-expect-error Test error handling + let treatments = useSplitTreatments('split1').treatments; + expect(treatments).toEqual({}); + // @ts-expect-error Test error handling + treatments = useSplitTreatments({ names: [true] }).treatments; + expect(treatments).toEqual({}); + + return null; + }) + ); + expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); + expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.'); + }); + + test('useSplitTreatments must update on SDK events', async () => { + const outerFactory = SplitSdk(sdkBrowser); + const mainClient = outerFactory.client() as any; + const user2Client = outerFactory.client('user_2') as any; + + let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; + + function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { + if (isReady || isReadyFromCache) { + expect(treatments).toEqual({ + split_test: { + treatment: 'on', + config: null, + } + }) + } else { + expect(treatments).toEqual({ + split_test: { + treatment: 'control', + config: null, + } + }) + } + } + + render( + + <> + + {() => countSplitContext++} + + + {() => { countSplitTreatments++; return null }} + + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); + expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. + validateTreatments(context); + countUseSplitTreatments++; + return null; + })} + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); + expect(context.client).toBe(user2Client); + validateTreatments(context); + countUseSplitTreatmentsUser2++; + return null; + })} + {React.createElement(() => { + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true }); + expect(context.client).toBe(user2Client); + validateTreatments(context); + countUseSplitTreatmentsUser2WithUpdate++; + return null; + })} + + + ); + + act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); + + // SplitContext renders 3 times: initially, when ready from cache, and when ready. + expect(countSplitContext).toEqual(3); + + // SplitTreatments and useSplitTreatments render when the context renders. + expect(countSplitTreatments).toEqual(countSplitContext); + expect(countUseSplitTreatments).toEqual(countSplitContext); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(4); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }); + + // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. + expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); + // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. + expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); + }); + + test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { + render( + React.createElement(() => { + // @ts-expect-error names and flagSets are mutually exclusive + const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + return null; + }) + ); + + expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); + }); + +}); diff --git a/src/__tests__/useTrack.test.tsx b/src/__tests__/useTrack.test.tsx index 317af05..11ecce4 100644 --- a/src/__tests__/useTrack.test.tsx +++ b/src/__tests__/useTrack.test.tsx @@ -21,16 +21,16 @@ describe('useTrack', () => { const value = 10; const properties = { prop1: 'prop1' }; - test('returns the track method binded to the client at Split context updated by SplitFactory.', () => { + test('returns the track method bound to the client at Split context updated by SplitFactory.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack(); - trackResult = bindedTrack(tt, eventType, value, properties); + boundTrack = useTrack(); + trackResult = boundTrack(tt, eventType, value, properties); return null; })} , @@ -40,17 +40,17 @@ describe('useTrack', () => { expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method binded to the client at Split context updated by SplitClient.', () => { + test('returns the track method bound to the client at Split context updated by SplitClient.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack(); - trackResult = bindedTrack(tt, eventType, value, properties); + boundTrack = useTrack(); + trackResult = boundTrack(tt, eventType, value, properties); return null; })} @@ -61,16 +61,16 @@ describe('useTrack', () => { expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method binded to a new client given a splitKey and optional trafficType.', () => { + test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => { const outerFactory = SplitSdk(sdkBrowser); - let bindedTrack; + let boundTrack; let trackResult; render( {React.createElement(() => { - bindedTrack = useTrack('user2', tt); - trackResult = bindedTrack(eventType, value, properties); + boundTrack = useTrack('user2', tt); + trackResult = boundTrack(eventType, value, properties); return null; })} , diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx index 1671948..b375d05 100644 --- a/src/__tests__/useTreatments.test.tsx +++ b/src/__tests__/useTreatments.test.tsx @@ -1,141 +1,24 @@ -import React from 'react'; -import { render, act } from '@testing-library/react'; - /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; -jest.mock('../constants', () => { - const actual = jest.requireActual('../constants'); - return { - ...actual, - getControlTreatmentsWithConfig: jest.fn(actual.getControlTreatmentsWithConfig), - }; -}); -import { CONTROL_WITH_CONFIG, getControlTreatmentsWithConfig } from '../constants'; -const logSpy = jest.spyOn(console, 'log'); +const useSplitTreatmentsMock = jest.fn(); +jest.mock('../useSplitTreatments', () => ({ + useSplitTreatments: useSplitTreatmentsMock +})); /** Test target */ -import { SplitFactory } from '../SplitFactory'; -import { SplitClient } from '../SplitClient'; import { useTreatments } from '../useTreatments'; describe('useTreatments', () => { - const featureFlagNames = ['split1']; - const attributes = { att1: 'att1' }; - - test('returns the treatments evaluated by the client at Split context updated by SplitFactory, or control if the client is not operational.', () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client(); - let treatments: SplitIO.TreatmentsWithConfig; - - render( - - {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - })} - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); - - // once operational (SDK_READY), it evaluates feature flags - act(() => client.__emitter__.emit(Event.SDK_READY)); - - expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); - - test('returns the Treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client('user2'); - let treatments: SplitIO.TreatmentsWithConfig; - - render( - - - {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - })} - - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); - - // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags - act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - - expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); - - test('returns the Treatments from a new client given a splitKey, or control if the client is not operational.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client('user2'); - let treatments; - - client.__emitter__.emit(Event.SDK_READY); - await client.destroy(); - - render( - - {React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes, 'user2'); - return null; - })} - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); - }); - - // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns Control Treatments if invoked outside Split context.', () => { - let treatments; - - render( - React.createElement(() => { - treatments = useTreatments(featureFlagNames, attributes); - return null; - }) - ); - expect(getControlTreatmentsWithConfig).toBeCalledWith(featureFlagNames); - expect(getControlTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); + test('calls useSplitTreatments with the correct arguments and returns the treatments.', () => { + const names = ['someFeature']; + const attributes = { someAttribute: 'someValue' }; + const treatments = { someFeature: { treatment: 'on', config: null } }; + useSplitTreatmentsMock.mockReturnValue({ treatments, isReady: false }); - /** - * Input validation. Passing invalid feature flag names or attributes while the Sdk - * is not ready doesn't emit errors, and logs meaningful messages instead. - */ - test('Input validation: invalid "names" and "attributes" params in useTreatments.', (done) => { - render( - React.createElement(() => { - // @ts-expect-error Test error handling - let treatments = useTreatments('split1'); - expect(treatments).toEqual({}); - // @ts-expect-error Test error handling - treatments = useTreatments([true]); - expect(treatments).toEqual({}); + expect(useTreatments(names, attributes, 'someKey')).toBe(treatments); - done(); - return null; - }) - ); - expect(logSpy).toBeCalledWith('[ERROR] split names must be a non-empty array.'); - expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid split name, split name must be a non-empty string.'); + expect(useSplitTreatmentsMock).toHaveBeenCalledTimes(1); + expect(useSplitTreatmentsMock).toHaveBeenCalledWith({ names, attributes, splitKey: 'someKey' }); }); }); diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index b7210c9..d1e665f 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -12,38 +12,42 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; import { withSplitTreatments } from '../withSplitTreatments'; -import { ISplitTreatmentsChildProps } from '../types'; import { getControlTreatmentsWithConfig } from '../constants'; describe('withSplitTreatments', () => { it(`passes Split props and outer props to the child. - In this test, the value of "props.treatments" is obteined by the function "getControlTreatmentsWithConfig", - and not "client.getTreatmentsWithConfig" since the client is not ready.`, (done) => { + In this test, the value of "props.treatments" is obtained by the function "getControlTreatmentsWithConfig", + and not "client.getTreatmentsWithConfig" since the client is not ready.`, () => { const featureFlagNames = ['split1', 'split2']; + const Component = withSplitFactory(sdkBrowser)<{ outerProp1: string, outerProp2: number }>( ({ outerProp1, outerProp2, factory }) => { const SubComponent = withSplitClient('user1')<{ outerProp1: string, outerProp2: number }>( withSplitTreatments(featureFlagNames)( - (props: ISplitTreatmentsChildProps & { outerProp1: string, outerProp2: number }) => { + (props) => { const clientMock = factory!.client('user1'); - expect(props.outerProp1).toBe('outerProp1'); - expect(props.outerProp2).toBe(2); expect((clientMock.getTreatmentsWithConfig as jest.Mock).mock.calls.length).toBe(0); - expect(props.treatments).toEqual(getControlTreatmentsWithConfig(featureFlagNames)); - expect(props.isReady).toBe(false); - expect(props.isReadyFromCache).toBe(false); - expect(props.hasTimedout).toBe(false); - expect(props.isTimedout).toBe(false); - expect(props.isDestroyed).toBe(false); - expect(props.lastUpdate).toBe(0); - done(); + + expect(props).toStrictEqual({ + factory: factory, client: clientMock, + outerProp1: 'outerProp1', outerProp2: 2, + treatments: getControlTreatmentsWithConfig(featureFlagNames), + isReady: false, + isReadyFromCache: false, + hasTimedout: false, + isTimedout: false, + isDestroyed: false, + lastUpdate: 0 + }); + return null; } ) ); return ; }); + render(); }); diff --git a/src/constants.ts b/src/constants.ts index 4f7be86..47020ef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,22 +30,14 @@ export const getControlTreatmentsWithConfig = (featureFlagNames: unknown): Split }; // Warning and error messages -export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; +export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactory. Config prop will be ignored.'; export const ERROR_SF_NO_CONFIG_AND_FACTORY: string = '[ERROR] SplitFactory must receive either a Split config or a Split factory as props.'; export const ERROR_SC_NO_FACTORY: string = '[ERROR] SplitClient does not have access to a Split factory. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; -export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; - -const ERROR_NO_USECONTEXT = '[ERROR] Check your React version since `useContext` hook is not available. Split hooks require version 16.8.0+ of React.'; - -export const ERROR_UC_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning null from `useClient` hook.'; - -export const ERROR_UM_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning null from `useManager` hook.'; - -export const ERROR_UT_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning control treatments from `useTreatments` hook.'; - -export const ERROR_UTRACK_NO_USECONTEXT: string = ERROR_NO_USECONTEXT + ' Returning a no-op function from `useTrack` hook.'; +export const WARN_ST_NO_CLIENT: string = '[WARN] SplitTreatments does not have access to a Split client. This is because it is not inside the scope of a SplitFactory component or SplitFactory was not properly instantiated.'; export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; + +export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/index.ts b/src/index.ts index 52a0de8..f25e6fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,11 +11,30 @@ export { SplitTreatments } from './SplitTreatments'; export { SplitClient } from './SplitClient'; export { SplitFactory } from './SplitFactory'; -// helper functions/hooks +// Hooks export { useClient } from './useClient'; export { useTreatments } from './useTreatments'; export { useTrack } from './useTrack'; export { useManager } from './useManager'; +export { useSplitClient } from './useSplitClient'; +export { useSplitTreatments } from './useSplitTreatments'; +export { useSplitManager } from './useSplitManager'; // SplitContext export { SplitContext } from './SplitContext'; + +// Types +export type { + GetTreatmentsOptions, + ISplitClientChildProps, + ISplitClientProps, + ISplitContextValues, + ISplitFactoryChildProps, + ISplitFactoryProps, + ISplitStatus, + ISplitTreatmentsChildProps, + ISplitTreatmentsProps, + IUpdateProps, + IUseSplitClientOptions, + IUseSplitTreatmentsOptions, +} from './types'; diff --git a/src/types.ts b/src/types.ts index d716fb7..83d4b26 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,10 @@ import SplitIO from '@splitsoftware/splitio/types/splitio'; +import type { ReactNode } from 'react'; /** * Split Status interface. It represents the current readiness state of the SDK. */ -interface ISplitStatus { +export interface ISplitStatus { /** * isReady indicates if the Split SDK client has triggered an SDK_READY event and thus is ready to be consumed. @@ -101,7 +102,7 @@ export interface ISplitFactoryChildProps extends ISplitContextValues { } /** * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. + * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. */ export interface ISplitFactoryProps extends IUpdateProps { @@ -124,7 +125,30 @@ export interface ISplitFactoryProps extends IUpdateProps { /** * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. */ - children: ((props: ISplitFactoryChildProps) => JSX.Element | null) | JSX.Element | null; + children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode; +} + +/** + * useSplitClient options interface. This is the options object accepted by useSplitClient hook, + * used to retrieve a client instance with the Split context, and listen for SDK events. + */ +export interface IUseSplitClientOptions extends IUpdateProps { + + /** + * The customer identifier. + */ + splitKey?: SplitIO.SplitKey; + + /** + * Traffic type associated with the customer identifier. + * If no provided here or at the config object, it will be required on the client.track() calls. + */ + trafficType?: string; + + /** + * An object of type Attributes used to evaluate the feature flags. + */ + attributes?: SplitIO.Attributes; } /** @@ -136,32 +160,44 @@ export interface ISplitClientChildProps extends ISplitContextValues { } /** * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. + * used to instantiate a new client instance, update the Split context, and listen for SDK events. */ -export interface ISplitClientProps extends IUpdateProps { +export interface ISplitClientProps extends IUseSplitClientOptions { /** - * The customer identifier. + * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. */ - splitKey: SplitIO.SplitKey; + children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; +} + +export type GetTreatmentsOptions = ({ /** - * Traffic type associated with the customer identifier. - * If no provided here or at the config object, it will be required on the client.track() calls. + * List of feature flag names to evaluate. Either this or the `flagSets` property must be provided. If both are provided, the `flagSets` option is ignored. */ - trafficType?: string; + names: string[]; + flagSets?: undefined; +} | { /** - * An object of type Attributes used to evaluate the feature flags. + * List of feature flag sets to evaluate. Either this or the `names` property must be provided. If both are provided, the `flagSets` option is ignored. */ - attributes?: SplitIO.Attributes; + flagSets: string[]; + names?: undefined; +}) & { /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. + * An object of type Attributes used to evaluate the feature flags. */ - children: ((props: ISplitClientChildProps) => JSX.Element | null) | JSX.Element | null; + attributes?: SplitIO.Attributes; } +/** + * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', + * depending on whether `names` or `flagSets` options are provided, and to retrieve the result along with the Split context. + */ +export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions; + /** * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. */ @@ -170,32 +206,25 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { /** * An object with the treatments with configs for a bulk of feature flags, returned by client.getTreatmentsWithConfig(). * Each existing configuration is a stringified version of the JSON you defined on the Split user interface. For example: + * + * ```js * { - * split1: { treatment: 'on', config: null } - * split2: { treatment: 'off', config: '{"bannerText":"Click here."}' } + * feature1: { treatment: 'on', config: null }, + * feature2: { treatment: 'off', config: '{"bannerText":"Click here."}' } * } + * ``` */ treatments: SplitIO.TreatmentsWithConfig; } /** - * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, - * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. + * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', + * depending on whether `names` or `flagSets` props are provided, and to pass the result to the child component. */ -export interface ISplitTreatmentsProps { - - /** - * list of feature flag names - */ - names: string[]; - - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; +export type ISplitTreatmentsProps = GetTreatmentsOptions & { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. */ - children: ((props: ISplitTreatmentsChildProps) => JSX.Element | null); + children: ((props: ISplitTreatmentsChildProps) => ReactNode); } diff --git a/src/useClient.ts b/src/useClient.ts index ddafee0..535eb98 100644 --- a/src/useClient.ts +++ b/src/useClient.ts @@ -5,9 +5,12 @@ import { useSplitClient } from './useSplitClient'; * It uses the 'useContext' hook to access the context, which is updated by * SplitFactory and SplitClient components in the hierarchy of components. * - * @return A Split Client instance, or null if used outside the scope of SplitFactory + * @returns A Split Client instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} + * + * @deprecated Replace with the new `useSplitClient` hook. */ -export function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { - return useSplitClient(key, trafficType, attributes).client; +export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { + return useSplitClient({ splitKey, trafficType, attributes }).client; } diff --git a/src/useManager.ts b/src/useManager.ts index 0a9b497..9ba101b 100644 --- a/src/useManager.ts +++ b/src/useManager.ts @@ -1,18 +1,16 @@ -import React from 'react'; -import { SplitContext } from './SplitContext'; -import { ERROR_UM_NO_USECONTEXT } from './constants'; -import { checkHooks } from './utils'; +import { useSplitManager } from './useSplitManager'; /** * 'useManager' is a hook that returns the Manager instance from the Split factory. * It uses the 'useContext' hook to access the factory at Split context, which is updated by * the SplitFactory component. * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory + * @returns A Split Manager instance, or null if used outside the scope of SplitFactory + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + * + * @deprecated Replace with the new `useSplitManager` hook. */ export function useManager(): SplitIO.IManager | null { - if (!checkHooks(ERROR_UM_NO_USECONTEXT)) return null; - const { factory } = React.useContext(SplitContext); - return factory ? factory.manager() : null; + return useSplitManager().manager; } diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 5b002e9..a0c696c 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,26 +1,67 @@ import React from 'react'; -import { SplitContext, INITIAL_CONTEXT } from './SplitContext'; -import { ERROR_UC_NO_USECONTEXT } from './constants'; -import { getSplitSharedClient, checkHooks, initAttributes, IClientWithContext } from './utils'; -import { ISplitContextValues } from './types'; +import { SplitContext } from './SplitContext'; +import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils'; +import { ISplitContextValues, IUseSplitClientOptions } from './types'; + +export const DEFAULT_UPDATE_OPTIONS = { + updateOnSdkUpdate: false, + updateOnSdkTimedout: false, + updateOnSdkReady: true, + updateOnSdkReadyFromCache: true, +}; /** * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. * - * @return A Split Context object + * @returns A Split Context object + * + * @example + * ```js + * const { factory, client, isReady, isReadyFromCache, hasTimedout, lastUpdate } = useSplitClient({ splitKey: 'user_id' }); + * ``` + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ -export function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues { - if (!checkHooks(ERROR_UC_NO_USECONTEXT)) return INITIAL_CONTEXT; +export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues { + const { + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, trafficType, attributes + } = { ...DEFAULT_UPDATE_OPTIONS, ...options }; const context = React.useContext(SplitContext); - let { factory, client } = context; - if (key && factory) { - client = getSplitSharedClient(factory, key, trafficType); + const { client: contextClient, factory } = context; + + let client = contextClient as IClientWithContext; + if (splitKey && factory) { + client = getSplitClient(factory, splitKey, trafficType); } initAttributes(client, attributes); - return client === context.client ? context : { - ...context, client, ...(client as IClientWithContext).__getStatus() + + const [, setLastUpdate] = React.useState(client ? client.lastUpdate : 0); + + // Handle client events + React.useEffect(() => { + if (!client) return; + + const update = () => setLastUpdate(client.lastUpdate); + + // Subscribe to SDK events + const status = getStatus(client); + if (!status.isReady && updateOnSdkReady) client.once(client.Event.SDK_READY, update); + if (!status.isReadyFromCache && updateOnSdkReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, update); + if (!status.hasTimedout && !status.isReady && updateOnSdkTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, update); + if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); + + return () => { + // Unsubscribe from events + client.off(client.Event.SDK_READY, update); + client.off(client.Event.SDK_READY_FROM_CACHE, update); + client.off(client.Event.SDK_READY_TIMED_OUT, update); + client.off(client.Event.SDK_UPDATE, update); + } + }, [client, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); + + return { + factory, client, ...getStatus(client) }; } diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts new file mode 100644 index 0000000..1ba6a23 --- /dev/null +++ b/src/useSplitManager.ts @@ -0,0 +1,25 @@ +import React from 'react'; +import { SplitContext } from './SplitContext'; +import { ISplitContextValues } from './types'; + +/** + * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. + * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactory component. + * + * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactory + * + * @example + * ```js + * const { manager, isReady } = useSplitManager(); + * ``` + * + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + */ +export function useSplitManager(): ISplitContextValues & { manager: SplitIO.IManager | null } { + // Update options are not supported, because updates can be controlled at the SplitFactory component. + const context = React.useContext(SplitContext); + return { + ...context, + manager: context.factory ? context.factory.manager() : null + }; +} diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index 14cab24..d5dbf6d 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -1,22 +1,32 @@ -import { getControlTreatmentsWithConfig, ERROR_UT_NO_USECONTEXT } from './constants'; -import { checkHooks, IClientWithContext } from './utils'; -import { ISplitTreatmentsChildProps } from './types'; -import { INITIAL_CONTEXT } from './SplitContext'; +import React from 'react'; +import { memoizeGetTreatmentsWithConfig } from './utils'; +import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types'; import { useSplitClient } from './useSplitClient'; /** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. + * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property object that contains feature flag evaluations. + * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, + * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. + * + * @returns A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. + * + * @example + * ```js + * const { treatments: { feature_1, feature_2 }, isReady, isReadyFromCache, hasTimedout, lastUpdate, ... } = useSplitTreatments({ names: ['feature_1', 'feature_2']}); + * ``` * - * @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} */ -export function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps { - const context = checkHooks(ERROR_UT_NO_USECONTEXT) ? useSplitClient(key) : INITIAL_CONTEXT; - const client = context.client; - const treatments = client && (client as IClientWithContext).__getStatus().isOperational ? - client.getTreatmentsWithConfig(splitNames, attributes) : - getControlTreatmentsWithConfig(splitNames); +export function useSplitTreatments(options: IUseSplitTreatmentsOptions): ISplitTreatmentsChildProps { + const context = useSplitClient({ ...options, attributes: undefined }); + const { client, lastUpdate } = context; + const { names, flagSets, attributes } = options; + + const getTreatmentsWithConfig = React.useMemo(memoizeGetTreatmentsWithConfig, []); + + // Shallow copy `client.getAttributes` result for memoization, as it returns the same reference unless `client.clearAttributes` is invoked. + // Note: the same issue occurs with the `names` and `attributes` arguments if they are mutated directly by the user instead of providing a new object. + const treatments = getTreatmentsWithConfig(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); return { ...context, diff --git a/src/useTrack.ts b/src/useTrack.ts index adea31e..b511018 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -1,6 +1,4 @@ -import { useClient } from './useClient'; -import { ERROR_UTRACK_NO_USECONTEXT } from './constants'; -import { checkHooks } from './utils'; +import { useSplitClient } from './useSplitClient'; // no-op function that returns false const noOpFalse = () => false; @@ -9,10 +7,12 @@ const noOpFalse = () => false; * 'useTrack' is a hook that returns the track method from a Split client. * It uses the 'useContext' hook to access the client from the Split context. * - * @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false. + * @returns A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ -export function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { - const client = checkHooks(ERROR_UTRACK_NO_USECONTEXT) ? useClient(key, trafficType) : null; +export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { + // All update options are false to avoid re-renders. The track method doesn't need the client to be operational. + const { client } = useSplitClient({ splitKey, trafficType, updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); return client ? client.track.bind(client) : noOpFalse; } diff --git a/src/useTreatments.ts b/src/useTreatments.ts index 4aed7d8..bd673fa 100644 --- a/src/useTreatments.ts +++ b/src/useTreatments.ts @@ -5,9 +5,12 @@ import { useSplitTreatments } from './useSplitTreatments'; * It uses the 'useContext' hook to access the client from the Split context, * and invokes the 'getTreatmentsWithConfig' method. * - * @return A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. + * @returns A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. + * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} + * + * @deprecated Replace with the new `useSplitTreatments` hook. */ -export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { - return useSplitTreatments(featureFlagNames, attributes, key).treatments; +export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { + return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments; } diff --git a/src/utils.ts b/src/utils.ts index 50f93f6..ee42523 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,8 @@ -import React from 'react'; +import memoizeOne from 'memoize-one'; +import shallowEqual from 'shallowequal'; import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { VERSION } from './constants'; +import { VERSION, WARN_NAMES_AND_FLAGSETS, getControlTreatmentsWithConfig } from './constants'; +import { ISplitStatus } from './types'; // Utils used to access singleton instances of Split factories and clients, and to gracefully shutdown all clients together. @@ -15,13 +17,14 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { hasTimedout: boolean; isDestroyed: boolean; }; + lastUpdate: number; } /** * FactoryWithClientInstances interface. */ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - sharedClientInstances: Set; + clientInstances: Set; config: SplitIO.IBrowserSettings; } @@ -36,7 +39,7 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC const newFactory = SplitSdk(config, (modules) => { modules.settings.version = VERSION; }) as IFactoryWithClients; - newFactory.sharedClientInstances = new Set(); + newFactory.clientInstances = new Set(); newFactory.config = config; __factories.set(config, newFactory); } @@ -44,38 +47,43 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithC } // idempotent operation -export function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { // factory.client is an idempotent operation - const client = factory.client(key, trafficType) as IClientWithContext; - if ((factory as IFactoryWithClients).sharedClientInstances) { - (factory as IFactoryWithClients).sharedClientInstances.add(client); + const client = (key !== undefined ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + + // Handle client lastUpdate + if (client.lastUpdate === undefined) { + const updateLastUpdate = () => { + const lastUpdate = Date.now(); + client.lastUpdate = lastUpdate > client.lastUpdate ? lastUpdate : client.lastUpdate + 1; + } + + client.lastUpdate = 0; + client.on(client.Event.SDK_READY, updateLastUpdate); + client.on(client.Event.SDK_READY_FROM_CACHE, updateLastUpdate); + client.on(client.Event.SDK_READY_TIMED_OUT, updateLastUpdate); + client.on(client.Event.SDK_UPDATE, updateLastUpdate); + } + + if ((factory as IFactoryWithClients).clientInstances) { + (factory as IFactoryWithClients).clientInstances.add(client); } return client; } export function destroySplitFactory(factory: IFactoryWithClients): Promise { - // call destroy of shared clients and main one - const destroyPromises = []; - factory.sharedClientInstances.forEach((client) => destroyPromises.push(client.destroy())); - destroyPromises.push(factory.client().destroy()); + // call destroy of clients + const destroyPromises: Promise[] = []; + factory.clientInstances.forEach((client) => destroyPromises.push(client.destroy())); // remove references to release allocated memory - factory.sharedClientInstances.clear(); + factory.clientInstances.clear(); __factories.delete(factory.config); return Promise.all(destroyPromises); } -// Utils used to access client status. -// They might be removed in the future, if the JS SDK extends its public API with a `getStatus` method - -export interface IClientStatus { - isReady: boolean; - isReadyFromCache: boolean; - hasTimedout: boolean; - isTimedout: boolean; - isDestroyed: boolean; -} - -export function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus { +// Util used to get client status. +// It might be removed in the future, if the JS SDK extends its public API with a `getStatus` method +export function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus { const status = client && (client as IClientWithContext).__getStatus(); const isReady = status ? status.isReady : false; const hasTimedout = status ? status.hasTimedout : false; @@ -85,29 +93,13 @@ export function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus isTimedout: hasTimedout && !isReady, hasTimedout, isDestroyed: status ? status.isDestroyed : false, + lastUpdate: client ? (client as IClientWithContext).lastUpdate || 0 : 0, }; } -// Other utils - -/** - * Checks if React.useContext is available, and logs given message if not - * - * @param message - * @returns boolean indicating if React.useContext is available - */ -export function checkHooks(message: string): boolean { - if (!React.useContext) { - console.log(message); - return false; - } else { - return true; - } -} - // Input validation utils that will be replaced eventually -export function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'split names'): false | string[] { +export function validateFeatureFlags(maybeFeatureFlags: unknown, listName = 'feature flag names'): false | string[] { if (Array.isArray(maybeFeatureFlags) && maybeFeatureFlags.length > 0) { const validatedArray: string[] = []; // Remove invalid values @@ -133,7 +125,7 @@ export function initAttributes(client: SplitIO.IBrowserClient | null, attributes const TRIMMABLE_SPACES_REGEX = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/; -function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'split name'): false | string { +function validateFeatureFlag(maybeFeatureFlag: unknown, item = 'feature flag name'): false | string { if (maybeFeatureFlag == undefined) { console.log(`[ERROR] you passed a null or undefined ${item}, ${item} must be a non-empty string.`); } else if (!isString(maybeFeatureFlag)) { @@ -170,3 +162,32 @@ function uniq(arr: string[]): string[] { function isString(val: unknown): val is string { return typeof val === 'string' || val instanceof String; } + +/** + * Gets a memoized version of the `client.getTreatmentsWithConfig` method. + * It is used to avoid duplicated impressions, because the result treatments are the same given the same `client` instance, `lastUpdate` timestamp, and list of feature flag `names` and `attributes`. + */ +export function memoizeGetTreatmentsWithConfig() { + return memoizeOne(evaluateFeatureFlags, argsAreEqual); +} + +function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { + return newArgs[0] === lastArgs[0] && // client + newArgs[1] === lastArgs[1] && // lastUpdate + shallowEqual(newArgs[2], lastArgs[2]) && // names + shallowEqual(newArgs[3], lastArgs[3]) && // attributes + shallowEqual(newArgs[4], lastArgs[4]) && // client attributes + shallowEqual(newArgs[5], lastArgs[5]); // flagSets +} + +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { + if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS); + + return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? + names ? + client.getTreatmentsWithConfig(names, attributes) : + client.getTreatmentsWithConfigByFlagSets(flagSets!, attributes) : + names ? + getControlTreatmentsWithConfig(names) : + {} // empty object when evaluating with flag sets and client is not ready +} diff --git a/types/SplitClient.d.ts b/types/SplitClient.d.ts deleted file mode 100644 index 3a7cb42..0000000 --- a/types/SplitClient.d.ts +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; -/** - * Common component used to handle the status and events of a Split client passed as prop. - * Reused by both SplitFactory (main client) and SplitClient (shared client) components. - */ -export declare class SplitComponent extends React.Component { - static defaultProps: { - updateOnSdkUpdate: boolean; - updateOnSdkTimedout: boolean; - updateOnSdkReady: boolean; - updateOnSdkReadyFromCache: boolean; - children: null; - factory: null; - client: null; - }; - static getDerivedStateFromProps(props: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }, state: ISplitContextValues): { - isReady: boolean; - isReadyFromCache: boolean; - hasTimedout: boolean; - isTimedout: boolean; - isDestroyed: boolean; - client: import("@splitsoftware/splitio/types/splitio").IBrowserClient | null; - factory: import("@splitsoftware/splitio/types/splitio").IBrowserSDK | null; - } | null; - readonly state: Readonly; - constructor(props: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }); - subscribeToEvents(client: SplitIO.IBrowserClient | null): void; - unsubscribeFromEvents(client: SplitIO.IBrowserClient | null): void; - setReady: () => void; - setReadyFromCache: () => void; - setTimedout: () => void; - setUpdate: () => void; - componentDidMount(): void; - componentDidUpdate(prevProps: ISplitClientProps & { - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }): void; - componentWillUnmount(): void; - render(): JSX.Element; -} -/** - * SplitClient will initialize a new SDK client and listen for its events in order to update the Split Context. - * Children components will have access to the new client when accessing Split Context. - * - * Unlike SplitFactory, the underlying SDK client can be changed during the component lifecycle - * if the component is updated with a different splitKey or trafficType prop. Since the client can change, - * its release is not handled by SplitClient but by its container SplitFactory component. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} - */ -export declare function SplitClient(props: ISplitClientProps): JSX.Element; diff --git a/types/SplitContext.d.ts b/types/SplitContext.d.ts deleted file mode 100644 index def9f03..0000000 --- a/types/SplitContext.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { ISplitContextValues } from './types'; -export declare const INITIAL_CONTEXT: ISplitContextValues; -/** - * Split Context is the React Context instance that represents our SplitIO global state. - * It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate) - * The context is created with default empty values, that eventually SplitFactory and SplitClient access and update. - */ -export declare const SplitContext: React.Context; diff --git a/types/SplitFactory.d.ts b/types/SplitFactory.d.ts deleted file mode 100644 index 34f8d5a..0000000 --- a/types/SplitFactory.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import { ISplitFactoryProps } from './types'; -/** - * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, - * and automatically shutdown and release resources when it is unmounted. SplitFactory must wrap other components and functions - * from this library, since they access the Split Context and its elements (factory, clients, etc). - * - * The underlying SDK factory and client is set on the constructor, and cannot be changed during the component lifecycle, - * even if the component is updated with a different config or factory prop. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK} - */ -export declare class SplitFactory extends React.Component { - static defaultProps: ISplitFactoryProps; - readonly state: Readonly<{ - factory: SplitIO.IBrowserSDK | null; - client: SplitIO.IBrowserClient | null; - }>; - readonly isFactoryExternal: boolean; - constructor(props: ISplitFactoryProps); - componentWillUnmount(): void; - render(): JSX.Element; -} diff --git a/types/SplitTreatments.d.ts b/types/SplitTreatments.d.ts deleted file mode 100644 index 70e213a..0000000 --- a/types/SplitTreatments.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { ISplitTreatmentsProps } from './types'; -/** - * SplitTreatments accepts a list of feature flag names and optional attributes. It access the client at SplitContext to - * call 'client.getTreatmentsWithConfig()' method, and passes the returned treatments to a child as a function. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} - */ -export declare class SplitTreatments extends React.Component { - private logWarning?; - private evaluateFeatureFlags; - render(): JSX.Element; - componentDidMount(): void; -} diff --git a/types/constants.d.ts b/types/constants.d.ts deleted file mode 100644 index 2d52bf2..0000000 --- a/types/constants.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export declare const VERSION: string; -export declare const ON: SplitIO.Treatment; -export declare const OFF: SplitIO.Treatment; -export declare const CONTROL: SplitIO.Treatment; -export declare const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig; -export declare const getControlTreatmentsWithConfig: (featureFlagNames: unknown) => SplitIO.TreatmentsWithConfig; -export declare const WARN_SF_CONFIG_AND_FACTORY: string; -export declare const ERROR_SF_NO_CONFIG_AND_FACTORY: string; -export declare const ERROR_SC_NO_FACTORY: string; -export declare const WARN_ST_NO_CLIENT: string; -export declare const ERROR_UC_NO_USECONTEXT: string; -export declare const ERROR_UM_NO_USECONTEXT: string; -export declare const ERROR_UT_NO_USECONTEXT: string; -export declare const ERROR_UTRACK_NO_USECONTEXT: string; -export declare const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string; diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 91b8d2a..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -export { withSplitFactory } from './withSplitFactory'; -export { withSplitClient } from './withSplitClient'; -export { withSplitTreatments } from './withSplitTreatments'; -export { SplitTreatments } from './SplitTreatments'; -export { SplitClient } from './SplitClient'; -export { SplitFactory } from './SplitFactory'; -export { useClient } from './useClient'; -export { useTreatments } from './useTreatments'; -export { useTrack } from './useTrack'; -export { useManager } from './useManager'; -export { SplitContext } from './SplitContext'; diff --git a/types/types.d.ts b/types/types.d.ts deleted file mode 100644 index 4bcef48..0000000 --- a/types/types.d.ts +++ /dev/null @@ -1,168 +0,0 @@ -/// -import SplitIO from '@splitsoftware/splitio/types/splitio'; -/** - * Split Status interface. It represents the current readiness state of the SDK. - */ -interface ISplitStatus { - /** - * isReady indicates if the Split SDK client has triggered an SDK_READY event and thus is ready to be consumed. - */ - isReady: boolean; - /** - * isReadyFromCache indicates if the Split SDK client has triggered an SDK_READY_FROM_CACHE event and thus is ready to be consumed, - * although the data in cache might be stale. - */ - isReadyFromCache: boolean; - /** - * isTimedout indicates if the Split SDK client has triggered an SDK_READY_TIMED_OUT event and is not ready to be consumed. - */ - isTimedout: boolean; - /** - * hasTimedout indicates if the Split SDK client has ever triggered an SDK_READY_TIMED_OUT event. - * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. - */ - hasTimedout: boolean; - /** - * isDestroyed indicates if the Split SDK client has been destroyed. - */ - isDestroyed: boolean; - /** - * Indicates when was the last status event, either SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT or SDK_UPDATE. - */ - lastUpdate: number; -} -/** - * Split Context Value interface. It is used to define the value types of Split Context - */ -export interface ISplitContextValues extends ISplitStatus { - /** - * Split factory instance - */ - factory: SplitIO.IBrowserSDK | null; - /** - * Split client instance - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} - */ - client: SplitIO.IBrowserClient | null; -} -/** - * Update Props interface. It defines the props used to configure what SDK events are listened to update the Split context. - * Only `SDK_UPDATE` and `SDK_READY_TIMED_OUT` are configurable. - * The `SDK_READY` event is always listened to update the Split context value 'isReady'. - */ -export interface IUpdateProps { - /** - * updateOnSdkUpdate indicates if the component will update the `SplitContext` in case of a `SDK_UPDATE` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_UPDATE. - * It's value is false by default. - */ - updateOnSdkUpdate?: boolean; - /** - * updateOnSdkTimedout indicates if the component will update the `SplitContext` in case of a `SDK_READY_TIMED_OUT` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY_TIMED_OUT. - * It's value is false by default. - */ - updateOnSdkTimedout?: boolean; - /** - * updateOnSdkReady indicates if the component will update the `SplitContext` in case of a `SDK_READY` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY. - * It's value is true by default. - */ - updateOnSdkReady?: boolean; - /** - * updateOnSdkReadyFromCache indicates if the component will update the `SplitContext` in case of a `SDK_READY_FROM_CACHE` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY_FROM_CACHE. - * This params is only relevant when using 'LOCALSTORAGE' as storage type, since otherwise the event is never emitted. - * It's value is true by default. - */ - updateOnSdkReadyFromCache?: boolean; -} -/** - * SplitFactory Child Props interface. These are the props that the child component receives from the 'SplitFactory' component. - */ -export interface ISplitFactoryChildProps extends ISplitContextValues { -} -/** - * SplitFactory Props interface. These are the props accepted by SplitFactory component, - * used to instantiate a factory and client instances, update the Split context, and listen for SDK events. - */ -export interface ISplitFactoryProps extends IUpdateProps { - /** - * Config object used to instantiate a Split factory - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration} - */ - config?: SplitIO.IBrowserSettings; - /** - * Split factory instance to use instead of creating a new one with the config object. - */ - factory?: SplitIO.IBrowserSDK; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; - /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. - */ - children: ((props: ISplitFactoryChildProps) => JSX.Element | null) | JSX.Element | null; -} -/** - * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. - */ -export interface ISplitClientChildProps extends ISplitContextValues { -} -/** - * SplitClient Props interface. These are the props accepted by SplitClient component, - * used to instantiate a new client instances, update the Split context, and listen for SDK events. - */ -export interface ISplitClientProps extends IUpdateProps { - /** - * The customer identifier. - */ - splitKey: SplitIO.SplitKey; - /** - * Traffic type associated with the customer identifier. - * If no provided here or at the config object, it will be required on the client.track() calls. - */ - trafficType?: string; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; - /** - * Children of the SplitFactory component. It can be a functional component (child as a function) or a React element. - */ - children: ((props: ISplitClientChildProps) => JSX.Element | null) | JSX.Element | null; -} -/** - * SplitTreatments Child Props interface. These are the props that the child component receives from the 'SplitTreatments' component. - */ -export interface ISplitTreatmentsChildProps extends ISplitContextValues { - /** - * An object with the treatments with configs for a bulk of feature flags, returned by client.getTreatmentsWithConfig(). - * Each existing configuration is a stringified version of the JSON you defined on the Split user interface. For example: - * { - * split1: { treatment: 'on', config: null } - * split2: { treatment: 'off', config: '{"bannerText":"Click here."}' } - * } - */ - treatments: SplitIO.TreatmentsWithConfig; -} -/** - * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, - * used to call 'client.getTreatmentsWithConfig()' and pass the result to the child component. - */ -export interface ISplitTreatmentsProps { - /** - * list of feature flag names - */ - names: string[]; - /** - * An object of type Attributes used to evaluate the feature flags. - */ - attributes?: SplitIO.Attributes; - /** - * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. - */ - children: ((props: ISplitTreatmentsChildProps) => JSX.Element | null); -} -export {}; diff --git a/types/useClient.d.ts b/types/useClient.d.ts deleted file mode 100644 index 75284f7..0000000 --- a/types/useClient.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 'useClient' is a hook that returns a client from the Split context. - * It uses the 'useContext' hook to access the context, which is updated by - * SplitFactory and SplitClient components in the hierarchy of components. - * - * @return A Split Client instance, or null if used outside the scope of SplitFactory - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} - */ -export declare function useClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null; diff --git a/types/useManager.d.ts b/types/useManager.d.ts deleted file mode 100644 index 68257aa..0000000 --- a/types/useManager.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 'useManager' is a hook that returns the Manager instance from the Split factory. - * It uses the 'useContext' hook to access the factory at Split context, which is updated by - * the SplitFactory component. - * - * @return A Split Manager instance, or null if used outside the scope of SplitFactory - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} - */ -export declare function useManager(): SplitIO.IManager | null; diff --git a/types/useSplitClient.d.ts b/types/useSplitClient.d.ts deleted file mode 100644 index 64297af..0000000 --- a/types/useSplitClient.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ISplitContextValues } from './types'; -/** - * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. - * It uses the 'useContext' hook to access the context, which is updated by SplitFactory and SplitClient components in the hierarchy of components. - * - * @return A Split Context object - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} - */ -export declare function useSplitClient(key?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): ISplitContextValues; diff --git a/types/useSplitTreatments.d.ts b/types/useSplitTreatments.d.ts deleted file mode 100644 index b83b813..0000000 --- a/types/useSplitTreatments.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ISplitTreatmentsChildProps } from './types'; -/** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property containing an object of feature flag evaluations (i.e., treatments). - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'getTreatmentsWithConfig' method. - * - * @return A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if split names do not exist. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} - */ -export declare function useSplitTreatments(splitNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): ISplitTreatmentsChildProps; diff --git a/types/useTrack.d.ts b/types/useTrack.d.ts deleted file mode 100644 index 6479f79..0000000 --- a/types/useTrack.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * 'useTrack' is a hook that returns the track method from a Split client. - * It uses the 'useContext' hook to access the client from the Split context. - * - * @return A track function binded to a Split client. If the client is not available, the result is a no-op function that returns false. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} - */ -export declare function useTrack(key?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track']; diff --git a/types/useTreatments.d.ts b/types/useTreatments.d.ts deleted file mode 100644 index 6b688e7..0000000 --- a/types/useTreatments.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * 'useTreatments' is a hook that returns an object of feature flag evaluations (i.e., treatments). - * It uses the 'useContext' hook to access the client from the Split context, - * and invokes the 'getTreatmentsWithConfig' method. - * - * @return A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} - */ -export declare function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, key?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig; diff --git a/types/utils.d.ts b/types/utils.d.ts deleted file mode 100644 index 71fc810..0000000 --- a/types/utils.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * ClientWithContext interface. - */ -export interface IClientWithContext extends SplitIO.IBrowserClient { - __getStatus(): { - isReady: boolean; - isReadyFromCache: boolean; - isOperational: boolean; - hasTimedout: boolean; - isDestroyed: boolean; - }; -} -/** - * FactoryWithClientInstances interface. - */ -export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - sharedClientInstances: Set; - config: SplitIO.IBrowserSettings; -} -export declare const __factories: Map; -export declare function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients; -export declare function getSplitSharedClient(factory: SplitIO.IBrowserSDK, key: SplitIO.SplitKey, trafficType?: string): IClientWithContext; -export declare function destroySplitFactory(factory: IFactoryWithClients): Promise; -export interface IClientStatus { - isReady: boolean; - isReadyFromCache: boolean; - hasTimedout: boolean; - isTimedout: boolean; - isDestroyed: boolean; -} -export declare function getStatus(client: SplitIO.IBrowserClient | null): IClientStatus; -/** - * Checks if React.useContext is available, and logs given message if not - * - * @param message - * @returns boolean indicating if React.useContext is available - */ -export declare function checkHooks(message: string): boolean; -export declare function validateFeatureFlags(maybeFeatureFlags: unknown, listName?: string): false | string[]; -/** - * Manage client attributes binding - */ -export declare function initAttributes(client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes): void; diff --git a/types/withSplitClient.d.ts b/types/withSplitClient.d.ts deleted file mode 100644 index 5a98bcf..0000000 --- a/types/withSplitClient.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitClientChildProps } from './types'; -/** - * High-Order Component for SplitClient. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitClient (see ISplitClientChildProps). - * - * @param splitKey The customer identifier. - * @param trafficType Traffic type associated with the customer identifier. If no provided here or at the config object, it will be required on the client.track() calls. - */ -export declare function withSplitClient(splitKey: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType, updateOnSdkUpdate?: boolean, updateOnSdkTimedout?: boolean, updateOnSdkReady?: boolean, updateOnSdkReadyFromCache?: boolean) => (props: OuterProps) => JSX.Element; diff --git a/types/withSplitFactory.d.ts b/types/withSplitFactory.d.ts deleted file mode 100644 index 183ac37..0000000 --- a/types/withSplitFactory.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitFactoryChildProps } from './types'; -/** - * High-Order Component for SplitFactory. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitFactory (see ISplitFactoryChildProps). - * - * @param config Config object used to instantiate a Split factory - * @param factory Split factory instance to use instead of creating a new one with the config object. - */ -export declare function withSplitFactory(config?: SplitIO.IBrowserSettings, factory?: SplitIO.IBrowserSDK, attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType, updateOnSdkUpdate?: boolean, updateOnSdkTimedout?: boolean, updateOnSdkReady?: boolean, updateOnSdkReadyFromCache?: boolean) => (props: OuterProps) => JSX.Element; diff --git a/types/withSplitTreatments.d.ts b/types/withSplitTreatments.d.ts deleted file mode 100644 index 848c42e..0000000 --- a/types/withSplitTreatments.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { ISplitTreatmentsChildProps } from './types'; -/** - * High-Order Component for SplitTreatments. - * The wrapped component receives all the props of the container, - * along with the passed props from SplitTreatments (see ISplitTreatmentsChildProps). - * - * @param names list of feature flag names - * @param attributes An object of type Attributes used to evaluate the feature flags. - */ -export declare function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes): (WrappedComponent: React.ComponentType) => (props: OuterProps) => JSX.Element;