Skip to content

Commit 318a5a7

Browse files
committed
Merge branch 'release/v0.28.12'
2 parents ccc07db + 4d45c93 commit 318a5a7

5 files changed

Lines changed: 137 additions & 10 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zeed",
33
"type": "module",
4-
"version": "0.28.11",
4+
"version": "0.28.12",
55
"packageManager": "pnpm@10.11.0",
66
"description": "🌱 Simple foundation library",
77
"author": {

src/common/schema/parse-env.spec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ describe('env.spec', () => {
3434

3535
it('parse prefix', async () => {
3636
const schema = object({
37-
ServiceName: string().default('generic').props({ desc: 'The name of the service' }),
37+
ServiceName: string().default('generic').props({ desc: 'The name of the service\n Multi Line \n\n ' }),
3838
servicePort: number().default(80).props({ desc: 'The port of the service' }),
3939
ServiceFlag: boolean().default(true),
40+
serviceDummy: string().default('dummy').props({ desc: 'Dummy value', envPrivate: true }),
4041
})
4142

4243
const r = parseSchemaEnv(schema, {
@@ -50,18 +51,20 @@ describe('env.spec', () => {
5051
Object {
5152
"ServiceFlag": true,
5253
"ServiceName": "generic",
54+
"serviceDummy": "dummy",
5355
"servicePort": 9999,
5456
}
5557
`)
5658

57-
expect(stringFromSchemaEnv(schema, 'APP_', true)).toMatchInlineSnapshot(`
59+
expect(stringFromSchemaEnv(schema, 'APP_', false)).toMatchInlineSnapshot(`
5860
"# The name of the service
59-
# APP_SERVICE_NAME=generic
61+
# Multi Line
62+
APP_SERVICE_NAME=generic
6063
6164
# The port of the service
62-
# APP_SERVICE_PORT=80
65+
APP_SERVICE_PORT=80
6366
64-
# APP_SERVICE_FLAG=true
67+
APP_SERVICE_FLAG=true
6568
"
6669
`)
6770
})

src/common/schema/parse-env.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isSchemaObjectFlat } from './utils'
88
declare module './schema' {
99
interface TypeProps {
1010
envDesc?: string
11+
envPrivate?: boolean // will not be documented
1112
}
1213
}
1314

@@ -37,10 +38,12 @@ export function parseSchemaEnv<T>(schema: Type<T>, env: any = process?.env ?? {}
3738
}) as T
3839
}
3940

40-
export function stringFromSchemaEnv<T>(schema: Type<T>, prefix = '', commentOut = false): string {
41+
export function stringFromSchemaEnv<T>(schema: Type<T>, prefix = '', commentOut = false, showPrivate = false): string {
4142
assert(isSchemaObjectFlat(schema), 'schema should be a flat object')
4243
const lines: string[] = []
4344
objectMap(schema._object!, (key, schema) => {
45+
if (schema._props?.envPrivate && !showPrivate)
46+
return
4447
const desc = schema._props?.envDesc ?? schema._props?.desc
4548
if (desc) {
4649
desc.trim().split('\n').forEach((line: string) => {

src/node/env.spec.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getEnvVariableRelaxed, setupEnv } from './env'
1+
import { getEnvVariableRelaxed, parseEnvString, parseEnvStringAlt, setupEnv } from './env'
22

33
describe('eNV', () => {
44
it('should respect both files', () => {
@@ -28,4 +28,82 @@ describe('eNV', () => {
2828
expect(getEnvVariableRelaxed('APROP', env)).toEqual('4')
2929
expect(getEnvVariableRelaxed('unknown', env)).toEqual(undefined)
3030
})
31+
32+
it('should parse a complex Docker .env file', () => {
33+
const envSample = `
34+
# This is a comment
35+
VAR1=unquotedValue
36+
VAR2= "double quoted value"
37+
VAR3 = 'single quoted value'
38+
VAR4=unquotedValue # inline comment
39+
VAR5=unquotedValue#not a comment
40+
VAR6="double quoted # not a comment"
41+
VAR7="double quoted value" # comment
42+
VAR8='$OTHER'
43+
VAR9='\${OTHER}'
44+
VAR10='Let\'s go!'
45+
VAR11="{"hello": \"json\"}"
46+
VAR12="some\tvalue"
47+
VAR13='some\tvalue'
48+
VAR14=some\tvalue
49+
`
50+
51+
expect(envSample).toMatchInlineSnapshot(`
52+
"
53+
# This is a comment
54+
VAR1=unquotedValue
55+
VAR2= "double quoted value"
56+
VAR3 = 'single quoted value'
57+
VAR4=unquotedValue # inline comment
58+
VAR5=unquotedValue#not a comment
59+
VAR6="double quoted # not a comment"
60+
VAR7="double quoted value" # comment
61+
VAR8='$OTHER'
62+
VAR9='\${OTHER}'
63+
VAR10='Let's go!'
64+
VAR11="{"hello": "json"}"
65+
VAR12="some value"
66+
VAR13='some value'
67+
VAR14=some value
68+
"
69+
`)
70+
71+
expect(parseEnvString(envSample)).toMatchInlineSnapshot(`
72+
Object {
73+
"VAR1": "unquotedValue",
74+
"VAR10": "Let's go!",
75+
"VAR11": "{"hello": "json"}",
76+
"VAR12": "some value",
77+
"VAR13": "some value",
78+
"VAR14": "some value",
79+
"VAR2": "double quoted value",
80+
"VAR3": "single quoted value",
81+
"VAR4": "unquotedValue # inline comment",
82+
"VAR5": "unquotedValue#not a comment",
83+
"VAR6": "double quoted # not a comment",
84+
"VAR7": ""double quoted value" # comment",
85+
"VAR8": "$OTHER",
86+
"VAR9": "\${OTHER}",
87+
}
88+
`)
89+
90+
expect(parseEnvStringAlt(envSample)).toMatchInlineSnapshot(`
91+
Object {
92+
"VAR1": "unquotedValue",
93+
"VAR10": "Let's go!",
94+
"VAR11": "{"hello": "json"}",
95+
"VAR12": "some value",
96+
"VAR13": "some value",
97+
"VAR14": "some value",
98+
"VAR2": "double quoted value",
99+
"VAR3": "single quoted value",
100+
"VAR4": "unquotedValue",
101+
"VAR5": "unquotedValue",
102+
"VAR6": "double quoted # not a comment",
103+
"VAR7": "double quoted value",
104+
"VAR8": "$OTHER",
105+
"VAR9": "\${OTHER}",
106+
}
107+
`)
108+
})
31109
})

src/node/env.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,51 @@ interface EnvOptions {
2121
mode?: string
2222
}
2323

24+
// BSD-2-Clause License from here https://github.com/motdotla/dotenv/blob/master/lib/main.js#L12
25+
// eslint-disable-next-line regexp/no-super-linear-backtracking
26+
const LINE = /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/gm
27+
28+
// https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#env-file-syntax
29+
export function parseEnvStringAlt(src: string): Record<string, string> {
30+
const obj: Record<string, string> = {}
31+
32+
// Convert buffer to string
33+
let lines = src.toString()
34+
35+
// Convert line breaks to same format
36+
lines = lines.replace(/\r\n?/g, '\n')
37+
38+
let match
39+
// eslint-disable-next-line no-cond-assign
40+
while ((match = LINE.exec(lines)) != null) {
41+
const key = match[1]
42+
43+
// Default undefined or null to empty string
44+
let value = (match[2] || '')
45+
46+
// Remove whitespace
47+
value = value.trim()
48+
49+
// Check if double quoted
50+
const maybeQuote = value[0]
51+
52+
// Remove surrounding quotes
53+
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2')
54+
55+
// Expand newlines if double quoted
56+
if (maybeQuote === '"') {
57+
value = value.replace(/\\n/g, '\n')
58+
value = value.replace(/\\r/g, '\r')
59+
}
60+
61+
// Add to object
62+
obj[key] = value
63+
}
64+
65+
return obj
66+
}
2467
// Parses src into an Object
25-
function parse(src: string, _options: EnvOptions = {}) {
68+
export function parseEnvString(src: string) {
2669
const obj: Record<string, string> = {}
2770

2871
// convert Buffers before splitting into lines and processing
@@ -109,7 +152,7 @@ export function setupEnv(options: EnvOptions = {}) {
109152

110153
function envOf(name: string) {
111154
return fs.existsSync(name)
112-
? parse(fs.readFileSync(name, { encoding }), { debug })
155+
? parseEnvStringAlt(fs.readFileSync(name, { encoding }))
113156
: {}
114157
}
115158

0 commit comments

Comments
 (0)