-
Notifications
You must be signed in to change notification settings - Fork 65
/
Copy pathcore.ts
106 lines (93 loc) · 3.3 KB
/
core.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { EnvError, EnvMissingError } from './errors'
import { CleanOptions, SpecsOutput, Spec, ValidatorSpec } from './types'
import { defaultReporter } from './reporter'
export const testOnlySymbol = Symbol('envalid - test only')
/**
* Validate a single env var, given a spec object
*
* @throws EnvError - If validation is unsuccessful
* @return - The cleaned value
*/
function validateVar<T>({
spec,
name,
rawValue,
}: {
name: string
rawValue: string | T
spec: ValidatorSpec<T>
}) {
if (typeof spec._parse !== 'function') {
throw new EnvError(`Invalid spec for "${name}"`)
}
const value = spec._parse(rawValue as string)
if (spec.choices) {
if (!Array.isArray(spec.choices)) {
throw new TypeError(`"choices" must be an array (in spec for "${name}")`)
} else if (!spec.choices.includes(value)) {
throw new EnvError(`Value "${value}" not in choices [${spec.choices}]`)
}
}
if (value == null) throw new EnvError(`Invalid value for env var "${name}"`)
return value
}
// Format a string error message for when a required env var is missing
function formatSpecDescription<T>(spec: Spec<T>) {
const egText = spec.example ? ` (eg. "${spec.example}")` : ''
const docsText = spec.docs ? `. See ${spec.docs}` : ''
return `${spec.desc}${egText}${docsText}`
}
const readRawEnvValue = <T>(env: unknown, k: keyof T | 'NODE_ENV'): string | T[keyof T] => {
return (env as any)[k]
}
const isTestOnlySymbol = (value: any): value is symbol => value === testOnlySymbol
/**
* Perform the central validation/sanitization logic on the full environment object
*/
export function getSanitizedEnv<S>(
environment: unknown,
specs: S,
options: CleanOptions<SpecsOutput<S>> = {},
): SpecsOutput<S> {
let cleanedEnv = {} as SpecsOutput<S>
const castedSpecs = specs as unknown as Record<keyof S, ValidatorSpec<unknown>>
const errors = {} as Record<keyof S, Error>
const varKeys = Object.keys(castedSpecs) as Array<keyof S>
const rawNodeEnv = readRawEnvValue(environment, 'NODE_ENV')
for (const k of varKeys) {
const spec = castedSpecs[k]
const rawValue = readRawEnvValue(environment, k)
// If no value was given and default/devDefault were provided, return the appropriate default
// value without passing it through validation
if (rawValue === undefined) {
// Use devDefault values only if NODE_ENV was explicitly set, and isn't 'production'
const usingDevDefault =
rawNodeEnv && rawNodeEnv !== 'production' && spec.hasOwnProperty('devDefault')
if (usingDevDefault) {
cleanedEnv[k] = spec.devDefault
if (isTestOnlySymbol(spec.devDefault) && rawNodeEnv != 'test') {
throw new EnvMissingError(formatSpecDescription(spec))
}
continue
}
if ('default' in spec) {
cleanedEnv[k] = spec.default
continue
}
}
try {
if (rawValue === undefined) {
cleanedEnv[k] = undefined
throw new EnvMissingError(formatSpecDescription(spec))
} else {
cleanedEnv[k] = validateVar({ name: k as string, spec, rawValue })
}
} catch (err) {
if (options?.reporter === null) throw err
if (err instanceof Error) errors[k] = err
}
}
const reporter = options?.reporter || defaultReporter
reporter({ errors, env: cleanedEnv })
return cleanedEnv
}