diff --git a/.eslintrc.js b/.eslintrc.js index 9eba935..bbebf16 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { 'plugins': [ 'react', '@typescript-eslint', + 'eslint-plugin-tsdoc', 'import' ], 'rules': { @@ -63,5 +64,11 @@ module.exports = { 'import/no-self-import': 'error', 'import/no-default-export': 'error', } + }, { + // Enable TSDoc rules for TypeScript files, allowing the use of JSDoc in JS files. + 'files': ['**/*.ts', '**/*.tsx'], + 'rules': { + 'tsdoc/syntax': 'warn' + } }] } diff --git a/CHANGES.txt b/CHANGES.txt index b800f1f..03167d5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,11 +1,14 @@ -2.0.0 (October XX, 2024) - - Added support for targeting rules based on large segments for browsers. - - Updated @splitsoftware/splitio package to version 10.29.0 that includes minor updates, and updated some transitive dependencies for vulnerability fixes. - - Renamed distribution folders from `/lib` to `/cjs` for CommonJS build, and `/es` to `/esm` for EcmaScript Modules build. +2.0.0 (November XX, 2024) + - Added support for targeting rules based on large segments. + - Added support for passing factory instances to the `factory` prop of the `SplitFactoryProvider` component from other SDK packages that extends the `SplitIO.IBrowserSDK` interface, such as `@splitsoftware/splitio-react-native`, `@splitsoftware/splitio-browserjs` and `@splitsoftware/browser-suite` packages. + - Updated @splitsoftware/splitio package to version 11.0.0 that includes major updates, and updated some transitive dependencies for vulnerability fixes. + - Renamed distribution folders from `/lib` to `/cjs` for CommonJS build, and `/es` to `/esm` for ECMAScript Modules build. + - Bugfixing - When the `config` prop is provided, the `SplitFactoryProvider` now makes the SDK factory and client instances available in the context immediately during the initial render, instead of waiting for the first SDK event (Related to https://github.com/splitio/react-client/issues/198). This change fixes a bug in the `useTrack` hook, which was not retrieving the client's `track` method during the initial render. - BREAKING CHANGES: - Updated error handling: using the library modules without wrapping them in a `SplitFactoryProvider` component will now throw an error instead of logging it, as the modules requires the `SplitContext` to work properly. + - Removed the `core.trafficType` configuration option and the `trafficType` parameter from the SDK `client()` method, `useSplitClient`, `useTrack`, and `SplitClient` component. This is because traffic types can no longer be bound to SDK clients in JavaScript SDK v11.0.0, and so the traffic type must be provided as first argument in the `track` method calls. - Removed deprecated modules: `SplitFactory` component, `useClient`, `useTreatments` and `useManager` hooks, and `withSplitFactory`, `withSplitClient` and `withSplitTreatments` high-order components. Refer to ./MIGRATION-GUIDE.md for instructions on how to migrate to the new alternatives. - - Renamed TypeScript interfaces `ISplitFactoryProps` to `ISplitFactoryProviderProps`, and `ISplitFactoryChildProps` to `ISplitFactoryProviderChildProps`. + - Renamed some TypeScript interfaces: `ISplitFactoryProps` to `ISplitFactoryProviderProps`, and `ISplitFactoryChildProps` to `ISplitFactoryProviderChildProps`. - Renamed `SplitSdk` to `SplitFactory` function, which is the underlying Split SDK factory, i.e., `import { SplitFactory } from '@splitsoftware/splitio'`. - Dropped support for React below 16.8.0, as the library components where rewritten using the React Hooks API available in React v16.8.0 and above. This refactor simplifies code maintenance and reduces bundle size. diff --git a/package-lock.json b/package-lock.json index a010821..daceee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.1-rc.0", + "version": "2.0.0-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.13.1-rc.0", + "version": "2.0.0-rc.2", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.28.1-rc.2", + "@splitsoftware/splitio": "11.0.0-rc.5", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -30,6 +30,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tsdoc": "^0.3.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", @@ -1494,6 +1495,46 @@ "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1548,13 +1589,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.28.1-rc.2", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.1-rc.2.tgz", - "integrity": "sha512-UwRlu3aBY/e2cDQUxDXZCnLisleOCSUgCQSIN8gGdAKO9QQHG1Vt2cxsxMIGRIIBWUIuuUJ2phVKHM/0M2ZPhw==", + "version": "11.0.0-rc.5", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.0.0-rc.5.tgz", + "integrity": "sha512-n8nDEKtGir4sb5n0vYSoORnK+nFXYCYvGFH55aBNEpUhjMlqu9ocEPtdvtBeuz30yIb18eiiSFAr74WgnPfZHA==", "dependencies": { - "@splitsoftware/splitio-commons": "1.17.1-rc.1", - "@types/google.analytics": "0.0.40", - "@types/ioredis": "^4.28.0", + "@splitsoftware/splitio-commons": "2.0.0-rc.6", "bloom-filters": "^3.0.0", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -1563,15 +1602,15 @@ "unfetch": "^4.2.0" }, "engines": { - "node": ">=6", - "npm": ">=3" + "node": ">=14.0.0" } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.17.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.1-rc.1.tgz", - "integrity": "sha512-mmDcWW2iyqQF/FzLgPoRA3KXpvswk/sDIhQGWTg3WPkapnA+e4WXb+U/TSGGB/Ig88NlM76FlxMDkrHnBayDXg==", + "version": "2.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.0-rc.6.tgz", + "integrity": "sha512-A4Dk02ShJBjXqtPro6ylBAPc344Unnyk7YmEmvQqv1x4VUKloLw76PI7FgUgG7bM2UH079jSIeUg8HePW2PL1g==", "dependencies": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" }, "peerDependencies": { @@ -1847,11 +1886,6 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, - "node_modules/@types/google.analytics": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", - "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==" - }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -4201,6 +4235,16 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz", + "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc-config": "0.17.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4876,10 +4920,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -5173,6 +5220,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -5513,12 +5572,15 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7848,6 +7910,12 @@ "node": ">=8" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9192,6 +9260,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -9205,12 +9282,12 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -11990,6 +12067,44 @@ "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, + "@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12035,13 +12150,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.28.1-rc.2", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.1-rc.2.tgz", - "integrity": "sha512-UwRlu3aBY/e2cDQUxDXZCnLisleOCSUgCQSIN8gGdAKO9QQHG1Vt2cxsxMIGRIIBWUIuuUJ2phVKHM/0M2ZPhw==", + "version": "11.0.0-rc.5", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.0.0-rc.5.tgz", + "integrity": "sha512-n8nDEKtGir4sb5n0vYSoORnK+nFXYCYvGFH55aBNEpUhjMlqu9ocEPtdvtBeuz30yIb18eiiSFAr74WgnPfZHA==", "requires": { - "@splitsoftware/splitio-commons": "1.17.1-rc.1", - "@types/google.analytics": "0.0.40", - "@types/ioredis": "^4.28.0", + "@splitsoftware/splitio-commons": "2.0.0-rc.6", "bloom-filters": "^3.0.0", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -12051,10 +12164,11 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.17.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.1-rc.1.tgz", - "integrity": "sha512-mmDcWW2iyqQF/FzLgPoRA3KXpvswk/sDIhQGWTg3WPkapnA+e4WXb+U/TSGGB/Ig88NlM76FlxMDkrHnBayDXg==", + "version": "2.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.0-rc.6.tgz", + "integrity": "sha512-A4Dk02ShJBjXqtPro6ylBAPc344Unnyk7YmEmvQqv1x4VUKloLw76PI7FgUgG7bM2UH079jSIeUg8HePW2PL1g==", "requires": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" } }, @@ -12269,11 +12383,6 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, - "@types/google.analytics": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", - "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==" - }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -14345,6 +14454,16 @@ "dev": true, "requires": {} }, + "eslint-plugin-tsdoc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz", + "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc-config": "0.17.0" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -14610,9 +14729,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, "function.prototype.name": { @@ -14817,6 +14936,15 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -15057,12 +15185,12 @@ } }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "requires": { - "has": "^1.0.3" + "hasown": "^2.0.2" } }, "is-date-object": { @@ -16775,6 +16903,12 @@ } } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17822,6 +17956,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -17835,12 +17975,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } diff --git a/package.json b/package.json index 814624a..2aae76b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.1-rc.0", + "version": "2.0.0-rc.2", "description": "A React library to easily integrate and use Split JS SDK", "main": "cjs/index.js", "module": "esm/index.js", @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.28.1-rc.2", + "@splitsoftware/splitio": "11.0.0-rc.5", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -84,6 +84,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tsdoc": "^0.3.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 7f5ebbb..ea42e95 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -8,7 +8,7 @@ import { useSplitClient } from './useSplitClient'; * Children components will have access to the new client when accessing Split Context. * * The underlying SDK client can be changed during the component lifecycle - * if the component is updated with a different splitKey or trafficType prop. + * if the component is updated with a different splitKey prop. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} */ diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx index 9e370d1..f0ff09b 100644 --- a/src/SplitFactoryProvider.tsx +++ b/src/SplitFactoryProvider.tsx @@ -3,8 +3,7 @@ import React from 'react'; import { SplitClient } from './SplitClient'; import { ISplitFactoryProviderProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus } from './utils'; -import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; +import { getSplitFactory, destroySplitFactory, getSplitClient, getStatus } from './utils'; import { SplitContext } from './SplitContext'; /** @@ -18,70 +17,29 @@ import { SplitContext } from './SplitContext'; * @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} */ export function SplitFactoryProvider(props: ISplitFactoryProviderProps) { - let { - config, factory: propFactory, - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate - } = { ...DEFAULT_UPDATE_OPTIONS, ...props }; + const { config, factory: propFactory } = props; - if (config && propFactory) { - console.log(WARN_SF_CONFIG_AND_FACTORY); - config = undefined; - } - - const [configFactory, setConfigFactory] = React.useState(null); const factory = React.useMemo(() => { - return propFactory || (configFactory && config === configFactory.config ? configFactory : null); - }, [config, propFactory, configFactory]); - const client = factory ? getSplitClient(factory) : null; + return propFactory || (config ? getSplitFactory(config) : undefined); + }, [config, propFactory]); + const client = factory ? getSplitClient(factory) : undefined; // Effect to initialize and destroy the factory React.useEffect(() => { + if (propFactory) { + if (config) console.log(WARN_SF_CONFIG_AND_FACTORY); + return; + } + if (config) { const factory = getSplitFactory(config); + factory.init && factory.init(); return () => { destroySplitFactory(factory); } } - }, [config]); - - // Effect to subscribe/unsubscribe to events - React.useEffect(() => { - const factory = config && getSplitFactory(config); - if (factory) { - const client = getSplitClient(factory); - const status = getStatus(client); - - // Unsubscribe from events and update state when first event is emitted - const update = () => { // eslint-disable-next-line no-use-before-define - unsubscribe(); - setConfigFactory(factory); - } - - const unsubscribe = () => { - 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); - } - - if (updateOnSdkReady) { - if (status.isReady) update(); - else client.once(client.Event.SDK_READY, update); - } - if (updateOnSdkReadyFromCache) { - if (status.isReadyFromCache) update(); - else client.once(client.Event.SDK_READY_FROM_CACHE, update); - } - if (updateOnSdkTimedout) { - if (status.hasTimedout) update(); - else client.once(client.Event.SDK_READY_TIMED_OUT, update); - } - if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); - - return unsubscribe; - } - }, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); + }, [config, propFactory]); return ( { return { SplitFactory: mockSdk() }; }); @@ -14,7 +14,7 @@ import { ISplitClientChildProps } from '../types'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; -import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; +import { INITIAL_STATUS, testAttributesBinding, TestComponentProps } from './testUtils/utils'; import { IClientWithContext } from '../utils'; import { EXCEPTION_NO_SFP } from '../constants'; @@ -24,13 +24,12 @@ describe('SplitClient', () => { render( - {({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { - expect(isReady).toBe(false); - expect(isReadyFromCache).toBe(false); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe(0); + {(childProps: ISplitClientChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client('user1'), + }); return null; }} @@ -50,14 +49,15 @@ describe('SplitClient', () => { {/* Equivalent to */} - {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { - expect(client).toBe(outerFactory.client()); - expect(isReady).toBe(true); - expect(isReadyFromCache).toBe(true); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); + {(childProps: ISplitClientChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory : outerFactory, + client: outerFactory.client(), + isReady: true, + isReadyFromCache: true, + lastUpdate: (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate + }); return null; }} @@ -162,7 +162,7 @@ describe('SplitClient', () => { expect(renderTimes).toBe(3); }); - test('rerender child only on SDK_READY event, as default behaviour.', async () => { + test('rerender child only on SDK_READY event, as default behavior.', async () => { const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -214,8 +214,7 @@ describe('SplitClient', () => { count++; // side effect in the render phase - if (!(client as IClientWithContext).__getStatus().isReady) { - console.log('emit'); + if (!(client as any).__getStatus().isReady) { (client as any).__emitter__.emit(Event.SDK_READY); } @@ -228,18 +227,19 @@ describe('SplitClient', () => { expect(count).toEqual(2); }); - test('renders a passed JSX.Element with a new SplitContext value.', (done) => { + test('renders a passed JSX.Element with a new SplitContext value.', () => { const outerFactory = SplitFactory(sdkBrowser); const Component = () => { return ( {(value) => { - expect(value.client).toBe(outerFactory.client('user2')); - expect(value.isReady).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.lastUpdate).toBe(0); - done(); + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: outerFactory, + client: outerFactory.client('user2'), + }); + return null; }} @@ -347,7 +347,7 @@ describe('SplitClient', () => { function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( - + {() => { testSwitch(done, splitKey); return null; diff --git a/src/__tests__/SplitContext.test.tsx b/src/__tests__/SplitContext.test.tsx index 4ffb11d..5388adb 100644 --- a/src/__tests__/SplitContext.test.tsx +++ b/src/__tests__/SplitContext.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { SplitContext } from '../SplitContext'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { INITIAL_CONTEXT } from './testUtils/utils'; +import { INITIAL_STATUS } from './testUtils/utils'; /** * Test default SplitContext value @@ -23,7 +23,11 @@ test('SplitContext.Consumer shows value when wrapped in a SplitFactoryProvider', {(value) => { - expect(value).toEqual(INITIAL_CONTEXT); + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: undefined, + client: undefined + }); return null; }} diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index 5efddd4..f212393 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitFactory'; +import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -17,22 +17,19 @@ import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; import { __factories, IClientWithContext } from '../utils'; import { WARN_SF_CONFIG_AND_FACTORY } from '../constants'; -import { INITIAL_CONTEXT } from './testUtils/utils'; +import { INITIAL_STATUS } from './testUtils/utils'; describe('SplitFactoryProvider', () => { test('passes no-ready props to the child if initialized with a config.', () => { render( - {({ factory, client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryProviderChildProps) => { - expect(factory).toBe(null); - expect(client).toBe(null); - expect(isReady).toBe(false); - expect(isReadyFromCache).toBe(false); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe(0); + {(childProps: ISplitFactoryProviderChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client(), + }); return null; }} @@ -48,15 +45,16 @@ describe('SplitFactoryProvider', () => { render( - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryProviderChildProps) => { - expect(factory).toBe(outerFactory); - expect(isReady).toBe(true); - expect(isReadyFromCache).toBe(true); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); - expect((factory as SplitIO.IBrowserSDK).settings.version).toBe(outerFactory.settings.version); + {(childProps: ISplitFactoryProviderChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory: outerFactory, + client: outerFactory.client(), + isReady: true, + isReadyFromCache: true, + lastUpdate: (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate + }); + expect((childProps.factory as SplitIO.IBrowserSDK).settings.version).toBe(outerFactory.settings.version); return null; }} @@ -90,7 +88,7 @@ describe('SplitFactoryProvider', () => { default: fail('Child must not be rerendered'); } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); + expect(factory).toBe(innerFactory || getLastInstance(SplitFactory)); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; @@ -99,7 +97,7 @@ describe('SplitFactoryProvider', () => { ); - const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + const innerFactory = getLastInstance(SplitFactory); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); @@ -174,7 +172,7 @@ describe('SplitFactoryProvider', () => { default: fail('Child must not be rerendered'); } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); + expect(factory).toBe(innerFactory || getLastInstance(SplitFactory)); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; @@ -183,7 +181,7 @@ describe('SplitFactoryProvider', () => { ); - const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + const innerFactory = getLastInstance(SplitFactory); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); @@ -229,7 +227,7 @@ describe('SplitFactoryProvider', () => { expect(renderTimes).toBe(3); }); - test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (config prop)', async () => { + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behavior (config prop)', async () => { let renderTimes = 0; let previousLastUpdate = -1; @@ -247,7 +245,7 @@ describe('SplitFactoryProvider', () => { default: fail('Child must not be rerendered'); } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); + expect(factory).toBe(innerFactory || getLastInstance(SplitFactory)); expect(lastUpdate).toBeGreaterThan(previousLastUpdate); renderTimes++; previousLastUpdate = lastUpdate; @@ -256,14 +254,14 @@ describe('SplitFactoryProvider', () => { ); - const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + const innerFactory = getLastInstance(SplitFactory); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); expect(renderTimes).toBe(2); }); - test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (factory prop)', async () => { + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behavior (factory prop)', async () => { const outerFactory = SplitFactory(sdkBrowser); let renderTimes = 0; let previousLastUpdate = -1; @@ -302,7 +300,11 @@ describe('SplitFactoryProvider', () => { return ( {(value) => { - expect(value).toEqual(INITIAL_CONTEXT); + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client(), + }); done(); return null; }} @@ -350,14 +352,14 @@ describe('SplitFactoryProvider', () => { case 5: expect(isReady).toBe(false); expect(hasTimedout).toBe(false); - expect(factory).toBe(null); + expect(factory).toBe(getLastInstance(SplitFactory)); return null; case 3: case 4: case 6: expect(isReady).toBe(true); expect(hasTimedout).toBe(true); - expect(factory).not.toBe(null); + expect(factory).toBe(getLastInstance(SplitFactory)); createdFactories.add(factory!); clientDestroySpies.push(jest.spyOn(factory!.client(), 'destroy')); return ( @@ -374,7 +376,7 @@ describe('SplitFactoryProvider', () => { }; const emitSdkEvents = () => { - const factory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + const factory = getLastInstance(SplitFactory); factory.client().__emitter__.emit(Event.SDK_READY_TIMED_OUT) factory.client().__emitter__.emit(Event.SDK_READY) }; diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts index db5fffe..09dca47 100644 --- a/src/__tests__/testUtils/mockSplitFactory.ts +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -1,5 +1,4 @@ import { EventEmitter } from 'events'; -import SplitIO from '@splitsoftware/splitio/types/splitio'; import jsSdkPackageJson from '@splitsoftware/splitio/package.json'; import reactSdkPackageJson from '../../../package.json'; @@ -26,7 +25,7 @@ function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey { }; } } -function buildInstanceId(key: any, trafficType: string | undefined) { +function buildInstanceId(key: any, trafficType?: string) { return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-${trafficType !== undefined ? trafficType : ''}`; } @@ -34,7 +33,7 @@ export function mockSdk() { return jest.fn((config: SplitIO.IBrowserSettings, __updateModules) => { - function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { + function mockClient(_key: SplitIO.SplitKey) { // Readiness let isReady = false; let isReadyFromCache = false; @@ -135,11 +134,10 @@ export function mockSdk() { // Cache of clients const __clients__: { [instanceId: string]: any } = {}; - const client = jest.fn((key?: string, trafficType?: string) => { + const client = jest.fn((key?: string) => { const clientKey = key || parseKey(config.core.key); - const clientTT = trafficType || config.core.trafficType; - const instanceId = buildInstanceId(clientKey, clientTT); - return __clients__[instanceId] || (__clients__[instanceId] = mockClient(clientKey, clientTT)); + const instanceId = buildInstanceId(clientKey); + return __clients__[instanceId] || (__clients__[instanceId] = mockClient(clientKey)); }); // Factory destroy @@ -165,3 +163,7 @@ export function mockSdk() { }); } + +export function getLastInstance(SplitFactoryMock: any) { + return SplitFactoryMock.mock.results.slice(-1)[0].value; +} diff --git a/src/__tests__/testUtils/sdkConfigs.ts b/src/__tests__/testUtils/sdkConfigs.ts index a3421a5..0ec97f5 100644 --- a/src/__tests__/testUtils/sdkConfigs.ts +++ b/src/__tests__/testUtils/sdkConfigs.ts @@ -1,5 +1,3 @@ -import SplitIO from '@splitsoftware/splitio/types/splitio'; - export const sdkBrowser: SplitIO.IBrowserSettings = { core: { authorizationKey: 'sdk-key', diff --git a/src/__tests__/testUtils/utils.tsx b/src/__tests__/testUtils/utils.tsx index d19143a..36a0c5f 100644 --- a/src/__tests__/testUtils/utils.tsx +++ b/src/__tests__/testUtils/utils.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { ISplitContextValues } from '../../types'; +import { ISplitStatus } from '../../types'; const { SplitFactory: originalSplitFactory } = jest.requireActual('@splitsoftware/splitio/client'); export interface TestComponentProps { @@ -116,9 +116,7 @@ export function testAttributesBinding(Component: React.FunctionComponent); } -export const INITIAL_CONTEXT: ISplitContextValues = { - client: null, - factory: null, +export const INITIAL_STATUS: ISplitStatus = { isReady: false, isReadyFromCache: false, isTimedout: false, diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index de852a8..400dad3 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -56,12 +56,12 @@ describe('useSplitClient', () => { {React.createElement(() => { (outerFactory.client as jest.Mock).mockClear(); - client = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; + client = useSplitClient({ splitKey: 'user2' }).client; return null; })} ); - expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); + expect(outerFactory.client as jest.Mock).toBeCalledWith('user2'); expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); }); @@ -70,7 +70,7 @@ describe('useSplitClient', () => { render( React.createElement(() => { useSplitClient(); - useSplitClient({ splitKey: 'user2', trafficType: 'user' }); + useSplitClient({ splitKey: 'user2' }); return null; }) ); @@ -81,7 +81,7 @@ describe('useSplitClient', () => { // eslint-disable-next-line react/prop-types const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => { - useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient }); + useSplitClient({ splitKey, attributes: attributesClient }); testSwitch(done, splitKey); return null; }; @@ -112,7 +112,7 @@ describe('useSplitClient', () => { {() => countSplitContext++} - @@ -120,8 +120,8 @@ describe('useSplitClient', () => { {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 SplitFactoryProvider has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` + // - Using config key: `const { client } = useSplitClient({ splitKey: sdkBrowser.core.key, attributes: { att1: 'att1' } });` + // - Disabling update props, since the wrapping SplitFactoryProvider has them enabled: `const { client } = useSplitClient({ attributes: { 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. @@ -141,7 +141,7 @@ describe('useSplitClient', () => { {() => { countSplitClientWithUpdate++; return null }} {React.createElement(() => { - useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; + useSplitClient({ splitKey: sdkBrowser.core.key, updateOnSdkUpdate: true }).client; countUseSplitClientWithUpdate++; return null; })} diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index c8f9dff..97fe026 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -14,6 +14,7 @@ import { getStatus } from '../utils'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { useSplitManager } from '../useSplitManager'; import { EXCEPTION_NO_SFP } from '../constants'; +import { INITIAL_STATUS } from './testUtils/utils'; describe('useSplitManager', () => { @@ -33,12 +34,7 @@ describe('useSplitManager', () => { manager: outerFactory.manager(), client: outerFactory.client(), factory: outerFactory, - hasTimedout: false, - isDestroyed: false, - isReady: false, - isReadyFromCache: false, - isTimedout: false, - lastUpdate: 0, + ...INITIAL_STATUS, }); act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); diff --git a/src/__tests__/useTrack.test.tsx b/src/__tests__/useTrack.test.tsx index e4fd2ad..2cdf7c4 100644 --- a/src/__tests__/useTrack.test.tsx +++ b/src/__tests__/useTrack.test.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { render } from '@testing-library/react'; +import React, { useEffect } from 'react'; +import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk } from './testUtils/mockSplitFactory'; +import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -13,6 +13,7 @@ import { sdkBrowser } from './testUtils/sdkConfigs'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { useTrack } from '../useTrack'; +import { useSplitClient } from '../useSplitClient'; import { EXCEPTION_NO_SFP } from '../constants'; describe('useTrack', () => { @@ -22,62 +23,62 @@ describe('useTrack', () => { const value = 10; const properties = { prop1: 'prop1' }; - test('returns the track method bound to the client at Split context updated by SplitFactoryProvider.', () => { + test('returns the track method of the client at Split context updated by SplitFactoryProvider.', () => { const outerFactory = SplitFactory(sdkBrowser); - let boundTrack; + let clientTrack; let trackResult; render( {React.createElement(() => { - boundTrack = useTrack(); - trackResult = boundTrack(tt, eventType, value, properties); + clientTrack = useTrack(); + trackResult = clientTrack(tt, eventType, value, properties); return null; })} , ); - const track = outerFactory.client().track as jest.Mock; + const track = outerFactory.client().track; + expect(track).toBe(clientTrack); expect(track).toBeCalledWith(tt, eventType, value, properties); expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method bound to the client at Split context updated by SplitClient.', () => { + test('returns the track method of the client at Split context updated by SplitClient.', () => { const outerFactory = SplitFactory(sdkBrowser); - let boundTrack; + let clientTrack; let trackResult; render( {React.createElement(() => { - boundTrack = useTrack(); - trackResult = boundTrack(tt, eventType, value, properties); + clientTrack = useTrack(); + trackResult = clientTrack(tt, eventType, value, properties); return null; })} ); - const track = outerFactory.client('user2').track as jest.Mock; + const track = outerFactory.client('user2').track; expect(track).toBeCalledWith(tt, eventType, value, properties); expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => { + test('returns the track method of a new client given a splitKey.', () => { const outerFactory = SplitFactory(sdkBrowser); - let boundTrack; let trackResult; render( {React.createElement(() => { - boundTrack = useTrack('user2', tt); - trackResult = boundTrack(eventType, value, properties); + const clientTrack = useTrack('user2'); + trackResult = clientTrack(tt, eventType, value, properties); return null; })} , ); - const track = outerFactory.client('user2', tt).track as jest.Mock; - expect(track).toBeCalledWith(eventType, value, properties); + const track = outerFactory.client('user2').track; + expect(track).toBeCalledWith(tt, eventType, value, properties); expect(track).toHaveReturnedWith(trackResult); }); @@ -85,12 +86,66 @@ describe('useTrack', () => { expect(() => { render( React.createElement(() => { - const track = useTrack('user2', tt); - track(eventType, value, properties); + const track = useTrack('user2'); + track(tt, eventType, value, properties); return null; }), ); }).toThrow(EXCEPTION_NO_SFP); }); + test('returns the track method of the client at Split context updated by SplitFactoryProvider (config prop).', () => { + let splitKey: string | undefined = undefined; + render( + + {React.createElement(() => { + const clientTrack = useTrack(splitKey); + + const { client } = useSplitClient({ splitKey }); + expect(clientTrack).toBe(client!.track); + + clientTrack(tt, eventType, value, properties); + + useEffect(() => { + clientTrack(tt, eventType, value, properties); + }, [clientTrack]); + return null; + })} + , + ); + + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY)); + splitKey = 'user2'; // `clientTrack` dependency changed + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_UPDATE)); + + let track = getLastInstance(SplitFactory).client().track; + expect(track).toBeCalledWith(tt, eventType, value, properties); + expect(track).toBeCalledTimes(4); // 3 from render + 1 from useEffect + + track = getLastInstance(SplitFactory).client('user2').track; + expect(track).toBeCalledWith(tt, eventType, value, properties); + expect(track).toBeCalledTimes(2); // 1 from render + 1 from useEffect (`clientTrack` dependency changed) + }); + + test('does not re-render on SDK events', () => { + render( + + {React.createElement(() => { + const clientTrack = useTrack(); + clientTrack(tt, eventType, value, properties); + + return null; + })} + , + ); + + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(getLastInstance(SplitFactory).client().track).toBeCalledTimes(1); + }); + }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index b5ef021..3d7e32e 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,6 +1,5 @@ import { CONTROL_WITH_CONFIG } from '../constants'; import { getControlTreatmentsWithConfig } from '../utils'; -import SplitIO from '@splitsoftware/splitio/types/splitio'; describe('getControlTreatmentsWithConfig', () => { diff --git a/src/types.ts b/src/types.ts index 88d7006..b77f29f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,3 @@ -import SplitIO from '@splitsoftware/splitio/types/splitio'; import type { ReactNode } from 'react'; /** @@ -50,7 +49,7 @@ export interface ISplitContextValues extends ISplitStatus { * * NOTE: This property is not recommended for direct use, as better alternatives are available. */ - factory: SplitIO.IBrowserSDK | null; + factory?: SplitIO.IBrowserSDK; /** * Split client instance. @@ -59,7 +58,7 @@ export interface ISplitContextValues extends ISplitStatus { * * @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; + client?: SplitIO.IBrowserClient; } /** @@ -145,12 +144,6 @@ export interface IUseSplitClientOptions extends IUpdateProps { */ 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. */ diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 94cfb93..2f75aef 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -11,7 +11,7 @@ export const DEFAULT_UPDATE_OPTIONS = { }; /** - * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. + * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key. * It uses the 'useContext' hook to access the context, which is updated by SplitFactoryProvider and SplitClient components in the hierarchy of components. * * @returns A Split Context object @@ -25,7 +25,7 @@ export const DEFAULT_UPDATE_OPTIONS = { */ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues { const { - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, trafficType, attributes + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, attributes } = { ...DEFAULT_UPDATE_OPTIONS, ...options }; const context = useSplitContext(); @@ -34,7 +34,7 @@ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextV let client = contextClient as IClientWithContext; if (splitKey && factory) { // @TODO `getSplitClient` starts client sync. Move side effects to useEffect - client = getSplitClient(factory, splitKey, trafficType); + client = getSplitClient(factory, splitKey); } initAttributes(client, attributes); diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index d2d7270..36c73eb 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -5,7 +5,7 @@ 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 SplitFactoryProvider component. * - * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactoryProvider or factory is not ready. + * @returns An object containing the Split context and the Split Manager instance, which is undefined if factory is not ready. * * @example * ```js @@ -14,11 +14,11 @@ import { ISplitContextValues } from './types'; * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} */ -export function useSplitManager(): ISplitContextValues & { manager: SplitIO.IManager | null } { +export function useSplitManager(): ISplitContextValues & { manager?: SplitIO.IManager } { // Update options are not supported, because updates can be controlled at the SplitFactoryProvider component. const context = useSplitContext(); return { ...context, - manager: context.factory ? context.factory.manager() : null + manager: context.factory ? context.factory.manager() : undefined }; } diff --git a/src/useTrack.ts b/src/useTrack.ts index b511018..4e7fc73 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -4,15 +4,16 @@ import { useSplitClient } from './useSplitClient'; 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. + * 'useTrack' is a hook that retrieves the track method from a Split client. + * It uses the 'useSplitClient' hook to access the client from the Split context. + * Basically, it is a shortcut for `const track = useSplitClient().client?.track || (() => 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. + * @returns A track function of the 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(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { +export function useTrack(splitKey?: SplitIO.SplitKey): 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; + const { client } = useSplitClient({ splitKey, updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); + return client ? client.track : noOpFalse; } diff --git a/src/utils.ts b/src/utils.ts index 9ae5a3c..06495e5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,12 +24,13 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { /** * FactoryWithClientInstances interface. */ -export interface IFactoryWithClients extends SplitIO.IBrowserSDK { +export interface IFactoryWithLazyInit extends SplitIO.IBrowserSDK { config: SplitIO.IBrowserSettings; + init(): void; } // exported for testing purposes -export const __factories: Map = new Map(); +export const __factories: Map = new Map(); // idempotent operation export function getSplitFactory(config: SplitIO.IBrowserSettings) { @@ -38,33 +39,34 @@ export function getSplitFactory(config: SplitIO.IBrowserSettings) { // @ts-expect-error. 2nd param is not part of type definitions. Used to overwrite the SDK version const newFactory = SplitFactory(config, (modules) => { modules.settings.version = VERSION; - }) as IFactoryWithClients; + modules.lazyInit = true; + }) as IFactoryWithLazyInit; newFactory.config = config; __factories.set(config, newFactory); } - return __factories.get(config) as IFactoryWithClients; + return __factories.get(config) as IFactoryWithLazyInit; } // idempotent operation -export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): IClientWithContext { // factory.client is an idempotent operation - const client = (key !== undefined ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + const client = (key !== undefined ? factory.client(key) : factory.client()) as IClientWithContext; // Remove EventEmitter warning emitted when using multiple SDK hooks or components. // Unlike JS SDK, users don't need to access the client directly, making the warning irrelevant. - client.setMaxListeners(0); + client.setMaxListeners && client.setMaxListeners(0); return client; } -export function destroySplitFactory(factory: IFactoryWithClients): Promise | undefined { +export function destroySplitFactory(factory: IFactoryWithLazyInit): Promise | undefined { __factories.delete(factory.config); return factory.destroy(); } // 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 { +export function getStatus(client?: SplitIO.IBrowserClient): ISplitStatus { const status = client && (client as IClientWithContext).__getStatus(); return { @@ -80,7 +82,7 @@ export function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus { /** * Manage client attributes binding */ -export function initAttributes(client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes) { +export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: SplitIO.Attributes) { if (client && attributes) client.setAttributes(attributes); } @@ -174,7 +176,7 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { 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[]) { +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _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) ?