Skip to content

Commit a66b1c7

Browse files
authored
Merge pull request #7194 from Shopify/04-03-04-03-remove-camelcase-keys
Remove camelcase-keys, replace with inline utility
2 parents de45230 + da3245f commit a66b1c7

File tree

8 files changed

+108
-37
lines changed

8 files changed

+108
-37
lines changed

packages/app/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757
"@shopify/theme": "3.93.0",
5858
"@shopify/theme-check-node": "3.24.0",
5959
"@shopify/toml-patch": "0.3.0",
60-
"camelcase-keys": "9.1.3",
6160
"chokidar": "3.6.0",
6261
"diff": "5.2.2",
6362
"esbuild": "0.27.4",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import camelcaseKeys from './camelcase-keys.js'
2+
import {describe, expect, test} from 'vitest'
3+
4+
describe('camelcaseKeys', () => {
5+
test('converts snake_case keys', () => {
6+
expect(camelcaseKeys({foo_bar: 1, baz_qux: 2})).toEqual({fooBar: 1, bazQux: 2})
7+
})
8+
9+
test('converts kebab-case keys', () => {
10+
expect(camelcaseKeys({'foo-bar': 1, 'baz-qux': 2})).toEqual({fooBar: 1, bazQux: 2})
11+
})
12+
13+
test('leaves camelCase keys unchanged', () => {
14+
expect(camelcaseKeys({alreadyCamel: 1})).toEqual({alreadyCamel: 1})
15+
})
16+
17+
test('handles null and undefined values', () => {
18+
expect(camelcaseKeys({foo_bar: null, baz_qux: undefined})).toEqual({fooBar: null, bazQux: undefined})
19+
})
20+
21+
test('handles arrays at top level', () => {
22+
expect(camelcaseKeys([{foo_bar: 1}])).toEqual([{foo_bar: 1}])
23+
})
24+
25+
test('does not recurse by default', () => {
26+
expect(camelcaseKeys({foo_bar: {nested_key: 1}})).toEqual({fooBar: {nested_key: 1}})
27+
})
28+
29+
test('recurses with deep: true', () => {
30+
expect(camelcaseKeys({foo_bar: {nested_key: 1}}, {deep: true})).toEqual({fooBar: {nestedKey: 1}})
31+
})
32+
33+
test('recurses into arrays with deep: true', () => {
34+
expect(camelcaseKeys({arr: [{nested_key: 1}]}, {deep: true})).toEqual({arr: [{nestedKey: 1}]})
35+
})
36+
37+
test('handles top-level arrays with deep: true', () => {
38+
expect(camelcaseKeys([{foo_bar: 1}], {deep: true})).toEqual([{fooBar: 1}])
39+
})
40+
41+
test('returns primitives unchanged', () => {
42+
expect(camelcaseKeys(null as any)).toBeNull()
43+
expect(camelcaseKeys('hello' as any)).toBe('hello')
44+
})
45+
46+
test('handles empty object', () => {
47+
expect(camelcaseKeys({})).toEqual({})
48+
})
49+
50+
test('preserves Date values with deep: true', () => {
51+
const date = new Date('2024-01-01')
52+
const result = camelcaseKeys({created_at: date}, {deep: true})
53+
expect(result).toEqual({createdAt: date})
54+
})
55+
56+
test('does not recurse into Date objects with deep: true', () => {
57+
const date = new Date('2024-01-01')
58+
const result: Record<string, unknown> = camelcaseKeys({foo_bar: date}, {deep: true})
59+
expect(result.fooBar).toBeInstanceOf(Date)
60+
})
61+
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {camelize} from '@shopify/cli-kit/common/string'
2+
3+
function isPlainObject(value: unknown): value is Record<string, unknown> {
4+
return (
5+
value !== null &&
6+
typeof value === 'object' &&
7+
!Array.isArray(value) &&
8+
Object.getPrototypeOf(value) === Object.prototype
9+
)
10+
}
11+
12+
function transformValue(value: unknown, options?: {deep?: boolean}): unknown {
13+
if (options?.deep && isPlainObject(value)) return camelcaseKeys(value, options)
14+
if (options?.deep && Array.isArray(value)) return camelcaseKeys(value, options)
15+
return value
16+
}
17+
18+
/**
19+
* Converts object keys from snake_case/kebab-case to camelCase.
20+
* Drop-in replacement for the camelcase-keys npm package.
21+
*
22+
* @param input - Object or array to transform.
23+
* @param options - Options object. Set deep: true for recursive transformation.
24+
* @returns A new object/array with camelCased keys.
25+
*/
26+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27+
export default function camelcaseKeys<T = any>(input: T, options?: {deep?: boolean}): T {
28+
if (Array.isArray(input)) {
29+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30+
return input.map((item) => (options?.deep ? camelcaseKeys(item, options) : item)) as any
31+
}
32+
33+
if (isPlainObject(input)) {
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
const result: Record<string, any> = {}
36+
for (const [key, value] of Object.entries(input)) {
37+
result[camelize(key)] = transformValue(value, options)
38+
}
39+
return result as T
40+
}
41+
42+
return input
43+
}

packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {writeAppLogsToFile} from './write-app-logs.js'
33
import {FunctionRunLog} from '../types.js'
44
import {MAX_CONSECUTIVE_RESUBSCRIBE_FAILURES} from '../utils.js'
55
import {testDeveloperPlatformClient} from '../../../models/app/app.test-data.js'
6+
import camelcaseKeys from '../camelcase-keys.js'
67
import {describe, expect, test, vi, beforeEach, afterEach} from 'vitest'
78
import * as components from '@shopify/cli-kit/node/ui/components'
89
import * as output from '@shopify/cli-kit/node/output'
9-
import camelcaseKeys from 'camelcase-keys'
1010
import {appManagementFqdn} from '@shopify/cli-kit/node/context/fqdn'
1111

1212
const JWT_TOKEN = 'jwtToken'

packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import {
1414
handleFetchAppLogsError,
1515
AppLogsOptions,
1616
} from '../utils.js'
17+
import camelcaseKeys from '../camelcase-keys.js'
1718
import {AppLogData, FunctionRunLog} from '../types.js'
1819
import {AppLogsError, AppLogsSuccess, DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
1920
import {outputContent, outputDebug, outputToken, outputWarn} from '@shopify/cli-kit/node/output'
2021
import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components'
21-
import camelcaseKeys from 'camelcase-keys'
2222
import {Writable} from 'stream'
2323

2424
export const pollAppLogs = async ({

packages/app/src/cli/services/app-logs/dev/write-app-logs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {writeAppLogsToFile} from './write-app-logs.js'
22
import {AppLogData, AppLogPayload, FunctionRunLog} from '../types.js'
3+
import camelcaseKeys from '../camelcase-keys.js'
34
import {joinPath} from '@shopify/cli-kit/node/path'
45
import {writeFile} from '@shopify/cli-kit/node/fs'
56
import {describe, expect, test, vi, beforeEach} from 'vitest'
6-
import camelcaseKeys from 'camelcase-keys'
77
import {formatLocalDate} from '@shopify/cli-kit/common/string'
88

99
vi.mock('@shopify/cli-kit/node/fs')

packages/app/src/cli/services/app-logs/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
ErrorResponse,
88
AppLogData,
99
} from './types.js'
10+
import camelcaseKeys from './camelcase-keys.js'
1011
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
1112
import {AppInterface} from '../../models/app/app.js'
1213
import {AppLogsSubscribeMutationVariables} from '../../api/graphql/app-management/generated/app-logs-subscribe.js'
1314
import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output'
1415
import {AbortError} from '@shopify/cli-kit/node/error'
15-
import camelcaseKeys from 'camelcase-keys'
1616
import {formatLocalDate} from '@shopify/cli-kit/common/string'
1717
import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components'
1818
import {Writable} from 'stream'

pnpm-lock.yaml

Lines changed: 0 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)