Skip to content

Commit 046acbc

Browse files
authored
Merge pull request #74 from constructive-io/devin/1774610281-unify-casing-libs
feat: unify casing libraries — komoji as origin of truth
2 parents 0f85906 + c8558c6 commit 046acbc

5 files changed

Lines changed: 2719 additions & 5278 deletions

File tree

packages/inflekt/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"makage": "0.1.10"
4242
},
4343
"dependencies": {
44-
"inflection": "^3.0.0"
44+
"inflection": "^3.0.0",
45+
"komoji": "workspace:*"
4546
}
4647
}

packages/inflekt/src/case.ts

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
/**
22
* Case transformation utilities
3+
*
4+
* Pure case transforms are delegated to komoji (the origin of truth).
5+
* This module re-exports them for backward compatibility and adds
6+
* inflekt-specific helpers (fixCapitalisedPlural, underscore, toScreamingSnake).
37
*/
48

5-
/**
6-
* Convert PascalCase to camelCase (lowercase first character)
7-
* @example "UserProfile" -> "userProfile"
8-
*/
9-
export function lcFirst(str: string): string {
10-
return str.charAt(0).toLowerCase() + str.slice(1);
11-
}
9+
import {
10+
lcFirst,
11+
ucFirst,
12+
toCamelCase as _toCamelCase,
13+
toPascalCase,
14+
toSnakeCase,
15+
toConstantCase,
16+
} from 'komoji';
1217

13-
/**
14-
* Convert camelCase to PascalCase (uppercase first character)
15-
* @example "userProfile" -> "UserProfile"
16-
*/
17-
export function ucFirst(str: string): string {
18-
return str.charAt(0).toUpperCase() + str.slice(1);
18+
// Re-export komoji functions directly
19+
export { lcFirst, ucFirst, toPascalCase, toSnakeCase, toConstantCase };
20+
21+
// Re-export toCamelCase — komoji's version accepts a second parameter
22+
// (stripLeadingNonAlphabetChars) so we wrap to keep the simpler inflekt signature
23+
export function toCamelCase(str: string): string {
24+
return _toCamelCase(str);
1925
}
2026

2127
/**
@@ -29,51 +35,20 @@ export function fixCapitalisedPlural(str: string): string {
2935

3036
/**
3137
* Convert PascalCase or camelCase to snake_case
38+
* @deprecated Use toSnakeCase from komoji instead
3239
* @example underscore('UserProfile') -> 'user_profile'
3340
* @example underscore('userProfile') -> 'user_profile'
3441
*/
3542
export function underscore(str: string): string {
36-
return str
37-
.replace(/([A-Z])/g, '_$1')
38-
.replace(/^_/, '')
39-
.toLowerCase();
40-
}
41-
42-
/**
43-
* Convert a hyphenated, underscored, or already-camelCased string to camelCase.
44-
* Handles both `-` and `_` delimiters.
45-
* @example toCamelCase('user-profile') -> 'userProfile'
46-
* @example toCamelCase('user_profile') -> 'userProfile'
47-
* @example toCamelCase('UserProfile') -> 'userProfile'
48-
*/
49-
export function toCamelCase(str: string): string {
50-
return str
51-
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
52-
.replace(/^(.)/, (_, char) => char.toLowerCase());
53-
}
54-
55-
/**
56-
* Convert a hyphenated, underscored, or already-camelCased string to PascalCase.
57-
* Handles both `-` and `_` delimiters.
58-
* @example toPascalCase('user-profile') -> 'UserProfile'
59-
* @example toPascalCase('user_profile') -> 'UserProfile'
60-
* @example toPascalCase('userProfile') -> 'UserProfile'
61-
*/
62-
export function toPascalCase(str: string): string {
63-
return str
64-
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
65-
.replace(/^(.)/, (_, char) => char.toUpperCase());
43+
return toSnakeCase(str);
6644
}
6745

6846
/**
6947
* Convert a camelCase or PascalCase string to SCREAMING_SNAKE_CASE.
48+
* @deprecated Use toConstantCase from komoji instead
7049
* @example toScreamingSnake('userProfile') -> 'USER_PROFILE'
7150
* @example toScreamingSnake('UserProfile') -> 'USER_PROFILE'
7251
*/
7352
export function toScreamingSnake(str: string): string {
74-
return str
75-
.replace(/([A-Z])/g, '_$1')
76-
.replace(/[-\s]/g, '_')
77-
.toUpperCase()
78-
.replace(/^_/, '');
53+
return toConstantCase(str);
7954
}

packages/komoji/__tests__/casing.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
lcFirst,
3+
ucFirst,
24
isValidIdentifier,
35
isValidIdentifierCamelized,
46
toCamelCase,
@@ -41,6 +43,40 @@ it('should validate valid JavaScript-like identifiers allowing internal hyphens'
4143
expect(isValidIdentifierCamelized('invalid-identifier-')).toBe(true);
4244
});
4345

