From 4f8697454491a4facb1240d2f0b202f3407f4059 Mon Sep 17 00:00:00 2001 From: Swaraj Jakanoor <169044732+swaraj792725@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:53 +0530 Subject: [PATCH 1/2] fix: format leading acronyms correctly in camelCase --- src/index.ts | 14 +++++++++++++- test/scule.test.ts | 13 ++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index f1983fd..3d01199 100644 --- a/src/index.ts +++ b/src/index.ts @@ -113,7 +113,19 @@ export function camelCase< T extends string | readonly string[], UserCaseOptions extends CaseOptions = CaseOptions, >(str?: T, opts?: UserCaseOptions) { - return lowerFirst(pascalCase(str || "", opts)) as CamelCase< + if (!str) { + return ""; + } + const parts = Array.isArray(str) ? str : splitByCase(str as string); + if (parts.length === 0) { + return ""; + } + const [first = "", ...rest] = parts; + const firstFormatted = first.toLowerCase(); + const restFormatted = rest.map((p) => + upperFirst(opts?.normalize ? p.toLowerCase() : p), + ); + return (firstFormatted + restFormatted.join("")) as CamelCase< T, UserCaseOptions["normalize"] >; diff --git a/test/scule.test.ts b/test/scule.test.ts index bd5ec3b..a796a9d 100644 --- a/test/scule.test.ts +++ b/test/scule.test.ts @@ -58,8 +58,19 @@ describe("pascalCase", () => { describe("camelCase", () => { test.each([ ["FooBarBaz", "fooBarBaz"], - ["FOO_BAR", "fooBar"], + ["APIBaseURL", "apiBaseURL"], + ["XMLParser", "xmlParser"], + ["HTTPSResponse", "httpsResponse"], ])("%s => %s", (input, expected) => { + expect(camelCase(input)).toMatchObject(expected); + }); + + test.each([ + ["FooBarBaz", "fooBarBaz"], + ["FOO_BAR", "fooBar"], + ["API_Base_URL", "apiBaseUrl"], + ["MY_API_KEY", "myApiKey"], + ])("%s (normalized) => %s", (input, expected) => { expect(camelCase(input, { normalize: true })).toMatchObject(expected); }); }); From 78c4377075e5ef5999579d2e06981d600c23369b Mon Sep 17 00:00:00 2001 From: Swaraj Jakanoor <169044732+swaraj792725@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:32:19 +0530 Subject: [PATCH 2/2] fix(camelCase): preserve mixed case array elements and handle leading splitters --- src/index.ts | 8 ++++++-- src/types.ts | 16 +++++++++++++++- test/scule.test.ts | 7 +++++++ test/types.test-d.ts | 4 ++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3d01199..07a71b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,12 +116,16 @@ export function camelCase< if (!str) { return ""; } - const parts = Array.isArray(str) ? str : splitByCase(str as string); + const rawParts = Array.isArray(str) ? str : splitByCase(str as string); + const parts = rawParts.filter(Boolean); if (parts.length === 0) { return ""; } const [first = "", ...rest] = parts; - const firstFormatted = first.toLowerCase(); + const firstFormatted = + opts?.normalize || first === first.toUpperCase() + ? first.toLowerCase() + : lowerFirst(first); const restFormatted = rest.map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p), ); diff --git a/src/types.ts b/src/types.ts index 9e7f1dd..69f4734 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,14 @@ type CapitalizedWords< Normalize > : Accumulator; +type CamelCaseWords< + T extends readonly string[], + Normalize extends boolean | undefined = false, +> = T extends readonly [infer F extends string, ...infer R extends string[]] + ? F extends "" + ? CamelCaseWords + : `${Normalize extends true ? Lowercase : F extends Uppercase ? Lowercase : Uncapitalize}${CapitalizedWords}` + : ""; type JoinLowercaseWords< T extends readonly string[], Joiner extends string, @@ -133,7 +141,13 @@ export type CamelCase< ? string : string[] extends T ? string - : Uncapitalize>; + : T extends string + ? SplitByCase extends readonly string[] + ? CamelCaseWords, Normalize> + : never + : T extends readonly string[] + ? CamelCaseWords + : never; export type KebabCase< T extends string | readonly string[], diff --git a/test/scule.test.ts b/test/scule.test.ts index a796a9d..cd7107f 100644 --- a/test/scule.test.ts +++ b/test/scule.test.ts @@ -57,10 +57,17 @@ describe("pascalCase", () => { describe("camelCase", () => { test.each([ + ["", ""], + ["-", ""], + [[], ""], ["FooBarBaz", "fooBarBaz"], ["APIBaseURL", "apiBaseURL"], ["XMLParser", "xmlParser"], ["HTTPSResponse", "httpsResponse"], + ["-foo", "foo"], + ["--foo-bar--", "fooBar"], + [["fooBar", "Baz"], "fooBarBaz"], + [["API", "Base", "URL"], "apiBaseURL"], ])("%s => %s", (input, expected) => { expect(camelCase(input)).toMatchObject(expected); }); diff --git a/test/types.test-d.ts b/test/types.test-d.ts index bd722b1..b49676b 100644 --- a/test/types.test-d.ts +++ b/test/types.test-d.ts @@ -72,10 +72,14 @@ describe("CamelCase", () => { assertType>("fooBaRb"); assertType>("fooBarBazQux"); assertType>("fooBar"); + assertType>("apiBaseURL"); + assertType>("apiBaseUrl"); }); test("array", () => { assertType>("fooBar"); + assertType>("fooBarBaz"); + assertType>("apiBaseURL"); }); });