From 30e588c874be6037ec9b6609d41e73d0ff9385ad Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 22 Jun 2026 11:29:09 -0500 Subject: [PATCH] add toConstantCase() placeholder transform method --- docs/reference/path-patterns.md | 1 + e2e/fixtures/case-maps-broken/konsistent.json | 3 +- e2e/fixtures/case-maps/konsistent.json | 3 +- .../case-maps/providers/graphql/index.ts | 2 + .../case-maps/providers/openai/index.ts | 2 + e2e/placeholder.test.ts | 3 ++ .../konsistent/src/core/case-utils.test.ts | 15 ++++++ packages/konsistent/src/core/case-utils.ts | 6 +++ .../konsistent/src/core/placeholder.test.ts | 53 +++++++++++++++++++ packages/konsistent/src/core/placeholder.ts | 13 +++++ packages/konsistent/src/core/template.test.ts | 8 +++ packages/konsistent/src/core/template.ts | 2 + 12 files changed, 109 insertions(+), 2 deletions(-) diff --git a/docs/reference/path-patterns.md b/docs/reference/path-patterns.md index 5781789..f6e770d 100644 --- a/docs/reference/path-patterns.md +++ b/docs/reference/path-patterns.md @@ -84,6 +84,7 @@ Inside `${...}` template substitutions, methods transform the captured value: | `${name.toCamelCase()}` | `my-thing` → `myThing` | | `${name.toKebabCase()}` | `MyThing` → `my-thing` | | `${name.toSnakeCase()}` | `MyThing` → `my_thing` | +| `${name.toConstantCase()}` | `my-thing` → `MY_THING` (uppercase snake case) | | `${name.toFlatCase()}` | `my-thing` → `mything` | | `${name.toNthSegment(0)}` | `my-thing` → `my` (split by `-`, return nth segment) | | `${name.toNthSegmentPascalCase(1)}` | `my-thing` → `Thing` | diff --git a/e2e/fixtures/case-maps-broken/konsistent.json b/e2e/fixtures/case-maps-broken/konsistent.json index 55611f2..f043bfe 100644 --- a/e2e/fixtures/case-maps-broken/konsistent.json +++ b/e2e/fixtures/case-maps-broken/konsistent.json @@ -20,7 +20,8 @@ "exportFunctions": [ { "name": "create${providerId.toPascalCase()}Provider" } ], - "exportTypes": ["${providerId.toPascalCase()}ProviderConfig"] + "exportTypes": ["${providerId.toPascalCase()}ProviderConfig"], + "exportConstants": ["${providerId.toConstantCase()}_PROVIDER_ID"] } } ] diff --git a/e2e/fixtures/case-maps/konsistent.json b/e2e/fixtures/case-maps/konsistent.json index 55611f2..f043bfe 100644 --- a/e2e/fixtures/case-maps/konsistent.json +++ b/e2e/fixtures/case-maps/konsistent.json @@ -20,7 +20,8 @@ "exportFunctions": [ { "name": "create${providerId.toPascalCase()}Provider" } ], - "exportTypes": ["${providerId.toPascalCase()}ProviderConfig"] + "exportTypes": ["${providerId.toPascalCase()}ProviderConfig"], + "exportConstants": ["${providerId.toConstantCase()}_PROVIDER_ID"] } } ] diff --git a/e2e/fixtures/case-maps/providers/graphql/index.ts b/e2e/fixtures/case-maps/providers/graphql/index.ts index 8d30625..0c95adb 100644 --- a/e2e/fixtures/case-maps/providers/graphql/index.ts +++ b/e2e/fixtures/case-maps/providers/graphql/index.ts @@ -2,6 +2,8 @@ export type GraphQLProviderConfig = { endpoint: string; }; +export const GRAPHQL_PROVIDER_ID = "graphql"; + export function createGraphQLProvider(config: GraphQLProviderConfig): unknown { return config; } diff --git a/e2e/fixtures/case-maps/providers/openai/index.ts b/e2e/fixtures/case-maps/providers/openai/index.ts index 2599de2..59ca31a 100644 --- a/e2e/fixtures/case-maps/providers/openai/index.ts +++ b/e2e/fixtures/case-maps/providers/openai/index.ts @@ -2,6 +2,8 @@ export type OpenAIProviderConfig = { apiKey: string; }; +export const OPENAI_PROVIDER_ID = "openai"; + export function createOpenAIProvider(config: OpenAIProviderConfig): unknown { return config; } diff --git a/e2e/placeholder.test.ts b/e2e/placeholder.test.ts index 4e3dbfe..4571182 100644 --- a/e2e/placeholder.test.ts +++ b/e2e/placeholder.test.ts @@ -1049,6 +1049,9 @@ describe("case-maps-broken fixture", () => { expect(error.stdout).toContain( 'Missing export type "OpenAIProviderConfig"' ); + expect(error.stdout).toContain( + 'Missing export constant "OPENAI_PROVIDER_ID"' + ); } }); }); diff --git a/packages/konsistent/src/core/case-utils.test.ts b/packages/konsistent/src/core/case-utils.test.ts index 650fd2a..0363757 100644 --- a/packages/konsistent/src/core/case-utils.test.ts +++ b/packages/konsistent/src/core/case-utils.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { splitWords, toCamelCase, + toConstantCase, toKebabCase, toPascalCase, toSnakeCase, @@ -76,3 +77,17 @@ describe("toSnakeCase", () => { expect(toSnakeCase("testUtils")).toBe("test_utils"); }); }); + +describe("toConstantCase", () => { + it("converts hyphenated", () => { + expect(toConstantCase("test-utils")).toBe("TEST_UTILS"); + }); + + it("converts snake_case", () => { + expect(toConstantCase("test_utils")).toBe("TEST_UTILS"); + }); + + it("converts camelCase", () => { + expect(toConstantCase("testUtils")).toBe("TEST_UTILS"); + }); +}); diff --git a/packages/konsistent/src/core/case-utils.ts b/packages/konsistent/src/core/case-utils.ts index cf0702a..e6b85b8 100644 --- a/packages/konsistent/src/core/case-utils.ts +++ b/packages/konsistent/src/core/case-utils.ts @@ -38,3 +38,9 @@ export function toSnakeCase(value: string): string { .map((w) => w.toLowerCase()) .join("_"); } + +export function toConstantCase(value: string): string { + return splitWords(value) + .map((w) => w.toUpperCase()) + .join("_"); +} diff --git a/packages/konsistent/src/core/placeholder.test.ts b/packages/konsistent/src/core/placeholder.test.ts index 51fcb44..23ca0bd 100644 --- a/packages/konsistent/src/core/placeholder.test.ts +++ b/packages/konsistent/src/core/placeholder.test.ts @@ -466,6 +466,59 @@ describe("PlaceholderValue", () => { }); }); + describe("toConstantCase", () => { + it("uppercases single word", () => { + expect(new PlaceholderValue({ value: "Openai" }).toConstantCase()).toBe( + "OPENAI" + ); + }); + + it("converts hyphens", () => { + expect( + new PlaceholderValue({ value: "test-utils" }).toConstantCase() + ).toBe("TEST_UTILS"); + }); + + it("converts snake_case", () => { + expect( + new PlaceholderValue({ value: "test_utils" }).toConstantCase() + ).toBe("TEST_UTILS"); + }); + + it("converts camelCase", () => { + expect( + new PlaceholderValue({ value: "testUtils" }).toConstantCase() + ).toBe("TEST_UTILS"); + }); + + it("uses pascalToKebabMap when entry exists", () => { + expect( + new PlaceholderValue({ + value: "GraphQL", + pascalToKebabMap: { GraphQL: "graphql" }, + }).toConstantCase() + ).toBe("GRAPHQL"); + }); + + it("uses camelToKebabMap when entry exists", () => { + expect( + new PlaceholderValue({ + value: "graphQL", + camelToKebabMap: { graphQL: "graphql" }, + }).toConstantCase() + ).toBe("GRAPHQL"); + }); + + it("converts hyphens in mapped value to underscores", () => { + expect( + new PlaceholderValue({ + value: "GraphQL", + pascalToKebabMap: { GraphQL: "graph-ql" }, + }).toConstantCase() + ).toBe("GRAPH_QL"); + }); + }); + describe("extract", () => { it("returns first capture group when regex has subgroups", () => { expect( diff --git a/packages/konsistent/src/core/placeholder.ts b/packages/konsistent/src/core/placeholder.ts index 645c344..1e275ce 100644 --- a/packages/konsistent/src/core/placeholder.ts +++ b/packages/konsistent/src/core/placeholder.ts @@ -1,5 +1,6 @@ import { toCamelCase, + toConstantCase, toKebabCase, toPascalCase, toSnakeCase, @@ -76,6 +77,18 @@ export class PlaceholderValue { return toSnakeCase(this.raw); } + /** + * Transform the value to CONSTANT_CASE, also known as uppercase snake case. + */ + toConstantCase(): string { + const mapped = + this.pascalToKebabMap?.[this.raw] ?? this.camelToKebabMap?.[this.raw]; + if (mapped) { + return mapped.replace(/-/g, "_").toUpperCase(); + } + return toConstantCase(this.raw); + } + toFlatCase(): string { return this.raw.replace(/[^a-zA-Z0-9]/g, "").toLowerCase(); } diff --git a/packages/konsistent/src/core/template.test.ts b/packages/konsistent/src/core/template.test.ts index 5ffc365..a2402d4 100644 --- a/packages/konsistent/src/core/template.test.ts +++ b/packages/konsistent/src/core/template.test.ts @@ -60,6 +60,14 @@ describe("resolveTemplate", () => { expect(result).toBe("test_utils_config.ts"); }); + it("resolves ${name.toConstantCase()}", () => { + const result = resolveTemplate({ + template: "${name.toConstantCase()}_CONFIG.ts", + placeholders: makePlaceholders({ map: { name: "test-utils" } }), + }); + expect(result).toBe("TEST_UTILS_CONFIG.ts"); + }); + it("resolves ${name.toFlatCase()}", () => { const result = resolveTemplate({ template: "${name.toFlatCase()}-config.ts", diff --git a/packages/konsistent/src/core/template.ts b/packages/konsistent/src/core/template.ts index 24d2068..040b50e 100644 --- a/packages/konsistent/src/core/template.ts +++ b/packages/konsistent/src/core/template.ts @@ -29,6 +29,8 @@ export function resolveTemplate(opts: { return placeholder.toKebabCase(); case "toSnakeCase": return placeholder.toSnakeCase(); + case "toConstantCase": + return placeholder.toConstantCase(); case "toFlatCase": return placeholder.toFlatCase(); case "toNthSegment":