Skip to content

Commit

Permalink
refactor: made adding a new loader easier
Browse files Browse the repository at this point in the history
Co-authored-by: KilianKilmister <[email protected]>
  • Loading branch information
k-yle and KilianKilmister committed Dec 11, 2024
1 parent 4790a82 commit 701a6e5
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 173 deletions.
65 changes: 65 additions & 0 deletions src/loaders/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { Environment } from '../types.js'

/**
* Parse out all env vars from a given env file string and return an object
*/
export function parseEnvString(envFileString: string): Environment {
// First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString())

// Next we stripe out all the empty lines
envFileString = stripEmptyLines(envFileString)

// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return parseEnvVars(envFileString)
}

/**
* Parse out all env vars from an env file string
*/
export function parseEnvVars(envString: string): Environment {
const envParseRegex = /^((.+?)[=](.*))$/gim
const matches: Environment = {}
let match
while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line
const key = match[2].trim()
let value: string | number | boolean = match[3].trim()

// remove any surrounding quotes
value = value
.replace(/(^['"]|['"]$)/g, '')
.replace(/\\n/g, '\n')

// Convert string to JS type if appropriate
if (value !== '' && !isNaN(+value)) {
matches[key] = +value
}
else if (value === 'true') {
matches[key] = true
}
else if (value === 'false') {
matches[key] = false
}
else {
matches[key] = value
}
}
return JSON.parse(JSON.stringify(matches)) as Environment
}

/**
* Strips out comments from env file string
*/
export function stripComments(envString: string): string {
const commentsRegex = /(^\s*#.*$)/gim
return envString.replace(commentsRegex, '')
}

/**
* Strips out newlines from env file string
*/
export function stripEmptyLines(envString: string): string {
const emptyLinesRegex = /(^\n)/gim
return envString.replace(emptyLinesRegex, '')
}
72 changes: 6 additions & 66 deletions src/parse-env-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { extname } from 'node:path'
import { pathToFileURL } from 'node:url'
import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import type { Environment } from './types.ts'
import { parseEnvString } from './loaders/env.js';

/**
* Gets the environment vars from an env file
Expand Down Expand Up @@ -34,74 +35,13 @@ export async function getEnvFileVars(envFilePath: string): Promise<Environment>
if (isPromise(env)) {
env = await env
}
return env;
}
else {
const file = readFileSync(absolutePath, { encoding: 'utf8' })
env = parseEnvString(file)
}
return env
}

/**
* Parse out all env vars from a given env file string and return an object
*/
export function parseEnvString(envFileString: string): Environment {
// First thing we do is stripe out all comments
envFileString = stripComments(envFileString.toString())

// Next we stripe out all the empty lines
envFileString = stripEmptyLines(envFileString)

// Merge the file env vars with the current process env vars (the file vars overwrite process vars)
return parseEnvVars(envFileString)
}

/**
* Parse out all env vars from an env file string
*/
export function parseEnvVars(envString: string): Environment {
const envParseRegex = /^((.+?)[=](.*))$/gim
const matches: Environment = {}
let match
while ((match = envParseRegex.exec(envString)) !== null) {
// Note: match[1] is the full env=var line
const key = match[2].trim()
let value: string | number | boolean = match[3].trim()

// remove any surrounding quotes
value = value
.replace(/(^['"]|['"]$)/g, '')
.replace(/\\n/g, '\n')
const file = readFileSync(absolutePath, { encoding: 'utf8' })

// Convert string to JS type if appropriate
if (value !== '' && !isNaN(+value)) {
matches[key] = +value
}
else if (value === 'true') {
matches[key] = true
}
else if (value === 'false') {
matches[key] = false
}
else {
matches[key] = value
}
switch (ext) {
default:
return parseEnvString(file)
}
return JSON.parse(JSON.stringify(matches)) as Environment
}

/**
* Strips out comments from env file string
*/
export function stripComments(envString: string): string {
const commentsRegex = /(^\s*#.*$)/gim
return envString.replace(commentsRegex, '')
}

/**
* Strips out newlines from env file string
*/
export function stripEmptyLines(envString: string): string {
const emptyLinesRegex = /(^\n)/gim
return envString.replace(emptyLinesRegex, '')
}
136 changes: 136 additions & 0 deletions test/loaders/env.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { assert } from 'chai'
import {
stripEmptyLines,
stripComments,
parseEnvVars,
parseEnvString,
} from '../../src/loaders/env.js'

describe('stripEmptyLines', (): void => {
it('should strip out all empty lines', (): void => {
const envString = stripEmptyLines(
'\nBOB=COOL\n\nNODE_ENV=dev\n\nANSWER=42 AND COUNTING\n\n',
)
assert(envString === 'BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n')
})
})

describe('stripComments', (): void => {
it('should strip out all full line comments', (): void => {
const envString = stripComments(
'#BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n#AnotherComment\n',
)
assert(envString === '\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n\n')
})

it('should not strip out #s from values', (): void => {
const envString = stripComments(
'#\nBOB=COMMENT#ELL\n#\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n#AnotherComment\n',
)
assert(
envString ===
'\nBOB=COMMENT#ELL\n\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n\n',
envString,
)
})
})

describe('parseEnvVars', (): void => {
it("should parse out all env vars in string when not ending with '\\n'", (): void => {
const envVars = parseEnvVars(
'BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\nNUMBER=42\nBOOLEAN=true',
)
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
assert(envVars.NUMBER === 42)
assert(envVars.BOOLEAN === true)
})

it("should parse out all env vars in string with format 'key=value'", (): void => {
const envVars = parseEnvVars(
'BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\n',
)
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
})

it('should ignore invalid lines', (): void => {
const envVars = parseEnvVars(
'BOB=COOL\nTHISIS$ANDINVALIDLINE\nANSWER=42 AND COUNTING\n',
)
assert(Object.keys(envVars).length === 2)
assert(envVars.BOB === 'COOL')
assert(envVars.ANSWER === '42 AND COUNTING')
})

it('should default an empty value to an empty string', (): void => {
const envVars = parseEnvVars('EMPTY=\n')
assert(envVars.EMPTY === '')
})

it('should escape double quoted values', (): void => {
const envVars = parseEnvVars('DOUBLE_QUOTES="double_quotes"\n')
assert(envVars.DOUBLE_QUOTES === 'double_quotes')
})

it('should escape single quoted values', (): void => {
const envVars = parseEnvVars("SINGLE_QUOTES='single_quotes'\n")
assert(envVars.SINGLE_QUOTES === 'single_quotes')
})

it('should preserve embedded double quotes', (): void => {
const envVars = parseEnvVars(
'DOUBLE=""""\nDOUBLE_ONE=\'"double_one"\'\nDOUBLE_TWO=""double_two""\n',
)
assert(envVars.DOUBLE === '""')
assert(envVars.DOUBLE_ONE === '"double_one"')
assert(envVars.DOUBLE_TWO === '"double_two"')
})

it('should preserve newlines when surrounded in quotes', (): void => {
const envVars = parseEnvVars(
'ONE_NEWLINE="ONE\\n"\nTWO_NEWLINES="HELLO\\nWORLD\\n"\nTHREE_NEWLINES="HELLO\\n\\nWOR\\nLD"\n',
)
assert(envVars.ONE_NEWLINE === 'ONE\n')
assert(envVars.TWO_NEWLINES === 'HELLO\nWORLD\n')
assert(envVars.THREE_NEWLINES === 'HELLO\n\nWOR\nLD')
})

it('should preserve embedded single quotes', (): void => {
const envVars = parseEnvVars(
"SINGLE=''''\nSINGLE_ONE=''single_one''\nSINGLE_TWO=\"'single_two'\"\n",
)
assert(envVars.SINGLE === "''")
assert(envVars.SINGLE_ONE === "'single_one'")
assert(envVars.SINGLE_TWO === "'single_two'")
})

it('should parse out all env vars ignoring spaces around = sign', (): void => {
const envVars = parseEnvVars(
'BOB = COOL\nNODE_ENV =dev\nANSWER= 42 AND COUNTING',
)
assert(envVars.BOB === 'COOL')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === '42 AND COUNTING')
})

it('should parse out all env vars ignoring spaces around = sign', (): void => {
const envVars = parseEnvVars(
'BOB = "COOL "\nNODE_ENV = dev\nANSWER= \' 42 AND COUNTING\'',
)
assert(envVars.BOB === 'COOL ')
assert(envVars.NODE_ENV === 'dev')
assert(envVars.ANSWER === ' 42 AND COUNTING')
})
})

describe('parseEnvString', (): void => {
it('should parse env vars and merge (overwrite) with process.env vars', (): void => {
const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n')
assert(env.BOB === 'COOL')
assert(env.NODE_ENV === 'dev')
assert(env.ANSWER === 42)
})
})
Loading

0 comments on commit 701a6e5

Please sign in to comment.