diff --git a/packages/app/package.json b/packages/app/package.json index 9be9754b59e..db14a4ebb6a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/cli/services/app-logs/camelcase-keys.test.ts b/packages/app/src/cli/services/app-logs/camelcase-keys.test.ts new file mode 100644 index 00000000000..aa035c367dd --- /dev/null +++ b/packages/app/src/cli/services/app-logs/camelcase-keys.test.ts @@ -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 = camelcaseKeys({foo_bar: date}, {deep: true}) + expect(result.fooBar).toBeInstanceOf(Date) + }) +}) diff --git a/packages/app/src/cli/services/app-logs/camelcase-keys.ts b/packages/app/src/cli/services/app-logs/camelcase-keys.ts new file mode 100644 index 00000000000..1d3c538f331 --- /dev/null +++ b/packages/app/src/cli/services/app-logs/camelcase-keys.ts @@ -0,0 +1,43 @@ +import {camelize} from '@shopify/cli-kit/common/string' + +function isPlainObject(value: unknown): value is Record { + 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(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 = {} + for (const [key, value] of Object.entries(input)) { + result[camelize(key)] = transformValue(value, options) + } + return result as T + } + + return input +} diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts index 5062ca94f98..fd71c317452 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts @@ -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' diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts index 424360a129e..8f9730afc6e 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts @@ -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 ({ diff --git a/packages/app/src/cli/services/app-logs/dev/write-app-logs.test.ts b/packages/app/src/cli/services/app-logs/dev/write-app-logs.test.ts index 2788df54a50..1e58368a1c3 100644 --- a/packages/app/src/cli/services/app-logs/dev/write-app-logs.test.ts +++ b/packages/app/src/cli/services/app-logs/dev/write-app-logs.test.ts @@ -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') diff --git a/packages/app/src/cli/services/app-logs/utils.ts b/packages/app/src/cli/services/app-logs/utils.ts index e3d46f31930..558995447e5 100644 --- a/packages/app/src/cli/services/app-logs/utils.ts +++ b/packages/app/src/cli/services/app-logs/utils.ts @@ -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' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9567a9bad9a..e9f59737a2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,9 +175,6 @@ importers: '@shopify/toml-patch': specifier: 0.3.0 version: 0.3.0 - camelcase-keys: - specifier: 9.1.3 - version: 9.1.3 chokidar: specifier: 3.6.0 version: 3.6.0 @@ -4594,18 +4591,10 @@ packages: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} - camelcase-keys@9.1.3: - resolution: {integrity: sha512-Rircqi9ch8AnZscQcsA1C47NFdaO3wukpmIRzYcDOrmvgt78hM/sj5pZhZNec2NM12uk5vTwRHZ4anGcrC4ZTg==} - engines: {node: '>=16'} - camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} - caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} @@ -6745,10 +6734,6 @@ packages: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - map-obj@5.0.0: - resolution: {integrity: sha512-2L3MIgJynYrZ3TYMriLDLWocz15okFakV6J12HXvMXDHui2x/zgChzg1u9mFFGbbGWE+GsLpQByt4POb9Or+uA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - markdown-it@14.1.1: resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true @@ -7530,10 +7515,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - quick-lru@6.1.2: - resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==} - engines: {node: '>=12'} - radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -13970,17 +13951,8 @@ snapshots: map-obj: 4.3.0 quick-lru: 4.0.1 - camelcase-keys@9.1.3: - dependencies: - camelcase: 8.0.0 - map-obj: 5.0.0 - quick-lru: 6.1.2 - type-fest: 4.41.0 - camelcase@5.3.1: {} - camelcase@8.0.0: {} - caniuse-lite@1.0.30001774: {} capital-case@1.0.4: @@ -16414,8 +16386,6 @@ snapshots: map-obj@4.3.0: {} - map-obj@5.0.0: {} - markdown-it@14.1.1: dependencies: argparse: 2.0.1 @@ -17261,8 +17231,6 @@ snapshots: quick-lru@5.1.1: {} - quick-lru@6.1.2: {} - radix3@1.1.2: {} rc@1.2.8: