Skip to content

Commit 24d3d4b

Browse files
authored
Merge branch 'main' into devin/1757606387-add-dependency-scan-workflow
2 parents aa82112 + 47891af commit 24d3d4b

9 files changed

Lines changed: 156 additions & 70 deletions

File tree

.github/workflows/manual-publish.yml

Lines changed: 0 additions & 42 deletions
This file was deleted.

.github/workflows/release-please.yml

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,20 @@ on:
44
push:
55
branches:
66
- main
7+
workflow_dispatch:
8+
inputs:
9+
dry-run:
10+
description: 'Is this a dry run. If so no package will be published.'
11+
type: boolean
12+
required: true
13+
prerelease:
14+
description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.'
15+
type: boolean
16+
required: true
717

818
jobs:
919
release-please:
20+
if: github.event_name == 'push'
1021
runs-on: ubuntu-latest
1122
outputs:
1223
release_created: ${{ steps.release.outputs.release_created }}
@@ -19,23 +30,22 @@ jobs:
1930
publish-package:
2031
runs-on: ubuntu-latest
2132
needs: ['release-please']
33+
if: ${{ needs.release-please.outputs.release_created == 'true' }}
2234
permissions:
2335
id-token: write
2436
contents: write
25-
if: ${{ needs.release-please.outputs.release_created == 'true' }}
2637
steps:
2738
- uses: actions/checkout@v4
2839

2940
- uses: actions/setup-node@v4
3041
with:
31-
node-version: 20.x
42+
node-version: 24.x
3243
registry-url: 'https://registry.npmjs.org'
3344

34-
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
35-
name: 'Get NPM token'
36-
with:
37-
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
38-
ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN'
45+
- name: Update NPM
46+
shell: bash
47+
# Must be greater than 11.5.1 for OIDC.
48+
run: npm install -g npm@11.6.2
3949

