Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle multilines in env files #1032

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "zx/index",
"path": "build/*.{js,cjs}",
"limit": "805 kB",
"limit": "805.5 kB",
"brotli": false,
"gzip": false
},
Expand All @@ -30,7 +30,7 @@
{
"name": "all",
"path": "build/*",
"limit": "842 kB",
"limit": "842.5 kB",
"brotli": false,
"gzip": false
}
Expand Down
61 changes: 52 additions & 9 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,58 @@ export const toCamelCase = (str: string) =>
export const parseBool = (v: string): boolean | string =>
({ true: true, false: false })[v] ?? v

export const parseDotenv = (content: string): NodeJS.ProcessEnv =>
content.split(/\r?\n/).reduce<NodeJS.ProcessEnv>((r, line) => {
if (line.startsWith('export ')) line = line.slice(7)
const i = line.indexOf('=')
const k = line.slice(0, i).trim()
const v = line.slice(i + 1).trim()
if (k && v) r[k] = v
return r
}, {})
// prettier-ignore
export const parseDotenv = (content: string): NodeJS.ProcessEnv => {
const e: Record<string, string> = {}
const kr = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/
const sr = /\s/
let k = ''
let b = ''
let q = ''
let i = 0
const cap = () => { if (b && k) {
if (!kr.test(k)) throw new Error(`Invalid identifier: ${k}`)
e[k] = b; b = ''; k = ''
}}
antongolub marked this conversation as resolved.
Show resolved Hide resolved

for (const c of content.replace(/\r\n?/mg, '\n')) {
if (i) {
if (c === '\n') i = 0
continue
}
if (!q) {
if (c === '#') {
i = 1
continue
}
if (c === '\n') {
cap()
continue
}
if (sr.test(c)) {
if (!k && b === 'export') b = ''
continue
}
if (c === '=') {
if (!k) { k = b; b = ''; continue }
}
}

if (c === '"' || c === "'" || c === '`') {
if (q === c) {
q = ''
cap()
continue
}
q = c
continue
}
b += c
}
cap()

return e
}

export const readEnvFromFile = (
filepath: string,
Expand Down
2 changes: 1 addition & 1 deletion test/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('cli', () => {
assert.ok(p.stderr.endsWith(cwd + '\n'))
})

test('supports `--env` options with file', async () => {
test('supports `--env` option', async () => {
const env = tmpfile(
'.env',
`FOO=BAR
Expand Down
71 changes: 40 additions & 31 deletions test/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,44 +140,53 @@ describe('util', () => {
assert.equal(toCamelCase('SOME_MORE_BIG_STR'), 'someMoreBigStr')
assert.equal(toCamelCase('kebab-input-str'), 'kebabInputStr')
})
})

test('parseDotenv()', () => {
assert.deepEqual(
parseDotenv('ENV=v1\nENV2=v2\n\n\n ENV3 = v3 \nexport ENV4=v4'),
{
test.only('parseDotenv()', () => {
const multiline = `SIMPLE=xyz123
# comment ###
NON_INTERPOLATED='raw text without variable interpolation'
MULTILINE = """
long text here, # not-comment
e.g. a private SSH key
"""
ENV=v1\nENV2=v2\n\n\n\t\t ENV3 = v3 \n export ENV4=v4
ENV5=v5 # comment
`

assert.deepEqual(parseDotenv(multiline), {
SIMPLE: 'xyz123',
NON_INTERPOLATED: 'raw text without variable interpolation',
MULTILINE: '\nlong text here, # not-comment\ne.g. a private SSH key\n',
ENV: 'v1',
ENV2: 'v2',
ENV3: 'v3',
ENV4: 'v4',
}
)
assert.deepEqual(parseDotenv(''), {})
ENV5: 'v5',
})

// TBD: multiline
const multiline = `SIMPLE=xyz123
NON_INTERPOLATED='raw text without variable interpolation'
MULTILINE = """
long text here,
e.g. a private SSH key
"""`
})

describe('readEnvFromFile()', () => {
test('handles correct proccess.env', () => {
const file = tempfile('.env', 'ENV=value1\nENV2=value24')
const env = readEnvFromFile(file)
assert.equal(env.ENV, 'value1')
assert.equal(env.ENV2, 'value24')
assert.ok(env.NODE_VERSION !== '')
assert.deepEqual(
parseDotenv(`FOO=BAR
BAR=FOO+`),
{ FOO: 'BAR', BAR: 'FOO+' }
)
})

test('handles correct some env', () => {
const file = tempfile('.env', 'ENV=value1\nENV2=value24')
const env = readEnvFromFile(file, { version: '1.0.0', name: 'zx' })
assert.equal(env.ENV, 'value1')
assert.equal(env.ENV2, 'value24')
assert.equal(env.version, '1.0.0')
assert.equal(env.name, 'zx')
describe('readEnvFromFile()', () => {
test('handles correct proccess.env', () => {
const file = tempfile('.env', 'ENV=value1\nENV2=value24')
const env = readEnvFromFile(file)
assert.equal(env.ENV, 'value1')
assert.equal(env.ENV2, 'value24')
assert.ok(env.NODE_VERSION !== '')
})

test('handles correct some env', () => {
const file = tempfile('.env', 'ENV=value1\nENV2=value24')
const env = readEnvFromFile(file, { version: '1.0.0', name: 'zx' })
assert.equal(env.ENV, 'value1')
assert.equal(env.ENV2, 'value24')
assert.equal(env.version, '1.0.0')
assert.equal(env.name, 'zx')
})
})
})
Loading