@dotenvx/dotenvx 1.0.0 → 1.1.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,19 @@
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.0.0...main)
5
+ ## [Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.1.0...main)
6
+
7
+ ## 1.1.0
8
+
9
+ ### Added
10
+
11
+ * Add TypeScript type definitions ([#272](https://github.com/dotenvx/dotenvx/pull/272))
12
+
13
+ ## 1.0.1
14
+
15
+ ### Changed
16
+
17
+ * 🐞 Fix expansion when preset on `process.env` and/or with `--overload` ([#271](https://github.com/dotenvx/dotenvx/pull/271))
6
18
 
7
19
  ## 1.0.0
8
20
 
@@ -34,6 +46,8 @@ All notable changes to this project will be documented in this file. See [standa
34
46
 
35
47
  This is a BIG release that sets the tone for `dotenvx`'s core offering and features while maintaining room for growth. Thank you everyone for your support and usage of `dotenvx` 🙏.
36
48
 
49
+ [blog post: "From dotenv to dotenvx: Next Generation Config Management"](https://dotenvx.com/blog/2024/06/24/dotenvx-next-generation-config-management.html)
50
+
37
51
  ## 0.45.0
38
52
 
39
53
  ### Changed
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
   
10
10
 
11
11
 
12
- ### Quickstart [![npm version](https://img.shields.io/npm/v/@dotenvx/dotenvx.svg)](https://www.npmjs.com/package/@dotenvx/dotenvx) [![npm installs](https://img.shields.io/npm/dm/@dotenvx/dotenvx)](https://www.npmjs.com/package/@dotenvx/dotenvx)
12
+ ### Quickstart [![npm version](https://img.shields.io/npm/v/@dotenvx/dotenvx.svg)](https://www.npmjs.com/package/@dotenvx/dotenvx) [![test count](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/motdotenv/bb76445765a9731e7d824a6efdf53524/raw/dotenvxTestCount.json)](https://github.com/dotenvx/dotenvx/tree/main/tests) [![npm installs](https://img.shields.io/npm/dm/@dotenvx/dotenvx)](https://www.npmjs.com/package/@dotenvx/dotenvx)
13
13
 
14
14
  Install and use it in code just like `dotenv`.
15
15
 
@@ -233,6 +233,17 @@ More examples
233
233
  Hello World
234
234
  ```
235
235
 
236
+ </details>
237
+ * <details><summary>Clojure 🌿</summary><br>
238
+
239
+ ```sh
240
+ $ echo "HELLO=World" > .env
241
+ $ echo '(println "Hello" (System/getenv "HELLO"))' > index.clj
242
+
243
+ $ dotenvx run -- clojure -M index.clj
244
+ Hello World
245
+ ```
246
+
236
247
  </details>
237
248
  * <details><summary>.NET 🔵</summary><br>
238
249
 
@@ -659,188 +670,10 @@ More examples
659
670
 
660
671
  &nbsp;
661
672
 
662
- ## More
663
-
664
- > Go deeper with [extensions](#extensions-), [advaned usage](#advanced-usage-), and [guides](#guides-).
665
-
666
- ### Extensions 🔌
667
-
668
- * <details><summary>`ext ls`</summary><br>
669
-
670
- Print all `.env` files in a tree structure.
671
-
672
- ```sh
673
- $ touch .env
674
- $ touch .env.production
675
- $ mkdir -p apps/backend
676
- $ touch apps/backend/.env
677
-
678
- $ dotenvx ext ls
679
- ├─ .env.production
680
- ├─ .env
681
- └─ apps
682
- └─ backend
683
- └─ .env
684
- ```
685
-
686
- </details>
687
- * <details><summary>`ext ls directory`</summary><br>
688
-
689
- Print all `.env` files inside a specified path to a directory.
690
-
691
- ```sh
692
- $ touch .env
693
- $ touch .env.production
694
- $ mkdir -p apps/backend
695
- $ touch apps/backend/.env
696
-
697
- $ dotenvx ext ls apps/backend
698
- └─ .env
699
- ```
700
-
701
- </details>
702
- * <details><summary>`ext ls -f`</summary><br>
703
-
704
- Glob `.env` filenames matching a wildcard.
705
-
706
- ```sh
707
- $ touch .env
708
- $ touch .env.production
709
- $ mkdir -p apps/backend
710
- $ touch apps/backend/.env
711
- $ touch apps/backend/.env.prod
712
-
713
- $ dotenvx ext ls -f **/.env.prod*
714
- ├─ .env.production
715
- └─ apps
716
- └─ backend
717
- └─ .env.prod
718
- ```
719
-
720
- </details>
721
- * <details><summary>`ext genexample`</summary><br>
722
-
723
- In one command, generate a `.env.example` file from your current `.env` file contents.
724
-
725
- ```sh
726
- $ echo "HELLO=World" > .env
727
-
728
- $ dotenvx ext genexample
729
- ✔ updated .env.example (1)
730
- ```
731
-
732
- ```ini
733
- # .env.example
734
- HELLO=""
735
- ```
736
-
737
- </details>
738
- * <details><summary>`ext genexample -f`</summary><br>
739
-
740
- Pass multiple `.env` files to generate your `.env.example` file from the combination of their contents.
741
-
742
- ```sh
743
- $ echo "HELLO=World" > .env
744
- $ echo "DB_HOST=example.com" > .env.production
745
-
746
- $ dotenvx ext genexample -f .env -f .env.production
747
- ✔ updated .env.example (2)
748
- ```
749
-
750
- ```ini
751
- # .env.example
752
- HELLO=""
753
- DB_HOST=""
754
- ```
755
-
756
- </details>
757
- * <details><summary>`ext genexample directory`</summary><br>
758
-
759
- Generate a `.env.example` file inside the specified directory. Useful for monorepos.
760
-
761
- ```sh
762
- $ echo "HELLO=World" > .env
763
- $ mkdir -p apps/backend
764
- $ echo "HELLO=Backend" > apps/backend/.env
765
-
766
- $ dotenvx ext genexample apps/backend
767
- ✔ updated .env.example (1)
768
- ```
769
-
770
- ```ini
771
- # apps/backend/.env.example
772
- HELLO=""
773
- ```
774
-
775
- </details>
776
- * <details><summary>`ext gitignore`</summary><br>
673
+ ## Advanced
777
674
 
778
- Gitignore your `.env` files.
779
-
780
- ```sh
781
- $ dotenvx ext gitignore
782
- creating .gitignore
783
- appending .env* to .gitignore
784
- done
785
- ```
786
-
787
- </details>
788
- * <details><summary>`ext precommit`</summary><br>
789
-
790
- Prevent `.env` files from being committed to code.
791
-
792
- ```sh
793
- $ dotenvx ext precommit
794
- [dotenvx][precommit] success
795
- ```
796
-
797
- </details>
798
- * <details><summary>`ext precommit --install`</summary><br>
799
-
800
- Install a shell script to `.git/hooks/pre-commit` to prevent accidentally committing any `.env` files to source control.
801
-
802
- ```sh
803
- $ dotenvx ext precommit --install
804
- [dotenvx][precommit] dotenvx precommit installed [.git/hooks/pre-commit]
805
- ```
806
-
807
- </details>
808
- * <details><summary>`ext prebuild`</summary><br>
809
-
810
- Prevent `.env` files from being built into your docker containers.
811
-
812
- Add it to your `Dockerfile`.
813
-
814
- ```sh
815
- RUN curl -fsS https://dotenvx.sh | sh
816
-
817
- ...
818
-
819
- RUN dotenvx ext prebuild
820
- CMD ["dotenvx", "run", "--", "node", "index.js"]
821
- ```
822
-
823
- </details>
824
- * <details><summary>`ext scan`</summary><br>
825
-
826
- Use [gitleaks](https://gitleaks.io) under the hood to scan for possible secrets in your code.
827
-
828
- ```sh
829
- $ dotenvx ext scan
830
-
831
-
832
- │╲
833
- │ ○
834
- ○ ░
835
- ░ gitleaks
836
-
837
- 100 commits scanned.
838
- no leaks found
839
- ```
840
-
841
- </details>
842
-
843
- ### Advanced usage 🎓
675
+ > Become a `dotenvx` power user.
676
+ >
844
677
 
845
678
  * <details><summary>`run` - Variable Expansion</summary><br>
846
679
 
@@ -1355,18 +1188,208 @@ More examples
1355
1188
 
1356
1189
  </details>
1357
1190
 
1358
- ### Guides 📖
1191
+ ### Extensions 🔌
1192
+
1193
+ * <details><summary>`ext ls`</summary><br>
1194
+
1195
+ Print all `.env` files in a tree structure.
1196
+
1197
+ ```sh
1198
+ $ touch .env
1199
+ $ touch .env.production
1200
+ $ mkdir -p apps/backend
1201
+ $ touch apps/backend/.env
1202
+
1203
+ $ dotenvx ext ls
1204
+ ├─ .env.production
1205
+ ├─ .env
1206
+ └─ apps
1207
+ └─ backend
1208
+ └─ .env
1209
+ ```
1210
+
1211
+ </details>
1212
+ * <details><summary>`ext ls directory`</summary><br>
1213
+
1214
+ Print all `.env` files inside a specified path to a directory.
1215
+
1216
+ ```sh
1217
+ $ touch .env
1218
+ $ touch .env.production
1219
+ $ mkdir -p apps/backend
1220
+ $ touch apps/backend/.env
1359
1221
 
1360
- * [quickstart guides](https://dotenvx.com/docs/quickstart)
1361
- * [run anywhere](https://dotenvx.com/docs/quickstart/run)
1362
- * [multi-environment](https://dotenvx.com/docs/quickstart/environments)
1363
- * [encrypted envs](https://dotenvx.com/docs/quickstart/encryption)
1364
- * [dotenvx/docs](https://dotenvx.com/docs)
1365
- * [languages](https://dotenvx.com/docs#languages)
1366
- * [frameworks](https://dotenvx.com/docs#frameworks)
1367
- * [platforms](https://dotenvx.com/docs#platforms)
1368
- * [ci/cd](https://dotenvx.com/docs#cis)
1222
+ $ dotenvx ext ls apps/backend
1223
+ └─ .env
1224
+ ```
1225
+
1226
+ </details>
1227
+ * <details><summary>`ext ls -f`</summary><br>
1228
+
1229
+ Glob `.env` filenames matching a wildcard.
1230
+
1231
+ ```sh
1232
+ $ touch .env
1233
+ $ touch .env.production
1234
+ $ mkdir -p apps/backend
1235
+ $ touch apps/backend/.env
1236
+ $ touch apps/backend/.env.prod
1237
+
1238
+ $ dotenvx ext ls -f **/.env.prod*
1239
+ ├─ .env.production
1240
+ └─ apps
1241
+ └─ backend
1242
+ └─ .env.prod
1243
+ ```
1244
+
1245
+ </details>
1246
+ * <details><summary>`ext genexample`</summary><br>
1247
+
1248
+ In one command, generate a `.env.example` file from your current `.env` file contents.
1249
+
1250
+ ```sh
1251
+ $ echo "HELLO=World" > .env
1252
+
1253
+ $ dotenvx ext genexample
1254
+ ✔ updated .env.example (1)
1255
+ ```
1256
+
1257
+ ```ini
1258
+ # .env.example
1259
+ HELLO=""
1260
+ ```
1261
+
1262
+ </details>
1263
+ * <details><summary>`ext genexample -f`</summary><br>
1264
+
1265
+ Pass multiple `.env` files to generate your `.env.example` file from the combination of their contents.
1266
+
1267
+ ```sh
1268
+ $ echo "HELLO=World" > .env
1269
+ $ echo "DB_HOST=example.com" > .env.production
1270
+
1271
+ $ dotenvx ext genexample -f .env -f .env.production
1272
+ ✔ updated .env.example (2)
1273
+ ```
1274
+
1275
+ ```ini
1276
+ # .env.example
1277
+ HELLO=""
1278
+ DB_HOST=""
1279
+ ```
1280
+
1281
+ </details>
1282
+ * <details><summary>`ext genexample directory`</summary><br>
1283
+
1284
+ Generate a `.env.example` file inside the specified directory. Useful for monorepos.
1285
+
1286
+ ```sh
1287
+ $ echo "HELLO=World" > .env
1288
+ $ mkdir -p apps/backend
1289
+ $ echo "HELLO=Backend" > apps/backend/.env
1290
+
1291
+ $ dotenvx ext genexample apps/backend
1292
+ ✔ updated .env.example (1)
1293
+ ```
1294
+
1295
+ ```ini
1296
+ # apps/backend/.env.example
1297
+ HELLO=""
1298
+ ```
1299
+
1300
+ </details>
1301
+ * <details><summary>`ext gitignore`</summary><br>
1302
+
1303
+ Gitignore your `.env` files.
1304
+
1305
+ ```sh
1306
+ $ dotenvx ext gitignore
1307
+ creating .gitignore
1308
+ appending .env* to .gitignore
1309
+ done
1310
+ ```
1311
+
1312
+ </details>
1313
+ * <details><summary>`ext precommit`</summary><br>
1314
+
1315
+ Prevent `.env` files from being committed to code.
1316
+
1317
+ ```sh
1318
+ $ dotenvx ext precommit
1319
+ [dotenvx][precommit] success
1320
+ ```
1321
+
1322
+ </details>
1323
+ * <details><summary>`ext precommit --install`</summary><br>
1324
+
1325
+ Install a shell script to `.git/hooks/pre-commit` to prevent accidentally committing any `.env` files to source control.
1326
+
1327
+ ```sh
1328
+ $ dotenvx ext precommit --install
1329
+ [dotenvx][precommit] dotenvx precommit installed [.git/hooks/pre-commit]
1330
+ ```
1331
+
1332
+ </details>
1333
+ * <details><summary>`ext prebuild`</summary><br>
1334
+
1335
+ Prevent `.env` files from being built into your docker containers.
1336
+
1337
+ Add it to your `Dockerfile`.
1338
+
1339
+ ```sh
1340
+ RUN curl -fsS https://dotenvx.sh | sh
1341
+
1342
+ ...
1343
+
1344
+ RUN dotenvx ext prebuild
1345
+ CMD ["dotenvx", "run", "--", "node", "index.js"]
1346
+ ```
1347
+
1348
+ </details>
1349
+ * <details><summary>`ext scan`</summary><br>
1350
+
1351
+ Use [gitleaks](https://gitleaks.io) under the hood to scan for possible secrets in your code.
1352
+
1353
+ ```sh
1354
+ $ dotenvx ext scan
1355
+
1356
+
1357
+ │╲
1358
+ │ ○
1359
+ ○ ░
1360
+ ░ gitleaks
1361
+
1362
+ 100 commits scanned.
1363
+ no leaks found
1364
+ ```
1365
+
1366
+ </details>
1367
+
1368
+ &nbsp;
1369
1369
 
1370
+ ## Guides
1371
+
1372
+ > Go deeper into using `dotenvx` with detailed framework and platform guides.
1373
+ >
1374
+
1375
+ * <a href="https://dotenvx.com/docs/platforms/digital-ocean">Digital Ocean <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/digitalocean.svg" alt="Digital Ocean Logo" width="20" height="20" style="fill:#0080FF;"></a>
1376
+ * <a href="https://dotenvx.com/docs/platforms/docker">Docker <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/docker.svg" alt="Docker Logo" width="20" height="20" style="fill:#2496ED;"></a>
1377
+ * <a href="https://dotenvx.com/docs/platforms/fly">Fly.io</a>
1378
+ * <a href="https://dotenvx.com/docs/cis/github-actions">GitHub Actions <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/github.svg" alt="GitHub Logo" width="20" height="20" style="fill:#181717;"></a>
1379
+ * <a href="https://dotenvx.com/docs/platforms/heroku">Heroku <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/heroku.svg" alt="Heroku Logo" width="20" height="20" style="fill:#430098;"></a>
1380
+ * <a href="https://dotenvx.com/docs/platforms/netlify">Netlify <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/netlify.svg" alt="Netlify Logo" width="20" height="20" style="fill:#00C7B7;"></a>
1381
+ * <a href="https://dotenvx.com/docs/package-managers/npm">NPM <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/npm.svg" alt="NPM Logo" width="20" height="20" style="fill:#CB3837;"></a>
1382
+ * <a href="https://dotenvx.com/docs/monorepos/nx">Nx <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/nx.svg" alt="Nx Logo" width="20" height="20" style="fill:#143055;"></a>
1383
+ * <a href="https://dotenvx.com/docs/platforms/render">Render <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/render.svg" alt="Render Logo" width="20" height="20" style="fill:#000000;"></a>
1384
+ * <a href="https://dotenvx.com/docs/platforms/railway">Railway <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/railway.svg" alt="Railway Logo" width="20" height="20" style="fill:#0B0D0E;"></a>
1385
+ * <a href="https://dotenvx.com/docs/monorepos/turborepo">Turborepo <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/turborepo.svg" alt="Turborepo Logo" width="20" height="20" style="fill:#EF4444;"></a>
1386
+ * <a href="https://dotenvx.com/docs/platforms/vercel">Vercel <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/vercel.svg" alt="Vercel Logo" width="20" height="20" style="fill:#000000;"></a>
1387
+ * [more](https://dotenvx.com/docs/guides)
1388
+ * <a href="https://dotenvx.com/docs/guides#node-js">Node.js <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/nodejs.svg" alt="Node.js Logo" width="20" height="20" style="fill:#5FA04E;"></a>
1389
+ * <a href="https://dotenvx.com/docs/guides#python">Python <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/python.svg" alt="Python Logo" width="20" height="20" style="fill:#3776AB;"></a>
1390
+ * <a href="https://dotenvx.com/docs/guides#php">PHP <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/php.svg" alt="PHP Logo" width="20" height="20" style="fill:#777BB4;"></a>
1391
+ * <a href="https://dotenvx.com/docs/guides#ruby">Ruby <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/ruby.svg" alt="Ruby Logo" width="20" height="20" style="fill:#CC342D;"></a>
1392
+ * <a href="https://dotenvx.com/docs/guides#rust">Rust <img src="https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/rust.svg" alt="Rust Logo" width="20" height="20" style="fill:#000000;"></a>
1370
1393
 
1371
1394
  &nbsp;
1372
1395
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0",
2
+ "version": "1.1.0",
3
3
  "name": "@dotenvx/dotenvx",
4
4
  "description": "a better dotenv–from the creator of `dotenv`",
5
5
  "author": "@motdotla",
@@ -15,6 +15,7 @@
15
15
  "CHANGELOG.md"
16
16
  ],
17
17
  "main": "src/lib/main.js",
18
+ "types": "src/lib/main.d.ts",
18
19
  "bin": {
19
20
  "dotenvx": "./src/cli/dotenvx.js",
20
21
  "git-dotenvx": "./src/cli/dotenvx.js"
@@ -36,7 +37,6 @@
36
37
  "conf": "^10.2.0",
37
38
  "diff": "^5.2.0",
38
39
  "dotenv": "^16.4.5",
39
- "dotenv-expand": "^11.0.6",
40
40
  "eciesjs": "^0.4.6",
41
41
  "execa": "^5.1.1",
42
42
  "glob": "^10.3.10",
@@ -0,0 +1,81 @@
1
+ // * /
2
+ // * (\\)? # is it escaped with a backslash?
3
+ // * (\$) # literal $
4
+ // * (?!\() # shouldnt be followed by parenthesis
5
+ // * (\{?) # first brace wrap opening
6
+ // * ([\w.]+) # key
7
+ // * (?::-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))? # optional default nested 3 times
8
+ // * (\}?) # last brace warp closing
9
+ // * /xi
10
+
11
+ const DOTENV_SUBSTITUTION_REGEX = /(\\)?(\$)(?!\()(\{?)([\w.]+)(?::?-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))?(\}?)/gi
12
+
13
+ function _resolveEscapeSequences (value) {
14
+ return value.replace(/\\\$/g, '$')
15
+ }
16
+
17
+ function interpolate (value, lookups) {
18
+ return value.replace(DOTENV_SUBSTITUTION_REGEX, (match, escaped, dollarSign, openBrace, key, defaultValue, closeBrace) => {
19
+ if (escaped === '\\') {
20
+ return match.slice(1)
21
+ } else {
22
+ if (lookups[key]) {
23
+ // avoid recursion from EXPAND_SELF=$EXPAND_SELF
24
+ if (lookups[key] === value) {
25
+ return lookups[key]
26
+ } else {
27
+ return interpolate(lookups[key], lookups)
28
+ }
29
+ }
30
+
31
+ if (defaultValue) {
32
+ if (defaultValue.startsWith('$')) {
33
+ return interpolate(defaultValue, lookups)
34
+ } else {
35
+ return defaultValue
36
+ }
37
+ }
38
+
39
+ return ''
40
+ }
41
+ })
42
+ }
43
+
44
+ function expand (options) {
45
+ let processEnv = process.env
46
+ if (options && options.processEnv != null) {
47
+ processEnv = options.processEnv
48
+ }
49
+
50
+ const combined = { ...processEnv, ...options.parsed }
51
+ const combinedReversed = { ...options.parsed, ...processEnv }
52
+
53
+ for (const key in options.parsed) {
54
+ const value = options.parsed[key]
55
+
56
+ // interpolate using both file and processEnv (file interpolation wins. used for --overload later)
57
+ const fileValue = _resolveEscapeSequences(interpolate(value, combined))
58
+ options.parsed[key] = fileValue
59
+
60
+ if (fileValue === _resolveEscapeSequences(value)) {
61
+ continue // no change means no expansion, move on
62
+ }
63
+
64
+ if (processEnv[key]) {
65
+ continue // already has a value in process.env, move on
66
+ }
67
+
68
+ // interpolate with processEnv only (used for default no overload)
69
+ const processEnvValue = interpolate(value, combinedReversed) // could be empty string ''
70
+ if (processEnvValue) {
71
+ processEnv[key] = _resolveEscapeSequences(processEnvValue) // set it
72
+ }
73
+ }
74
+
75
+ return {
76
+ parsed: options.parsed,
77
+ processEnv
78
+ }
79
+ }
80
+
81
+ module.exports.expand = expand
@@ -1,9 +1,9 @@
1
1
  const dotenv = require('dotenv')
2
- const dotenvExpand = require('dotenv-expand')
3
2
  const dotenvEval = require('./dotenvEval')
3
+ const dotenvExpand = require('./dotenvExpand')
4
4
  const decryptValue = require('./decryptValue')
5
5
 
6
- function parseDecryptEvalExpand (src, privateKey = null) {
6
+ function parseDecryptEvalExpand (src, privateKey = null, processEnv = process.env) {
7
7
  // parse
8
8
  const parsed = dotenv.parse(src)
9
9
 
@@ -22,19 +22,20 @@ function parseDecryptEvalExpand (src, privateKey = null) {
22
22
  }
23
23
  const evaled = dotenvEval.eval(inputParsed).parsed
24
24
 
25
- const expandPlease = {
26
- processEnv: {},
27
- parsed: { ...process.env, ...evaled } // always treat as overload, then later in the code the inject method takes care of actually setting on process.env via overload or not. this functions job is just to determine what the value would be
25
+ // expanded
26
+ const inputEvaled = {
27
+ processEnv,
28
+ parsed: evaled
28
29
  }
29
- const expanded = dotenvExpand.expand(expandPlease).parsed
30
+ const expanded = dotenvExpand.expand(inputEvaled)
30
31
 
31
- // but then for logging only log the original keys existing in parsed. this feels unnecessarily complex - like dotenv-expand should support the ability to inject additional `process.env` or objects as it sees fit to the object it wants to expand
32
+ // for logging only log the original keys existing in parsed. this feels unnecessarily complex - like dotenv-expand should support the ability to inject additional `process.env` or objects as it sees fit to the object it wants to expand
32
33
  const result = {}
33
34
  for (const key in parsed) {
34
- result[key] = expanded[key]
35
+ result[key] = expanded.parsed[key]
35
36
  }
36
37
 
37
- return result
38
+ return { parsed: result, processEnv }
38
39
  }
39
40
 
40
41
  module.exports = parseDecryptEvalExpand
@@ -0,0 +1,284 @@
1
+ import type { URL } from 'url';
2
+ import type { Change } from 'diff';
3
+
4
+ export interface DotenvParseOutput {
5
+ [name: string]: string;
6
+ }
7
+
8
+ /**
9
+ * Parses a string or buffer in the .env file format into an object.
10
+ *
11
+ * @see https://dotenvx.com/docs
12
+ * @param src - contents to be parsed. example: `'DB_HOST=localhost'`
13
+ * @returns an object with keys and values based on `src`. example: `{ DB_HOST : 'localhost' }`
14
+ */
15
+ export function parse<T extends DotenvParseOutput = DotenvParseOutput>(
16
+ src: string | Buffer
17
+ ): T;
18
+
19
+ export interface DotenvConfigOptions {
20
+ /** *
21
+ * Specify a custom path if your file containing environment variables is located elsewhere.
22
+ * Can also be an array of strings, specifying multiple paths.
23
+ *
24
+ * @default require('path').resolve(process.cwd(), '.env')
25
+ * @example require('@dotenvx/dotenvx').config({ path: '/custom/path/to/.env' })
26
+ * @example require('@dotenvx/dotenvx').config({ path: ['/path/to/first.env', '/path/to/second.env'] })
27
+ */
28
+ path?: string | string[] | URL;
29
+
30
+ /**
31
+ * Specify the encoding of your file containing environment variables.
32
+ *
33
+ * @default 'utf8'
34
+ * @example require('@dotenvx/dotenvx').config({ encoding: 'latin1' })
35
+ */
36
+ encoding?: string;
37
+
38
+ /**
39
+ * Turn on logging to help debug why certain keys or values are not being set as you expect.
40
+ *
41
+ * @default false
42
+ * @example require('@dotenvx/dotenvx').config({ debug: process.env.DEBUG })
43
+ */
44
+ debug?: boolean;
45
+
46
+ /**
47
+ * Override any environment variables that have already been set on your machine with values from your .env file.
48
+ * @default false
49
+ * @example require('@dotenvx/dotenvx').config({ override: true })
50
+ * @alias overload
51
+ */
52
+ override?: boolean;
53
+
54
+ /**
55
+ * @default false
56
+ * @alias override
57
+ */
58
+ overload?: boolean;
59
+
60
+ /**
61
+ * Specify an object to write your secrets to. Defaults to process.env environment variables.
62
+ *
63
+ * @default process.env
64
+ * @example const processEnv = {}; require('@dotenvx/dotenvx').config({ processEnv: processEnv })
65
+ */
66
+ processEnv?: DotenvPopulateInput;
67
+
68
+ /**
69
+ * Pass the DOTENV_KEY directly to config options. Defaults to looking for process.env.DOTENV_KEY environment variable. Note this only applies to decrypting .env.vault files. If passed as null or undefined, or not passed at all, dotenv falls back to its traditional job of parsing a .env file.
70
+ *
71
+ * @default undefined
72
+ * @example require('@dotenvx/dotenvx').config({ DOTENV_KEY: 'dotenv://:key_1234…@dotenvx.com/vault/.env.vault?environment=production' })
73
+ */
74
+ DOTENV_KEY?: string;
75
+
76
+ /**
77
+ * Do not warn for missing .env files
78
+ */
79
+ convention?: string;
80
+ }
81
+
82
+ export interface DotenvConfigOutput {
83
+ error?: Error;
84
+ parsed?: DotenvParseOutput;
85
+ }
86
+
87
+ export interface DotenvPopulateInput {
88
+ [name: string]: string;
89
+ }
90
+
91
+ /**
92
+ * Loads `.env` file contents into process.env by default. If `DOTENV_KEY` is present, it smartly attempts to load encrypted `.env.vault` file contents into process.env.
93
+ *
94
+ * @see https://dotenvx.com/docs
95
+ *
96
+ * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
97
+ * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
98
+ *
99
+ */
100
+ export function config(options?: DotenvConfigOptions): DotenvConfigOutput;
101
+
102
+ /**
103
+ * Loads `.env` file contents into process.env.
104
+ *
105
+ * @see https://dotenvx.com/docs
106
+ *
107
+ * @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
108
+ * @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
109
+ *
110
+ */
111
+ export function configDotenv(options?: DotenvConfigOptions): DotenvConfigOutput;
112
+
113
+ /**
114
+ * Decrypt ciphertext
115
+ *
116
+ * @see https://dotenvx.com/docs
117
+ *
118
+ * @param encrypted - the encrypted ciphertext string
119
+ * @param keyStr - the decryption key string
120
+ */
121
+ export function decrypt(encrypted: string, keyStr: string): string;
122
+
123
+ export type EncryptRowOutput = {
124
+ keys: string[];
125
+ filepath: string;
126
+ envFilepath: string;
127
+ publicKey: string;
128
+ privateKey: string;
129
+ privateKeyName: string;
130
+ privateKeyAdded: boolean;
131
+ envSrc: string;
132
+ changed: boolean;
133
+ error?: Error;
134
+ };
135
+
136
+ export type EncryptOutput = {
137
+ processedEnvFiles: EncryptRowOutput[];
138
+ changedFilepaths: string[];
139
+ unchangedFilepaths: string[];
140
+ };
141
+
142
+ /**
143
+ * Encrypt plaintext
144
+ *
145
+ * @see https://dotenvx.com/docs
146
+ * @param envFile - path to the .env file
147
+ */
148
+ export function encrypt(envFile: string): EncryptOutput;
149
+
150
+ export type VaultEncryptOutput = {
151
+ dotenvKeys: Record<string, string>;
152
+ dotenvKeysFile: string;
153
+ addedKeys: string[];
154
+ existingKeys: string[];
155
+ dotenvVaultFile: string;
156
+ addedVaults: string[];
157
+ existingVaults: string[];
158
+ addedDotenvFilenames: string[];
159
+ envFile: string | string[];
160
+ };
161
+
162
+ /**
163
+ * Encrypt plaintext
164
+ *
165
+ * @see https://dotenvx.com/docs
166
+ * @param directory - current working directory
167
+ * @param envFile - path to the .env file(s)
168
+ */
169
+ export function vaultEncrypt(
170
+ directory: string,
171
+ envFile: string | string[]
172
+ ): VaultEncryptOutput;
173
+
174
+ /**
175
+ * List all env files in the current working directory
176
+ *
177
+ * @param directory - current working directory
178
+ * @param envFile - glob pattern to match env files
179
+ */
180
+ export function ls(directory: string, envFile: string): string[];
181
+
182
+ /**
183
+ * Get the value of a key from the .env file
184
+ *
185
+ * @param [key] - the key to get the value of
186
+ * @param [envs] - the environment(s) to get the value from
187
+ * @param [overload] - whether to overload the value from the .env file
188
+ * @param [DOTENV_KEY] - the decryption key string
189
+ * @param [all] - whether to return all values
190
+ */
191
+ export function get(
192
+ key?: string,
193
+ envs: string[] = [],
194
+ overload = false,
195
+ DOTENV_KEY = '',
196
+ all = false
197
+ ): Record<string, string | undefined> | string | undefined;
198
+
199
+ export type SetOutput = {
200
+ key: string;
201
+ value: string;
202
+ filepath: string;
203
+ envFilepath: string;
204
+ envSrc: string;
205
+ changed: boolean;
206
+ encryptedValue?: string;
207
+ publicKey?: string;
208
+ privateKey?: string;
209
+ privateKeyAdded?: boolean;
210
+ privateKeyName?: string;
211
+ error?: Error;
212
+ };
213
+
214
+ /**
215
+ * Set the value of a key in the .env file
216
+ *
217
+ * @param key - the key to set the value of
218
+ * @param value - the value to set
219
+ * @param envFile - the path to the .env file
220
+ * @param [encrypt] - whether to encrypt the value
221
+ */
222
+ export function set(
223
+ key: string,
224
+ value: string,
225
+ envFile: string | string,
226
+ encrypt?: boolean
227
+ ): EncryptOutput;
228
+
229
+ type StatusRow = {
230
+ filename: string;
231
+ filepath: string;
232
+ environment: string;
233
+ raw: string;
234
+ decrypted: any;
235
+ differences: Change[];
236
+ };
237
+
238
+ export type StatusOutput = {
239
+ changes: StatusRow[];
240
+ nochanges: StatusRow[];
241
+ untracked: StatusRow[];
242
+ };
243
+
244
+ /**
245
+ * Check the differences between the .env file and the decrypted values
246
+ *
247
+ * @param directory - current working directory
248
+ */
249
+ export function status(directory: string): StatusOutput;
250
+
251
+ export type GenExampleOutput = {
252
+ envExampleFile: string;
253
+ envFile: string | string[];
254
+ exampleFilepath: string;
255
+ addedKeys: string[];
256
+ injected: Record<string, string>;
257
+ preExisted: Record<string, string>;
258
+ };
259
+
260
+ /**
261
+ * Generate an example .env file
262
+ *
263
+ * @param directory - current working directory
264
+ * @param envFile - path to the .env file(s)
265
+ */
266
+ export function genexample(
267
+ directory: string,
268
+ envFile: string
269
+ ): GenExampleOutput;
270
+
271
+ export type Settings = {
272
+ DOTENVX_SETTINGS_FILEPATH: string;
273
+ };
274
+
275
+ type KeyOfSettings = Extract<keyof Settings, string>;
276
+
277
+ /**
278
+ * Get the dotenvx settings
279
+ *
280
+ * @param [key] - the key to get the value of
281
+ */
282
+ export function settings(
283
+ key: KeyOfSettings | undefined | null = null
284
+ ): Settings;
package/src/lib/main.js CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  const path = require('path')
2
3
  const { logger } = require('./../shared/logger')
3
4
  const dotenv = require('dotenv')
@@ -18,6 +19,8 @@ const dotenvOptionPaths = require('./helpers/dotenvOptionPaths')
18
19
  const { setLogLevel } = require('../shared/logger')
19
20
 
20
21
  // proxies to dotenv
22
+
23
+ /** @type {import('./main').config} */
21
24
  const config = function (options = {}) {
22
25
  // allow user to set processEnv to write to
23
26
  let processEnv = process.env
@@ -44,29 +47,46 @@ const config = function (options = {}) {
44
47
  for (const optionPath of optionPaths) {
45
48
  // if DOTENV_KEY is set then assume we are checking envVaultFile
46
49
  if (DOTENV_KEY) {
47
- envs.push({ type: 'envVaultFile', value: path.join(path.dirname(optionPath), '.env.vault') })
50
+ envs.push({
51
+ type: 'envVaultFile',
52
+ value: path.join(path.dirname(optionPath), '.env.vault')
53
+ })
48
54
  } else {
49
55
  envs.push({ type: 'envFile', value: optionPath })
50
56
  }
51
57
  }
52
58
 
53
- const {
54
- processedEnvs,
55
- readableFilepaths,
56
- uniqueInjectedKeys
57
- } = new Run(envs, overload, DOTENV_KEY, processEnv).run()
59
+ const { processedEnvs, readableFilepaths, uniqueInjectedKeys } = new Run(
60
+ envs,
61
+ overload,
62
+ DOTENV_KEY,
63
+ processEnv
64
+ ).run()
58
65
 
59
66
  let lastError
67
+ /** @type {Record<string, string>} */
60
68
  const parsedAll = {}
61
69
 
62
70
  for (const processedEnv of processedEnvs) {
63
71
  if (processedEnv.type === 'envVaultFile') {
64
- logger.verbose(`loading env from encrypted ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
65
- logger.debug(`decrypting encrypted env from ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
72
+ logger.verbose(
73
+ `loading env from encrypted ${processedEnv.filepath} (${path.resolve(
74
+ processedEnv.filepath
75
+ )})`
76
+ )
77
+ logger.debug(
78
+ `decrypting encrypted env from ${
79
+ processedEnv.filepath
80
+ } (${path.resolve(processedEnv.filepath)})`
81
+ )
66
82
  }
67
83
 
68
84
  if (processedEnv.type === 'envFile') {
69
- logger.verbose(`loading env from ${processedEnv.filepath} (${path.resolve(processedEnv.filepath)})`)
85
+ logger.verbose(
86
+ `loading env from ${processedEnv.filepath} (${path.resolve(
87
+ processedEnv.filepath
88
+ )})`
89
+ )
70
90
  }
71
91
 
72
92
  if (processedEnv.error) {
@@ -76,7 +96,9 @@ const config = function (options = {}) {
76
96
  // do not warn for conventions (too noisy)
77
97
  if (!options.convention) {
78
98
  logger.warnv(processedEnv.error)
79
- logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.filepath}] and re-run [dotenvx run -- yourcommand]`)
99
+ logger.help(
100
+ `? add one with [echo "HELLO=World" > ${processedEnv.filepath}] and re-run [dotenvx run -- yourcommand]`
101
+ )
80
102
  }
81
103
  } else {
82
104
  logger.warnv(processedEnv.error)
@@ -99,8 +121,12 @@ const config = function (options = {}) {
99
121
  // verbose/debug preExisted key/value
100
122
  const preExisted = processedEnv.preExisted
101
123
  for (const [key, value] of Object.entries(preExisted)) {
102
- logger.verbose(`${key} pre-exists (protip: use --overload to override)`)
103
- logger.debug(`${key} pre-exists as ${value} (protip: use --overload to override)`)
124
+ logger.verbose(
125
+ `${key} pre-exists (protip: use --overload to override)`
126
+ )
127
+ logger.debug(
128
+ `${key} pre-exists as ${value} (protip: use --overload to override)`
129
+ )
104
130
  }
105
131
  }
106
132
  }
@@ -126,47 +152,66 @@ const config = function (options = {}) {
126
152
  }
127
153
  }
128
154
 
155
+ /** @type {import('./main').configDotenv} */
129
156
  const configDotenv = function (options) {
130
157
  return dotenv.configDotenv(options)
131
158
  }
132
159
 
160
+ /** @type {import('./main').parse} */
133
161
  const parse = function (src) {
134
162
  return dotenv.parse(src)
135
163
  }
136
164
 
165
+ /** @type {import('./main').vaultEncrypt} */
137
166
  const vaultEncrypt = function (directory, envFile) {
138
167
  return new VaultEncrypt(directory, envFile).run()
139
168
  }
140
169
 
170
+ /** @type {import('./main').ls} */
141
171
  const ls = function (directory, envFile) {
142
172
  return new Ls(directory, envFile).run()
143
173
  }
144
174
 
175
+ /** @type {import('./main').genexample} */
145
176
  const genexample = function (directory, envFile) {
146
177
  return new Genexample(directory, envFile).run()
147
178
  }
148
179
 
149
- const get = function (key, envs = [], overload = false, DOTENV_KEY = '', all = false) {
180
+ /** @type {import('./main').get} */
181
+ const get = function (
182
+ key,
183
+ envs = [],
184
+ overload = false,
185
+ DOTENV_KEY = '',
186
+ all = false
187
+ ) {
150
188
  return new Get(key, envs, overload, DOTENV_KEY, all).run()
151
189
  }
152
190
 
191
+ /** @type {import('./main').set} */
153
192
  const set = function (key, value, envFile, encrypt) {
154
193
  return new Sets(key, value, envFile, encrypt).run()
155
194
  }
156
195
 
196
+ /** @type {import('./main').encrypt} */
157
197
  const encrypt = function (envFile) {
158
198
  return new Encrypt(envFile).run()
159
199
  }
160
200
 
201
+ /** @type {import('./main').status} */
161
202
  const status = function (directory) {
162
203
  return new Status(directory).run()
163
204
  }
164
205
 
206
+ /** @type {import('./main').settings} */
165
207
  const settings = function (key = null) {
208
+ // @ts-ignore
166
209
  return new Settings(key).run()
167
210
  }
168
211
 
169
212
  // misc/cleanup
213
+
214
+ /** @type {import('./main').decrypt} */
170
215
  const decrypt = function (encrypted, keyStr) {
171
216
  try {
172
217
  return dotenv.decrypt(encrypted, keyStr)
@@ -174,9 +219,15 @@ const decrypt = function (encrypted, keyStr) {
174
219
  switch (e.code) {
175
220
  case 'DECRYPTION_FAILED':
176
221
  // more helpful error when decryption fails
177
- logger.error('[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.')
178
- logger.help('[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.')
179
- logger.debug(`[DECRYPTION_FAILED] DOTENV_KEY is ${process.env.DOTENV_KEY}`)
222
+ logger.error(
223
+ '[DECRYPTION_FAILED] Unable to decrypt .env.vault with DOTENV_KEY.'
224
+ )
225
+ logger.help(
226
+ '[DECRYPTION_FAILED] Run with debug flag [dotenvx run --debug -- yourcommand] or manually run [echo $DOTENV_KEY] to compare it to the one in .env.keys.'
227
+ )
228
+ logger.debug(
229
+ `[DECRYPTION_FAILED] DOTENV_KEY is ${process.env.DOTENV_KEY}`
230
+ )
180
231
  process.exit(1)
181
232
  break
182
233
  default:
@@ -57,7 +57,9 @@ class Genexample {
57
57
  }
58
58
 
59
59
  const currentEnvExample = dotenv.configDotenv({ path: exampleFilepath }).parsed
60
+ /** @type {Record<string, string>} */
60
61
  const injected = {}
62
+ /** @type {Record<string, string>} */
61
63
  const preExisted = {}
62
64
 
63
65
  for (const key of [...keys]) {
@@ -21,6 +21,7 @@ class Get {
21
21
 
22
22
  // typical scenario - return only envs that were identified in the .env file
23
23
  // iterate over all processedEnvs.parsed and grab from processEnv
24
+ /** @type {Record<string, string>} */
24
25
  const result = {}
25
26
  for (const processedEnv of processedEnvs) {
26
27
  // parsed means we saw the key in a file or --env flag. this effectively filters out any preset machine envs - while still respecting complex evaluating, expansion, and overload. in other words, the value might be the machine value because the key was displayed in a .env file
@@ -63,7 +63,8 @@ class Run {
63
63
  row.string = env
64
64
 
65
65
  try {
66
- const parsed = parseDecryptEvalExpand(env)
66
+ const { parsed } = parseDecryptEvalExpand(env, null, this.processEnv)
67
+
67
68
  row.parsed = parsed
68
69
  this.readableStrings.add(env)
69
70
 
@@ -93,7 +94,7 @@ class Run {
93
94
 
94
95
  // if DOTENV_PRIVATE_KEY_* already set in process.env then use it
95
96
  const privateKey = smartDotenvPrivateKey(envFilepath)
96
- const parsed = parseDecryptEvalExpand(src, privateKey)
97
+ const { parsed } = parseDecryptEvalExpand(src, privateKey, this.processEnv)
97
98
  row.parsed = parsed
98
99
 
99
100
  const { injected, preExisted } = this._inject(this.processEnv, parsed, this.overload)
@@ -162,7 +163,7 @@ class Run {
162
163
 
163
164
  try {
164
165
  // parse this. it's the equivalent of the .env file
165
- const parsed = parseDecryptEvalExpand(decrypted)
166
+ const { parsed } = parseDecryptEvalExpand(decrypted, null, this.processEnv)
166
167
  row.parsed = parsed
167
168
 
168
169
  const { injected, preExisted } = this._inject(this.processEnv, parsed, this.overload)