Skip to content

Commit

Permalink
refactor: assemble dotenv utils (#1043)
Browse files Browse the repository at this point in the history
* refactor: assemble dotenv utils

continues #1034

* chore: prettier
  • Loading branch information
antongolub authored Dec 28, 2024
1 parent edef66e commit f1ca807
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 147 deletions.
56 changes: 28 additions & 28 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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}}'
18 changes: 18 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
14 changes: 0 additions & 14 deletions docs/v7/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
9 changes: 5 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
46 changes: 42 additions & 4 deletions src/goods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@

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,
identity,
isStringLiteral,
parseBool,
parseDuration,
readEnvFromFile,
toCamelCase,
} from './util.js'
import {
fs,
minimist,
nodeFetch,
type RequestInfo,
Expand Down Expand Up @@ -220,8 +221,45 @@ 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), {})
export const dotenv = (() => {
const parse = (content: string | Buffer): NodeJS.ProcessEnv =>
content
.toString()
.split(/\r?\n/)
.reduce<NodeJS.ProcessEnv>((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,
}
})()
22 changes: 0 additions & 22 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NodeJS.ProcessEnv>((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),
}
}
6 changes: 5 additions & 1 deletion test/export.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
91 changes: 60 additions & 31 deletions test/goods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
})
})
})
})
Loading

0 comments on commit f1ca807

Please sign in to comment.