46+
describe('lcFirst', () => {
47+
test('lowercases the first character', () => {
48+
expect(lcFirst('UserProfile')).toBe('userProfile');
49+
expect(lcFirst('User')).toBe('user');
50+
expect(lcFirst('ABC')).toBe('aBC');
51+
});
52+
53+
test('handles already lowercase strings', () => {
54+
expect(lcFirst('user')).toBe('user');
55+
});
56+
57+
test('handles single character', () => {
58+
expect(lcFirst('A')).toBe('a');
59+
expect(lcFirst('a')).toBe('a');
60+
});
61+
});
62+
63+
describe('ucFirst', () => {
64+
test('uppercases the first character', () => {
65+
expect(ucFirst('userProfile')).toBe('UserProfile');
66+
expect(ucFirst('user')).toBe('User');
67+
expect(ucFirst('abc')).toBe('Abc');
68+
});
69+
70+
test('handles already uppercase strings', () => {
71+
expect(ucFirst('User')).toBe('User');
72+
});
73+
74+
test('handles single character', () => {
75+
expect(ucFirst('a')).toBe('A');
76+
expect(ucFirst('A')).toBe('A');
77+
});
78+
});
79+
4480
describe('toPascalCase', () => {
4581
test('converts normal string', () => {
4682
expect(toPascalCase('hello_world')).toBe('HelloWorld');
@@ -56,6 +92,18 @@ describe('toPascalCase', () => {
5692
expect(toPascalCase('hello___world--great')).toBe('HelloWorldGreat');
5793
});
5894

95+
test('handles consecutive mixed separators', () => {
96+
expect(toPascalCase('my__double_under')).toBe('MyDoubleUnder');
97+
expect(toPascalCase('my-_mixed-_sep')).toBe('MyMixedSep');
98+
expect(toPascalCase('my--double-dash')).toBe('MyDoubleDash');
99+
});
100+
101+
test('handles leading separators', () => {
102+
expect(toPascalCase('_private')).toBe('Private');
103+
expect(toPascalCase('__double')).toBe('Double');
104+
expect(toPascalCase('-leading')).toBe('Leading');
105+
});
106+
59107
test('handles single word', () => {
60108
expect(toPascalCase('word')).toBe('Word');
61109
});
@@ -67,6 +115,11 @@ describe('toPascalCase', () => {
67115
test('handles string with numbers', () => {
68116
expect(toPascalCase('version1_2_3')).toBe('Version123');
69117
});
118+
119+
test('handles spaces', () => {
120+
expect(toPascalCase('my table name')).toBe('MyTableName');
121+
expect(toPascalCase('My Table Name')).toBe('MyTableName');
122+
});
70123
});
71124

72125
describe('toCamelCase', () => {

packages/komoji/src/index.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
1+
/**
2+
* Lowercase the first character of a string
3+
* @example "UserProfile" -> "userProfile"
4+
*/
5+
export function lcFirst(str: string): string {
6+
return str.charAt(0).toLowerCase() + str.slice(1);
7+
}
8+
9+
/**
10+
* Uppercase the first character of a string
11+
* @example "userProfile" -> "UserProfile"
12+
*/
13+
export function ucFirst(str: string): string {
14+
return str.charAt(0).toUpperCase() + str.slice(1);
15+
}
16+
117
export function toPascalCase(str: string) {
218
return str
3-
.replace(/(^|_|\s|-)(\w)/g, (_: any, __: any, letter: string) =>
4-
letter.toUpperCase()
5-
)
6-
.replace(/[_\s-]/g, '');
19+
// Convert what follows one-or-more separators into upper case (handles consecutive separators)
20+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
21+
// Ensure the first character is always uppercase
22+
.replace(/^./, (c) => c.toUpperCase());
723
}
824

925
export function toCamelCase(

0 commit comments

Comments
 (0)