From 8770be5951e2f6899318ef21b053703cabe9306e Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Tue, 14 Nov 2023 09:25:56 +0100 Subject: [PATCH] feat(parser): allow to customize interpolation of value --- src/env.ts | 9 +++++++-- src/parser.ts | 32 ++++++++++++++++++++++++++------ src/processor.ts | 17 +++++++++++++---- test/env.spec.ts | 27 +++++++++++++++++++++++++++ test/parser.spec.ts | 24 ++++++++++++++++++++---- test/processor.spec.ts | 26 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src/env.ts b/src/env.ts index c851dad..32585d0 100644 --- a/src/env.ts +++ b/src/env.ts @@ -42,13 +42,18 @@ export class Env> { */ static async create }>( appRoot: URL, - schema: Schema + schema: Schema, + options?: { onVariableRead?: (key: string, value: string) => string | Promise } ): Promise< Env<{ [K in keyof Schema]: ReturnType }> > { - 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)) } diff --git a/src/parser.ts b/src/parser.ts index ab0537d..3051a52 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -52,12 +52,23 @@ import dotenv, { DotenvParseOutput } from 'dotenv' export class EnvParser { #envContents: string #preferProcessEnv: boolean = true + #onVariableRead?: (key: string, value: string) => string | Promise - constructor(envContents: string, options?: { ignoreProcessEnv: boolean }) { + constructor( + envContents: string, + options?: { + ignoreProcessEnv?: boolean + onVariableRead?: (key: string, value: string) => string | Promise + } + ) { if (options?.ignoreProcessEnv) { this.#preferProcessEnv = false } + if (options?.onVariableRead) { + this.#onVariableRead = options.onVariableRead + } + this.#envContents = envContents } @@ -174,12 +185,21 @@ export class EnvParser { /** * Parse the env string to an object of environment variables. */ - parse(): DotenvParseOutput { + async parse(): Promise { const envCollection = dotenv.parse(this.#envContents.trim()) - return Object.keys(envCollection).reduce((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 } } diff --git a/src/processor.ts b/src/processor.ts index a2e246e..24b3d59 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -20,14 +20,20 @@ export class EnvProcessor { */ #appRoot: URL - constructor(appRoot: URL) { + #onVariableRead?: (key: string, value: string) => string | Promise + + constructor( + appRoot: URL, + options?: { onVariableRead?: (key: string, value: string) => string | Promise } + ) { this.#appRoot = appRoot + this.#onVariableRead = options?.onVariableRead } /** * Parse env variables from raw contents */ - #processContents(envContents: string, store: Record) { + async #processContents(envContents: string, store: Record) { /** * Collected env variables */ @@ -35,7 +41,10 @@ export class EnvProcessor { 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] @@ -70,7 +79,7 @@ export class EnvProcessor { * Collected env variables */ const envValues: Record = {} - envFiles.forEach(({ contents }) => this.#processContents(contents, envValues)) + await Promise.all(envFiles.map(({ contents }) => this.#processContents(contents, envValues))) return envValues } diff --git a/test/env.spec.ts b/test/env.spec.ts index 0ae5e9c..6e4ca68 100644 --- a/test/env.spec.ts +++ b/test/env.spec.ts @@ -96,4 +96,31 @@ test.group('Env', (group) => { assert.equal(env.get('PORT'), 3000) expectTypeOf(env.get('PORT')).toEqualTypeOf() }) + + 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() + }) }) diff --git a/test/parser.spec.ts b/test/parser.spec.ts index d198e7b..5a7e3f3 100644 --- a/test/parser.spec.ts +++ b/test/parser.spec.ts @@ -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() assert.deepEqual(parsed, { 'PORT': '3333', @@ -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() assert.deepEqual(parsed, { 'ENV_USER': 'romain', @@ -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() assert.deepEqual(parsed, { 'ENV_USER': 'virk', @@ -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() 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', + }) + }) }) diff --git a/test/processor.spec.ts b/test/processor.spec.ts index fe7e87a..6e2d83c 100644 --- a/test/processor.spec.ts +++ b/test/processor.spec.ts @@ -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