Skip to content

Commit a8c459c

Browse files
committed
feat(env): export findCaseInsensitiveEnvKey helper
Expose findCaseInsensitiveEnvKey() as a public utility for finding environment variable keys with case-insensitive matching. Changes: - Export function (was private helper) - Rename: findCaseInsensitiveKey → findCaseInsensitiveEnvKey - Rename params for clarity: - obj → env (more descriptive for env objects) - upperProp → upperEnvVarName (clearer intent) - Add comprehensive JSDoc with examples - Update all internal call sites Use cases: - Finding PATH when env has "Path" or "path" - Cross-platform env var lookups - Custom case-insensitive env access patterns Performance: - Fast path: checks length before expensive toUpperCase() - Early exit on first match - Optimized for common case of exact matches
1 parent e3c9bc1 commit a8c459c

1 file changed

Lines changed: 109 additions & 81 deletions

File tree

src/env.ts

Lines changed: 109 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,67 +12,22 @@ const NumberIsFinite = Number.isFinite
1212
const NumberParseInt = Number.parseInt
1313
const StringCtor = String
1414

15-
/**
16-
* Convert an environment variable value to a boolean.
17-
*/
18-
/*@__NO_SIDE_EFFECTS__*/
19-
export function envAsBoolean(value: unknown, defaultValue = false): boolean {
20-
if (typeof value === 'string') {
21-
const trimmed = value.trim()
22-
return trimmed === '1' || trimmed.toLowerCase() === 'true'
23-
}
24-
if (value === null || value === undefined) {
25-
return !!defaultValue
26-
}
27-
return !!value
28-
}
29-
30-
/**
31-
* Convert an environment variable value to a number.
32-
*/
33-
/*@__NO_SIDE_EFFECTS__*/
34-
export function envAsNumber(value: unknown, defaultValue = 0): number {
35-
const numOrNaN = NumberParseInt(String(value), 10)
36-
const numMayBeNegZero = NumberIsFinite(numOrNaN)
37-
? numOrNaN
38-
: NumberCtor(defaultValue)
39-
// Ensure -0 is treated as 0.
40-
return numMayBeNegZero || 0
41-
}
42-
43-
/**
44-
* Convert an environment variable value to a trimmed string.
45-
*/
46-
/*@__NO_SIDE_EFFECTS__*/
47-
export function envAsString(value: unknown, defaultValue = ''): string {
48-
if (typeof value === 'string') {
49-
return value.trim()
50-
}
51-
if (value === null || value === undefined) {
52-
return defaultValue === '' ? defaultValue : StringCtor(defaultValue).trim()
53-
}
54-
return StringCtor(value).trim()
55-
}
56-
57-
/**
58-
* Helper to find a case-insensitive key match in an object.
59-
* Uses fast path checks to minimize expensive toUpperCase() calls.
60-
*/
61-
function findCaseInsensitiveKey(
62-
obj: Record<string, string | undefined>,
63-
upperProp: string,
64-
): string | undefined {
65-
const targetLength = upperProp.length
66-
for (const key of Object.keys(obj)) {
67-
// Fast path: bail early if lengths don't match.
68-
if (key.length !== targetLength) continue
69-
// Only call toUpperCase if length matches.
70-
if (key.toUpperCase() === upperProp) {
71-
return key
72-
}
73-
}
74-
return undefined
75-
}
15+
// Common environment variables that have case sensitivity issues on Windows.
16+
// These are checked with case-insensitive matching when exact matches fail.
17+
const caseInsensitiveKeys = new Set([
18+
'APPDATA',
19+
'COMSPEC',
20+
'HOME',
21+
'LOCALAPPDATA',
22+
'PATH',
23+
'PATHEXT',
24+
'PROGRAMFILES',
25+
'SYSTEMROOT',
26+
'TEMP',
27+
'TMP',
28+
'USERPROFILE',
29+
'WINDIR',
30+
])
7631

