casemorph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-03-28)
4
+
5
+ ### Features
6
+
7
+ - **String transforms**: `camelCase`, `snakeCase`, `kebabCase`, `pascalCase`, `constantCase`
8
+ - **Deep object transforms**: `toCamelCase`, `toSnakeCase`, `toKebabCase`, `toPascalCase`, `toConstantCase`
9
+ - **Full type inference**: string literal types transform at compile-time, deep object keys remap via mapped types
10
+ - **Acronym-aware**: `XMLParser` correctly splits to `xml` + `parser` (not `x` + `m` + `l` + `parser`)
11
+ - **Leaf preservation**: Date, RegExp, Map, Set, Error, and functions are never recursed into
12
+ - **Zero dependencies**
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 best-package
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # casemorph
2
+
3
+ **Your API speaks `snake_case`. Your code speaks `camelCase`. casemorph transforms the keys — and the types follow.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/casemorph)](https://www.npmjs.com/package/casemorph)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/casemorph)](https://bundlephobia.com/package/casemorph)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/best-package/casemorph/ci.yml)](https://github.com/best-package/casemorph/actions)
9
+ [![license](https://img.shields.io/npm/l/casemorph)](./LICENSE)
10
+
11
+ ## The Problem
12
+
13
+ Every REST API integration involves the same ritual: receive `snake_case` keys, manually map them to `camelCase`, lose all type safety in the process — or litter your code with `as` assertions.
14
+
15
+ ```ts
16
+ // You do this. Every time. And it's wrong.
17
+ const user = response.data as User; // 🤞 Hope the API matches your types
18
+ ```
19
+
20
+ ## The Solution
21
+
22
+ ```ts
23
+ import { toCamelCase } from "casemorph";
24
+
25
+ const user = toCamelCase(apiResponse);
26
+
27
+ user.firstName; // ✅ Autocomplete works — type is string
28
+ user.billingAddress.streetName; // ✅ Deep transformation
29
+ user.first_name; // ❌ Property 'first_name' does not exist
30
+ ```
31
+
32
+ One function call. Keys transform at runtime. Types transform at compile-time. No assertions. No manual mappings. The types follow the data.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install casemorph
38
+ pnpm add casemorph
39
+ bun add casemorph
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```ts
45
+ import { toCamelCase, toSnakeCase } from "casemorph";
46
+
47
+ // API response → your code
48
+ const apiResponse = {
49
+ user_id: 1,
50
+ first_name: "Alice",
51
+ billing_address: { street_name: "123 Main St", zip_code: "75001" },
52
+ recent_orders: [{ order_id: 42, total_amount: 99.99 }],
53
+ };
54
+
55
+ const user = toCamelCase(apiResponse);
56
+ user.firstName; // "Alice"
57
+ user.billingAddress.zipCode; // "75001"
58
+ user.recentOrders[0].totalAmount; // 99.99
59
+
60
+ // Your code → API request
61
+ const payload = toSnakeCase({ userId: 1, firstName: "Bob" });
62
+ payload.user_id; // 1
63
+ payload.first_name; // "Bob"
64
+ ```
65
+
66
+ ## API Reference
67
+
68
+ ### Object Transforms
69
+
70
+ Deeply transform all keys of an object (or array of objects). Primitives, `Date`, `RegExp`, `Map`, `Set`, and `Error` instances are preserved as-is.
71
+
72
+ | Function | Target | Example key |
73
+ |---|---|---|
74
+ | `toCamelCase(data)` | camelCase | `userName` |
75
+ | `toSnakeCase(data)` | snake_case | `user_name` |
76
+ | `toKebabCase(data)` | kebab-case | `user-name` |
77
+ | `toPascalCase(data)` | PascalCase | `UserName` |
78
+ | `toConstantCase(data)` | CONSTANT_CASE | `USER_NAME` |
79
+
80
+ ```ts
81
+ // Handles nested objects, arrays of objects, and mixed content
82
+ toCamelCase({
83
+ recent_orders: [{ order_id: 1, line_items: [{ product_name: "Widget" }] }],
84
+ });
85
+ // { recentOrders: [{ orderId: 1, lineItems: [{ productName: "Widget" }] }] }
86
+ ```
87
+
88
+ ### String Transforms
89
+
90
+ Transform a single string between naming conventions. When called with a string literal, the return type is the **transformed literal type**.
91
+
92
+ | Function | Example |
93
+ |---|---|
94
+ | `camelCase(str)` | `camelCase("user_name")` → `"userName"` (type: `"userName"`) |
95
+ | `snakeCase(str)` | `snakeCase("userName")` → `"user_name"` (type: `"user_name"`) |
96
+ | `kebabCase(str)` | `kebabCase("userName")` → `"user-name"` (type: `"user-name"`) |
97
+ | `pascalCase(str)` | `pascalCase("user_name")` → `"UserName"` (type: `"UserName"`) |
98
+ | `constantCase(str)` | `constantCase("userName")` → `"USER_NAME"` (type: `"USER_NAME"`) |
99
+
100
+ ### Types
101
+
102
+ All type-level transforms are exported for advanced use in your own types:
103
+
104
+ ```ts
105
+ import type { CamelCase, CamelCaseKeys, SnakeCase, SnakeCaseKeys } from "casemorph";
106
+
107
+ // String-level
108
+ type A = CamelCase<"user_name">; // "userName"
109
+ type B = SnakeCase<"XMLHttpRequest">; // "xml_http_request"
110
+
111
+ // Object-level
112
+ type ApiResponse = { user_id: number; first_name: string };
113
+ type AppModel = CamelCaseKeys<ApiResponse>; // { userId: number; firstName: string }
114
+ ```
115
+
116
+ **Available types**: `CamelCase`, `SnakeCase`, `KebabCase`, `PascalCase`, `ConstantCase`, `CamelCaseKeys`, `SnakeCaseKeys`, `KebabCaseKeys`, `PascalCaseKeys`, `ConstantCaseKeys`.
117
+
118
+ ## Acronym Handling
119
+
120
+ casemorph correctly splits acronyms — both at runtime and at the type level:
121
+
122
+ ```ts
123
+ camelCase("XMLParser"); // "xmlParser" (not "xmlparser" or "xMLParser")
124
+ camelCase("HTMLElement"); // "htmlElement"
125
+ camelCase("getHTTPResponse"); // "getHttpResponse"
126
+ snakeCase("XMLHttpRequest"); // "xml_http_request"
127
+ ```
128
+
129
+ The algorithm detects boundaries between consecutive uppercase letters and the start of a new word, matching the behavior developers expect.
130
+
131
+ ## What Gets Transformed
132
+
133
+ | Input | Transformed? |
134
+ |---|---|
135
+ | Plain objects (`{}`) | Yes — keys are renamed, values recursed |
136
+ | Arrays | Yes — each element is recursed |
137
+ | Nested objects | Yes — to arbitrary depth |
138
+ | `Date`, `RegExp`, `Error` | No — returned as-is |
139
+ | `Map`, `Set` | No — returned as-is |
140
+ | Functions | No — returned as-is |
141
+ | Primitives (`string`, `number`, `null`, ...) | No — returned as-is |
142
+
143
+ ## Benchmarks
144
+
145
+ Measured on Apple M4, Node.js 24.x, averaged over 2s runs:
146
+
147
+ | Operation | ops/sec |
148
+ |---|---|
149
+ | `camelCase(string)` — simple | ~3,200,000 |
150
+ | `camelCase(string)` — acronym | ~2,400,000 |
151
+ | `toCamelCase(obj)` — flat (8 keys) | ~340,000 |
152
+ | `toCamelCase(obj)` — deep (nested + 10-item array) | ~37,000 |
153
+ | `toSnakeCase(obj)` — flat (8 keys) | ~326,000 |
154
+ | naive `key.replace(/_([a-z])/g, ...)` — flat (8 keys) | ~760,000 |
155
+
156
+ The naive approach is faster for simple snake→camel on flat objects because it skips format detection and acronym handling. casemorph handles **all** formats bidirectionally, detects acronyms, recurses arbitrarily deep, and provides full type inference — for roughly 2x the cost of a hand-rolled regex.
157
+
158
+ ## Compatibility
159
+
160
+ - Node.js >= 20
161
+ - Bun
162
+ - Deno
163
+ - Cloudflare Workers
164
+ - Any edge runtime (zero dependencies, pure ESM + CJS)
165
+
166
+ ## Bundle Size
167
+
168
+ **387 bytes** minified + brotli. Zero dependencies.
169
+
170
+ ## Contributing
171
+
172
+ ```bash
173
+ git clone https://github.com/best-package/casemorph.git
174
+ cd casemorph
175
+ npm install
176
+ npm test # run tests
177
+ npm run bench # run benchmarks
178
+ npm run typecheck # verify types
179
+ npm run check # lint + format
180
+ ```
181
+
182
+ ## License
183
+
184
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ // src/transform.ts
4
+ var LOWER_TO_UPPER = /([a-z\d])([A-Z])/g;
5
+ var ACRONYM_BOUNDARY = /([A-Z]+)([A-Z][a-z])/g;
6
+ var SEPARATORS = /[_\-\s]+/;
7
+ function splitWords(str) {
8
+ if (!str) return [];
9
+ return str.replace(LOWER_TO_UPPER, "$1\0$2").replace(ACRONYM_BOUNDARY, "$1\0$2").split(SEPARATORS).join("\0").split("\0").filter(Boolean).map((w) => w.toLowerCase());
10
+ }
11
+ function joinCamel(words) {
12
+ if (words.length === 0) return "";
13
+ return words[0] + words.slice(1).reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), "");
14
+ }
15
+ function joinSnake(words) {
16
+ return words.join("_");
17
+ }
18
+ function joinKebab(words) {
19
+ return words.join("-");
20
+ }
21
+ function joinPascal(words) {
22
+ return words.reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), "");
23
+ }
24
+ function joinConstant(words) {
25
+ return words.map((w) => w.toUpperCase()).join("_");
26
+ }
27
+ function isPlainObject(value) {
28
+ if (typeof value !== "object" || value === null) return false;
29
+ const proto = Object.getPrototypeOf(value);
30
+ return proto === Object.prototype || proto === null;
31
+ }
32
+ function deepTransformKeys(data, transform) {
33
+ if (Array.isArray(data)) {
34
+ return data.map((item) => deepTransformKeys(item, transform));
35
+ }
36
+ if (!isPlainObject(data)) return data;
37
+ const out = {};
38
+ for (const key of Object.keys(data)) {
39
+ out[transform(key)] = deepTransformKeys(data[key], transform);
40
+ }
41
+ return out;
42
+ }
43
+ function camelCase(str) {
44
+ return joinCamel(splitWords(str));
45
+ }
46
+ function snakeCase(str) {
47
+ return joinSnake(splitWords(str));
48
+ }
49
+ function kebabCase(str) {
50
+ return joinKebab(splitWords(str));
51
+ }
52
+ function pascalCase(str) {
53
+ return joinPascal(splitWords(str));
54
+ }
55
+ function constantCase(str) {
56
+ return joinConstant(splitWords(str));
57
+ }
58
+ function toCamelCase(data) {
59
+ return deepTransformKeys(data, (k) => joinCamel(splitWords(k)));
60
+ }
61
+ function toSnakeCase(data) {
62
+ return deepTransformKeys(data, (k) => joinSnake(splitWords(k)));
63
+ }
64
+ function toKebabCase(data) {
65
+ return deepTransformKeys(data, (k) => joinKebab(splitWords(k)));
66
+ }
67
+ function toPascalCase(data) {
68
+ return deepTransformKeys(data, (k) => joinPascal(splitWords(k)));
69
+ }
70
+ function toConstantCase(data) {
71
+ return deepTransformKeys(data, (k) => joinConstant(splitWords(k)));
72
+ }
73
+
74
+ exports.camelCase = camelCase;
75
+ exports.constantCase = constantCase;
76
+ exports.kebabCase = kebabCase;
77
+ exports.pascalCase = pascalCase;
78
+ exports.snakeCase = snakeCase;
79
+ exports.toCamelCase = toCamelCase;
80
+ exports.toConstantCase = toConstantCase;
81
+ exports.toKebabCase = toKebabCase;
82
+ exports.toPascalCase = toPascalCase;
83
+ exports.toSnakeCase = toSnakeCase;
84
+ //# sourceMappingURL=index.cjs.map
85
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transform.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type {\n\tCamelCase,\n\tCamelCaseKeys,\n\tConstantCase,\n\tConstantCaseKeys,\n\tKebabCase,\n\tKebabCaseKeys,\n\tPascalCase,\n\tPascalCaseKeys,\n\tSnakeCase,\n\tSnakeCaseKeys,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Word splitting — mirrors the type-level SplitWords<S>\n// ---------------------------------------------------------------------------\n\n/** Boundary between a lowercase letter (or digit) and an uppercase letter. */\nconst LOWER_TO_UPPER = /([a-z\\d])([A-Z])/g;\n\n/** Boundary between an acronym and the start of the next word. */\nconst ACRONYM_BOUNDARY = /([A-Z]+)([A-Z][a-z])/g;\n\n/** Any explicit separator (underscore, hyphen, whitespace). */\nconst SEPARATORS = /[_\\-\\s]+/;\n\n/**\n * Split any casing convention into lowercase word fragments.\n *\n * Handles snake_case, kebab-case, CONSTANT_CASE, camelCase, PascalCase,\n * and acronyms like `\"XMLHttpRequest\"` → `[\"xml\", \"http\", \"request\"]`.\n */\nfunction splitWords(str: string): string[] {\n\tif (!str) return [];\n\treturn str\n\t\t.replace(LOWER_TO_UPPER, \"$1\\x00$2\")\n\t\t.replace(ACRONYM_BOUNDARY, \"$1\\x00$2\")\n\t\t.split(SEPARATORS)\n\t\t.join(\"\\x00\")\n\t\t.split(\"\\x00\")\n\t\t.filter(Boolean)\n\t\t.map((w) => w.toLowerCase());\n}\n\n// ---------------------------------------------------------------------------\n// Join helpers — one per target format\n// ---------------------------------------------------------------------------\n\nfunction joinCamel(words: string[]): string {\n\tif (words.length === 0) return \"\";\n\treturn (\n\t\twords[0] + words.slice(1).reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), \"\")\n\t);\n}\n\nfunction joinSnake(words: string[]): string {\n\treturn words.join(\"_\");\n}\n\nfunction joinKebab(words: string[]): string {\n\treturn words.join(\"-\");\n}\n\nfunction joinPascal(words: string[]): string {\n\treturn words.reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), \"\");\n}\n\nfunction joinConstant(words: string[]): string {\n\treturn words.map((w) => w.toUpperCase()).join(\"_\");\n}\n\n// ---------------------------------------------------------------------------\n// Deep key transformation\n// ---------------------------------------------------------------------------\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst proto = Object.getPrototypeOf(value) as unknown;\n\treturn proto === Object.prototype || proto === null;\n}\n\nfunction deepTransformKeys(data: unknown, transform: (key: string) => string): unknown {\n\tif (Array.isArray(data)) {\n\t\treturn data.map((item) => deepTransformKeys(item, transform));\n\t}\n\tif (!isPlainObject(data)) return data;\n\n\tconst out: Record<string, unknown> = {};\n\tfor (const key of Object.keys(data)) {\n\t\tout[transform(key)] = deepTransformKeys(data[key], transform);\n\t}\n\treturn out;\n}\n\n// ---------------------------------------------------------------------------\n// String-level transforms (public)\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a string to camelCase.\n *\n * @example\n * camelCase('user_name') // 'userName'\n * camelCase('XMLParser') // 'xmlParser'\n * camelCase('content-type') // 'contentType'\n */\nexport function camelCase<S extends string>(str: S): CamelCase<S> {\n\treturn joinCamel(splitWords(str)) as CamelCase<S>;\n}\n\n/**\n * Convert a string to snake_case.\n *\n * @example\n * snakeCase('userName') // 'user_name'\n * snakeCase('XMLParser') // 'xml_parser'\n * snakeCase('content-type') // 'content_type'\n */\nexport function snakeCase<S extends string>(str: S): SnakeCase<S> {\n\treturn joinSnake(splitWords(str)) as SnakeCase<S>;\n}\n\n/**\n * Convert a string to kebab-case.\n *\n * @example\n * kebabCase('userName') // 'user-name'\n * kebabCase('XMLParser') // 'xml-parser'\n * kebabCase('user_name') // 'user-name'\n */\nexport function kebabCase<S extends string>(str: S): KebabCase<S> {\n\treturn joinKebab(splitWords(str)) as KebabCase<S>;\n}\n\n/**\n * Convert a string to PascalCase.\n *\n * @example\n * pascalCase('user_name') // 'UserName'\n * pascalCase('XMLParser') // 'XmlParser'\n * pascalCase('kebab-case') // 'KebabCase'\n */\nexport function pascalCase<S extends string>(str: S): PascalCase<S> {\n\treturn joinPascal(splitWords(str)) as PascalCase<S>;\n}\n\n/**\n * Convert a string to CONSTANT_CASE.\n *\n * @example\n * constantCase('userName') // 'USER_NAME'\n * constantCase('XMLParser') // 'XML_PARSER'\n * constantCase('kebab-case') // 'KEBAB_CASE'\n */\nexport function constantCase<S extends string>(str: S): ConstantCase<S> {\n\treturn joinConstant(splitWords(str)) as ConstantCase<S>;\n}\n\n// ---------------------------------------------------------------------------\n// Object-level transforms (public)\n// ---------------------------------------------------------------------------\n\n/**\n * Deeply transform all keys of an object (or array of objects) to camelCase.\n * Primitives, Dates, RegExps, Maps, Sets, and other non-plain objects are preserved.\n *\n * @example\n * const result = toCamelCase({ user_name: 'Alice', billing_address: { zip_code: '75001' } })\n * result.userName // 'Alice'\n * result.billingAddress.zipCode // '75001'\n */\nexport function toCamelCase<T>(data: T): CamelCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinCamel(splitWords(k))) as CamelCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to snake_case.\n *\n * @example\n * const result = toSnakeCase({ userName: 'Alice', billingAddress: { zipCode: '75001' } })\n * result.user_name // 'Alice'\n * result.billing_address.zip_code // '75001'\n */\nexport function toSnakeCase<T>(data: T): SnakeCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinSnake(splitWords(k))) as SnakeCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to kebab-case.\n *\n * @example\n * const result = toKebabCase({ userName: 'Alice' })\n * result['user-name'] // 'Alice'\n */\nexport function toKebabCase<T>(data: T): KebabCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinKebab(splitWords(k))) as KebabCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to PascalCase.\n *\n * @example\n * const result = toPascalCase({ user_name: 'Alice' })\n * result.UserName // 'Alice'\n */\nexport function toPascalCase<T>(data: T): PascalCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinPascal(splitWords(k))) as PascalCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to CONSTANT_CASE.\n *\n * @example\n * const result = toConstantCase({ userName: 'Alice' })\n * result.USER_NAME // 'Alice'\n */\nexport function toConstantCase<T>(data: T): ConstantCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinConstant(splitWords(k))) as ConstantCaseKeys<T>;\n}\n"]}
@@ -0,0 +1,154 @@
1
+ /** True when `C` is a single uppercase ASCII letter (A-Z). */
2
+ type IsUpperLetter<C extends string> = Uppercase<C> extends C ? Lowercase<C> extends C ? false : true : false;
3
+ /**
4
+ * True when the first character of `S` is a lowercase letter or digit.
5
+ * Used to detect the end of an acronym run (e.g. the 'a' in "XMLParser").
6
+ */
7
+ type IsNextLower<S extends string> = S extends `${infer H}${string}` ? IsUpperLetter<H> extends true ? false : H extends "_" | "-" | " " ? false : true : false;
8
+ /**
9
+ * Splits any casing convention into a tuple of raw word fragments.
10
+ *
11
+ * Handles snake_case, kebab-case, CONSTANT_CASE, camelCase, PascalCase,
12
+ * and acronyms (e.g. `"XMLHttpRequest"` → `["XML", "Http", "Request"]`).
13
+ */
14
+ type SplitWords<S extends string> = _Split<S, "", false>;
15
+ type _Split<S extends string, W extends string, PU extends boolean> = S extends `${infer H}${infer T}` ? H extends "_" | "-" | " " ? W extends "" ? _Split<T, "", false> : [W, ..._Split<T, "", false>] : IsUpperLetter<H> extends true ? PU extends true ? IsNextLower<T> extends true ? W extends "" ? _Split<T, H, true> : [W, ..._Split<T, H, true>] : _Split<T, `${W}${H}`, true> : W extends "" ? _Split<T, H, true> : [W, ..._Split<T, H, true>] : _Split<T, `${W}${H}`, false> : W extends "" ? [] : [W];
16
+ type JoinCamel<W extends readonly string[]> = W extends readonly [
17
+ infer F extends string,
18
+ ...infer R extends string[]
19
+ ] ? `${Lowercase<F>}${JoinPascal<R>}` : "";
20
+ type JoinPascal<W extends readonly string[]> = W extends readonly [
21
+ infer F extends string,
22
+ ...infer R extends string[]
23
+ ] ? `${Capitalize<Lowercase<F>>}${JoinPascal<R>}` : "";
24
+ type JoinSep<W extends readonly string[], S extends string> = W extends readonly [
25
+ infer F extends string,
26
+ ...infer R extends string[]
27
+ ] ? R extends readonly [string, ...string[]] ? `${Lowercase<F>}${S}${JoinSep<R, S>}` : Lowercase<F> : "";
28
+ type JoinConstant<W extends readonly string[]> = W extends readonly [
29
+ infer F extends string,
30
+ ...infer R extends string[]
31
+ ] ? R extends readonly [string, ...string[]] ? `${Uppercase<F>}_${JoinConstant<R>}` : Uppercase<F> : "";
32
+ /** Transform a string literal to `camelCase`. */
33
+ type CamelCase<S extends string> = string extends S ? string : JoinCamel<SplitWords<S>>;
34
+ /** Transform a string literal to `snake_case`. */
35
+ type SnakeCase<S extends string> = string extends S ? string : JoinSep<SplitWords<S>, "_">;
36
+ /** Transform a string literal to `kebab-case`. */
37
+ type KebabCase<S extends string> = string extends S ? string : JoinSep<SplitWords<S>, "-">;
38
+ /** Transform a string literal to `PascalCase`. */
39
+ type PascalCase<S extends string> = string extends S ? string : JoinPascal<SplitWords<S>>;
40
+ /** Transform a string literal to `CONSTANT_CASE`. */
41
+ type ConstantCase<S extends string> = string extends S ? string : JoinConstant<SplitWords<S>>;
42
+ /** Object types that should be treated as opaque leaves (never recursed into). */
43
+ type Leaf = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown>;
44
+ /** Deeply transform all keys of `T` to camelCase. */
45
+ type CamelCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? CamelCaseKeys<U>[] : T extends object ? {
46
+ [K in keyof T as CamelCase<K & string>]: CamelCaseKeys<T[K]>;
47
+ } : T;
48
+ /** Deeply transform all keys of `T` to snake_case. */
49
+ type SnakeCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? SnakeCaseKeys<U>[] : T extends object ? {
50
+ [K in keyof T as SnakeCase<K & string>]: SnakeCaseKeys<T[K]>;
51
+ } : T;
52
+ /** Deeply transform all keys of `T` to kebab-case. */
53
+ type KebabCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? KebabCaseKeys<U>[] : T extends object ? {
54
+ [K in keyof T as KebabCase<K & string>]: KebabCaseKeys<T[K]>;
55
+ } : T;
56
+ /** Deeply transform all keys of `T` to PascalCase. */
57
+ type PascalCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? PascalCaseKeys<U>[] : T extends object ? {
58
+ [K in keyof T as PascalCase<K & string>]: PascalCaseKeys<T[K]>;
59
+ } : T;
60
+ /** Deeply transform all keys of `T` to CONSTANT_CASE. */
61
+ type ConstantCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? ConstantCaseKeys<U>[] : T extends object ? {
62
+ [K in keyof T as ConstantCase<K & string>]: ConstantCaseKeys<T[K]>;
63
+ } : T;
64
+
65
+ /**
66
+ * Convert a string to camelCase.
67
+ *
68
+ * @example
69
+ * camelCase('user_name') // 'userName'
70
+ * camelCase('XMLParser') // 'xmlParser'
71
+ * camelCase('content-type') // 'contentType'
72
+ */
73
+ declare function camelCase<S extends string>(str: S): CamelCase<S>;
74
+ /**
75
+ * Convert a string to snake_case.
76
+ *
77
+ * @example
78
+ * snakeCase('userName') // 'user_name'
79
+ * snakeCase('XMLParser') // 'xml_parser'
80
+ * snakeCase('content-type') // 'content_type'
81
+ */
82
+ declare function snakeCase<S extends string>(str: S): SnakeCase<S>;
83
+ /**
84
+ * Convert a string to kebab-case.
85
+ *
86
+ * @example
87
+ * kebabCase('userName') // 'user-name'
88
+ * kebabCase('XMLParser') // 'xml-parser'
89
+ * kebabCase('user_name') // 'user-name'
90
+ */
91
+ declare function kebabCase<S extends string>(str: S): KebabCase<S>;
92
+ /**
93
+ * Convert a string to PascalCase.
94
+ *
95
+ * @example
96
+ * pascalCase('user_name') // 'UserName'
97
+ * pascalCase('XMLParser') // 'XmlParser'
98
+ * pascalCase('kebab-case') // 'KebabCase'
99
+ */
100
+ declare function pascalCase<S extends string>(str: S): PascalCase<S>;
101
+ /**
102
+ * Convert a string to CONSTANT_CASE.
103
+ *
104
+ * @example
105
+ * constantCase('userName') // 'USER_NAME'
106
+ * constantCase('XMLParser') // 'XML_PARSER'
107
+ * constantCase('kebab-case') // 'KEBAB_CASE'
108
+ */
109
+ declare function constantCase<S extends string>(str: S): ConstantCase<S>;
110
+ /**
111
+ * Deeply transform all keys of an object (or array of objects) to camelCase.
112
+ * Primitives, Dates, RegExps, Maps, Sets, and other non-plain objects are preserved.
113
+ *
114
+ * @example
115
+ * const result = toCamelCase({ user_name: 'Alice', billing_address: { zip_code: '75001' } })
116
+ * result.userName // 'Alice'
117
+ * result.billingAddress.zipCode // '75001'
118
+ */
119
+ declare function toCamelCase<T>(data: T): CamelCaseKeys<T>;
120
+ /**
121
+ * Deeply transform all keys of an object (or array of objects) to snake_case.
122
+ *
123
+ * @example
124
+ * const result = toSnakeCase({ userName: 'Alice', billingAddress: { zipCode: '75001' } })
125
+ * result.user_name // 'Alice'
126
+ * result.billing_address.zip_code // '75001'
127
+ */
128
+ declare function toSnakeCase<T>(data: T): SnakeCaseKeys<T>;
129
+ /**
130
+ * Deeply transform all keys of an object (or array of objects) to kebab-case.
131
+ *
132
+ * @example
133
+ * const result = toKebabCase({ userName: 'Alice' })
134
+ * result['user-name'] // 'Alice'
135
+ */
136
+ declare function toKebabCase<T>(data: T): KebabCaseKeys<T>;
137
+ /**
138
+ * Deeply transform all keys of an object (or array of objects) to PascalCase.
139
+ *
140
+ * @example
141
+ * const result = toPascalCase({ user_name: 'Alice' })
142
+ * result.UserName // 'Alice'
143
+ */
144
+ declare function toPascalCase<T>(data: T): PascalCaseKeys<T>;
145
+ /**
146
+ * Deeply transform all keys of an object (or array of objects) to CONSTANT_CASE.
147
+ *
148
+ * @example
149
+ * const result = toConstantCase({ userName: 'Alice' })
150
+ * result.USER_NAME // 'Alice'
151
+ */
152
+ declare function toConstantCase<T>(data: T): ConstantCaseKeys<T>;
153
+
154
+ export { type CamelCase, type CamelCaseKeys, type ConstantCase, type ConstantCaseKeys, type KebabCase, type KebabCaseKeys, type PascalCase, type PascalCaseKeys, type SnakeCase, type SnakeCaseKeys, camelCase, constantCase, kebabCase, pascalCase, snakeCase, toCamelCase, toConstantCase, toKebabCase, toPascalCase, toSnakeCase };
@@ -0,0 +1,154 @@
1
+ /** True when `C` is a single uppercase ASCII letter (A-Z). */
2
+ type IsUpperLetter<C extends string> = Uppercase<C> extends C ? Lowercase<C> extends C ? false : true : false;
3
+ /**
4
+ * True when the first character of `S` is a lowercase letter or digit.
5
+ * Used to detect the end of an acronym run (e.g. the 'a' in "XMLParser").
6
+ */
7
+ type IsNextLower<S extends string> = S extends `${infer H}${string}` ? IsUpperLetter<H> extends true ? false : H extends "_" | "-" | " " ? false : true : false;
8
+ /**
9
+ * Splits any casing convention into a tuple of raw word fragments.
10
+ *
11
+ * Handles snake_case, kebab-case, CONSTANT_CASE, camelCase, PascalCase,
12
+ * and acronyms (e.g. `"XMLHttpRequest"` → `["XML", "Http", "Request"]`).
13
+ */
14
+ type SplitWords<S extends string> = _Split<S, "", false>;
15
+ type _Split<S extends string, W extends string, PU extends boolean> = S extends `${infer H}${infer T}` ? H extends "_" | "-" | " " ? W extends "" ? _Split<T, "", false> : [W, ..._Split<T, "", false>] : IsUpperLetter<H> extends true ? PU extends true ? IsNextLower<T> extends true ? W extends "" ? _Split<T, H, true> : [W, ..._Split<T, H, true>] : _Split<T, `${W}${H}`, true> : W extends "" ? _Split<T, H, true> : [W, ..._Split<T, H, true>] : _Split<T, `${W}${H}`, false> : W extends "" ? [] : [W];
16
+ type JoinCamel<W extends readonly string[]> = W extends readonly [
17
+ infer F extends string,
18
+ ...infer R extends string[]
19
+ ] ? `${Lowercase<F>}${JoinPascal<R>}` : "";
20
+ type JoinPascal<W extends readonly string[]> = W extends readonly [
21
+ infer F extends string,
22
+ ...infer R extends string[]
23
+ ] ? `${Capitalize<Lowercase<F>>}${JoinPascal<R>}` : "";
24
+ type JoinSep<W extends readonly string[], S extends string> = W extends readonly [
25
+ infer F extends string,
26
+ ...infer R extends string[]
27
+ ] ? R extends readonly [string, ...string[]] ? `${Lowercase<F>}${S}${JoinSep<R, S>}` : Lowercase<F> : "";
28
+ type JoinConstant<W extends readonly string[]> = W extends readonly [
29
+ infer F extends string,
30
+ ...infer R extends string[]
31
+ ] ? R extends readonly [string, ...string[]] ? `${Uppercase<F>}_${JoinConstant<R>}` : Uppercase<F> : "";
32
+ /** Transform a string literal to `camelCase`. */
33
+ type CamelCase<S extends string> = string extends S ? string : JoinCamel<SplitWords<S>>;
34
+ /** Transform a string literal to `snake_case`. */
35
+ type SnakeCase<S extends string> = string extends S ? string : JoinSep<SplitWords<S>, "_">;
36
+ /** Transform a string literal to `kebab-case`. */
37
+ type KebabCase<S extends string> = string extends S ? string : JoinSep<SplitWords<S>, "-">;
38
+ /** Transform a string literal to `PascalCase`. */
39
+ type PascalCase<S extends string> = string extends S ? string : JoinPascal<SplitWords<S>>;
40
+ /** Transform a string literal to `CONSTANT_CASE`. */
41
+ type ConstantCase<S extends string> = string extends S ? string : JoinConstant<SplitWords<S>>;
42
+ /** Object types that should be treated as opaque leaves (never recursed into). */
43
+ type Leaf = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown>;
44
+ /** Deeply transform all keys of `T` to camelCase. */
45
+ type CamelCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? CamelCaseKeys<U>[] : T extends object ? {
46
+ [K in keyof T as CamelCase<K & string>]: CamelCaseKeys<T[K]>;
47
+ } : T;
48
+ /** Deeply transform all keys of `T` to snake_case. */
49
+ type SnakeCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? SnakeCaseKeys<U>[] : T extends object ? {
50
+ [K in keyof T as SnakeCase<K & string>]: SnakeCaseKeys<T[K]>;
51
+ } : T;
52
+ /** Deeply transform all keys of `T` to kebab-case. */
53
+ type KebabCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? KebabCaseKeys<U>[] : T extends object ? {
54
+ [K in keyof T as KebabCase<K & string>]: KebabCaseKeys<T[K]>;
55
+ } : T;
56
+ /** Deeply transform all keys of `T` to PascalCase. */
57
+ type PascalCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? PascalCaseKeys<U>[] : T extends object ? {
58
+ [K in keyof T as PascalCase<K & string>]: PascalCaseKeys<T[K]>;
59
+ } : T;
60
+ /** Deeply transform all keys of `T` to CONSTANT_CASE. */
61
+ type ConstantCaseKeys<T> = T extends Leaf ? T : T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? ConstantCaseKeys<U>[] : T extends object ? {
62
+ [K in keyof T as ConstantCase<K & string>]: ConstantCaseKeys<T[K]>;
63
+ } : T;
64
+
65
+ /**
66
+ * Convert a string to camelCase.
67
+ *
68
+ * @example
69
+ * camelCase('user_name') // 'userName'
70
+ * camelCase('XMLParser') // 'xmlParser'
71
+ * camelCase('content-type') // 'contentType'
72
+ */
73
+ declare function camelCase<S extends string>(str: S): CamelCase<S>;
74
+ /**
75
+ * Convert a string to snake_case.
76
+ *
77
+ * @example
78
+ * snakeCase('userName') // 'user_name'
79
+ * snakeCase('XMLParser') // 'xml_parser'
80
+ * snakeCase('content-type') // 'content_type'
81
+ */
82
+ declare function snakeCase<S extends string>(str: S): SnakeCase<S>;
83
+ /**
84
+ * Convert a string to kebab-case.
85
+ *
86
+ * @example
87
+ * kebabCase('userName') // 'user-name'
88
+ * kebabCase('XMLParser') // 'xml-parser'
89
+ * kebabCase('user_name') // 'user-name'
90
+ */
91
+ declare function kebabCase<S extends string>(str: S): KebabCase<S>;
92
+ /**
93
+ * Convert a string to PascalCase.
94
+ *
95
+ * @example
96
+ * pascalCase('user_name') // 'UserName'
97
+ * pascalCase('XMLParser') // 'XmlParser'
98
+ * pascalCase('kebab-case') // 'KebabCase'
99
+ */
100
+ declare function pascalCase<S extends string>(str: S): PascalCase<S>;
101
+ /**
102
+ * Convert a string to CONSTANT_CASE.
103
+ *
104
+ * @example
105
+ * constantCase('userName') // 'USER_NAME'
106
+ * constantCase('XMLParser') // 'XML_PARSER'
107
+ * constantCase('kebab-case') // 'KEBAB_CASE'
108
+ */
109
+ declare function constantCase<S extends string>(str: S): ConstantCase<S>;
110
+ /**
111
+ * Deeply transform all keys of an object (or array of objects) to camelCase.
112
+ * Primitives, Dates, RegExps, Maps, Sets, and other non-plain objects are preserved.
113
+ *
114
+ * @example
115
+ * const result = toCamelCase({ user_name: 'Alice', billing_address: { zip_code: '75001' } })
116
+ * result.userName // 'Alice'
117
+ * result.billingAddress.zipCode // '75001'
118
+ */
119
+ declare function toCamelCase<T>(data: T): CamelCaseKeys<T>;
120
+ /**
121
+ * Deeply transform all keys of an object (or array of objects) to snake_case.
122
+ *
123
+ * @example
124
+ * const result = toSnakeCase({ userName: 'Alice', billingAddress: { zipCode: '75001' } })
125
+ * result.user_name // 'Alice'
126
+ * result.billing_address.zip_code // '75001'
127
+ */
128
+ declare function toSnakeCase<T>(data: T): SnakeCaseKeys<T>;
129
+ /**
130
+ * Deeply transform all keys of an object (or array of objects) to kebab-case.
131
+ *
132
+ * @example
133
+ * const result = toKebabCase({ userName: 'Alice' })
134
+ * result['user-name'] // 'Alice'
135
+ */
136
+ declare function toKebabCase<T>(data: T): KebabCaseKeys<T>;
137
+ /**
138
+ * Deeply transform all keys of an object (or array of objects) to PascalCase.
139
+ *
140
+ * @example
141
+ * const result = toPascalCase({ user_name: 'Alice' })
142
+ * result.UserName // 'Alice'
143
+ */
144
+ declare function toPascalCase<T>(data: T): PascalCaseKeys<T>;
145
+ /**
146
+ * Deeply transform all keys of an object (or array of objects) to CONSTANT_CASE.
147
+ *
148
+ * @example
149
+ * const result = toConstantCase({ userName: 'Alice' })
150
+ * result.USER_NAME // 'Alice'
151
+ */
152
+ declare function toConstantCase<T>(data: T): ConstantCaseKeys<T>;
153
+
154
+ export { type CamelCase, type CamelCaseKeys, type ConstantCase, type ConstantCaseKeys, type KebabCase, type KebabCaseKeys, type PascalCase, type PascalCaseKeys, type SnakeCase, type SnakeCaseKeys, camelCase, constantCase, kebabCase, pascalCase, snakeCase, toCamelCase, toConstantCase, toKebabCase, toPascalCase, toSnakeCase };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ // src/transform.ts
2
+ var LOWER_TO_UPPER = /([a-z\d])([A-Z])/g;
3
+ var ACRONYM_BOUNDARY = /([A-Z]+)([A-Z][a-z])/g;
4
+ var SEPARATORS = /[_\-\s]+/;
5
+ function splitWords(str) {
6
+ if (!str) return [];
7
+ return str.replace(LOWER_TO_UPPER, "$1\0$2").replace(ACRONYM_BOUNDARY, "$1\0$2").split(SEPARATORS).join("\0").split("\0").filter(Boolean).map((w) => w.toLowerCase());
8
+ }
9
+ function joinCamel(words) {
10
+ if (words.length === 0) return "";
11
+ return words[0] + words.slice(1).reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), "");
12
+ }
13
+ function joinSnake(words) {
14
+ return words.join("_");
15
+ }
16
+ function joinKebab(words) {
17
+ return words.join("-");
18
+ }
19
+ function joinPascal(words) {
20
+ return words.reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), "");
21
+ }
22
+ function joinConstant(words) {
23
+ return words.map((w) => w.toUpperCase()).join("_");
24
+ }
25
+ function isPlainObject(value) {
26
+ if (typeof value !== "object" || value === null) return false;
27
+ const proto = Object.getPrototypeOf(value);
28
+ return proto === Object.prototype || proto === null;
29
+ }
30
+ function deepTransformKeys(data, transform) {
31
+ if (Array.isArray(data)) {
32
+ return data.map((item) => deepTransformKeys(item, transform));
33
+ }
34
+ if (!isPlainObject(data)) return data;
35
+ const out = {};
36
+ for (const key of Object.keys(data)) {
37
+ out[transform(key)] = deepTransformKeys(data[key], transform);
38
+ }
39
+ return out;
40
+ }
41
+ function camelCase(str) {
42
+ return joinCamel(splitWords(str));
43
+ }
44
+ function snakeCase(str) {
45
+ return joinSnake(splitWords(str));
46
+ }
47
+ function kebabCase(str) {
48
+ return joinKebab(splitWords(str));
49
+ }
50
+ function pascalCase(str) {
51
+ return joinPascal(splitWords(str));
52
+ }
53
+ function constantCase(str) {
54
+ return joinConstant(splitWords(str));
55
+ }
56
+ function toCamelCase(data) {
57
+ return deepTransformKeys(data, (k) => joinCamel(splitWords(k)));
58
+ }
59
+ function toSnakeCase(data) {
60
+ return deepTransformKeys(data, (k) => joinSnake(splitWords(k)));
61
+ }
62
+ function toKebabCase(data) {
63
+ return deepTransformKeys(data, (k) => joinKebab(splitWords(k)));
64
+ }
65
+ function toPascalCase(data) {
66
+ return deepTransformKeys(data, (k) => joinPascal(splitWords(k)));
67
+ }
68
+ function toConstantCase(data) {
69
+ return deepTransformKeys(data, (k) => joinConstant(splitWords(k)));
70
+ }
71
+
72
+ export { camelCase, constantCase, kebabCase, pascalCase, snakeCase, toCamelCase, toConstantCase, toKebabCase, toPascalCase, toSnakeCase };
73
+ //# sourceMappingURL=index.js.map
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transform.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n\tCamelCase,\n\tCamelCaseKeys,\n\tConstantCase,\n\tConstantCaseKeys,\n\tKebabCase,\n\tKebabCaseKeys,\n\tPascalCase,\n\tPascalCaseKeys,\n\tSnakeCase,\n\tSnakeCaseKeys,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Word splitting — mirrors the type-level SplitWords<S>\n// ---------------------------------------------------------------------------\n\n/** Boundary between a lowercase letter (or digit) and an uppercase letter. */\nconst LOWER_TO_UPPER = /([a-z\\d])([A-Z])/g;\n\n/** Boundary between an acronym and the start of the next word. */\nconst ACRONYM_BOUNDARY = /([A-Z]+)([A-Z][a-z])/g;\n\n/** Any explicit separator (underscore, hyphen, whitespace). */\nconst SEPARATORS = /[_\\-\\s]+/;\n\n/**\n * Split any casing convention into lowercase word fragments.\n *\n * Handles snake_case, kebab-case, CONSTANT_CASE, camelCase, PascalCase,\n * and acronyms like `\"XMLHttpRequest\"` → `[\"xml\", \"http\", \"request\"]`.\n */\nfunction splitWords(str: string): string[] {\n\tif (!str) return [];\n\treturn str\n\t\t.replace(LOWER_TO_UPPER, \"$1\\x00$2\")\n\t\t.replace(ACRONYM_BOUNDARY, \"$1\\x00$2\")\n\t\t.split(SEPARATORS)\n\t\t.join(\"\\x00\")\n\t\t.split(\"\\x00\")\n\t\t.filter(Boolean)\n\t\t.map((w) => w.toLowerCase());\n}\n\n// ---------------------------------------------------------------------------\n// Join helpers — one per target format\n// ---------------------------------------------------------------------------\n\nfunction joinCamel(words: string[]): string {\n\tif (words.length === 0) return \"\";\n\treturn (\n\t\twords[0] + words.slice(1).reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), \"\")\n\t);\n}\n\nfunction joinSnake(words: string[]): string {\n\treturn words.join(\"_\");\n}\n\nfunction joinKebab(words: string[]): string {\n\treturn words.join(\"-\");\n}\n\nfunction joinPascal(words: string[]): string {\n\treturn words.reduce((acc, w) => acc + w.charAt(0).toUpperCase() + w.slice(1), \"\");\n}\n\nfunction joinConstant(words: string[]): string {\n\treturn words.map((w) => w.toUpperCase()).join(\"_\");\n}\n\n// ---------------------------------------------------------------------------\n// Deep key transformation\n// ---------------------------------------------------------------------------\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst proto = Object.getPrototypeOf(value) as unknown;\n\treturn proto === Object.prototype || proto === null;\n}\n\nfunction deepTransformKeys(data: unknown, transform: (key: string) => string): unknown {\n\tif (Array.isArray(data)) {\n\t\treturn data.map((item) => deepTransformKeys(item, transform));\n\t}\n\tif (!isPlainObject(data)) return data;\n\n\tconst out: Record<string, unknown> = {};\n\tfor (const key of Object.keys(data)) {\n\t\tout[transform(key)] = deepTransformKeys(data[key], transform);\n\t}\n\treturn out;\n}\n\n// ---------------------------------------------------------------------------\n// String-level transforms (public)\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a string to camelCase.\n *\n * @example\n * camelCase('user_name') // 'userName'\n * camelCase('XMLParser') // 'xmlParser'\n * camelCase('content-type') // 'contentType'\n */\nexport function camelCase<S extends string>(str: S): CamelCase<S> {\n\treturn joinCamel(splitWords(str)) as CamelCase<S>;\n}\n\n/**\n * Convert a string to snake_case.\n *\n * @example\n * snakeCase('userName') // 'user_name'\n * snakeCase('XMLParser') // 'xml_parser'\n * snakeCase('content-type') // 'content_type'\n */\nexport function snakeCase<S extends string>(str: S): SnakeCase<S> {\n\treturn joinSnake(splitWords(str)) as SnakeCase<S>;\n}\n\n/**\n * Convert a string to kebab-case.\n *\n * @example\n * kebabCase('userName') // 'user-name'\n * kebabCase('XMLParser') // 'xml-parser'\n * kebabCase('user_name') // 'user-name'\n */\nexport function kebabCase<S extends string>(str: S): KebabCase<S> {\n\treturn joinKebab(splitWords(str)) as KebabCase<S>;\n}\n\n/**\n * Convert a string to PascalCase.\n *\n * @example\n * pascalCase('user_name') // 'UserName'\n * pascalCase('XMLParser') // 'XmlParser'\n * pascalCase('kebab-case') // 'KebabCase'\n */\nexport function pascalCase<S extends string>(str: S): PascalCase<S> {\n\treturn joinPascal(splitWords(str)) as PascalCase<S>;\n}\n\n/**\n * Convert a string to CONSTANT_CASE.\n *\n * @example\n * constantCase('userName') // 'USER_NAME'\n * constantCase('XMLParser') // 'XML_PARSER'\n * constantCase('kebab-case') // 'KEBAB_CASE'\n */\nexport function constantCase<S extends string>(str: S): ConstantCase<S> {\n\treturn joinConstant(splitWords(str)) as ConstantCase<S>;\n}\n\n// ---------------------------------------------------------------------------\n// Object-level transforms (public)\n// ---------------------------------------------------------------------------\n\n/**\n * Deeply transform all keys of an object (or array of objects) to camelCase.\n * Primitives, Dates, RegExps, Maps, Sets, and other non-plain objects are preserved.\n *\n * @example\n * const result = toCamelCase({ user_name: 'Alice', billing_address: { zip_code: '75001' } })\n * result.userName // 'Alice'\n * result.billingAddress.zipCode // '75001'\n */\nexport function toCamelCase<T>(data: T): CamelCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinCamel(splitWords(k))) as CamelCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to snake_case.\n *\n * @example\n * const result = toSnakeCase({ userName: 'Alice', billingAddress: { zipCode: '75001' } })\n * result.user_name // 'Alice'\n * result.billing_address.zip_code // '75001'\n */\nexport function toSnakeCase<T>(data: T): SnakeCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinSnake(splitWords(k))) as SnakeCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to kebab-case.\n *\n * @example\n * const result = toKebabCase({ userName: 'Alice' })\n * result['user-name'] // 'Alice'\n */\nexport function toKebabCase<T>(data: T): KebabCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinKebab(splitWords(k))) as KebabCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to PascalCase.\n *\n * @example\n * const result = toPascalCase({ user_name: 'Alice' })\n * result.UserName // 'Alice'\n */\nexport function toPascalCase<T>(data: T): PascalCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinPascal(splitWords(k))) as PascalCaseKeys<T>;\n}\n\n/**\n * Deeply transform all keys of an object (or array of objects) to CONSTANT_CASE.\n *\n * @example\n * const result = toConstantCase({ userName: 'Alice' })\n * result.USER_NAME // 'Alice'\n */\nexport function toConstantCase<T>(data: T): ConstantCaseKeys<T> {\n\treturn deepTransformKeys(data, (k) => joinConstant(splitWords(k))) as ConstantCaseKeys<T>;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "casemorph",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe deep case transformation for objects — snake_case, camelCase, kebab-case, PascalCase, CONSTANT_CASE with full TypeScript inference",
5
+ "author": "Ludovic Blondon",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": ["dist", "README.md", "LICENSE", "CHANGELOG.md"],
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "keywords": [
24
+ "typescript",
25
+ "camelcase",
26
+ "snake-case",
27
+ "kebab-case",
28
+ "pascal-case",
29
+ "case-conversion",
30
+ "deep-transform",
31
+ "type-safe",
32
+ "object-keys",
33
+ "naming-convention",
34
+ "camelcase-keys",
35
+ "snakecase-keys"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "dev": "tsup --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "test:coverage": "vitest run --coverage",
43
+ "test:types": "vitest run --typecheck.only",
44
+ "lint": "biome lint .",
45
+ "format": "biome format --write .",
46
+ "format:check": "biome format .",
47
+ "check": "biome check .",
48
+ "check:fix": "biome check --fix .",
49
+ "typecheck": "tsc --noEmit",
50
+ "bench": "tsx benchmarks/bench.ts",
51
+ "size": "size-limit",
52
+ "prepublishOnly": "npm run check && npm run typecheck && npm run test && npm run build",
53
+ "ci": "npm run check && npm run typecheck && npm run test:coverage && npm run build && npm run size"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/best-package/casemorph.git"
58
+ },
59
+ "size-limit": [
60
+ {
61
+ "path": "dist/index.js",
62
+ "limit": "2 KB"
63
+ }
64
+ ],
65
+ "devDependencies": {
66
+ "@biomejs/biome": "^1",
67
+ "@size-limit/preset-small-lib": "^11",
68
+ "@vitest/coverage-v8": "^4",
69
+ "expect-type": "^1",
70
+ "size-limit": "^11",
71
+ "tinybench": "^3",
72
+ "tsup": "^8",
73
+ "tsx": "^4",
74
+ "typescript": "~5.8",
75
+ "vitest": "^4"
76
+ }
77
+ }