Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"@shopify/theme": "3.93.0",
"@shopify/theme-check-node": "3.24.0",
"@shopify/toml-patch": "0.3.0",
"camelcase-keys": "9.1.3",
"chokidar": "3.6.0",
"diff": "5.2.2",
"esbuild": "0.27.4",
Expand Down
61 changes: 61 additions & 0 deletions packages/app/src/cli/services/app-logs/camelcase-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import camelcaseKeys from './camelcase-keys.js'
import {describe, expect, test} from 'vitest'

describe('camelcaseKeys', () => {
test('converts snake_case keys', () => {
expect(camelcaseKeys({foo_bar: 1, baz_qux: 2})).toEqual({fooBar: 1, bazQux: 2})
})

test('converts kebab-case keys', () => {
expect(camelcaseKeys({'foo-bar': 1, 'baz-qux': 2})).toEqual({fooBar: 1, bazQux: 2})
})

test('leaves camelCase keys unchanged', () => {
expect(camelcaseKeys({alreadyCamel: 1})).toEqual({alreadyCamel: 1})
})

test('handles null and undefined values', () => {
expect(camelcaseKeys({foo_bar: null, baz_qux: undefined})).toEqual({fooBar: null, bazQux: undefined})
})

test('handles arrays at top level', () => {
expect(camelcaseKeys([{foo_bar: 1}])).toEqual([{foo_bar: 1}])
})

test('does not recurse by default', () => {
expect(camelcaseKeys({foo_bar: {nested_key: 1}})).toEqual({fooBar: {nested_key: 1}})
})

test('recurses with deep: true', () => {
expect(camelcaseKeys({foo_bar: {nested_key: 1}}, {deep: true})).toEqual({fooBar: {nestedKey: 1}})
})

test('recurses into arrays with deep: true', () => {
expect(camelcaseKeys({arr: [{nested_key: 1}]}, {deep: true})).toEqual({arr: [{nestedKey: 1}]})
})

test('handles top-level arrays with deep: true', () => {
expect(camelcaseKeys([{foo_bar: 1}], {deep: true})).toEqual([{fooBar: 1}])
})

test('returns primitives unchanged', () => {
expect(camelcaseKeys(null as any)).toBeNull()
expect(camelcaseKeys('hello' as any)).toBe('hello')
})

test('handles empty object', () => {
expect(camelcaseKeys({})).toEqual({})
})

test('preserves Date values with deep: true', () => {
const date = new Date('2024-01-01')
const result = camelcaseKeys({created_at: date}, {deep: true})
expect(result).toEqual({createdAt: date})
})

test('does not recurse into Date objects with deep: true', () => {
const date = new Date('2024-01-01')
const result: Record<string, unknown> = camelcaseKeys({foo_bar: date}, {deep: true})
expect(result.fooBar).toBeInstanceOf(Date)
})
})
43 changes: 43 additions & 0 deletions packages/app/src/cli/services/app-logs/camelcase-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {camelize} from '@shopify/cli-kit/common/string'

function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype
)
}

function transformValue(value: unknown, options?: {deep?: boolean}): unknown {
if (options?.deep && isPlainObject(value)) return camelcaseKeys(value, options)
if (options?.deep && Array.isArray(value)) return camelcaseKeys(value, options)
return value
}

/**
* Converts object keys from snake_case/kebab-case to camelCase.
* Drop-in replacement for the camelcase-keys npm package.
*
* @param input - Object or array to transform.
* @param options - Options object. Set deep: true for recursive transformation.
* @returns A new object/array with camelCased keys.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function camelcaseKeys<T = any>(input: T, options?: {deep?: boolean}): T {
if (Array.isArray(input)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return input.map((item) => (options?.deep ? camelcaseKeys(item, options) : item)) as any
}

if (isPlainObject(input)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: Record<string, any> = {}
for (const [key, value] of Object.entries(input)) {
result[camelize(key)] = transformValue(value, options)
}
return result as T
}

return input
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {writeAppLogsToFile} from './write-app-logs.js'
import {FunctionRunLog} from '../types.js'
import {MAX_CONSECUTIVE_RESUBSCRIBE_FAILURES} from '../utils.js'
import {testDeveloperPlatformClient} from '../../../models/app/app.test-data.js'
import camelcaseKeys from '../camelcase-keys.js'
import {describe, expect, test, vi, beforeEach, afterEach} from 'vitest'
import * as components from '@shopify/cli-kit/node/ui/components'
import * as output from '@shopify/cli-kit/node/output'
import camelcaseKeys from 'camelcase-keys'
import {appManagementFqdn} from '@shopify/cli-kit/node/context/fqdn'

const JWT_TOKEN = 'jwtToken'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import {
handleFetchAppLogsError,
AppLogsOptions,
} from '../utils.js'
import camelcaseKeys from '../camelcase-keys.js'
import {AppLogData, FunctionRunLog} from '../types.js'
import {AppLogsError, AppLogsSuccess, DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
import {outputContent, outputDebug, outputToken, outputWarn} from '@shopify/cli-kit/node/output'
import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components'
import camelcaseKeys from 'camelcase-keys'
import {Writable} from 'stream'

export const pollAppLogs = async ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {writeAppLogsToFile} from './write-app-logs.js'
import {AppLogData, AppLogPayload, FunctionRunLog} from '../types.js'
import camelcaseKeys from '../camelcase-keys.js'
import {joinPath} from '@shopify/cli-kit/node/path'
import {writeFile} from '@shopify/cli-kit/node/fs'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import camelcaseKeys from 'camelcase-keys'
import {formatLocalDate} from '@shopify/cli-kit/common/string'

vi.mock('@shopify/cli-kit/node/fs')
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/services/app-logs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
ErrorResponse,
AppLogData,
} from './types.js'
import camelcaseKeys from './camelcase-keys.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {AppInterface} from '../../models/app/app.js'
import {AppLogsSubscribeMutationVariables} from '../../api/graphql/app-management/generated/app-logs-subscribe.js'
import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output'
import {AbortError} from '@shopify/cli-kit/node/error'
import camelcaseKeys from 'camelcase-keys'
import {formatLocalDate} from '@shopify/cli-kit/common/string'
import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components'
import {Writable} from 'stream'
Expand Down
32 changes: 0 additions & 32 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading