Commit efc4800

Mikhail Avdeev <39246971+easymikey@users.noreply.github.com>
2024-12-27 12:38:12
feat: provide .env API (#1034)
closes #975 * chore: lint test * feat: provide support for .env files injects via API * chore: lint * chore: update test --------- Co-authored-by: Anton Golub <antongolub@antongolub.com>
1 parent ae83b4b
docs/v7/api.md
@@ -204,3 +204,17 @@ The [yaml](https://www.npmjs.com/package/yaml) package.
 ```js
 console.log(YAML.parse('foo: bar').foo)
 ```
+
+
+## loadDotenv
+
+Read env files and collects it into environment variables.
+
+```js
+const env = loadDotenv(env1, env2)
+console.log((await $({ env })`echo $FOO`).stdout)
+---
+const env = loadDotenv(env1)
+$.env = env
+console.log((await $`echo $FOO`).stdout)
+```
\ No newline at end of file
src/goods.ts
@@ -21,6 +21,7 @@ import {
   isStringLiteral,
   parseBool,
   parseDuration,
+  readEnvFromFile,
   toCamelCase,
 } from './util.js'
 import {
@@ -217,3 +218,10 @@ export async function spinner<T>(
     }
   })
 }
+
+/**
+ *
+ * Read env files and collects it into environment variables
+ */
+export const loadDotenv = (...files: string[]): NodeJS.ProcessEnv =>
+  files.reduce<NodeJS.ProcessEnv>((m, f) => readEnvFromFile(f, m), {})
test/cli.test.js
@@ -17,21 +17,8 @@ import { test, describe, before, after } from 'node:test'
 import { fileURLToPath } from 'node:url'
 import net from 'node:net'
 import getPort from 'get-port'
-import {
-  argv,
-  importPath,
-  injectGlobalRequire,
-  isMain,
-  main,
-  normalizeExt,
-  runScript,
-  printUsage,
-  scriptFromStdin,
-  scriptFromHttp,
-  transformMarkdown,
-  writeAndImport,
-} from '../build/cli.js'
-import { $, path, fs, tmpfile, tmpdir } from '../build/index.js'
+import { $, path, tmpfile, tmpdir, fs } from '../build/index.js'
+import { isMain, normalizeExt, transformMarkdown } from '../build/cli.js'
 
 const __filename = fileURLToPath(import.meta.url)
 const spawn = $.spawn
test/core.test.js
@@ -26,24 +26,20 @@ import {
   resolveDefaults,
   cd,
   syncProcessCwd,
-  log,
-  kill,
-  defaults,
   within,
   usePowerShell,
   usePwsh,
   useBash,
 } from '../build/core.js'
 import {
+  tempfile,
   fs,
-  nothrow,
-  quiet,
+  quote,
+  quotePowerShell,
   sleep,
-  tempfile,
-  tempdir,
+  quiet,
   which,
 } from '../build/index.js'
-import { quote, quotePowerShell } from '../build/util.js'
 
 describe('core', () => {
   describe('resolveDefaults()', () => {
test/export.test.js
@@ -331,6 +331,7 @@ describe('index', () => {
     assert.equal(typeof index.globby.isGitIgnored, 'function', 'index.globby.isGitIgnored')
     assert.equal(typeof index.globby.isGitIgnoredSync, 'function', 'index.globby.isGitIgnoredSync')
     assert.equal(typeof index.kill, 'function', 'index.kill')
+    assert.equal(typeof index.loadDotenv, 'function', 'index.loadDotenv')
     assert.equal(typeof index.log, 'function', 'index.log')
     assert.equal(typeof index.minimist, 'function', 'index.minimist')
     assert.equal(typeof index.nothrow, 'function', 'index.nothrow')
test/goods.test.js
@@ -13,9 +13,9 @@
 // limitations under the License.
 
 import assert from 'node:assert'
-import { test, describe } from 'node:test'
-import { $, chalk } from '../build/index.js'
-import { echo, sleep, parseArgv } from '../build/goods.js'
+import { test, describe, after } from 'node:test'
+import { $, chalk, fs, tempfile } from '../build/index.js'
+import { echo, sleep, parseArgv, loadDotenv } from '../build/goods.js'
 
 describe('goods', () => {
   function zx(script) {
@@ -173,4 +173,45 @@ describe('goods', () => {
       }
     )
   })
+
+  describe('loadDotenv()', () => {
+    const env1 = tempfile(
+      '.env',
+      `FOO=BAR
+            BAR=FOO+`
+    )
+    const env2 = tempfile('.env.default', `BAR2=FOO2`)
+
+    after(() => {
+      fs.remove(env1)
+      fs.remove(env2)
+    })
+
+    test('handles multiple dotenv files', async () => {
+      const env = loadDotenv(env1, env2)
+
+      assert.equal((await $({ env })`echo $FOO`).stdout, 'BAR\n')
+      assert.equal((await $({ env })`echo $BAR`).stdout, 'FOO+\n')
+      assert.equal((await $({ env })`echo $BAR2`).stdout, 'FOO2\n')
+    })
+
+    test('handles replace evn', async () => {
+      const env = loadDotenv(env1)
+      $.env = env
+      assert.equal((await $`echo $FOO`).stdout, 'BAR\n')
+      assert.equal((await $`echo $BAR`).stdout, 'FOO+\n')
+      $.env = process.env
+    })
+
+    test('handle error', async () => {
+      try {
+        loadDotenv('./.env')
+
+        assert.throw()
+      } catch (e) {
+        assert.equal(e.code, 'ENOENT')
+        assert.equal(e.errno, -2)
+      }
+    })
+  })
 })
test/util.test.js
@@ -14,7 +14,8 @@
 
 import assert from 'node:assert'
 import fs from 'node:fs'
-import { test, describe } from 'node:test'
+import { test, describe, after } from 'node:test'
+import { fs as fsCore } from '../build/index.js'
 import {
   formatCmd,
   isString,
@@ -164,8 +165,10 @@ e.g. a private SSH key
 })
 
 describe('readEnvFromFile()', () => {
+  const file = tempfile('.env', 'ENV=value1\nENV2=value24')
+  after(() => fsCore.remove(file))
+
   test('handles correct proccess.env', () => {
-    const file = tempfile('.env', 'ENV=value1\nENV2=value24')
     const env = readEnvFromFile(file)
     assert.equal(env.ENV, 'value1')
     assert.equal(env.ENV2, 'value24')
@@ -173,7 +176,6 @@ describe('readEnvFromFile()', () => {
   })
 
   test('handles correct some env', () => {
-    const file = tempfile('.env', 'ENV=value1\nENV2=value24')
     const env = readEnvFromFile(file, { version: '1.0.0', name: 'zx' })
     assert.equal(env.ENV, 'value1')
     assert.equal(env.ENV2, 'value24')