@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 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.20.0...main)
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 [options] [command] [command] [args...]
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 [options] inject env at runtime [dotenvx run -- yourcommand]
1553
- get [options] [key] return a single environment variable
1554
- set [options] <KEY> <value> set a single environment variable
1555
- encrypt [options] convert .env file(s) to encrypted .env file(s)
1556
- decrypt [options] convert encrypted .env file(s) to plain .env file(s)
1557
- ls [options] [directory] print all .env files in a tree structure
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.20.1",
2
+ "version": "1.21.1",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -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('[key]', 'environment variable name')
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('[key]', 'environment variable key name')
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
- const envs = []
6
-
7
- if (['development', 'test', 'production'].includes(nodeEnv)) {
8
- envs.push({ type: 'envFile', value: `.env.${nodeEnv}.local` })
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 &> /dev/null
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 [brew install dotenvx/brew/dotenvx]"
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
@@ -0,0 +1,11 @@
1
+ // Remove [options] from help text. example:
2
+
3
+ function removeOptionsHelpParts (lines) {
4
+ for (let i = 0; i < lines.length; i++) {
5
+ lines[i] = lines[i].replace(' [options]', '')
6
+ }
7
+
8
+ return lines
9
+ }
10
+
11
+ module.exports = removeOptionsHelpParts
@@ -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 escapedValue = util.inspect(replaceValue, { showHidden: false, depth: null, colors: false })
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 with [echo "!${file}" >> .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 (or not gitignored)`)
65
- error.help = `? encrypt it with [dotenvx encrypt -f ${file}] or add ${file} to .gitignore with [echo ".env*" >> .gitignore]`
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 = `success (with ${pluralize('warning', warnings.length)})`
79
+ successMessage += ` with warnings (${warnings.length})`
75
80
  }
76
81
 
77
82
  return {