From f1ca807532cce3a9523826a5f6c884d60a85c593 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 28 Dec 2024 11:47:38 +0300 Subject: [PATCH] refactor: assemble dotenv utils (#1043) * refactor: assemble dotenv utils continues #1034 * chore: prettier --- .github/workflows/codeql.yml | 56 +++++++++++----------- docs/api.md | 18 +++++++ docs/v7/api.md | 14 ------ src/cli.ts | 9 ++-- src/goods.ts | 46 ++++++++++++++++-- src/util.ts | 22 --------- test/export.test.js | 6 ++- test/goods.test.js | 91 ++++++++++++++++++++++++------------ test/util.test.js | 43 ----------------- 9 files changed, 158 insertions(+), 147 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a1c680a116..3fb56b5776 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,10 +1,10 @@ -name: "CodeQL Advanced" +name: 'CodeQL Advanced' on: push: - branches: [ "main" ] + branches: ['main'] pull_request: - branches: [ "main" ] + branches: ['main'] schedule: - cron: '28 6 * * 3' @@ -28,29 +28,29 @@ jobs: fail-fast: false matrix: include: - - language: javascript-typescript - build-mode: none + - language: javascript-typescript + build-mode: none steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: '/language:${{matrix.language}}' diff --git a/docs/api.md b/docs/api.md index 93b6000c91..320a0342d7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -364,3 +364,21 @@ The [yaml](https://www.npmjs.com/package/yaml) package. ```js console.log(YAML.parse('foo: bar').foo) ``` + +## dotenv +[dotenv](https://www.npmjs.com/package/dotenv)-like environment variables loading API + +```js +// parse +const raw = 'FOO=BAR\nBAZ=QUX' +const data = dotenv.parse(raw) // {FOO: 'BAR', BAZ: 'QUX'} +await fs.writeFile('.env', raw) + +// load +const env = dotenv.load('.env') +await $({ env })`echo $FOO`.stdout // BAR + +// config +dotenv.config('.env') +process.env.FOO // BAR +``` diff --git a/docs/v7/api.md b/docs/v7/api.md index 1601009c8b..b7fd77d4dc 100644 --- a/docs/v7/api.md +++ b/docs/v7/api.md @@ -204,17 +204,3 @@ 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 diff --git a/src/cli.ts b/src/cli.ts index 6ad9384080..0f31c26c2e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -18,16 +18,17 @@ import url from 'node:url' import { $, ProcessOutput, + parseArgv, updateArgv, - fetch, chalk, + dotenv, + fetch, fs, path, VERSION, - parseArgv, } from './index.js' import { installDeps, parseDeps } from './deps.js' -import { readEnvFromFile, randomId } from './util.js' +import { randomId } from './util.js' import { createRequire } from './vendor.js' const EXT = '.mjs' @@ -89,7 +90,7 @@ export async function main() { if (argv.cwd) $.cwd = argv.cwd if (argv.env) { const envPath = path.resolve($.cwd ?? process.cwd(), argv.env) - $.env = readEnvFromFile(envPath, process.env) + $.env = { ...process.env, ...dotenv.load(envPath) } } if (argv.verbose) $.verbose = true if (argv.quiet) $.quiet = true diff --git a/src/goods.ts b/src/goods.ts index 88ae1dd4fb..511d725527 100644 --- a/src/goods.ts +++ b/src/goods.ts @@ -14,6 +14,7 @@ import assert from 'node:assert' import { createInterface } from 'node:readline' +import { default as path } from 'node:path' import { $, within, ProcessOutput } from './core.js' import { type Duration, @@ -21,10 +22,10 @@ import { isStringLiteral, parseBool, parseDuration, - readEnvFromFile, toCamelCase, } from './util.js' import { + fs, minimist, nodeFetch, type RequestInfo, @@ -220,8 +221,45 @@ export async function spinner( } /** - * * Read env files and collects it into environment variables */ -export const loadDotenv = (...files: string[]): NodeJS.ProcessEnv => - files.reduce((m, f) => readEnvFromFile(f, m), {}) +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/util.ts b/src/util.ts index ca5cb47b51..2f63edffb5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -357,25 +357,3 @@ 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 => - content.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 - }, {}) - -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/export.test.js b/test/export.test.js index fc616c1043..5593841efd 100644 --- a/test/export.test.js +++ b/test/export.test.js @@ -154,6 +154,11 @@ describe('index', () => { assert.equal(typeof index.defaults.sync, 'boolean', 'index.defaults.sync') assert.equal(typeof index.defaults.timeoutSignal, 'string', 'index.defaults.timeoutSignal') assert.equal(typeof index.defaults.verbose, 'boolean', 'index.defaults.verbose') + assert.equal(typeof index.dotenv, 'object', 'index.dotenv') + assert.equal(typeof index.dotenv.config, 'function', 'index.dotenv.config') + 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.echo, 'function', 'index.echo') assert.equal(typeof index.expBackoff, 'function', 'index.expBackoff') assert.equal(typeof index.fetch, 'function', 'index.fetch') @@ -331,7 +336,6 @@ 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') diff --git a/test/goods.test.js b/test/goods.test.js index 04882e865b..18db48ebe9 100644 --- a/test/goods.test.js +++ b/test/goods.test.js @@ -15,7 +15,7 @@ import assert from 'node:assert' import { test, describe, after } from 'node:test' import { $, chalk, fs, tempfile } from '../build/index.js' -import { echo, sleep, parseArgv, loadDotenv } from '../build/goods.js' +import { echo, sleep, parseArgv, dotenv } from '../build/goods.js' describe('goods', () => { function zx(script) { @@ -174,44 +174,73 @@ describe('goods', () => { ) }) - describe('loadDotenv()', () => { - const env1 = tempfile( - '.env', - `FOO=BAR - BAR=FOO+` - ) - const env2 = tempfile('.env.default', `BAR2=FOO2`) + describe('dotenv', () => { + test('parse()', () => { + assert.deepEqual( + dotenv.parse('ENV=v1\nENV2=v2\n\n\n ENV3 = v3 \nexport ENV4=v4'), + { + ENV: 'v1', + ENV2: 'v2', + ENV3: 'v3', + ENV4: 'v4', + } + ) + assert.deepEqual(dotenv.parse(''), {}) + + // TBD: multiline + const multiline = `SIMPLE=xyz123 +NON_INTERPOLATED='raw text without variable interpolation' +MULTILINE = """ +long text here, +e.g. a private SSH key +"""` + }) - after(() => { - fs.remove(env1) - fs.remove(env2) + describe('load()', () => { + const file1 = tempfile('.env.1', 'ENV1=value1\nENV2=value2') + const file2 = tempfile('.env.2', 'ENV2=value222\nENV3=value3') + after(() => Promise.all([fs.remove(file1), fs.remove(file2)])) + + test('loads env from files', () => { + const env = dotenv.load(file1, file2) + assert.equal(env.ENV1, 'value1') + assert.equal(env.ENV2, 'value2') + assert.equal(env.ENV3, 'value3') + }) + + test('throws error on ENOENT', () => { + try { + dotenv.load('./.env') + assert.throw() + } catch (e) { + assert.equal(e.code, 'ENOENT') + assert.equal(e.errno, -2) + } + }) }) - test('handles multiple dotenv files', async () => { - const env = loadDotenv(env1, env2) + describe('loadSafe()', () => { + const file1 = tempfile('.env.1', 'ENV1=value1\nENV2=value2') + const file2 = '.env.notexists' - 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') - }) + after(() => fs.remove(file1)) - 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('loads env from files', () => { + const env = dotenv.loadSafe(file1, file2) + assert.equal(env.ENV1, 'value1') + assert.equal(env.ENV2, 'value2') + }) }) - test('handle error', async () => { - try { - loadDotenv('./.env') + describe('config()', () => { + test('updates process.env', () => { + const file1 = tempfile('.env.1', 'ENV1=value1') - assert.throw() - } catch (e) { - assert.equal(e.code, 'ENOENT') - assert.equal(e.errno, -2) - } + assert.equal(process.env.ENV1, undefined) + dotenv.config(file1) + assert.equal(process.env.ENV1, 'value1') + delete process.env.ENV1 + }) }) }) }) diff --git a/test/util.test.js b/test/util.test.js index 3daf96b7f3..6f0609ece4 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -30,8 +30,6 @@ import { tempfile, preferLocalBin, toCamelCase, - parseDotenv, - readEnvFromFile, } from '../build/util.js' describe('util', () => { @@ -142,44 +140,3 @@ describe('util', () => { assert.equal(toCamelCase('kebab-input-str'), 'kebabInputStr') }) }) - -test('parseDotenv()', () => { - assert.deepEqual( - parseDotenv('ENV=v1\nENV2=v2\n\n\n ENV3 = v3 \nexport ENV4=v4'), - { - ENV: 'v1', - ENV2: 'v2', - ENV3: 'v3', - ENV4: 'v4', - } - ) - assert.deepEqual(parseDotenv(''), {}) - - // TBD: multiline - const multiline = `SIMPLE=xyz123 -NON_INTERPOLATED='raw text without variable interpolation' -MULTILINE = """ -long text here, -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 env = readEnvFromFile(file) - assert.equal(env.ENV, 'value1') - assert.equal(env.ENV2, 'value24') - assert.ok(env.NODE_VERSION !== '') - }) - - test('handles correct some env', () => { - 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') - }) -})