@dotenvx/dotenvx 2.12.0 → 2.14.0

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,24 @@
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/v2.12.0...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.14.0...main)
6
+
7
+ ## [2.14.0](https://github.com/dotenvx/dotenvx/compare/v2.13.0...v2.14.0) (2026-07-16)
8
+
9
+ ### Added
10
+
11
+ * Add `dotenvx run --validate` for simple env validation against your `.env.example` ([#906](https://github.com/dotenvx/dotenvx/pull/906))
12
+ * Add `dotenvx validate` for a pre-check of validation against your `.env.example` file ([#906](https://github.com/dotenvx/dotenvx/pull/906))
13
+
14
+ ## [2.13.0](https://github.com/dotenvx/dotenvx/compare/v2.12.0...v2.13.0) (2026-07-16)
15
+
16
+ ### Added
17
+
18
+ * Add `get --eval-export` flag ([#904](https://github.com/dotenvx/dotenvx/pull/904))
19
+
20
+ ### Changed
21
+
22
+ * Make `get --eval` posix safe ([#904](https://github.com/dotenvx/dotenvx/pull/904))
6
23
 
7
24
  ## [2.12.0](https://github.com/dotenvx/dotenvx/compare/v2.11.3...v2.12.0) (2026-07-16)
8
25
 
package/README.md CHANGED
@@ -1453,6 +1453,54 @@ Hello production
1453
1453
 
1454
1454
  Available log levels are `error, warn, info, verbose, debug, silly` ([source](https://docs.npmjs.com/cli/v8/using-npm/logging#setting-log-levels))
1455
1455
 
1456
+ </details>
1457
+ <details><summary>`run --validate`</summary><br>
1458
+
1459
+ Validate your environment against `.env.example`.
1460
+
1461
+ ```ini
1462
+ # .env.example
1463
+ DATABASE_URL=
1464
+ API_KEY=
1465
+ SENTRY_DSN= # optional
1466
+ ```
1467
+
1468
+ ```sh
1469
+ $ dotenvx run --validate -- node index.js
1470
+ [VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
1471
+ ```
1472
+
1473
+ Validation errors are reported without stopping your command. Combine `--validate` with `--strict` to exit with code `1` before the command runs.
1474
+
1475
+ ```sh
1476
+ $ dotenvx run --validate --strict -- node index.js
1477
+ ```
1478
+
1479
+ Any inline comment containing the word `optional` marks that key as optional. If `.env.example` is missing, dotenvx reports `MISSING_ENV_EXAMPLE`. An empty `.env.example` is valid and declares no required variables.
1480
+
1481
+ </details>
1482
+ <details><summary>`validate`</summary><br>
1483
+
1484
+ Validate `.env` file(s) against `.env.example` without running a command.
1485
+
1486
+ ```ini
1487
+ # .env.example
1488
+ DATABASE_URL=
1489
+ API_KEY=
1490
+ SENTRY_DSN= # optional
1491
+ ```
1492
+
1493
+ ```sh
1494
+ $ dotenvx validate
1495
+ [VALIDATION_FAILED] missing required (DATABASE_URL, API_KEY). fix: [https://github.com/dotenvx/dotenvx/issues/907]
1496
+ ```
1497
+
1498
+ Use `-f` and `-fk` to validate a specific env file and keys file. The command exits with code `1` when validation fails and prints errors to stderr. On success, it exits with code `0`.
1499
+
1500
+ ```sh
1501
+ $ dotenvx validate -f .env.production -fk .env.keys
1502
+ ```
1503
+
1456
1504
  </details>
1457
1505
  <details><summary>`run --strict`</summary><br>
1458
1506
 
@@ -2825,9 +2873,10 @@ Commands:
2825
2873
  decrypt decrypt .env file(s)
2826
2874
  keypair [KEY] print public/private keys for .env file(s)
2827
2875
  ls [directory] print all .env files in a tree structure
2876
+ gitignore append to .gitignore
2828
2877
  genexample [directory]
2829
2878
  generate .env.example
2830
- gitignore append to .gitignore
2879
+ validate validate .env file(s) against .env.example
2831
2880
  precommit [directory]
2832
2881
  prevent committing .env files to code
2833
2882
  prebuild [directory]
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.12.0",
2
+ "version": "2.14.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a secure dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "funding": "https://dotenvx.com",
45
45
  "dependencies": {
46
- "@dotenvx/primitives": "^2.0.0",
46
+ "@dotenvx/primitives": "^2.1.0",
47
47
  "@dotenvx/tooling": "^1.0.2",
48
48
  "yocto-spinner": "^1.2.1"
49
49
  },
@@ -76,10 +76,11 @@ async function get (key) {
76
76
  console.log(single)
77
77
  }
78
78
  } else {
79
- if (options.format === 'eval') {
79
+ if (options.format === 'eval' || options.format === 'eval-export') {
80
+ const prefix = options.format === 'eval-export' ? 'export ' : ''
80
81
  let inline = ''
81
82
  for (const [key, value] of Object.entries(parsed)) {
82
- inline += `${key}=${escape(value)}\n`
83
+ inline += `${prefix}${key}=${escape(value)}\n`
83
84
  }
84
85
  inline = inline.trim()
85
86
 
@@ -15,6 +15,7 @@ const maskEnvSrc = require('../../lib/helpers/maskEnvSrc')
15
15
  const maskProcessedEnvs = require('../../lib/helpers/maskProcessedEnvs')
16
16
  const redactedValues = require('../../lib/helpers/redactedValues')
17
17
  const { redactOutput } = require('../../lib/helpers/redactOutput')
18
+ const validateEnvExample = require('../../lib/helpers/validateEnvExample')
18
19
 
19
20
  const { determine } = require('./../../lib/helpers/envResolution')
20
21
 
@@ -135,6 +136,20 @@ async function run () {
135
136
  maskProcessedEnvs(processedEnvs, commandEnv, showChar)
136
137
  }
137
138
 
139
+ if (options.validate) {
140
+ const error = validateEnvExample(process.env)
141
+
142
+ if (error) {
143
+ if (ignore.includes(error.code)) {
144
+ logger.verbose(`ignored: ${error.message}`)
145
+ } else if (options.strict) {
146
+ throw error
147
+ } else {
148
+ logger.error(error.messageWithHelp || error.message)
149
+ }
150
+ }
151
+ }
152
+
138
153
  for (const processedEnv of processedEnvs) {
139
154
  if (processedEnv.type === 'envFile') {
140
155
  logger.verbose(`loading env from ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
@@ -0,0 +1,84 @@
1
+ const { logger } = require('./../../shared/logger')
2
+
3
+ const envsResolver = require('./../../lib/resolvers/envs')
4
+ const catchAndLog = require('./../../lib/helpers/catchAndLog')
5
+ const createSpinner = require('../../lib/helpers/createSpinner')
6
+ const Session = require('../../db/session')
7
+ const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
8
+ const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
9
+ const buildCommandEnvs = require('../../lib/helpers/buildCommandEnvs')
10
+ const resolveEnvKeysFile = require('../../lib/helpers/resolveEnvKeysFile')
11
+ const validateEnvExample = require('../../lib/helpers/validateEnvExample')
12
+
13
+ const { determine } = require('./../../lib/helpers/envResolution')
14
+
15
+ async function validate () {
16
+ const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts()))
17
+ const spinnerOptions = typeof this.optsWithGlobals === 'function' ? this.optsWithGlobals() : options
18
+ const spinner = await createSpinner({ ...spinnerOptions, ...options, text: 'validating' })
19
+ const ignore = options.ignore || []
20
+ const validateEnv = { ...process.env }
21
+ let errorCount = 0
22
+
23
+ logger.debug(`options: ${JSON.stringify(options)}`)
24
+
25
+ try {
26
+ let envs = buildCommandEnvs(this.envs, options.convention)
27
+ envs = determine(envs, process.env)
28
+
29
+ const sesh = new Session()
30
+ const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
31
+ const noKeychain = options.native === false || options.noNative === true
32
+
33
+ const { processedEnvs } = await envsResolver({
34
+ envs,
35
+ overload: options.overload,
36
+ processEnv: validateEnv,
37
+ envKeysFile: resolveEnvKeysFile(options.envKeysFile),
38
+ noArmor,
39
+ noKeychain,
40
+ token: options.token,
41
+ onStatus: (text) => {
42
+ if (spinner && text) {
43
+ spinner.text = text
44
+ }
45
+ }
46
+ })
47
+
48
+ for (const processedEnv of processedEnvs) {
49
+ for (const error of processedEnv.errors || []) {
50
+ if (ignore.includes(error.code)) {
51
+ logger.verbose(`ignored: ${error.message}`)
52
+ continue
53
+ }
54
+
55
+ errorCount += 1
56
+ logger.error(error.messageWithHelp || error.message)
57
+ }
58
+ }
59
+
60
+ const validationError = validateEnvExample(validateEnv)
61
+ if (validationError) {
62
+ if (ignore.includes(validationError.code)) {
63
+ logger.verbose(`ignored: ${validationError.message}`)
64
+ } else {
65
+ errorCount += 1
66
+ logger.error(validationError.messageWithHelp || validationError.message)
67
+ }
68
+ }
69
+
70
+ if (spinner) spinner.stop()
71
+
72
+ if (errorCount > 0) {
73
+ process.exit(1)
74
+ } else {
75
+ logger.success('▣ validated')
76
+ }
77
+ } catch (error) {
78
+ if (spinner) spinner.stop()
79
+ catchAndLog(error)
80
+ process.exit(1)
81
+ }
82
+ }
83
+
84
+ module.exports = validate
@@ -66,6 +66,7 @@ program.command('run')
66
66
  .option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
67
67
  .option('--redact', 'redact injected values except keys ending in _PLAIN', false)
68
68
  .option('-o, --overload', 'override existing env variables (by default, existing env vars take precedence over .env files)')
69
+ .option('--validate', 'validate against .env.example', false)
69
70
  .option('--strict', 'process.exit(1) on any errors', false)
70
71
  .option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
71
72
  .option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
@@ -94,7 +95,7 @@ program.command('get')
94
95
  .option('--mask [characters]', 'mask values, optionally setting visible characters')
95
96
  .option('-pp, --pretty-print', 'pretty print output')
96
97
  .option('--pp', 'pretty print output (alias)')
97
- .option('--format <type>', 'format of the output (json, shell, colon, eval)', 'json')
98
+ .option('--format <type>', 'format of the output (json, shell, colon, eval, eval-export)', 'json')
98
99
  .option('--no-armor', 'disable Dotenvx Armor features')
99
100
  .option('--no-native', 'disable OS secret store features')
100
101
  .action(function (...args) {
@@ -182,6 +183,15 @@ program.command('ls')
182
183
  return require('./actions/ls').apply(this, args)
183
184
  })
184
185
 
186
+ // dotenvx gitignore
187
+ program.command('gitignore')
188
+ .description('append to .gitignore')
189
+ .addHelpText('after', examples.gitignore)
190
+ .option('--pattern <patterns...>', 'pattern(s) to gitignore', ['.env*'])
191
+ .action(function (...args) {
192
+ return require('./actions/ext/gitignore').apply(this, args)
193
+ })
194
+
185
195
  // dotenvx genexample
186
196
  program.command('genexample')
187
197
  .description('generate .env.example')
@@ -191,13 +201,21 @@ program.command('genexample')
191
201
  return require('./actions/ext/genexample').apply(this, args)
192
202
  })
193
203
 
194
- // dotenvx gitignore
195
- program.command('gitignore')
196
- .description('append to .gitignore')
197
- .addHelpText('after', examples.gitignore)
198
- .option('--pattern <patterns...>', 'pattern(s) to gitignore', ['.env*'])
204
+ // dotenvx validate
205
+ program.command('validate')
206
+ .description('validate .env file(s) against .env.example')
207
+ .option('-e, --env <strings...>', 'environment variable(s) set as string (example: "HELLO=World")', collectEnvs('env'), [])
208
+ .option('-f, --env-file <path>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
209
+ .option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
210
+ .option('-o, --overload', 'override existing env variables (by default, existing env vars take precedence over .env files)')
211
+ .option('--convention <name>', 'load a .env convention (available conventions: [\'nextjs\', \'flow\'])')
212
+ .option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
213
+ .option('--token <token>', 'set Armor ⛨ token')
214
+ .option('--no-armor', 'disable Dotenvx Armor features')
215
+ .option('--no-native', 'disable OS secret store features')
199
216
  .action(function (...args) {
200
- return require('./actions/ext/gitignore').apply(this, args)
217
+ this.envs = envs
218
+ return require('./actions/validate').apply(this, args)
201
219
  })
202
220
 
203
221
  // dotenvx precommit
@@ -12,6 +12,7 @@ const ISSUE_BY_CODE = {
12
12
  MALFORMED_ENCRYPTED_DATA: 'https://github.com/dotenvx/dotenvx/issues/467',
13
13
  MISPAIRED_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/752',
14
14
  MISSING_DIRECTORY: 'https://github.com/dotenvx/dotenvx/issues/758',
15
+ MISSING_ENV_EXAMPLE: 'https://github.com/dotenvx/dotenvx/issues/905',
15
16
  MISSING_ENV_FILE: 'https://github.com/dotenvx/dotenvx/issues/484',
16
17
  MISSING_ENV_KEYS_FILE: 'https://github.com/dotenvx/dotenvx/issues/775',
17
18
  MISSING_ENV_FILES: 'https://github.com/dotenvx/dotenvx/issues/760',
@@ -22,6 +23,7 @@ const ISSUE_BY_CODE = {
22
23
  MISSING_VALUE: 'https://github.com/dotenvx/dotenvx/issues/864',
23
24
  FILE_NOT_WRITABLE: 'https://github.com/dotenvx/dotenvx/issues/890',
24
25
  PRECOMMIT_HOOK_MODIFY_FAILED: 'try again or report error',
26
+ VALIDATION_FAILED: 'https://github.com/dotenvx/dotenvx/issues/907',
25
27
  WRONG_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/466'
26
28
  }
27
29
 
@@ -207,6 +209,18 @@ class Errors {
207
209
  return e
208
210
  }
209
211
 
212
+ missingEnvExample () {
213
+ const code = 'MISSING_ENV_EXAMPLE'
214
+ const message = `[${code}] missing .env.example file`
215
+ const help = `fix: [${ISSUE_BY_CODE[code]}]`
216
+
217
+ const e = new Error(message)
218
+ e.code = code
219
+ e.help = help
220
+ e.messageWithHelp = `${message}. ${help}`
221
+ return e
222
+ }
223
+
210
224
  missingEnvKeysFile () {
211
225
  const code = 'MISSING_ENV_KEYS_FILE'
212
226
  const envKeysFilepath = this.envKeysFilepath || '.env.keys'
@@ -305,6 +319,18 @@ class Errors {
305
319
  return e
306
320
  }
307
321
 
322
+ validationFailed () {
323
+ const code = 'VALIDATION_FAILED'
324
+ const message = `[${code}] ${this.message}`
325
+ const help = `fix: [${ISSUE_BY_CODE[code]}]`
326
+
327
+ const e = new Error(message)
328
+ e.code = code
329
+ e.help = help
330
+ e.messageWithHelp = `${message}. ${help}`
331
+ return e
332
+ }
333
+
308
334
  precommitHookModifyFailed () {
309
335
  const code = 'PRECOMMIT_HOOK_MODIFY_FAILED'
310
336
  const message = `[${code}] failed to modify pre-commit hook: ${this.error.message}`
@@ -1,5 +1,6 @@
1
1
  function escape (value) {
2
- return JSON.stringify(value)
2
+ const quote = String.fromCharCode(39)
3
+ return quote + value.replaceAll(quote, quote + '\\' + quote + quote) + quote
3
4
  }
4
5
 
5
6
  module.exports = escape
@@ -0,0 +1,40 @@
1
+ function optionalKeys (comments) {
2
+ const keys = new Set()
3
+
4
+ for (const [key, values] of Object.entries(comments)) {
5
+ if (values.some(comment => comment && /\boptional\b/i.test(comment))) {
6
+ keys.add(key)
7
+ }
8
+ }
9
+
10
+ return keys
11
+ }
12
+
13
+ function validate (example = {}, env = {}, options = {}) {
14
+ const errors = []
15
+ const missingRequired = []
16
+ const optional = optionalKeys(options.comments || {})
17
+
18
+ for (const key of Object.keys(example)) {
19
+ const value = env[key]
20
+ const missing = !Object.prototype.hasOwnProperty.call(env, key) || value.trim() === ''
21
+ if (!optional.has(key) && missing) {
22
+ missingRequired.push(key)
23
+ }
24
+ }
25
+
26
+ if (missingRequired.length > 0) {
27
+ errors.push({
28
+ code: 'MISSING_REQUIRED',
29
+ keys: missingRequired,
30
+ message: `missing required (${missingRequired.join(', ')})`
31
+ })
32
+ }
33
+
34
+ return {
35
+ valid: errors.length === 0,
36
+ errors
37
+ }
38
+ }
39
+
40
+ module.exports = validate
@@ -0,0 +1,24 @@
1
+ const fs = require('fs')
2
+ const { scan } = require('@dotenvx/primitives')
3
+
4
+ const Errors = require('./errors')
5
+ const validate = require('./validate')
6
+
7
+ function validateEnvExample (env = process.env, options = {}) {
8
+ const filepath = options.filepath || '.env.example'
9
+
10
+ if (!fs.existsSync(filepath)) {
11
+ return new Errors().missingEnvExample()
12
+ }
13
+
14
+ const exampleSrc = fs.readFileSync(filepath, 'utf8')
15
+ const { parsed: example, comments } = scan(exampleSrc)
16
+ const validation = validate(example, env, { comments })
17
+
18
+ if (!validation.valid) {
19
+ const message = validation.errors.map(error => error.message).join('; ')
20
+ return new Errors({ message }).validationFailed()
21
+ }
22
+ }
23
+
24
+ module.exports = validateEnvExample
package/src/lib/main.js CHANGED
@@ -20,6 +20,7 @@ const setTransform = require('./transforms/set')
20
20
  const buildEnvs = require('./helpers/buildEnvs')
21
21
  const buildConfigEnvs = require('./helpers/buildConfigEnvs')
22
22
  const { determine } = require('./helpers/envResolution')
23
+ const escape = require('./helpers/escape')
23
24
  const fsx = require('./helpers/fsx')
24
25
  const decryptKeyValue = require('./helpers/cryptography/decryptKeyValue')
25
26
  const Errors = require('./helpers/errors')
@@ -358,10 +359,11 @@ const get = async function (key, options = {}) {
358
359
  return single
359
360
  }
360
361
  } else {
361
- if (options.format === 'eval') {
362
+ if (options.format === 'eval' || options.format === 'eval-export') {
363
+ const prefix = options.format === 'eval-export' ? 'export ' : ''
362
364
  let inline = ''
363
365
  for (const [key, value] of Object.entries(parsed)) {
364
- inline += `${key}=${escape(value)}\n`
366
+ inline += `${prefix}${key}=${escape(value)}\n`
365
367
  }
366
368
  inline = inline.trim()
367
369