From 36b42d84fd29bcea18fc502ca7afeae70f993a51 Mon Sep 17 00:00:00 2001 From: Mikhail Avdeev <39246971+easymikey@users.noreply.github.com> Date: Tue, 24 Dec 2024 11:23:50 +0300 Subject: [PATCH] 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 --- .size-limit.json | 6 +++--- docs/cli.md | 13 +++++++++++++ man/zx.1 | 2 ++ src/cli.ts | 9 +++++++-- src/util.ts | 21 ++++++++++++++++++++ test/cli.test.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++ test/util.test.js | 29 ++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 5 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 6c1dfffd49..eedbac486c 100644 --- a/.size-limit.json +++ b/.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 } diff --git a/docs/cli.md b/docs/cli.md index 1c00b717b2..43ddbbd9e1 100644 --- a/docs/cli.md +++ b/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`. diff --git a/man/zx.1 b/man/zx.1 index b6f23a9c47..8279582e1d 100644 --- a/man/zx.1 +++ b/man/zx.1 @@ -31,6 +31,8 @@ install dependencies npm registry, defaults to https://registry.npmjs.org/ .SS --repl start repl +.SS --env= +path to env file .SS --version, -v print current zx version .SS --help, -h diff --git a/src/cli.ts b/src/cli.ts index 74d2b60853..23c281e8f3 100644 --- a/src/cli.ts +++ b/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 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 diff --git a/src/util.ts b/src/util.ts index a79186ad97..b3fc5a5938 100644 --- a/src/util.ts +++ b/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((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), + } +} diff --git a/test/cli.test.js b/test/cli.test.js index 0d724006b8..ebaa02f93b 100644 --- a/test/cli.test.js +++ b/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() diff --git a/test/util.test.js b/test/util.test.js index 7eef6914dd..1ad6f28421 100644 --- a/test/util.test.js +++ b/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') + }) +})