7732
/**
7833
* Create a case-insensitive environment variable Proxy for Windows compatibility.
@@ -112,22 +67,6 @@ export function createEnvProxy(
11267
base: NodeJS.ProcessEnv,
11368
overrides?: Record<string, string | undefined>,
11469
): NodeJS.ProcessEnv {
115-
// Common environment variables that have case sensitivity issues on Windows.
116-
// These are checked with case-insensitive matching when exact matches fail.
117-
const caseInsensitiveKeys = new Set([
118-
'PATH',
119-
'TEMP',
120-
'TMP',
121-
'HOME',
122-
'USERPROFILE',
123-
'APPDATA',
124-
'LOCALAPPDATA',
125-
'PROGRAMFILES',
126-
'SYSTEMROOT',
127-
'WINDIR',
128-
'COMSPEC',
129-
'PATHEXT',
130-
])
13170

13271
return new Proxy(
13372
{},
@@ -152,13 +91,13 @@ export function createEnvProxy(
15291
if (caseInsensitiveKeys.has(upperProp)) {
15392
// Check overrides with case variations.
15493
if (overrides) {
155-
const key = findCaseInsensitiveKey(overrides, upperProp)
94+
const key = findCaseInsensitiveEnvKey(overrides, upperProp)
15695
if (key !== undefined) {
15796
return overrides[key]
15897
}
15998
}
16099
// Check base with case variations.
161-
const key = findCaseInsensitiveKey(base, upperProp)
100+
const key = findCaseInsensitiveEnvKey(base, upperProp)
162101
if (key !== undefined) {
163102
return base[key]
164103
}
@@ -210,10 +149,10 @@ export function createEnvProxy(
210149
// Case-insensitive check.
211150
const upperProp = prop.toUpperCase()
212151
if (caseInsensitiveKeys.has(upperProp)) {
213-
if (overrides && findCaseInsensitiveKey(overrides, upperProp) !== undefined) {
152+
if (overrides && findCaseInsensitiveEnvKey(overrides, upperProp) !== undefined) {
214153
return true
215154
}
216-
if (findCaseInsensitiveKey(base, upperProp) !== undefined) {
155+
if (findCaseInsensitiveEnvKey(base, upperProp) !== undefined) {
217156
return true
218157
}
219158
}
@@ -231,3 +170,92 @@ export function createEnvProxy(
231170
},
232171
) as NodeJS.ProcessEnv
233172
}
173+
174+
/**
175+
* Convert an environment variable value to a boolean.
176+
*/
177+
/*@__NO_SIDE_EFFECTS__*/
178+
export function envAsBoolean(value: unknown, defaultValue = false): boolean {
179+
if (typeof value === 'string') {
180+
const trimmed = value.trim()
181+
return trimmed === '1' || trimmed.toLowerCase() === 'true'
182+
}
183+
if (value === null || value === undefined) {
184+
return !!defaultValue
185+
}
186+
return !!value
187+
}
188+
189+
/**
190+
* Convert an environment variable value to a number.
191+
*/
192+
/*@__NO_SIDE_EFFECTS__*/
193+
export function envAsNumber(value: unknown, defaultValue = 0): number {
194+
const numOrNaN = NumberParseInt(String(value), 10)
195+
const numMayBeNegZero = NumberIsFinite(numOrNaN)
196+
? numOrNaN
197+
: NumberCtor(defaultValue)
198+
// Ensure -0 is treated as 0.
199+
return numMayBeNegZero || 0
200+
}
201+
202+
/**
203+
* Convert an environment variable value to a trimmed string.
204+
*/
205+
/*@__NO_SIDE_EFFECTS__*/
206+
export function envAsString(value: unknown, defaultValue = ''): string {
207+
if (typeof value === 'string') {
208+
return value.trim()
209+
}
210+
if (value === null || value === undefined) {
211+
return defaultValue === '' ? defaultValue : StringCtor(defaultValue).trim()
212+
}
213+
return StringCtor(value).trim()
214+
}
215+
216+
/**
217+
* Find a case-insensitive environment variable key match.
218+
* Searches for an environment variable key that matches the given uppercase name,
219+
* using optimized fast-path checks to minimize expensive toUpperCase() calls.
220+
*
221+
* **Use Cases:**
222+
* - Finding PATH when env object has "Path" or "path"
223+
* - Cross-platform env var access where case may vary
224+
* - Custom case-insensitive env lookups
225+
*
226+
* **Performance:**
227+
* - Fast path: Checks length first (O(1)) before toUpperCase (expensive)
228+
* - Only converts to uppercase when length matches
229+
* - Early exit on first match
230+
*
231+
* @param env - Environment object or env-like record to search
232+
* @param upperEnvVarName - Uppercase environment variable name to find (e.g., 'PATH')
233+
* @returns The actual key from env that matches (e.g., 'Path'), or undefined
234+
*
235+
* @example
236+
* // Find PATH regardless of case
237+
* const envObj = { Path: 'C:\\Windows', NODE_ENV: 'test' }
238+
* const key = findCaseInsensitiveEnvKey(envObj, 'PATH')
239+
* console.log(key) // 'Path'
240+
* console.log(envObj[key]) // 'C:\\Windows'
241+
*
242+
* @example
243+
* // Not found returns undefined
244+
* const key = findCaseInsensitiveEnvKey({}, 'MISSING')
245+
* console.log(key) // undefined
246+
*/
247+
export function findCaseInsensitiveEnvKey(
248+
env: Record<string, string | undefined>,
249+
upperEnvVarName: string,
250+
): string | undefined {
251+
const targetLength = upperEnvVarName.length
252+
for (const key of Object.keys(env)) {
253+
// Fast path: bail early if lengths don't match.
254+
if (key.length !== targetLength) continue
255+
// Only call toUpperCase if length matches.
256+
if (key.toUpperCase() === upperEnvVarName) {
257+
return key
258+
}
259+
}
260+
return undefined
261+
}

0 commit comments

Comments
 (0)