diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f9dc7..7e48d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -## [Unreleased](https://github.com/motdotla/dotenv-expand/compare/v11.0.7...master) +## [Unreleased](https://github.com/motdotla/dotenv-expand/compare/v12.0.0...master) + +## [12.0.0](https://github.com/motdotla/dotenv-expand/compare/v11.0.7...v12.0.0) (2024-11-16) + +### Added + +* 🎉 support alternate value expansion ([#131](https://github.com/motdotla/dotenv-expand/pull/131)) + +### Changed + +* 🎉 Expansion logic rewritten to match [dotenvx's](https://github.com/dotenvx/dotenvx). (*note: I recommend dotenvx over dotenv-expand when you are ready. I'm putting all my effort there for a unified standard .env implementation that works everywhere and matches bash, docker-compose, and more. In some cases it slightly improves on them. This leads to more reliability for your secrets and config.) ([#131](https://github.com/motdotla/dotenv-expand/pull/131)) +* ⚠️ BREAKING: do NOT expand in reverse order. Instead, order your .env file keys from first to last as they depend on each other for expansion - principle of least surprise. ([#131](https://github.com/motdotla/dotenv-expand/pull/131)) ## [11.0.7](https://github.com/motdotla/dotenv-expand/compare/v11.0.6...v11.0.7) (2024-11-13) diff --git a/lib/main.js b/lib/main.js index e57074e..a893f79 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,79 +1,91 @@ 'use strict' -// * / -// * (\\)? # is it escaped with a backslash? -// * (\$) # literal $ -// * (?!\() # shouldnt be followed by parenthesis -// * (\{?) # first brace wrap opening -// * ([\w.]+) # key -// * (?::-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))? # optional default nested 3 times -// * (\}?) # last brace warp closing -// * /xi - -const DOTENV_SUBSTITUTION_REGEX = /(\\)?(\$)(?!\()(\{?)([\w.]+)(?::?-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))?(\}?)/gi - function _resolveEscapeSequences (value) { return value.replace(/\\\$/g, '$') } -function interpolate (value, processEnv, parsed) { - return value.replace(DOTENV_SUBSTITUTION_REGEX, (match, escaped, dollarSign, openBrace, key, defaultValue, closeBrace) => { - if (escaped === '\\') { - return match.slice(1) - } else { - if (processEnv[key]) { - if (processEnv[key] === parsed[key]) { - return processEnv[key] - } else { - // scenario: PASSWORD_EXPAND_NESTED=${PASSWORD_EXPAND} - return interpolate(processEnv[key], processEnv, parsed) - } - } +function expandValue (value, processEnv, runningParsed) { + const env = { ...runningParsed, ...processEnv } // process.env wins - if (parsed[key]) { - // avoid recursion from EXPAND_SELF=$EXPAND_SELF - if (parsed[key] !== value) { - return interpolate(parsed[key], processEnv, parsed) - } - } + const regex = /(? { ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS, '/default/path:with/colon') ct.equal(parsed.UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS2, '/default/path:with/colon') - ct.equal(parsed.NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS, '/default/path:with/colon') - ct.equal(parsed.NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS2, '/default/path:with/colon') + ct.equal(parsed.NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS, ':-/default/path:with/colon') + ct.equal(parsed.NO_CURLY_BRACES_UNDEFINED_EXPAND_DEFAULT_SPECIAL_CHARACTERS2, '-/default/path:with/colon') ct.end() }) @@ -454,7 +454,7 @@ t.test('handles two dollar signs', ct => { const dotenv = require('dotenv').config({ path: 'tests/.env.test', processEnv: {} }) const parsed = dotenvExpand.expand(dotenv).parsed - ct.equal(parsed.TWO_DOLLAR_SIGNS, 'abcd$') + ct.equal(parsed.TWO_DOLLAR_SIGNS, 'abcd$$1234') ct.end() }) @@ -535,7 +535,7 @@ t.test('expands recursively', ct => { ct.end() }) -t.test('expands recursively reverse order', ct => { +t.test('CANNOT expand recursively reverse order (ORDER YOUR .env file for least surprise)', ct => { const dotenv = { parsed: { BACKEND_API_HEALTH_CHECK_URL: '${MOCK_SERVER_HOST}/ci-health-check', @@ -546,8 +546,8 @@ t.test('expands recursively reverse order', ct => { const parsed = dotenvExpand.expand(dotenv).parsed ct.equal(parsed.MOCK_SERVER_PORT, '8090') - ct.equal(parsed.MOCK_SERVER_HOST, 'http://localhost:8090') - ct.equal(parsed.BACKEND_API_HEALTH_CHECK_URL, 'http://localhost:8090/ci-health-check') + ct.equal(parsed.MOCK_SERVER_HOST, 'http://localhost:') + ct.equal(parsed.BACKEND_API_HEALTH_CHECK_URL, '/ci-health-check') ct.end() }) @@ -571,11 +571,30 @@ t.test('expands recursively but is smart enough to not attempt expansion of a pr const dotenv = require('dotenv').config({ path: 'tests/.env.test' }) dotenvExpand.expand(dotenv) + ct.equal(process.env.PASSWORD, 'pas$word') ct.equal(process.env.PASSWORD_EXPAND, 'pas$word') ct.equal(process.env.PASSWORD_EXPAND_SIMPLE, 'pas$word') - ct.equal(process.env.PASSWORD, 'pas$word') - ct.equal(process.env.PASSWORD_EXPAND_NESTED, 'pas$word') ct.equal(process.env.PASSWORD_EXPAND_NESTED, 'pas$word') + ct.equal(process.env.PASSWORD_EXPAND_NESTED_NESTED, 'pas$word') + + ct.end() +}) + +t.test('expands alternate logic', ct => { + const dotenv = require('dotenv').config({ path: 'tests/.env.test' }) + dotenvExpand.expand(dotenv) + + ct.equal(process.env.ALTERNATE, 'alternate') + + ct.end() +}) + +t.test('expands alternate logic when not set', ct => { + process.env.USE_IF_SET = '' + const dotenv = require('dotenv').config({ path: 'tests/.env.test' }) + dotenvExpand.expand(dotenv) + + ct.equal(process.env.ALTERNATE, '') ct.end() })