Skip to content

Commit 3014e7f

Browse files
committed
feat(env): add createEnvProxy helper for Windows compatibility
Export createEnvProxy() utility function that provides case-insensitive environment variable access for Windows compatibility. Features: - Opt-in helper for users who need Windows env var compatibility - Handles PATH, TEMP, HOME, etc. with any casing - Smart priority: overrides > exact match > case-insensitive fallback - Full Proxy implementation with proper handlers - Well-documented with usage examples Use cases: - Cross-platform test environments - Windows-compatible child process spawning - Merging env overrides with case-insensitive access Performance note: Has Proxy overhead - only use when needed. Related: Complements spawn fix from v1.3.5
1 parent 5fd9752 commit 3014e7f

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

src/env.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,169 @@ export function envAsString(value: unknown, defaultValue = ''): string {
5353
}
5454
return StringCtor(value).trim()
5555
}
56+
57+
/**
58+
* Create a case-insensitive environment variable Proxy for Windows compatibility.
59+
* On Windows, environment variables are case-insensitive (PATH vs Path vs path).
60+
* This Proxy provides consistent access regardless of case, with priority given
61+
* to exact matches, then case-insensitive matches for known vars.
62+
*
63+
* **Use Cases:**
64+
* - Cross-platform test environments needing consistent env var access
65+
* - Windows compatibility when passing env to child processes
66+
* - Merging environment overrides while preserving case-insensitive lookups
67+
*
68+
* **Performance Note:**
69+
* Proxy operations have runtime overhead. Only use when Windows case-insensitive
70+
* access is required. For most use cases, process.env directly is sufficient.
71+
*
72+
* @param base - Base environment object (usually process.env)
73+
* @param overrides - Optional overrides to merge
74+
* @returns Proxy that handles case-insensitive env var access
75+
*
76+
* @example
77+
* // Create a Proxy with overrides
78+
* const env = createEnvProxy(process.env, { NODE_ENV: 'test' })
79+
* console.log(env.PATH) // Works with any case: PATH, Path, path
80+
* console.log(env.NODE_ENV) // 'test'
81+
*
82+
* @example
83+
* // Pass to child process spawn
84+
* import { createEnvProxy } from '@socketsecurity/lib/env'
85+
* import { spawn } from '@socketsecurity/lib/spawn'
86+
*
87+
* spawn('node', ['script.js'], {
88+
* env: createEnvProxy(process.env, { NODE_ENV: 'test' })
89+
* })
90+
*/
91+
export function createEnvProxy(
92+
base: NodeJS.ProcessEnv,
93+
overrides?: Record<string, string | undefined>,
94+
): NodeJS.ProcessEnv {
95+
// Common environment variables that have case sensitivity issues on Windows.
96+
// These are checked with case-insensitive matching when exact matches fail.
97+
const caseInsensitiveKeys = new Set([
98+
'PATH',
99+
'TEMP',
100+
'TMP',
101+
'HOME',
102+
'USERPROFILE',
103+
'APPDATA',
104+
'LOCALAPPDATA',
105+
'PROGRAMFILES',
106+
'SYSTEMROOT',
107+
'WINDIR',
108+
'COMSPEC',
109+
'PATHEXT',
110+
])
111+
112+
return new Proxy(
113+
{},
114+
{
115+
get(_target, prop) {
116+
if (typeof prop !== 'string') {
117+
return undefined
118+
}
119+
120+
// Priority 1: Check overrides for exact match.
121+
if (overrides && prop in overrides) {
122+
return overrides[prop]
123+
}
124+
125+
// Priority 2: Check base for exact match.
126+
if (prop in base) {
127+
return base[prop]
128+
}
129+
130+
// Priority 3: Case-insensitive lookup for known keys.
131+
const upperProp = prop.toUpperCase()
132+
if (caseInsensitiveKeys.has(upperProp)) {
133+
// Check overrides with case variations.
134+
if (overrides) {
135+
for (const key of Object.keys(overrides)) {
136+
if (key.toUpperCase() === upperProp) {
137+
return overrides[key]
138+
}
139+
}
140+
}
141+
// Check base with case variations.
142+
for (const key of Object.keys(base)) {
143+
if (key.toUpperCase() === upperProp) {
144+
return base[key]
145+
}
146+
}
147+
}
148+
149+
return undefined
150+
},
151+
152+
ownKeys(_target) {
153+
const keys = new Set<string>([
154+
...Object.keys(base),
155+
...(overrides ? Object.keys(overrides) : []),
156+
])
157+
return [...keys]
158+
},
159+
160+
getOwnPropertyDescriptor(_target, prop) {
161+
if (typeof prop !== 'string') {
162+
return undefined
163+
}
164+
165+
// Use the same lookup logic as get().
166+
const value = this.get?.(_target, prop, _target)
167+
return value !== undefined
168+
? {
169+
enumerable: true,
170+
configurable: true,
171+
writable: true,
172+
value,
173+
}
174+
: undefined
175+
},
176+
177+
has(_target, prop) {
178+
if (typeof prop !== 'string') {
179+
return false
180+
}
181+
182+
// Check overrides.
183+
if (overrides && prop in overrides) {
184+
return true
185+
}
186+
187+
// Check base.
188+
if (prop in base) {
189+
return true
190+
}
191+
192+
// Case-insensitive check.
193+
const upperProp = prop.toUpperCase()
194+
if (caseInsensitiveKeys.has(upperProp)) {
195+
if (overrides) {
196+
for (const key of Object.keys(overrides)) {
197+
if (key.toUpperCase() === upperProp) {
198+
return true
199+
}
200+
}
201+
}
202+
for (const key of Object.keys(base)) {
203+
if (key.toUpperCase() === upperProp) {
204+
return true
205+
}
206+
}
207+
}
208+
209+
return false
210+
},
211+
212+
set(_target, prop, value) {
213+
if (typeof prop === 'string' && overrides) {
214+
overrides[prop] = value
215+
return true
216+
}
217+
return false
218+
},
219+
},
220+
) as NodeJS.ProcessEnv
221+
}

0 commit comments

Comments
 (0)