Commit 36b42d8

Mikhail Avdeev <39246971+easymikey@users.noreply.github.com>
2024-12-24 09:23:50
feat(cli): add env files support (#1022)
* feat: add support env files * feat: update size limit * chore: update test * fix: review * chore: update size limit * fix: update env path * chore: update size limit * chore: update man * chore: update size limit * fix: replace split by limit * refactor: update parseDotenv * docs: update docs * fix: add line trim * test: add test for file reading error * docs: update docs * chore: prettify test * chore: delete dot
1 parent 504a960
docs/cli.md
@@ -118,6 +118,19 @@ Set the current working directory.
 zx --cwd=/foo/bar script.mjs
 ```
 
+## --env
+Specify a env file.
+
+```bash
+zx --env=/path/to/some.env script.mjs
+```
+
+When cwd option is specified, it will used as base path: `--cwd='/foo/bar' --env='../.env'` → `/foo/.env`
+
+```bash
+zx --cwd=/foo/bar --env=/path/to/some.env script.mjs
+```
+
 ## --ext
 
 Override the default (temp) script extension. Default is `.mjs`.
man/zx.1
@@ -31,6 +31,8 @@ install dependencies
 npm registry, defaults to https://registry.npmjs.org/
 .SS --repl
 start repl
+.SS --env=<path>
+path to env file
 .SS --version, -v
 print current zx version
 .SS --help, -h
src/cli.ts
@@ -27,7 +27,7 @@ import {
   parseArgv,
 } from './index.js'
 import { installDeps, parseDeps } from './deps.js'
-import { randomId } from './util.js'
+import { readEnvFromFile, randomId } from './util.js'
 import { createRequire } from './vendor.js'
 
 const EXT = '.mjs'
@@ -66,6 +66,7 @@ export function printUsage() {
    --version, -v        print current zx version
    --help, -h           print help
    --repl               start repl
+   --env=<path>         path to env file
    --experimental       enables experimental features (deprecated)
 
  ${chalk.italic('Full documentation:')} ${chalk.underline('https://google.github.io/zx/')}
@@ -74,7 +75,7 @@ export function printUsage() {
 
 // prettier-ignore
 export const argv = parseArgv(process.argv.slice(2), {
-  string: ['shell', 'prefix', 'postfix', 'eval', 'cwd', 'ext', 'registry'],
+  string: ['shell', 'prefix', 'postfix', 'eval', 'cwd', 'ext', 'registry', 'env'],
   boolean: ['version', 'help', 'quiet', 'verbose', 'install', 'repl', 'experimental', 'prefer-local'],
   alias: { e: 'eval', i: 'install', v: 'version', h: 'help', l: 'prefer-local' },
   stopEarly: true,
@@ -86,6 +87,10 @@ export async function main() {
   await import('./globals.js')
   argv.ext = normalizeExt(argv.ext)
   if (argv.cwd) $.cwd = argv.cwd
+  if (argv.env) {
+    const envPath = path.resolve($.cwd ?? process.cwd(), argv.env)
+    $.env = readEnvFromFile(envPath, process.env)
+  }
   if (argv.verbose) $.verbose = true
   if (argv.quiet) $.quiet = true
   if (argv.shell) $.shell = argv.shell
src/util.ts
@@ -357,3 +357,24 @@ export const toCamelCase = (str: string) =>
 
 export const parseBool = (v: string): boolean | string =>
   ({ true: true, false: false })[v] ?? v
+
+export const parseDotenv = (content: string): NodeJS.ProcessEnv => {
+  return content.split(/\r?\n/).reduce<NodeJS.ProcessEnv>((r, line) => {
+    const [k] = line.trim().split('=', 1)
+    const v = line.trim().slice(k.length + 1)
+    if (k && v) r[k] = v
+    return r
+  }, {})
+}
+
+export const readEnvFromFile = (
+  filepath: string,
+  env: NodeJS.ProcessEnv = process.env
+): NodeJS.ProcessEnv => {
+  const content = fs.readFileSync(path.resolve(filepath), 'utf8')
+
+  return {
+    ...env,
+    ...parseDotenv(content),
+  }
+}
test/cli.test.js
@@ -150,6 +150,55 @@ describe('cli', () => {
     assert.ok(p.stderr.endsWith(cwd + '\n'))
   })
 
+  test('supports `--env` options with file', async () => {
+    const env = tmpfile(
+      '.env',
+      `FOO=BAR
+      BAR=FOO+`
+    )
+    const file = `
+    console.log((await $\`echo $FOO\`).stdout);
+    console.log((await $\`echo $BAR\`).stdout)
+    `
+
+    const out = await $`node build/cli.js --env=${env} <<< ${file}`
+    fs.remove(env)
+    assert.equal(out.stdout, 'BAR\n\nFOO+\n\n')
+  })
+
+  test('supports `--env` and `--cwd` options with file', async () => {
+    const env = tmpfile(
+      '.env',
+      `FOO=BAR
+      BAR=FOO+`
+    )
+    const dir = tmpdir()
+    const file = `
+      console.log((await $\`echo $FOO\`).stdout);
+      console.log((await $\`echo $BAR\`).stdout)
+      `
+
+    const out =
+      await $`node build/cli.js --cwd=${dir} --env=${env}  <<< ${file}`
+    fs.remove(env)
+    fs.remove(dir)
+    assert.equal(out.stdout, 'BAR\n\nFOO+\n\n')
+  })
+
+  test('supports handling errors with the `--env` option', async () => {
+    const file = `
+      console.log((await $\`echo $FOO\`).stdout);
+      console.log((await $\`echo $BAR\`).stdout)
+      `
+    try {
+      await $`node build/cli.js --env=./env <<< ${file}`
+      fs.remove(env)
+      assert.throw()
+    } catch (e) {
+      assert.equal(e.exitCode, 1)
+    }
+  })
+
   test('scripts from https 200', async () => {
     const resp = await fs.readFile(path.resolve('test/fixtures/echo.http'))
     const port = await getPort()
test/util.test.js
@@ -29,6 +29,8 @@ import {
   tempfile,
   preferLocalBin,
   toCamelCase,
+  parseDotenv,
+  readEnvFromFile,
 } from '../build/util.js'
 
 describe('util', () => {
@@ -139,3 +141,30 @@ describe('util', () => {
     assert.equal(toCamelCase('kebab-input-str'), 'kebabInputStr')
   })
 })
+
+test('parseDotenv()', () => {
+  assert.deepEqual(parseDotenv('ENV=value1\nENV2=value24'), {
+    ENV: 'value1',
+    ENV2: 'value24',
+  })
+  assert.deepEqual(parseDotenv(''), {})
+})
+
+describe('readEnvFromFile()', () => {
+  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')
+    assert.ok(env.NODE_VERSION !== '')
+  })
+
+  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')
+    assert.equal(env.version, '1.0.0')
+    assert.equal(env.name, 'zx')
+  })
+})
.size-limit.json
@@ -2,14 +2,14 @@
   {
     "name": "zx/core",
     "path": ["build/core.cjs", "build/util.cjs", "build/vendor-core.cjs"],
-    "limit": "76 kB",
+    "limit": "77 kB",
     "brotli": false,
     "gzip": false
   },
   {
     "name": "zx/index",
     "path": "build/*.{js,cjs}",
-    "limit": "804 kB",
+    "limit": "805 kB",
     "brotli": false,
     "gzip": false
   },
@@ -30,7 +30,7 @@
   {
     "name": "all",
     "path": "build/*",
-    "limit": "841 kB",
+    "limit": "842 kB",
     "brotli": false,
     "gzip": false
   }