From 109205bca790a211526a5cb656d1a5793974b9c1 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Mon, 30 Dec 2024 12:53:10 +0300 Subject: [PATCH] feat: expose dotenv API (#1032) * feat: handle multilines in env files continues #974 * fix: check donenv names * fix: handle dotenv comments * fix: handle tabs in dotenvs * fix: handle backtick in dotenv * chore: parseDotenv tweak ups * chore: shrink a few bytes * chore: dotenv parse imprs * chore: move custom dotenv parser to external pkg * chore: linting * chore: rebase * feat: reexport `envapi` --- .size-limit.json | 6 +++--- docs/api.md | 3 ++- package-lock.json | 8 ++++++++ package.json | 1 + scripts/build-dts.mjs | 1 + scripts/build-tests.mjs | 2 +- src/goods.ts | 44 ----------------------------------------- src/index.ts | 1 + src/vendor-extra.ts | 1 + test/cli.test.js | 2 +- test/export.test.js | 7 +++++++ test/goods.test.js | 27 ++++++++++++++++++------- test/index.test.js | 2 ++ 13 files changed, 48 insertions(+), 57 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 6560a4f3a6..6fcc8c0557 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -9,7 +9,7 @@ { "name": "zx/index", "path": "build/*.{js,cjs}", - "limit": "805 kB", + "limit": "806 kB", "brotli": false, "gzip": false }, @@ -23,14 +23,14 @@ { "name": "vendor", "path": "build/vendor-*", - "limit": "761 kB", + "limit": "763 kB", "brotli": false, "gzip": false }, { "name": "all", "path": "build/*", - "limit": "842 kB", + "limit": "844 kB", "brotli": false, "gzip": false } diff --git a/docs/api.md b/docs/api.md index 320a0342d7..23e58ef045 100644 --- a/docs/api.md +++ b/docs/api.md @@ -366,7 +366,8 @@ console.log(YAML.parse('foo: bar').foo) ``` ## dotenv -[dotenv](https://www.npmjs.com/package/dotenv)-like environment variables loading API +The [envapi](https://www.npmjs.com/package/envapi) package. +An API to interact with environment vars in [dotenv](https://www.npmjs.com/package/dotenv) format. ```js // parse diff --git a/package-lock.json b/package-lock.json index 5e549eeaa9..391f07de13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "create-require": "^1.1.1", "depseek": "^0.4.1", "dts-bundle-generator": "^9.5.1", + "envapi": "^0.1.0", "esbuild": "^0.24.2", "esbuild-node-externals": "^1.16.0", "esbuild-plugin-entry-chunks": "^0.1.15", @@ -2505,6 +2506,13 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/envapi": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/envapi/-/envapi-0.1.0.tgz", + "integrity": "sha512-qlegDm1lfcl+azrvph6SAPPs1TY6HYZJ6SWf+iW76mD3VAzl7pxSRXLDnCVe75HOX+9OZIQeuO7a16bOcnPFbg==", + "dev": true, + "license": "MIT" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", diff --git a/package.json b/package.json index 8b9efb2407..392372cd90 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "create-require": "^1.1.1", "depseek": "^0.4.1", "dts-bundle-generator": "^9.5.1", + "envapi": "^0.1.0", "esbuild": "^0.24.2", "esbuild-node-externals": "^1.16.0", "esbuild-plugin-entry-chunks": "^0.1.15", diff --git a/scripts/build-dts.mjs b/scripts/build-dts.mjs index 373f2fb258..cd0f8ae555 100644 --- a/scripts/build-dts.mjs +++ b/scripts/build-dts.mjs @@ -45,6 +45,7 @@ const entries = [ // '@webpod/ps', '@webpod/ingrid', 'depseek', + 'envapi', ], // args['external-inlines'], }, output, diff --git a/scripts/build-tests.mjs b/scripts/build-tests.mjs index 1aea865a03..56a0456407 100644 --- a/scripts/build-tests.mjs +++ b/scripts/build-tests.mjs @@ -26,7 +26,7 @@ const modules = [ ['core', core], ['cli', cli], ['index', index], - ['vendor', vendor, ['chalk', 'depseek', 'fs', 'glob', 'minimist', 'ps', 'which', 'YAML',]], + ['vendor', vendor, ['chalk', 'depseek', 'dotenv', 'fs', 'glob', 'minimist', 'ps', 'which', 'YAML',]], ] const root = path.resolve(new URL(import.meta.url).pathname, '../..') const filePath = path.resolve(root, `test/export.test.js`) diff --git a/src/goods.ts b/src/goods.ts index 511d725527..297a652b86 100644 --- a/src/goods.ts +++ b/src/goods.ts @@ -219,47 +219,3 @@ export async function spinner( } }) } - -/** - * Read env files and collects it into environment variables - */ -export const dotenv = (() => { - const parse = (content: string | Buffer): NodeJS.ProcessEnv => - content - .toString() - .split(/\r?\n/) - .reduce((r, line) => { - if (line.startsWith('export ')) line = line.slice(7) - const i = line.indexOf('=') - const k = line.slice(0, i).trim() - const v = line.slice(i + 1).trim() - if (k && v) r[k] = v - return r - }, {}) - - const _load = ( - read: (file: string) => string, - ...files: string[] - ): NodeJS.ProcessEnv => - files - .reverse() - .reduce((m, f) => Object.assign(m, parse(read(path.resolve(f)))), {}) - const load = (...files: string[]): NodeJS.ProcessEnv => - _load((file) => fs.readFileSync(file, 'utf8'), ...files) - const loadSafe = (...files: string[]): NodeJS.ProcessEnv => - _load( - (file: string): string => - fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '', - ...files - ) - - const config = (def = '.env', ...files: string[]): NodeJS.ProcessEnv => - Object.assign(process.env, loadSafe(def, ...files)) - - return { - parse, - load, - loadSafe, - config, - } -})() diff --git a/src/index.ts b/src/index.ts index 59f92e0f79..2de80d056a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export * from './goods.js' export { minimist, chalk, + dotenv, fs, which, YAML, diff --git a/src/vendor-extra.ts b/src/vendor-extra.ts index c411551adc..3a29fd3251 100644 --- a/src/vendor-extra.ts +++ b/src/vendor-extra.ts @@ -116,3 +116,4 @@ export const fs: typeof import('fs-extra') = _fs export { depseekSync as depseek } from 'depseek' export { default as minimist } from 'minimist' +export { default as dotenv } from 'envapi' diff --git a/test/cli.test.js b/test/cli.test.js index a15170f6ba..193f67b4fc 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -137,7 +137,7 @@ describe('cli', () => { assert.ok(p.stderr.endsWith(cwd + '\n')) }) - test('supports `--env` options with file', async () => { + test('supports `--env` option', async () => { const env = tmpfile( '.env', `FOO=BAR diff --git a/test/export.test.js b/test/export.test.js index 5593841efd..a2a0e5f5c1 100644 --- a/test/export.test.js +++ b/test/export.test.js @@ -159,6 +159,7 @@ describe('index', () => { assert.equal(typeof index.dotenv.load, 'function', 'index.dotenv.load') assert.equal(typeof index.dotenv.loadSafe, 'function', 'index.dotenv.loadSafe') assert.equal(typeof index.dotenv.parse, 'function', 'index.dotenv.parse') + assert.equal(typeof index.dotenv.stringify, 'function', 'index.dotenv.stringify') assert.equal(typeof index.echo, 'function', 'index.echo') assert.equal(typeof index.expBackoff, 'function', 'index.expBackoff') assert.equal(typeof index.fetch, 'function', 'index.fetch') @@ -420,6 +421,12 @@ describe('vendor', () => { assert.equal(typeof vendor.chalk, 'function', 'vendor.chalk') assert.equal(typeof vendor.chalk.level, 'number', 'vendor.chalk.level') assert.equal(typeof vendor.depseek, 'function', 'vendor.depseek') + assert.equal(typeof vendor.dotenv, 'object', 'vendor.dotenv') + assert.equal(typeof vendor.dotenv.config, 'function', 'vendor.dotenv.config') + assert.equal(typeof vendor.dotenv.load, 'function', 'vendor.dotenv.load') + assert.equal(typeof vendor.dotenv.loadSafe, 'function', 'vendor.dotenv.loadSafe') + assert.equal(typeof vendor.dotenv.parse, 'function', 'vendor.dotenv.parse') + assert.equal(typeof vendor.dotenv.stringify, 'function', 'vendor.dotenv.stringify') assert.equal(typeof vendor.fs, 'object', 'vendor.fs') assert.equal(typeof vendor.fs.Dir, 'function', 'vendor.fs.Dir') assert.equal(typeof vendor.fs.Dirent, 'function', 'vendor.fs.Dirent') diff --git a/test/goods.test.js b/test/goods.test.js index 18db48ebe9..c620451c67 100644 --- a/test/goods.test.js +++ b/test/goods.test.js @@ -14,8 +14,8 @@ import assert from 'node:assert' import { test, describe, after } from 'node:test' -import { $, chalk, fs, tempfile } from '../build/index.js' -import { echo, sleep, parseArgv, dotenv } from '../build/goods.js' +import { $, chalk, fs, tempfile, dotenv } from '../build/index.js' +import { echo, sleep, parseArgv } from '../build/goods.js' describe('goods', () => { function zx(script) { @@ -176,6 +176,7 @@ describe('goods', () => { describe('dotenv', () => { test('parse()', () => { + assert.deepEqual(dotenv.parse(''), {}) assert.deepEqual( dotenv.parse('ENV=v1\nENV2=v2\n\n\n ENV3 = v3 \nexport ENV4=v4'), { @@ -185,15 +186,27 @@ describe('goods', () => { ENV4: 'v4', } ) - assert.deepEqual(dotenv.parse(''), {}) - // TBD: multiline const multiline = `SIMPLE=xyz123 -NON_INTERPOLATED='raw text without variable interpolation' +# comment ### +NON_INTERPOLATED='raw text without variable interpolation' MULTILINE = """ -long text here, +long text here, # not-comment e.g. a private SSH key -"""` +""" +ENV=v1\nENV2=v2\n\n\n\t\t ENV3 = v3 \n export ENV4=v4 +ENV5=v5 # comment +` + assert.deepEqual(dotenv.parse(multiline), { + SIMPLE: 'xyz123', + NON_INTERPOLATED: 'raw text without variable interpolation', + MULTILINE: 'long text here, # not-comment\ne.g. a private SSH key', + ENV: 'v1', + ENV2: 'v2', + ENV3: 'v3', + ENV4: 'v4', + ENV5: 'v5', + }) }) describe('load()', () => { diff --git a/test/index.test.js b/test/index.test.js index 768d06fc5b..53368f8e22 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -30,6 +30,7 @@ import { ProcessOutput, ProcessPromise, defaults, + dotenv, minimist, chalk, fs, @@ -106,6 +107,7 @@ describe('index', () => { assert(which) assert(YAML) assert(ps) + assert(dotenv) // utils assert(quote)