Skip to content

Commit c0ec2d2

Browse files
authored
chore: adding camelcase flag keys to useFlags hook (#1174)
depends on #1166 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes the shape/behavior of values returned by deprecated `useFlags()` (key transformation, filtering, and proxy evaluation mapping), which can affect consumers relying on raw keys or object enumeration semantics. > > **Overview** > **Deprecated `useFlags()` now returns camelCased flag keys by default** (matching legacy `launchdarkly-react-client-sdk`) while still recording evaluations against the original flag keys. > > This introduces a shared `toCamelCase` utility, filters out `$` system keys from the returned flags object, and adds a `LDReactClient.shouldUseCamelCaseFlagKeys()` switch (wired from `LDReactClientOptions.useCamelCaseFlagKeys`, defaulting to `true`, and implemented for the noop/server client). > > The hook’s subscription effect now re-syncs flags and resets its variation cache on context (re-identify) changes, avoids an extra mount render, and the test suite is expanded/updated to cover camelCase behavior, proxy semantics (`in`, `Object.keys`, spread), variation call mapping, and system key filtering. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 841ba58. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/launchdarkly/js-core/pull/1174" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
1 parent bdaee5d commit c0ec2d2

10 files changed

Lines changed: 554 additions & 97 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { toCamelCase } from '../../../src/client/deprecated-hooks/flagKeyUtils';
2+
3+
it('converts kebab-case to camelCase', () => {
4+
expect(toCamelCase('my-flag-key')).toBe('myFlagKey');
5+
});
6+
7+
it('converts snake_case to camelCase', () => {
8+
expect(toCamelCase('my_flag_key')).toBe('myFlagKey');
9+
});
10+
11+
it('converts dot.separated to camelCase', () => {
12+
expect(toCamelCase('my.flag.key')).toBe('myFlagKey');
13+
});
14+
15+
it('lowercases ALL_CAPS first word', () => {
16+
expect(toCamelCase('MY_FLAG')).toBe('myFlag');
17+
});
18+
19+
it('preserves already-camelCase keys', () => {
20+
expect(toCamelCase('myFlagKey')).toBe('myFlagKey');
21+
});
22+
23+
it('handles HTMLParser (ALLCAPS boundary)', () => {
24+
expect(toCamelCase('HTMLParser')).toBe('htmlParser');
25+
});
26+
27+
it('handles a single word with no separators', () => {
28+
expect(toCamelCase('flag')).toBe('flag');
29+
});
30+
31+
it('handles runs of multiple separators', () => {
32+
expect(toCamelCase('my--flag')).toBe('myFlag');
33+
expect(toCamelCase('my_.flag')).toBe('myFlag');
34+
});
35+
36+
it('handles empty string', () => {
37+
expect(toCamelCase('')).toBe('');
38+
});
39+
40+
// ─── Already camelCase (idempotency) ──────────────────────────────────────────
41+
42+
it('preserves multi-hump camelCase', () => {
43+
expect(toCamelCase('myBigFlagKey')).toBe('myBigFlagKey');
44+
});
45+
46+
it('preserves short camelCase', () => {
47+
expect(toCamelCase('xFlag')).toBe('xFlag');
48+
});
49+
50+
// ─── All caps ─────────────────────────────────────────────────────────────────
51+
52+
it('lowercases a single all-caps word with no separators', () => {
53+
expect(toCamelCase('ALLFLAG')).toBe('allflag');
54+
});
55+
56+
it('handles multi-word ALL_CAPS', () => {
57+
expect(toCamelCase('ALL_CAPS_FLAG')).toBe('allCapsFlag');
58+
});
59+
60+
it('lowercases short all-caps abbreviations', () => {
61+
expect(toCamelCase('URL')).toBe('url');
62+
expect(toCamelCase('API')).toBe('api');
63+
});
64+
65+
it('returns an all-lowercase word with no separators unchanged', () => {
66+
expect(toCamelCase('flagkey')).toBe('flagkey');
67+
});
68+
69+
it('converts PascalCase to camelCase', () => {
70+
expect(toCamelCase('MyFlagKey')).toBe('myFlagKey');
71+
});
72+
73+
// NOTE: This case should never happen as LaunchDarkly should handle invalid
74+
// characters already.
75+
it('preserves special characters that are not separators', () => {
76+
expect(toCamelCase('my@flag')).toBe('my@flag');
77+
expect(toCamelCase('my#flag!')).toBe('my#flag!');
78+
expect(toCamelCase('flag$key')).toBe('flag$key');
79+
expect(toCamelCase('my+flag')).toBe('my+flag');
80+
});
81+
82+
it('keeps digits within a word token', () => {
83+
expect(toCamelCase('flag2value')).toBe('flag2value');
84+
});
85+
86+
it('camelCases around digit-only segments separated by dashes', () => {
87+
expect(toCamelCase('my-flag-123')).toBe('myFlag123');
88+
expect(toCamelCase('flag-2-value')).toBe('flag2Value');
89+
});
90+
91+
it('handles a leading digit segment', () => {
92+
expect(toCamelCase('123-flag')).toBe('123Flag');
93+
});
94+
95+
it('preserves digits adjacent to camelCase boundaries', () => {
96+
expect(toCamelCase('my2ndFlag')).toBe('my2ndFlag');
97+
});
98+
99+
it('detects digit-to-uppercase boundary in camelCase keys', () => {
100+
expect(toCamelCase('my2Flag')).toBe('my2Flag');
101+
expect(toCamelCase('flag2Value')).toBe('flag2Value');
102+
expect(toCamelCase('x2Y')).toBe('x2Y');
103+
});
104+
105+
// NOTE: This case should never happen as LaunchDarkly should handle invalid
106+
// characters already.
107+
it('ignores a leading separator', () => {
108+
expect(toCamelCase('-my-flag')).toBe('myFlag');
109+
});
110+
111+
it('ignores a trailing separator', () => {
112+
expect(toCamelCase('my-flag-')).toBe('myFlag');
113+
});
114+
115+
it('ignores leading and trailing separators together', () => {
116+
expect(toCamelCase('.my.flag.')).toBe('myFlag');
117+
});
118+
119+
it('handles mixed separator types in one key', () => {
120+
expect(toCamelCase('my-flag_key.value')).toBe('myFlagKeyValue');
121+
});
122+
123+
it('returns a single lowercase character unchanged', () => {
124+
expect(toCamelCase('a')).toBe('a');
125+
});
126+
127+
it('lowercases a single uppercase character', () => {
128+
expect(toCamelCase('A')).toBe('a');
129+
});

packages/sdk/react/__tests__/client/deprecated-hooks/renderHelpers.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { LDReactClientContextValue } from '../../../src/client/LDClient';
44
import { LDReactContext } from '../../../src/client/provider/LDReactContext';
55
import { makeMockClient } from '../mockClient';
66

7-
export function makeWrapper(mockClient: ReturnType<typeof makeMockClient>) {
7+
export function makeWrapper(
8+
mockClient: ReturnType<typeof makeMockClient>,
9+
contextOverrides?: Partial<LDReactClientContextValue>,
10+
) {
811
const contextValue: LDReactClientContextValue = {
912
client: mockClient,
1013
initializedState: 'unknown',
14+
...contextOverrides,
1115
};
1216

1317
return function Wrapper({ children }: { children: React.ReactNode }) {

0 commit comments

Comments
 (0)