Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const config = envSchema({
schema: S.object().prop('PORT', S.number().default(3000).required()),
data: data, // optional, default: process.env
dotenv: true, // load .env if it is there, default: false
expandEnv: true, // expand environment variables like $VAR or ${VAR}, default: false
expandEnv: true, // expand environment variables like $VAR, ${VAR}, ${VAR:-default} or ${VAR-default} (\$ for a literal $), default: false
})

console.log(config)
Expand Down
49 changes: 43 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,51 @@ const separator = {
}
}

// Same syntax as dotenv-expand, which was used before: $VAR, ${VAR},
// ${VAR:-default} (VAR unset or empty), ${VAR-default} (VAR unset) and \$
const EXPANSION_RE = /\\\$|\$\{([A-Z_][A-Z0-9_]*)(:?-)([^}]*)\}|\$\{?([A-Z_][A-Z0-9_]*)\}?/gi

function expandVariables (obj) {
// Expand environment variables in the format $VAR or ${VAR}
const raw = Object.assign({}, obj)
const expanded = {}

// References are resolved against the expanded value of the referenced
// variable, whatever the order of the keys
function expand (key, stack) {
if (Object.prototype.hasOwnProperty.call(expanded, key)) {
return expanded[key]
}
if (stack.includes(key)) {
// circular reference, keep it as written
return undefined
}
const value = raw[key]
if (typeof value !== 'string') {
return value
}
stack.push(key)
const result = interpolate(value, stack)
stack.pop()
expanded[key] = result
return result
}

function interpolate (value, stack) {
return value.replace(EXPANSION_RE, (match, name, operator, fallback, plainName) => {
if (match === '\\$') {
return '$'
}
const resolved = raw[name || plainName] === undefined ? undefined : expand(name || plainName, stack)
if (operator && (resolved === undefined || (operator === ':-' && resolved === ''))) {
return interpolate(fallback, stack)
}
return resolved === undefined ? match : String(resolved)
})
}

for (const key in obj) {
const value = obj[key]
if (typeof value === 'string') {
obj[key] = value.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/gi, (match, varName) => {
return obj[varName] !== undefined ? obj[varName] : match
})
if (typeof obj[key] === 'string') {
obj[key] = expand(key, [])
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions test/expand.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

/* eslint-disable no-template-curly-in-string */

const { test } = require('node:test')
const makeTest = require('./make-test')
const { join } = require('node:path')
Expand Down Expand Up @@ -89,6 +91,65 @@ const tests = [
confExpected: {
MESSAGE: 'Hello $UNDEFINED_VAR world'
}
},
{
name: 'simple object - ok - expandEnv resolves chained references whatever the key order',
schema: {
type: 'object',
properties: {
DATABASE_URL: { type: 'string' },
DB_HOST: { type: 'string' }
}
},
expandEnv: true,
isOk: true,
data: {
DATABASE_URL: 'postgres://$DB_HOST/app',
DB_HOST: '${DB_NAME}.internal',
DB_NAME: 'db'
},
confExpected: {
DATABASE_URL: 'postgres://db.internal/app',
DB_HOST: 'db.internal'
}
},
{
name: 'simple object - ok - expandEnv supports default values and escaped dollar signs',
schema: {
type: 'object',
properties: {
UNSET: { type: 'string' },
EMPTY: { type: 'string' },
EMPTY_DASH: { type: 'string' },
SET: { type: 'string' },
PRICE: { type: 'string' },
URL: { type: 'string' },
LOOP: { type: 'string' }
}
},
expandEnv: true,
isOk: true,
data: {
BLANK: '',
HOST: 'h',
UNSET: '${MISSING:-http://$HOST}',
EMPTY: '${BLANK:-fallback}',
EMPTY_DASH: '${BLANK-fallback}',
SET: '${HOST:-fallback}',
PRICE: '\\$5',
PORT: 3000,
URL: 'http://h:$PORT',
LOOP: 'a$LOOP'
},
confExpected: {
UNSET: 'http://h',
EMPTY: 'fallback',
EMPTY_DASH: '',
SET: 'h',
PRICE: '$5',
URL: 'http://h:3000',
LOOP: 'a$LOOP'
}
}
]

Expand Down
Loading