diff --git a/.gitignore b/.gitignore index 204ddd1..767609d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .DS_Store /.vscode /node_modules -/lib -/es +/cjs +/esm /umd /types /coverage diff --git a/CHANGES.txt b/CHANGES.txt index a386c49..fa3e393 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +1.14.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. + 1.13.0 (September 6, 2024) - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. diff --git a/package-lock.json b/package-lock.json index 3197930..063dd91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "1.13.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "1.13.1-rc.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.28.0", + "@splitsoftware/splitio": "10.28.1-rc.2", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -1548,11 +1548,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", - "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", + "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==", "dependencies": { - "@splitsoftware/splitio-commons": "1.17.0", + "@splitsoftware/splitio-commons": "1.17.1-rc.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -1568,9 +1568,9 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", - "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", + "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==", "dependencies": { "tslib": "^2.3.1" }, @@ -12035,11 +12035,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", - "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", + "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==", "requires": { - "@splitsoftware/splitio-commons": "1.17.0", + "@splitsoftware/splitio-commons": "1.17.1-rc.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -12051,9 +12051,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", - "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", + "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==", "requires": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index caddcf5..ad946ca 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "1.13.1-rc.0", "description": "A React library to easily integrate and use Split JS SDK", - "main": "lib/index.js", - "module": "es/index.js", + "main": "cjs/index.js", + "module": "esm/index.js", "types": "types/index.d.ts", "files": [ "README.md", @@ -12,17 +12,17 @@ "LICENSE", "CHANGES.txt", "src", - "lib", - "es", + "cjs", + "esm", "types" ], "sideEffects": false, "scripts": { - "build:cjs": "rimraf lib/* types/* && tsc -m commonjs --outDir lib -d true --declarationDir types", - "build:esm": "rimraf es/* && tsc", + "build:cjs": "rimraf cjs/* types/* && tsc -m commonjs --outDir cjs -d true --declarationDir types", + "build:esm": "rimraf esm/* && tsc", "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", + "postbuild": "replace 'REACT_SDK_VERSION_NUMBER' $npm_package_version ./cjs/constants.js ./esm/constants.js ./umd -r", "check": "npm run check:lint && npm run check:types", "check:lint": "eslint 'src/**/*.ts*'", "check:types": "tsc --noEmit", @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.28.0", + "@splitsoftware/splitio": "10.28.1-rc.2", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 22e325c..af927c4 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -1,14 +1,16 @@ import React from 'react'; import { SplitContext } from './SplitContext'; -import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; +import { ISplitClientProps, ISplitContextValues } from './types'; import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils'; import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; +type ISplitComponentProps = ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes }; + /** * Common component used to handle the status and events of a Split client passed as prop. * Reused by both SplitFactoryProvider (main client) and SplitClient (any client) components. */ -export class SplitComponent extends React.Component { +export class SplitComponent extends React.Component { static defaultProps = { children: null, @@ -20,7 +22,7 @@ export class SplitComponent extends React.Component; - constructor(props: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }) { + constructor(props: ISplitComponentProps) { super(props); const { factory, client } = props; @@ -93,7 +95,7 @@ export class SplitComponent extends React.Component(null); - const factory = propFactory || (configFactory && config === configFactory.config ? configFactory : null); + const factory = React.useMemo(() => { + return propFactory || (configFactory && config === configFactory.config ? configFactory : null); + }, [config, propFactory, configFactory]); const client = factory ? getSplitClient(factory) : null; // Effect to initialize and destroy the factory diff --git a/src/__tests__/SplitFactory.test.tsx b/src/__tests__/SplitFactory.test.tsx index 9832cd7..07e649d 100644 --- a/src/__tests__/SplitFactory.test.tsx +++ b/src/__tests__/SplitFactory.test.tsx @@ -31,7 +31,7 @@ describe('SplitFactory', () => { expect(isTimedout).toBe(false); expect(isDestroyed).toBe(false); expect(lastUpdate).toBe(0); - expect((factory as SplitIO.ISDK).settings.version).toContain('react-'); + expect((factory as SplitIO.IBrowserSDK).settings.version).toContain('react-'); return null; }} @@ -55,7 +55,7 @@ describe('SplitFactory', () => { expect(isTimedout).toBe(false); expect(isDestroyed).toBe(false); expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); - expect((factory as SplitIO.ISDK).settings.version).toBe(outerFactory.settings.version); + expect((factory as SplitIO.IBrowserSDK).settings.version).toBe(outerFactory.settings.version); return null; }} @@ -239,7 +239,7 @@ describe('SplitFactory', () => { {({ factory }) => { expect(__factories.size).toBe(1); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); + destroyMainClientSpy = jest.spyOn((factory as SplitIO.IBrowserSDK).client(), 'destroy'); return ( {({ client }) => { @@ -267,7 +267,7 @@ describe('SplitFactory', () => { {({ factory }) => { // if factory is provided as a prop, `factories` cache is not modified expect(__factories.size).toBe(0); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); + destroyMainClientSpy = jest.spyOn((factory as SplitIO.IBrowserSDK).client(), 'destroy'); return ( {({ client }) => { diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index ec094c0..61cd932 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -14,7 +14,7 @@ const logSpy = jest.spyOn(console, 'log'); import { ISplitFactoryChildProps } from '../types'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; -import { SplitContext } from '../SplitContext'; +import { INITIAL_CONTEXT, SplitContext } from '../SplitContext'; import { __factories, IClientWithContext } from '../utils'; import { WARN_SF_CONFIG_AND_FACTORY } from '../constants'; @@ -55,7 +55,7 @@ describe('SplitFactoryProvider', () => { expect(isTimedout).toBe(false); expect(isDestroyed).toBe(false); expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); - expect((factory as SplitIO.ISDK).settings.version).toBe(outerFactory.settings.version); + expect((factory as SplitIO.IBrowserSDK).settings.version).toBe(outerFactory.settings.version); return null; }} @@ -301,11 +301,7 @@ describe('SplitFactoryProvider', () => { return ( {(value) => { - expect(value.factory).toBe(null); - expect(value.client).toBe(null); - expect(value.isReady).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.lastUpdate).toBe(0); + expect(value).toEqual(INITIAL_CONTEXT); done(); return null; }} @@ -338,7 +334,7 @@ describe('SplitFactoryProvider', () => { test('cleans up on update and unmount if config prop is provided.', () => { let renderTimes = 0; - const createdFactories = new Set(); + const createdFactories = new Set(); const clientDestroySpies: jest.SpyInstance[] = []; const outerFactory = SplitSdk(sdkBrowser); diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts index e0a85ee..db5fffe 100644 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ b/src/__tests__/testUtils/mockSplitSdk.ts @@ -30,105 +30,105 @@ function buildInstanceId(key: any, trafficType: string | undefined) { return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-${trafficType !== undefined ? trafficType : ''}`; } -function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { - // Readiness - let isReady = false; - let isReadyFromCache = false; - let hasTimedout = false; - let isDestroyed = false; - let lastUpdate = 0; - - function syncLastUpdate() { - const dateNow = Date.now(); - lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; - } - - const __emitter__ = new EventEmitter(); - __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); - - let attributesCache = {}; - - // Client methods - const track: jest.Mock = jest.fn(() => { - return true; - }); - 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((attributes) => { - attributesCache = Object.assign(attributesCache, attributes); - return true; - }); - const clearAttributes: jest.Mock = jest.fn(() => { - attributesCache = {}; - return true; - }); - const getAttributes: jest.Mock = jest.fn(() => { - return attributesCache; - }); - const ready: jest.Mock = jest.fn(() => { - return new Promise((res, rej) => { - if (isReady) res(); - else { __emitter__.on(Event.SDK_READY, res); } - if (hasTimedout) rej(); - else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } - }); - }); - const __getStatus = () => ({ - isReady, - isReadyFromCache, - isTimedout: hasTimedout && !isReady, - hasTimedout, - isDestroyed, - isOperational: (isReady || isReadyFromCache) && !isDestroyed, - lastUpdate, - }); - const destroy: jest.Mock = jest.fn(() => { - isDestroyed = true; - syncLastUpdate(); - // __emitter__.removeAllListeners(); - return Promise.resolve(); - }); - - return Object.assign(Object.create(__emitter__), { - getTreatmentsWithConfig, - getTreatmentsWithConfigByFlagSets, - track, - ready, - destroy, - Event, - setAttributes, - clearAttributes, - getAttributes, - // EventEmitter exposed to trigger events manually - __emitter__, - // 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. - // Useful when you want to reuse the same mock between tests after emitting events or destroying the instance. - __restore() { - isReady = isReadyFromCache = hasTimedout = isDestroyed = false; - lastUpdate = 0; - } - }); -} - export function mockSdk() { return jest.fn((config: SplitIO.IBrowserSettings, __updateModules) => { + function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { + // Readiness + let isReady = false; + let isReadyFromCache = false; + let hasTimedout = false; + let isDestroyed = false; + let lastUpdate = 0; + + function syncLastUpdate() { + const dateNow = Date.now(); + lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; + } + + const __emitter__ = new EventEmitter(); + __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); + + let attributesCache = {}; + + // Client methods + const track: jest.Mock = jest.fn(() => { + return true; + }); + 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((attributes) => { + attributesCache = Object.assign(attributesCache, attributes); + return true; + }); + const clearAttributes: jest.Mock = jest.fn(() => { + attributesCache = {}; + return true; + }); + const getAttributes: jest.Mock = jest.fn(() => { + return attributesCache; + }); + const ready: jest.Mock = jest.fn(() => { + return new Promise((res, rej) => { + if (isReady) res(); + else { __emitter__.on(Event.SDK_READY, res); } + if (hasTimedout) rej(); + else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } + }); + }); + const __getStatus = () => ({ + isReady, + isReadyFromCache, + isTimedout: hasTimedout && !isReady, + hasTimedout, + isDestroyed, + isOperational: (isReady || isReadyFromCache) && !isDestroyed, + lastUpdate, + }); + const destroy: jest.Mock = jest.fn(() => { + isDestroyed = true; + syncLastUpdate(); + // __emitter__.removeAllListeners(); + return Promise.resolve(); + }); + + return Object.assign(Object.create(__emitter__), { + getTreatmentsWithConfig, + getTreatmentsWithConfigByFlagSets, + track, + ready, + destroy, + Event, + setAttributes, + clearAttributes, + getAttributes, + // EventEmitter exposed to trigger events manually + __emitter__, + // 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. + // Useful when you want to reuse the same mock between tests after emitting events or destroying the instance. + __restore() { + isReady = isReadyFromCache = hasTimedout = isDestroyed = false; + lastUpdate = 0; + } + }); + } + // Manager const names: jest.Mock = jest.fn().mockReturnValue([]); const manager: jest.Mock = jest.fn().mockReturnValue({ names }); @@ -142,10 +142,16 @@ export function mockSdk() { return __clients__[instanceId] || (__clients__[instanceId] = mockClient(clientKey, clientTT)); }); + // Factory destroy + const destroy = jest.fn(() => { + return Promise.all(Object.keys(__clients__).map(instanceId => __clients__[instanceId].destroy())); + }); + // SDK factory const factory = { client, manager, + destroy, __names__: names, __clients__, settings: Object.assign({ diff --git a/src/utils.ts b/src/utils.ts index efcb17e..5915875 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,7 +25,6 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { * FactoryWithClientInstances interface. */ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - clientInstances: Set; config: SplitIO.IBrowserSettings; } @@ -33,14 +32,13 @@ export interface IFactoryWithClients extends SplitIO.IBrowserSDK { export const __factories: Map = new Map(); // idempotent operation -export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients { +export function getSplitFactory(config: SplitIO.IBrowserSettings) { if (!__factories.has(config)) { // SplitSDK is not an idempotent operation // @ts-expect-error. 2nd param is not part of type definitions. Used to overwrite the SDK version const newFactory = SplitSdk(config, (modules) => { modules.settings.version = VERSION; }) as IFactoryWithClients; - newFactory.clientInstances = new Set(); newFactory.config = config; __factories.set(config, newFactory); } @@ -56,20 +54,12 @@ export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.Split // Unlike JS SDK, users don't need to access the client directly, making the warning irrelevant. client.setMaxListeners(0); - if ((factory as IFactoryWithClients).clientInstances) { - (factory as IFactoryWithClients).clientInstances.add(client); - } return client; } -export function destroySplitFactory(factory: IFactoryWithClients): Promise { - // call destroy of clients - const destroyPromises: Promise[] = []; - factory.clientInstances.forEach((client) => destroyPromises.push(client.destroy())); - // remove references to release allocated memory - factory.clientInstances.clear(); +export function destroySplitFactory(factory: IFactoryWithClients): Promise | undefined { __factories.delete(factory.config); - return Promise.all(destroyPromises); + return factory.destroy(); } // Util used to get client status. diff --git a/tsconfig.json b/tsconfig.json index a409207..aae24e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": false, /* Generates corresponding '.d.ts' file. */ "sourceMap": false, /* Generates corresponding '.map' file. */ - "outDir": "./es", /* Redirect output structure to the directory. */ + "outDir": "./esm", /* Redirect output structure to the directory. */ "resolveJsonModule": true, /* Include modules imported with .json extension. */ "importHelpers": true, /* Import emit helpers from 'tslib'. */