Skip to content

Commit

Permalink
feat(parser): allow to customize interpolation of value
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainLanz committed Nov 14, 2023
1 parent f4de0fb commit 8770be5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 16 deletions.
9 changes: 7 additions & 2 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ export class Env<EnvValues extends Record<string, any>> {
*/
static async create<Schema extends { [key: string]: ValidateFn<unknown> }>(
appRoot: URL,
schema: Schema
schema: Schema,
options?: { onVariableRead?: (key: string, value: string) => string | Promise<string> }
): Promise<
Env<{
[K in keyof Schema]: ReturnType<Schema[K]>
}>
> {
const values = await new EnvProcessor(appRoot).process()
const processor = new EnvProcessor(appRoot, {
onVariableRead: options?.onVariableRead,
})

const values = await processor.process()
const validator = this.rules(schema)
return new Env(validator.validate(values))
}
Expand Down
32 changes: 26 additions & 6 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,23 @@ import dotenv, { DotenvParseOutput } from 'dotenv'
export class EnvParser {
#envContents: string
#preferProcessEnv: boolean = true
#onVariableRead?: (key: string, value: string) => string | Promise<string>

constructor(envContents: string, options?: { ignoreProcessEnv: boolean }) {
constructor(
envContents: string,
options?: {
ignoreProcessEnv?: boolean
onVariableRead?: (key: string, value: string) => string | Promise<string>
}
) {
if (options?.ignoreProcessEnv) {
this.#preferProcessEnv = false
}

if (options?.onVariableRead) {
this.#onVariableRead = options.onVariableRead
}

this.#envContents = envContents
}

Expand Down Expand Up @@ -174,12 +185,21 @@ export class EnvParser {
/**
* Parse the env string to an object of environment variables.
*/
parse(): DotenvParseOutput {
async parse(): Promise<DotenvParseOutput> {
const envCollection = dotenv.parse(this.#envContents.trim())

return Object.keys(envCollection).reduce<DotenvParseOutput>((result, key) => {
result[key] = this.#getValue(key, envCollection)
return result
}, {})
let result: DotenvParseOutput = {}

for (const key in envCollection) {
const value = this.#getValue(key, envCollection)

if (this.#onVariableRead) {
result[key] = await this.#onVariableRead(key, value)
} else {
result[key] = value
}
}

return result
}
}
17 changes: 13 additions & 4 deletions src/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,31 @@ export class EnvProcessor {
*/
#appRoot: URL

constructor(appRoot: URL) {
#onVariableRead?: (key: string, value: string) => string | Promise<string>

constructor(
appRoot: URL,
options?: { onVariableRead?: (key: string, value: string) => string | Promise<string> }
) {
this.#appRoot = appRoot
this.#onVariableRead = options?.onVariableRead
}

/**
* Parse env variables from raw contents
*/
#processContents(envContents: string, store: Record<string, any>) {
async #processContents(envContents: string, store: Record<string, any>) {
/**
* Collected env variables
*/
if (!envContents.trim()) {
return store
}

const values = new EnvParser(envContents).parse()
const parser = new EnvParser(envContents, {
onVariableRead: this.#onVariableRead,
})
const values = await parser.parse()
Object.keys(values).forEach((key) => {
let value = process.env[key]

Expand Down Expand Up @@ -70,7 +79,7 @@ export class EnvProcessor {
* Collected env variables
*/
const envValues: Record<string, any> = {}
envFiles.forEach(({ contents }) => this.#processContents(contents, envValues))
await Promise.all(envFiles.map(({ contents }) => this.#processContents(contents, envValues)))
return envValues
}

Expand Down
27 changes: 27 additions & 0 deletions test/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,31 @@ test.group('Env', (group) => {
assert.equal(env.get('PORT'), 3000)
expectTypeOf(env.get('PORT')).toEqualTypeOf<number>()
})

test('validate and process environment variables with custom hook', async ({
assert,
expectTypeOf,
cleanup,
fs,
}) => {
cleanup(() => {
delete process.env.PORT
})

await fs.create('.env', 'PORT=3000')
const env = await Env.create(
fs.baseUrl,
{
PORT: Env.schema.string(),
},
{
onVariableRead(_key, _value) {
return 'abc'
},
}
)

assert.equal(env.get('PORT'), 'abc')
expectTypeOf(env.get('PORT')).toEqualTypeOf<string>()
})
})
24 changes: 20 additions & 4 deletions test/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test.group('Env Parser', () => {
].join('\n')

const parser = new EnvParser(envString)
const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'PORT': '3333',
Expand Down Expand Up @@ -66,7 +66,7 @@ test.group('Env Parser', () => {
const envString = ['ENV_USER=romain', 'REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString, { ignoreProcessEnv: true })

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'ENV_USER': 'romain',
Expand All @@ -87,7 +87,7 @@ test.group('Env Parser', () => {
const envString = ['ENV_USER=romain', 'REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString)

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'ENV_USER': 'virk',
Expand All @@ -108,10 +108,26 @@ test.group('Env Parser', () => {
const envString = ['REDIS-USER=$ENV_USER'].join('\n')
const parser = new EnvParser(envString)

const parsed = parser.parse()
const parsed = await parser.parse()
expectTypeOf(parsed).toEqualTypeOf<DotenvParseOutput>()
assert.deepEqual(parsed, {
'REDIS-USER': 'virk',
})
})

test('use custom hook to interpolate values', async ({ assert }) => {
const envString = ['PORT=3333', 'HOST=127.0.0.1'].join('\n')

const parser = new EnvParser(envString, {
onVariableRead(_key, _value) {
return '1'
},
})
const parsed = await parser.parse()

assert.deepEqual(parsed, {
PORT: '1',
HOST: '1',
})
})
})
26 changes: 26 additions & 0 deletions test/processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ test.group('Env processor', () => {
assert.deepEqual(values, { HOST: 'localhost', PORT: '3000' })
})

test('process .env file with custom hook', async ({ assert, cleanup, fs }) => {
cleanup(() => {
delete process.env.PORT
delete process.env.HOST
})

await fs.create(
'.env',
`
HOST=localhost
PORT=3000
`
)

const app = new EnvProcessor(fs.baseUrl, {
onVariableRead(_key, _value) {
return '1'
},
})

const values = await app.process()
assert.equal(process.env.HOST, '1')
assert.equal(process.env.PORT, '1')
assert.deepEqual(values, { HOST: '1', PORT: '1' })
})

test('process .env.local and .env files', async ({ assert, cleanup, fs }) => {
cleanup(() => {
delete process.env.PORT
Expand Down

0 comments on commit 8770be5

Please sign in to comment.