4050
- name: Install Dependencies
4151
run: npm install
@@ -56,3 +66,32 @@ jobs:
5666
uses: ./.github/actions/publish-docs
5767
with:
5868
github_token: ${{ secrets.GITHUB_TOKEN }}
69+
70+
manual-publish-package:
71+
runs-on: ubuntu-latest
72+
if: github.event_name == 'workflow_dispatch'
73+
permissions:
74+
id-token: write
75+
contents: write
76+
steps:
77+
- uses: actions/checkout@v4
78+
79+
- uses: actions/setup-node@v4
80+
with:
81+
node-version: 24.x
82+
registry-url: 'https://registry.npmjs.org'
83+
84+
- name: Update NPM
85+
shell: bash
86+
# Must be greater than 11.5.1 for OIDC.
87+
run: npm install -g npm@11.6.2
88+
89+
- name: Install Dependencies
90+
run: npm install
91+
92+
- id: publish-npm
93+
name: Publish NPM Package
94+
uses: ./.github/actions/publish-npm
95+
with:
96+
dry-run: ${{ inputs.dry-run }}
97+
prerelease: ${{ inputs.prerelease }}

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "3.8.1"
2+
".": "3.9.0"
33
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to the LaunchDarkly Client-side SDK for React will be documented in this file. For the source code for versions 2.13.0 and earlier, see the corresponding tags in the [js-client-sdk](https://github.com/launchdarkly/js-client-sdk) repository; this code was previously in a monorepo package there. See also the [JavaScript SDK changelog](https://github.com/launchdarkly/js-client-sdk/blob/main/CHANGELOG.md), since the React SDK inherits all of the underlying functionality of the JavaScript SDK; this file covers only changes that are specific to the React interface. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [3.9.0](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.8.1...launchdarkly-react-client-sdk-v3.9.0) (2025-09-30)
6+
7+
8+
### Features
9+
10+
* Add experimental debug override functionality. ([63dd495](https://github.com/launchdarkly/react-client-sdk/commit/63dd495f9e393daf24597f9806b7b8ef010a434c))
11+
* Add experimental debug override functionality. ([4c4f3a3](https://github.com/launchdarkly/react-client-sdk/commit/4c4f3a3b9c62f715f5ec57bac25245a2b5a3a2f2))
12+
* Add experimental debug override functionality. ([#362](https://github.com/launchdarkly/react-client-sdk/issues/362)) ([63dd495](https://github.com/launchdarkly/react-client-sdk/commit/63dd495f9e393daf24597f9806b7b8ef010a434c))
13+
14+
15+
### Bug Fixes
16+
17+
* Fix a race condition when handling multiple flag updates. ([#365](https://github.com/launchdarkly/react-client-sdk/issues/365)) ([8d46bbd](https://github.com/launchdarkly/react-client-sdk/commit/8d46bbdda97ec045ea132fd997ad50a3a840338c))
18+
519
## [3.8.1](https://github.com/launchdarkly/react-client-sdk/compare/launchdarkly-react-client-sdk-v3.8.0...launchdarkly-react-client-sdk-v3.8.1) (2025-05-30)
620

721

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "launchdarkly-react-client-sdk",
3-
"version": "3.8.1",
3+
"version": "3.9.0",
44
"description": "LaunchDarkly SDK for React",
55
"author": "LaunchDarkly <team@launchdarkly.com>",
66
"license": "Apache-2.0",

src/provider.test.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,18 @@ describe('LDProvider', () => {
456456
const mockSetState = jest.spyOn(instance, 'setState');
457457

458458
await instance.componentDidMount();
459-
const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState;
459+
460+
// Each set of the state depends on the previous state, so to re-create the final state, we need to call the
461+
// setState function for each call.
462+
let finalState = previousState;
463+
464+
for(const call of mockSetState.mock.calls) {
465+
const setStateFunction = call[0] as (p: ProviderState) => ProviderState;
466+
finalState = setStateFunction(finalState);
467+
}
460468

461469
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
462-
expect(setStateFunction(previousState)).toEqual({
470+
expect(finalState).toMatchObject({
463471
flags: { anotherTestFlag: true, testFlag: false },
464472
unproxiedFlags: { 'another-test-flag': true, 'test-flag': false },
465473
flagKeyMap: { anotherTestFlag: 'another-test-flag', testFlag: 'test-flag' },
@@ -480,16 +488,56 @@ describe('LDProvider', () => {
480488
const mockSetState = jest.spyOn(instance, 'setState');
481489

482490
await instance.componentDidMount();
483-
const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState;
491+
492+
// Each set of the state depends on the previous state, so to re-create the final state, we need to call the
493+
// setState function for each call.
494+
let finalState = previousState;
495+
496+
for (const call of mockSetState.mock.calls) {
497+
const setStateFunction = call[0] as (p: ProviderState) => ProviderState;
498+
finalState = setStateFunction(finalState);
499+
}
484500

485501
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
486-
expect(setStateFunction(previousState)).toEqual({
502+
expect(finalState).toMatchObject({
487503
flagKeyMap: {},
488504
unproxiedFlags: { 'another-test-flag': false, 'test-flag': false },
489505
flags: { 'another-test-flag': false, 'test-flag': false },
490506
});
491507
});
492508

509+
test('handles deletion of flags', async () => {
510+
mockLDClient.on.mockImplementation((_e: string, cb: (c: LDFlagChangeset) => void) => {
511+
cb({ 'another-test-flag': { current: undefined, previous: true }, 'test-flag': { current: false, previous: true } });
512+
});
513+
const props: ProviderConfig = { clientSideID, reactOptions: { useCamelCaseFlagKeys: false } };
514+
const LaunchDarklyApp = (
515+
<LDProvider {...props}>
516+
<App />
517+
</LDProvider>
518+
);
519+
const instance = create(LaunchDarklyApp).root.findByType(LDProvider).instance as EnhancedComponent;
520+
const mockSetState = jest.spyOn(instance, 'setState');
521+
522+
await instance.componentDidMount();
523+
524+
// Each set of the state depends on the previous state, so to re-create the final state, we need to call the
525+
// setState function for each call.
526+
let finalState = previousState;
527+
528+
for (const call of mockSetState.mock.calls) {
529+
const setStateFunction = call[0] as (p: ProviderState) => ProviderState;
530+
finalState = setStateFunction(finalState);
531+
}
532+
533+
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
534+
expect(finalState).toMatchObject({
535+
flagKeyMap: {},
536+
unproxiedFlags: { 'test-flag': false },
537+
flags: { 'test-flag': false },
538+
});
539+
});
540+
493541
test(`if props.deferInitialization is true, ld client will only initialize once props.user is defined`, async () => {
494542
options = { ...options, bootstrap: {} };
495543
const props: ProviderConfig = { clientSideID, deferInitialization: true, options };

src/provider.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,19 @@ class LDProvider extends Component<PropsWithChildren<ProviderConfig>, ProviderSt
5757
ldClient.on('change', (changes: LDFlagChangeset) => {
5858
const reactOptions = this.getReactOptions();
5959
const updates = getFlattenedFlagsFromChangeset(changes, targetFlags);
60-
const unproxiedFlags = {
61-
...this.state.unproxiedFlags,
62-
...updates,
63-
};
60+
6461
if (Object.keys(updates).length > 0) {
65-
this.setState((prevState) => ({
66-
...prevState,
67-
unproxiedFlags,
68-
...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags),
69-
}));
62+
this.setState((prevState: ProviderState) => {
63+
const unproxiedFlags = {
64+
...prevState.unproxiedFlags,
65+
...updates,
66+
};
67+
return {
68+
...prevState,
69+
unproxiedFlags,
70+
...getFlagsProxy(ldClient, unproxiedFlags, reactOptions, targetFlags),
71+
}
72+
});
7073
}
7174
});
7275
};

src/types.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import { LDClient, LDContext, LDFlagSet, LDOptions } from 'launchdarkly-js-client-sdk';
1+
import { LDClient, LDContext, LDFlagSet, LDOptions as CommonLDOptions } from 'launchdarkly-js-client-sdk';
22
import * as React from 'react';
33
import defaultReactContext from './context';
44

5+
export interface LDOptions extends CommonLDOptions {
6+
/**
7+
* Whether analytics events should be sent only when you call variation (true), or also when you
8+
* call allFlags (false).
9+
*
10+
* By default, this is true.
11+
*/
12+
sendEventsOnlyForVariation?: boolean
13+
}
14+
515
/**
616
* Initialization options for the LaunchDarkly React SDK. These are in addition to the options exposed
7-
* by [[LDOptions]] which are common to both the JavaScript and React SDKs.
17+
* by {@link LDOptions} which are common to both the JavaScript and React SDKs.
818
*/
919
export interface LDReactOptions {
1020
/**

src/withLDProvider.test.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,17 @@ describe('withLDProvider', () => {
130130
const mockSetState = jest.spyOn(instance, 'setState');
131131

132132
await instance.componentDidMount();
133-
const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState;
133+
// Each set of the state depends on the previous state, so to re-create the final state, we need to call the
134+
// setState function for each call.
135+
let finalState = previousState;
136+
137+
for (const call of mockSetState.mock.calls) {
138+
const setStateFunction = call[0] as (p: ProviderState) => ProviderState;
139+
finalState = setStateFunction(finalState);
140+
}
134141

135142
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
136-
expect(setStateFunction(previousState)).toEqual({
143+
expect(finalState).toMatchObject({
137144
flags: { anotherTestFlag: true, testFlag: false },
138145
unproxiedFlags: { 'test-flag': false, 'another-test-flag': true },
139146
flagKeyMap: { testFlag: 'test-flag', anotherTestFlag: 'another-test-flag' },
@@ -149,10 +156,17 @@ describe('withLDProvider', () => {
149156
const mockSetState = jest.spyOn(instance, 'setState');
150157

151158
await instance.componentDidMount();
152-
const setStateFunction = mockSetState.mock?.lastCall?.[0] as (p: ProviderState) => ProviderState;
159+
// Each set of the state depends on the previous state, so to re-create the final state, we need to call the
160+
// setState function for each call.
161+
let finalState = previousState;
162+
163+
for (const call of mockSetState.mock.calls) {
164+
const setStateFunction = call[0] as (p: ProviderState) => ProviderState;
165+
finalState = setStateFunction(finalState);
166+
}
153167

154168
expect(mockLDClient.on).toHaveBeenCalledWith('change', expect.any(Function));
155-
expect(setStateFunction(previousState)).toEqual({
169+
expect(finalState).toMatchObject({
156170
flags: { 'test-flag': false, 'another-test-flag': false },
157171
unproxiedFlags: { 'test-flag': false, 'another-test-flag': false },
158172
flagKeyMap: {},

0 commit comments

Comments
 (0)