@dotenvx/dotenvx 1.20.1 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/README.md +8 -7
- package/package.json +1 -1
- package/src/cli/dotenvx.js +8 -2
- package/src/lib/helpers/conventions.js +7 -19
- package/src/lib/helpers/installPrecommitHook.js +2 -2
- package/src/lib/helpers/parseDecryptEvalExpand.js +15 -1
- package/src/lib/helpers/quotes.js +36 -0
- package/src/lib/helpers/removeOptionsHelpParts.js +11 -0
- package/src/lib/helpers/replace.js +8 -15
- package/src/lib/services/precommit.js +11 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
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.
|
|
4
4
|
|
|
5
|
-
## [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.
|
|
5
|
+
## [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.21.1...main)
|
|
6
|
+
|
|
7
|
+
## 1.21.1
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* for `--convention nextjs` ingnore `.env.local` for TEST environment ([#425](https://github.com/dotenvx/dotenvx/pull/425))
|
|
12
|
+
* for `precommit` redirect missing `dotenvx` command using POSIX compliant redirection ([#424](https://github.com/dotenvx/dotenvx/pull/424))
|
|
13
|
+
* make parent `dotenvx help` command less noisy by removing `[options]`. run `dotenvx COMMAND -h` to list all available options like always ([#429](https://github.com/dotenvx/dotenvx/pull/429))
|
|
14
|
+
|
|
15
|
+
## 1.21.0
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
* treat single quotes literally ([#423](https://github.com/dotenvx/dotenvx/pull/423))
|
|
20
|
+
* respect user chosen quotes ([#423](https://github.com/dotenvx/dotenvx/pull/423) [#377](https://github.com/dotenvx/dotenvx/issues/377))
|
|
21
|
+
|
|
22
|
+
🎓 now if you choose to single quote, double quote, no quote, or backtick your value it will be respected - including for encrypted values. this more intuitively handles complex cases like escaped characters, literals, and json.
|
|
23
|
+
|
|
24
|
+
## 1.20.1
|
|
6
25
|
|
|
7
26
|
### Changed
|
|
8
27
|
|
package/README.md
CHANGED
|
@@ -1536,7 +1536,7 @@ More examples
|
|
|
1536
1536
|
|
|
1537
1537
|
```sh
|
|
1538
1538
|
$ dotenvx help
|
|
1539
|
-
Usage: dotenvx
|
|
1539
|
+
Usage: dotenvx run -- yourcommand
|
|
1540
1540
|
|
|
1541
1541
|
a better dotenv–from the creator of `dotenv`
|
|
1542
1542
|
|
|
@@ -1549,12 +1549,13 @@ More examples
|
|
|
1549
1549
|
-h, --help display help for command
|
|
1550
1550
|
|
|
1551
1551
|
Commands:
|
|
1552
|
-
run
|
|
1553
|
-
get [
|
|
1554
|
-
set
|
|
1555
|
-
encrypt
|
|
1556
|
-
decrypt
|
|
1557
|
-
|
|
1552
|
+
run inject env at runtime [dotenvx run -- yourcommand]
|
|
1553
|
+
get [KEY] return a single environment variable
|
|
1554
|
+
set <KEY> <value> set a single environment variable
|
|
1555
|
+
encrypt convert .env file(s) to encrypted .env file(s)
|
|
1556
|
+
decrypt convert encrypted .env file(s) to plain .env file(s)
|
|
1557
|
+
keypair [KEY] print public/private keys for .env file(s)
|
|
1558
|
+
ls [directory] print all .env files in a tree structure
|
|
1558
1559
|
|
|
1559
1560
|
Advanced:
|
|
1560
1561
|
pro 🏆 pro
|
package/package.json
CHANGED
package/src/cli/dotenvx.js
CHANGED
|
@@ -9,6 +9,7 @@ const examples = require('./examples')
|
|
|
9
9
|
const packageJson = require('./../lib/helpers/packageJson')
|
|
10
10
|
const executeDynamic = require('./../lib/helpers/executeDynamic')
|
|
11
11
|
const removeDynamicHelpSection = require('./../lib/helpers/removeDynamicHelpSection')
|
|
12
|
+
const removeOptionsHelpParts = require('./../lib/helpers/removeOptionsHelpParts')
|
|
12
13
|
|
|
13
14
|
// for use with run
|
|
14
15
|
const envs = []
|
|
@@ -21,6 +22,7 @@ function collectEnvs (type) {
|
|
|
21
22
|
|
|
22
23
|
// global log levels
|
|
23
24
|
program
|
|
25
|
+
.usage('run -- yourcommand')
|
|
24
26
|
.option('-l, --log-level <level>', 'set log level', 'info')
|
|
25
27
|
.option('-q, --quiet', 'sets log level to error')
|
|
26
28
|
.option('-v, --verbose', 'sets log level to verbose')
|
|
@@ -65,8 +67,9 @@ program.command('run')
|
|
|
65
67
|
// dotenvx get
|
|
66
68
|
const getAction = require('./actions/get')
|
|
67
69
|
program.command('get')
|
|
70
|
+
.usage('[KEY] [options]')
|
|
68
71
|
.description('return a single environment variable')
|
|
69
|
-
.argument('[
|
|
72
|
+
.argument('[KEY]', 'environment variable name')
|
|
70
73
|
.option('-e, --env <strings...>', 'environment variable(s) set as string (example: "HELLO=World")', collectEnvs('env'), [])
|
|
71
74
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
|
|
72
75
|
.option('-fv, --env-vault-file <paths...>', 'path(s) to your .env.vault file(s)', collectEnvs('envVaultFile'), [])
|
|
@@ -83,6 +86,7 @@ program.command('get')
|
|
|
83
86
|
// dotenvx set
|
|
84
87
|
const setAction = require('./actions/set')
|
|
85
88
|
program.command('set')
|
|
89
|
+
.usage('<KEY> <value> [options]')
|
|
86
90
|
.description('set a single environment variable')
|
|
87
91
|
.addHelpText('after', examples.set)
|
|
88
92
|
.allowUnknownOption()
|
|
@@ -125,8 +129,9 @@ program.command('decrypt')
|
|
|
125
129
|
// dotenvx keypair
|
|
126
130
|
const keypairAction = require('./actions/keypair')
|
|
127
131
|
program.command('keypair')
|
|
132
|
+
.usage('[KEY] [options]')
|
|
128
133
|
.description('print public/private keys for .env file(s)')
|
|
129
|
-
.argument('[
|
|
134
|
+
.argument('[KEY]', 'environment variable key name')
|
|
130
135
|
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)')
|
|
131
136
|
.option('-pp, --pretty-print', 'pretty print output')
|
|
132
137
|
.option('--format <type>', 'format of the output (json, shell)', 'json')
|
|
@@ -196,6 +201,7 @@ program.helpInformation = function () {
|
|
|
196
201
|
const lines = originalHelp.split('\n')
|
|
197
202
|
|
|
198
203
|
removeDynamicHelpSection(lines)
|
|
204
|
+
removeOptionsHelpParts(lines)
|
|
199
205
|
|
|
200
206
|
// Filter out the hidden command from the help output
|
|
201
207
|
const filteredLines = lines.filter(line =>
|
|
@@ -1,26 +1,14 @@
|
|
|
1
1
|
function conventions (convention) {
|
|
2
2
|
if (convention === 'nextjs') {
|
|
3
3
|
const nodeEnv = process.env.NODE_ENV || 'development'
|
|
4
|
+
const canonicalEnv = ['development', 'test', 'production'].includes(nodeEnv) && nodeEnv
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (['development', 'production'].includes(nodeEnv)) {
|
|
12
|
-
envs.push({ type: 'envFile', value: '.env.local' })
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (['development', 'test', 'production'].includes(nodeEnv)) {
|
|
16
|
-
envs.push({ type: 'envFile', value: `.env.${nodeEnv}` })
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (['development', 'test', 'production'].includes(nodeEnv)) {
|
|
20
|
-
envs.push({ type: 'envFile', value: '.env' })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return envs
|
|
6
|
+
return [
|
|
7
|
+
canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}.local` },
|
|
8
|
+
canonicalEnv !== 'test' && { type: 'envFile', value: '.env.local' },
|
|
9
|
+
canonicalEnv && { type: 'envFile', value: `.env.${canonicalEnv}` },
|
|
10
|
+
{ type: 'envFile', value: '.env' }
|
|
11
|
+
].filter(Boolean)
|
|
24
12
|
} else {
|
|
25
13
|
throw new Error(`INVALID_CONVENTION: '${convention}'. permitted conventions: ['nextjs']`)
|
|
26
14
|
}
|
|
@@ -3,10 +3,10 @@ const path = require('path')
|
|
|
3
3
|
|
|
4
4
|
const HOOK_SCRIPT = `#!/bin/sh
|
|
5
5
|
|
|
6
|
-
if ! command -v dotenvx
|
|
6
|
+
if ! command -v dotenvx 2>&1 >/dev/null
|
|
7
7
|
then
|
|
8
8
|
echo "[dotenvx][precommit] 'dotenvx' command not found"
|
|
9
|
-
echo "[dotenvx][precommit] ? install it with [
|
|
9
|
+
echo "[dotenvx][precommit] ? install it with [curl -fsS https://dotenvx.sh | sh]"
|
|
10
10
|
echo "[dotenvx][precommit] ? other install options [https://dotenvx.com/docs/install]"
|
|
11
11
|
exit 1
|
|
12
12
|
fi
|
|
@@ -3,6 +3,7 @@ const dotenvEval = require('./dotenvEval')
|
|
|
3
3
|
const dotenvExpand = require('./dotenvExpand')
|
|
4
4
|
const decryptValue = require('./decryptValue')
|
|
5
5
|
const truncate = require('./truncate')
|
|
6
|
+
const quotes = require('./quotes')
|
|
6
7
|
|
|
7
8
|
function warning (e, key, privateKey = null) {
|
|
8
9
|
const warning = new Error(`[${e.code}] could not decrypt ${key} using private key '${truncate(privateKey)}'`)
|
|
@@ -15,8 +16,10 @@ function warning (e, key, privateKey = null) {
|
|
|
15
16
|
function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.env) {
|
|
16
17
|
const warnings = []
|
|
17
18
|
|
|
18
|
-
// parse
|
|
19
|
+
// parse and quotes
|
|
19
20
|
const parsed = dotenv.parse(src)
|
|
21
|
+
const _quotes = quotes(src)
|
|
22
|
+
const originalParsed = { ...parsed }
|
|
20
23
|
for (const key in parsed) {
|
|
21
24
|
try {
|
|
22
25
|
const decryptedValue = decryptValue(parsed[key], privateKey)
|
|
@@ -39,7 +42,13 @@ function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.en
|
|
|
39
42
|
parsed: evaled
|
|
40
43
|
}
|
|
41
44
|
const expanded = dotenvExpand.expand(inputEvaled)
|
|
45
|
+
|
|
42
46
|
for (const key in expanded.parsed) {
|
|
47
|
+
// unset eval and expansion for single quotes
|
|
48
|
+
if (_quotes[key] === "'") {
|
|
49
|
+
expanded.parsed[key] = originalParsed[key] // reset to original
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
try {
|
|
44
53
|
const decryptedValue = decryptValue(expanded.parsed[key], privateKey)
|
|
45
54
|
expanded.parsed[key] = decryptedValue
|
|
@@ -48,6 +57,11 @@ function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.en
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
for (const key in processEnv) {
|
|
60
|
+
// unset eval and expansion for single quotes
|
|
61
|
+
if (_quotes[key] === "'") {
|
|
62
|
+
processEnv[key] = originalParsed[key] // reset to original
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
try {
|
|
52
66
|
const decryptedValue = decryptValue(processEnv[key], privateKey)
|
|
53
67
|
processEnv[key] = decryptedValue
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
|
2
|
+
|
|
3
|
+
function quotes (src) {
|
|
4
|
+
const obj = {}
|
|
5
|
+
// Convert buffer to string
|
|
6
|
+
let lines = src.toString()
|
|
7
|
+
|
|
8
|
+
// Convert line breaks to same format
|
|
9
|
+
lines = lines.replace(/\r\n?/mg, '\n')
|
|
10
|
+
|
|
11
|
+
let match
|
|
12
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
13
|
+
const key = match[1]
|
|
14
|
+
|
|
15
|
+
// Default undefined or null to empty string
|
|
16
|
+
let value = (match[2] || '')
|
|
17
|
+
|
|
18
|
+
// Remove whitespace
|
|
19
|
+
value = value.trim()
|
|
20
|
+
|
|
21
|
+
// Check if double quoted
|
|
22
|
+
const maybeQuote = value[0]
|
|
23
|
+
|
|
24
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
|
|
25
|
+
|
|
26
|
+
if (maybeQuote === value[0]) {
|
|
27
|
+
obj[key] = ''
|
|
28
|
+
} else {
|
|
29
|
+
obj[key] = maybeQuote
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return obj
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = quotes
|
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
const util = require('util')
|
|
2
1
|
const dotenv = require('dotenv')
|
|
3
2
|
|
|
3
|
+
const quotes = require('./quotes')
|
|
4
4
|
const escapeForRegex = require('./escapeForRegex')
|
|
5
5
|
const escapeDollarSigns = require('./escapeDollarSigns')
|
|
6
6
|
|
|
7
7
|
function replace (src, key, replaceValue) {
|
|
8
8
|
let output
|
|
9
|
-
let
|
|
10
|
-
|
|
11
|
-
if (replaceValue.includes('\n')) {
|
|
12
|
-
escapedValue = JSON.stringify(replaceValue) // use JSON stringify if string contains newlines
|
|
13
|
-
escapedValue = escapedValue.replace(/\\n/g, '\n') // fix up newlines
|
|
14
|
-
escapedValue = escapedValue.replace(/\\r/g, '\r')
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// prevents test\test (and similar) from becoming test\\test and then test\\\\test, etc recursively after each encrypt/decrypt combo
|
|
18
|
-
if (replaceValue.includes('\\')) {
|
|
19
|
-
escapedValue = escapedValue.replace(/\\\\/g, '\\')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let newPart = `${key}=${escapedValue}`
|
|
9
|
+
let newPart = ''
|
|
23
10
|
|
|
24
11
|
const parsed = dotenv.parse(src)
|
|
12
|
+
const _quotes = quotes(src)
|
|
25
13
|
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
|
|
14
|
+
const quote = _quotes[key]
|
|
15
|
+
newPart += `${key}=${quote}${replaceValue}${quote}`
|
|
16
|
+
|
|
26
17
|
const originalValue = parsed[key]
|
|
27
18
|
const escapedOriginalValue = escapeForRegex(originalValue)
|
|
28
19
|
|
|
@@ -52,6 +43,8 @@ function replace (src, key, replaceValue) {
|
|
|
52
43
|
// $2 preserves export
|
|
53
44
|
output = src.replace(currentPart, `$1$2${saferInput}`)
|
|
54
45
|
} else {
|
|
46
|
+
newPart += `${key}="${replaceValue}"`
|
|
47
|
+
|
|
55
48
|
// append
|
|
56
49
|
if (src.endsWith('\n')) {
|
|
57
50
|
newPart = newPart + '\n'
|
|
@@ -4,7 +4,6 @@ const ignore = require('ignore')
|
|
|
4
4
|
|
|
5
5
|
const Ls = require('../services/ls')
|
|
6
6
|
|
|
7
|
-
const pluralize = require('./../helpers/pluralize')
|
|
8
7
|
const isFullyEncrypted = require('./../helpers/isFullyEncrypted')
|
|
9
8
|
const InstallPrecommitHook = require('./../helpers/installPrecommitHook')
|
|
10
9
|
const childProcess = require('child_process')
|
|
@@ -27,8 +26,8 @@ class Precommit {
|
|
|
27
26
|
warnings: []
|
|
28
27
|
}
|
|
29
28
|
} else {
|
|
29
|
+
let count = 0
|
|
30
30
|
const warnings = []
|
|
31
|
-
let successMessage = 'success'
|
|
32
31
|
let gitignore = MISSING_GITIGNORE
|
|
33
32
|
|
|
34
33
|
// 1. check for .gitignore file
|
|
@@ -45,13 +44,15 @@ class Precommit {
|
|
|
45
44
|
const lsService = new Ls(process.cwd(), undefined, this.excludeEnvFile)
|
|
46
45
|
const dotenvFiles = lsService.run()
|
|
47
46
|
dotenvFiles.forEach(file => {
|
|
47
|
+
count += 1
|
|
48
|
+
|
|
48
49
|
// check if file is going to be commited
|
|
49
50
|
if (this._isFileToBeCommitted(file)) {
|
|
50
51
|
// check if that file is being ignored
|
|
51
52
|
if (ig.ignores(file)) {
|
|
52
53
|
if (file === '.env.example' || file === '.env.vault') {
|
|
53
54
|
const warning = new Error(`${file} (currently ignored but should not be)`)
|
|
54
|
-
warning.help = `? add !${file} to .gitignore
|
|
55
|
+
warning.help = `? add !${file} to .gitignore [echo "!${file}" >> .gitignore]`
|
|
55
56
|
warnings.push(warning)
|
|
56
57
|
}
|
|
57
58
|
} else {
|
|
@@ -61,8 +62,8 @@ class Precommit {
|
|
|
61
62
|
|
|
62
63
|
// if contents are encrypted don't raise an error
|
|
63
64
|
if (!encrypted) {
|
|
64
|
-
const error = new Error(`${file} not encrypted
|
|
65
|
-
error.help = `? encrypt it
|
|
65
|
+
const error = new Error(`${file} not protected (encrypted or gitignored)`)
|
|
66
|
+
error.help = `? encrypt it [dotenvx encrypt -f ${file}] or gitignore it [echo "${file}" >> .gitignore]`
|
|
66
67
|
throw error
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -70,8 +71,12 @@ class Precommit {
|
|
|
70
71
|
}
|
|
71
72
|
})
|
|
72
73
|
|
|
74
|
+
let successMessage = `.env files (${count}) protected (encrypted or gitignored)`
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
successMessage = 'zero .env files'
|
|
77
|
+
}
|
|
73
78
|
if (warnings.length > 0) {
|
|
74
|
-
successMessage
|
|
79
|
+
successMessage += ` with warnings (${warnings.length})`
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
return {
|