Skip to content

Commit f1ca807

Browse files
authored
refactor: assemble dotenv utils (#1043)
* refactor: assemble dotenv utils continues #1034 * chore: prettier
1 parent edef66e commit f1ca807

File tree

9 files changed

+158
-147
lines changed

9 files changed

+158
-147
lines changed

.github/workflows/codeql.yml

+28-28
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
name: "CodeQL Advanced"
1+
name: 'CodeQL Advanced'
22

33
on:
44
push:
5-
branches: [ "main" ]
5+
branches: ['main']
66
pull_request:
7-
branches: [ "main" ]
7+
branches: ['main']
88
schedule:
99
- cron: '28 6 * * 3'
1010

@@ -28,29 +28,29 @@ jobs:
2828
fail-fast: false
2929
matrix:
3030
include:
31-
- language: javascript-typescript
32-
build-mode: none
31+
- language: javascript-typescript
32+
build-mode: none
3333
steps:
34-
- name: Checkout repository
35-
uses: actions/checkout@v4
36-
37-
- name: Initialize CodeQL
38-
uses: github/codeql-action/init@v3
39-
with:
40-
languages: ${{ matrix.language }}
41-
build-mode: ${{ matrix.build-mode }}
42-
43-
- if: matrix.build-mode == 'manual'
44-
shell: bash
45-
run: |
46-
echo 'If you are using a "manual" build mode for one or more of the' \
47-
'languages you are analyzing, replace this with the commands to build' \
48-
'your code, for example:'
49-
echo ' make bootstrap'
50-
echo ' make release'
51-
exit 1
52-
53-
- name: Perform CodeQL Analysis
54-
uses: github/codeql-action/analyze@v3
55-
with:
56-
category: "/language:${{matrix.language}}"
34+
- name: Checkout repository
35+
uses: actions/checkout@v4
36+
37+
- name: Initialize CodeQL
38+
uses: github/codeql-action/init@v3
39+
with:
40+
languages: ${{ matrix.language }}
41+
build-mode: ${{ matrix.build-mode }}
42+
43+
- if: matrix.build-mode == 'manual'
44+
shell: bash
45+
run: |
46+
echo 'If you are using a "manual" build mode for one or more of the' \
47+
'languages you are analyzing, replace this with the commands to build' \
48+
'your code, for example:'
49+
echo ' make bootstrap'
50+
echo ' make release'
51+
exit 1
52+
53+
- name: Perform CodeQL Analysis
54+
uses: github/codeql-action/analyze@v3
55+
with:
56+
category: '/language:${{matrix.language}}'

docs/api.md

+18
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,21 @@ The [yaml](https://www.npmjs.com/package/yaml) package.
364364
```js
365365
console.log(YAML.parse('foo: bar').foo)
366366
```
367+
368+
## dotenv
369+
[dotenv](https://www.npmjs.com/package/dotenv)-like environment variables loading API
370+
371+
```js
372+
// parse
373+
const raw = 'FOO=BAR\nBAZ=QUX'
374+
const data = dotenv.parse(raw) // {FOO: 'BAR', BAZ: 'QUX'}
375+
await fs.writeFile('.env', raw)
376+
377+
// load
378+
const env = dotenv.load('.env')
379+
await $({ env })`echo $FOO`.stdout // BAR
380+
381+
// config
382+
dotenv.config('.env')
383+
process.env.FOO // BAR
384+
```

docs/v7/api.md

-14
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,3 @@ The [yaml](https://www.npmjs.com/package/yaml) package.
204204
```js
205205
console.log(YAML.parse('foo: bar').foo)
206206
```
207-
208-
209-
## loadDotenv
210-
211-
Read env files and collects it into environment variables.
212-
213-
```js
214-
const env = loadDotenv(env1, env2)
215-
console.log((await $({ env })`echo $FOO`).stdout)
216-
---
217-
const env = loadDotenv(env1)
218-
$.env = env
219-
console.log((await $`echo $FOO`).stdout)
220-
```

src/cli.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,17 @@ import url from 'node:url'
1818
import {
1919
$,
2020
ProcessOutput,
21+
parseArgv,
2122
updateArgv,
22-
fetch,
2323
chalk,
24+
dotenv,
25+
fetch,
2426
fs,
2527
path,
2628
VERSION,
27-
parseArgv,
2829
} from './index.js'
2930
import { installDeps, parseDeps } from './deps.js'
30-
import { readEnvFromFile, randomId } from './util.js'
31+
import { randomId } from './util.js'
3132
import { createRequire } from './vendor.js'
3233

3334
const EXT = '.mjs'
@@ -89,7 +90,7 @@ export async function main() {
8990
if (argv.cwd) $.cwd = argv.cwd
9091
if (argv.env) {
9192
const envPath = path.resolve($.cwd ?? process.cwd(), argv.env)
92-
$.env = readEnvFromFile(envPath, process.env)
93+
$.env = { ...process.env, ...dotenv.load(envPath) }
9394
}
9495
if (argv.verbose) $.verbose = true
9596
if (argv.quiet) $.quiet = true

src/goods.ts

+42-4
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414

1515
import assert from 'node:assert'
1616
import { createInterface } from 'node:readline'
17+
import { default as path } from 'node:path'
1718
import { $, within, ProcessOutput } from './core.js'
1819
import {
1920
type Duration,
2021
identity,
2122
isStringLiteral,
2223
parseBool,
2324
parseDuration,
24-
readEnvFromFile,
2525
toCamelCase,
2626
} from './util.js'
2727
import {
28+
fs,
2829
minimist,
2930
nodeFetch,
3031
type RequestInfo,
@@ -220,8 +221,45 @@ export async function spinner<T>(
220221
}
221222

222223
/**
223-
*
224224
* Read env files and collects it into environment variables
225225
*/
226-
export const loadDotenv = (...files: string[]): NodeJS.ProcessEnv =>
227-
files.reduce<NodeJS.ProcessEnv>((m, f) => readEnvFromFile(f, m), {})
226+
export const dotenv = (() => {
227+
const parse = (content: string | Buffer): NodeJS.ProcessEnv =>
228+
content
229+
.toString()
230+
.split(/\r?\n/)
231+
.reduce<NodeJS.ProcessEnv>((r, line) => {
232+
if (line.startsWith('export ')) line = line.slice(7)
233+
const i = line.indexOf('=')
234+
const k = line.slice(0, i).trim()
235+
const v = line.slice(i + 1).trim()
236+
if (k && v) r[k] = v
237+
return r
238+
}, {})
239+
240+
const _load = (
241+
read: (file: string) => string,
242+
...files: string[]
243+
): NodeJS.ProcessEnv =>
244+
files
245+
.reverse()
246+
.reduce((m, f) => Object.assign(m, parse(read(path.resolve(f)))), {})
247+
const load = (...files: string[]): NodeJS.ProcessEnv =>
248+
_load((file) => fs.readFileSync(file, 'utf8'), ...files)
249+
const loadSafe = (...files: string[]): NodeJS.ProcessEnv =>
250+
_load(
251+
(file: string): string =>
252+
fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '',
253+
...files
254+
)
255+
256+
const config = (def = '.env', ...files: string[]): NodeJS.ProcessEnv =>
257+
Object.assign(process.env, loadSafe(def, ...files))
258+
259+
return {
260+
parse,
261+
load,
262+
loadSafe,
263+
config,
264+
}
265+
})()

src/util.ts

-22
Original file line numberDiff line numberDiff line change
@@ -357,25 +357,3 @@ export const toCamelCase = (str: string) =>
357357

358358
export const parseBool = (v: string): boolean | string =>
359359
({ true: true, false: false })[v] ?? v
360-
361-
export const parseDotenv = (content: string): NodeJS.ProcessEnv =>
362-
content.split(/\r?\n/).reduce<NodeJS.ProcessEnv>((r, line) => {
363-
if (line.startsWith('export ')) line = line.slice(7)
364-
const i = line.indexOf('=')
365-
const k = line.slice(0, i).trim()
366-
const v = line.slice(i + 1).trim()
367-
if (k && v) r[k] = v
368-
return r
369-
}, {})
370-
371-
export const readEnvFromFile = (
372-
filepath: string,
373-
env: NodeJS.ProcessEnv = process.env
374-
): NodeJS.ProcessEnv => {
375-
const content = fs.readFileSync(path.resolve(filepath), 'utf8')
376-
377-
return {
378-
...env,
379-
...parseDotenv(content),
380-
}
381-
}

test/export.test.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ describe('index', () => {
154154
assert.equal(typeof index.defaults.sync, 'boolean', 'index.defaults.sync')
155155
assert.equal(typeof index.defaults.timeoutSignal, 'string', 'index.defaults.timeoutSignal')
156156
assert.equal(typeof index.defaults.verbose, 'boolean', 'index.defaults.verbose')
157+
assert.equal(typeof index.dotenv, 'object', 'index.dotenv')
158+
assert.equal(typeof index.dotenv.config, 'function', 'index.dotenv.config')
159+
assert.equal(typeof index.dotenv.load, 'function', 'index.dotenv.load')
160+
assert.equal(typeof index.dotenv.loadSafe, 'function', 'index.dotenv.loadSafe')
161+
assert.equal(typeof index.dotenv.parse, 'function', 'index.dotenv.parse')
157162
assert.equal(typeof index.echo, 'function', 'index.echo')
158163
assert.equal(typeof index.expBackoff, 'function', 'index.expBackoff')
159164
assert.equal(typeof index.fetch, 'function', 'index.fetch')
@@ -331,7 +336,6 @@ describe('index', () => {
331336
assert.equal(typeof index.globby.isGitIgnored, 'function', 'index.globby.isGitIgnored')
332337
assert.equal(typeof index.globby.isGitIgnoredSync, 'function', 'index.globby.isGitIgnoredSync')
333338
assert.equal(typeof index.kill, 'function', 'index.kill')
334-
assert.equal(typeof index.loadDotenv, 'function', 'index.loadDotenv')
335339
assert.equal(typeof index.log, 'function', 'index.log')
336340
assert.equal(typeof index.minimist, 'function', 'index.minimist')
337341
assert.equal(typeof index.nothrow, 'function', 'index.nothrow')

test/goods.test.js

+60-31
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import assert from 'node:assert'
1616
import { test, describe, after } from 'node:test'
1717
import { $, chalk, fs, tempfile } from '../build/index.js'
18-
import { echo, sleep, parseArgv, loadDotenv } from '../build/goods.js'
18+
import { echo, sleep, parseArgv, dotenv } from '../build/goods.js'
1919

2020
describe('goods', () => {
2121
function zx(script) {
@@ -174,44 +174,73 @@ describe('goods', () => {
174174
)
175175
})
176176

177-
describe('loadDotenv()', () => {
178-
const env1 = tempfile(
179-
'.env',
180-
`FOO=BAR
181-
BAR=FOO+`
182-
)
183-
const env2 = tempfile('.env.default', `BAR2=FOO2`)
177+
describe('dotenv', () => {
178+
test('parse()', () => {
179+
assert.deepEqual(
180+
dotenv.parse('ENV=v1\nENV2=v2\n\n\n ENV3 = v3 \nexport ENV4=v4'),
181+
{
182+
ENV: 'v1',
183+
ENV2: 'v2',
184+
ENV3: 'v3',
185+
ENV4: 'v4',
186+
}
187+
)
188+
assert.deepEqual(dotenv.parse(''), {})
189+
190+
// TBD: multiline
191+
const multiline = `SIMPLE=xyz123
192+
NON_INTERPOLATED='raw text without variable interpolation'
193+
MULTILINE = """
194+
long text here,
195+
e.g. a private SSH key
196+
"""`
197+
})
184198

185-
after(() => {
186-
fs.remove(env1)
187-
fs.remove(env2)
199+
describe('load()', () => {
200+
const file1 = tempfile('.env.1', 'ENV1=value1\nENV2=value2')
201+
const file2 = tempfile('.env.2', 'ENV2=value222\nENV3=value3')
202+
after(() => Promise.all([fs.remove(file1), fs.remove(file2)]))
203+
204+
test('loads env from files', () => {
205+
const env = dotenv.load(file1, file2)
206+
assert.equal(env.ENV1, 'value1')
207+
assert.equal(env.ENV2, 'value2')
208+
assert.equal(env.ENV3, 'value3')
209+
})
210+
211+
test('throws error on ENOENT', () => {
212+
try {
213+
dotenv.load('./.env')
214+
assert.throw()
215+
} catch (e) {
216+
assert.equal(e.code, 'ENOENT')
217+
assert.equal(e.errno, -2)
218+
}
219+
})
188220
})
189221

190-
test('handles multiple dotenv files', async () => {
191-
const env = loadDotenv(env1, env2)
222+
describe('loadSafe()', () => {
223+
const file1 = tempfile('.env.1', 'ENV1=value1\nENV2=value2')
224+
const file2 = '.env.notexists'
192225

193-
assert.equal((await $({ env })`echo $FOO`).stdout, 'BAR\n')
194-
assert.equal((await $({ env })`echo $BAR`).stdout, 'FOO+\n')
195-
assert.equal((await $({ env })`echo $BAR2`).stdout, 'FOO2\n')
196-
})
226+
after(() => fs.remove(file1))
197227

198-
test('handles replace evn', async () => {
199-
const env = loadDotenv(env1)
200-
$.env = env
201-
assert.equal((await $`echo $FOO`).stdout, 'BAR\n')
202-
assert.equal((await $`echo $BAR`).stdout, 'FOO+\n')
203-
$.env = process.env
228+
test('loads env from files', () => {
229+
const env = dotenv.loadSafe(file1, file2)
230+
assert.equal(env.ENV1, 'value1')
231+
assert.equal(env.ENV2, 'value2')
232+
})
204233
})
205234

206-
test('handle error', async () => {
207-
try {
208-
loadDotenv('./.env')
235+
describe('config()', () => {
236+
test('updates process.env', () => {
237+
const file1 = tempfile('.env.1', 'ENV1=value1')
209238

210-
assert.throw()
211-
} catch (e) {
212-
assert.equal(e.code, 'ENOENT')
213-
assert.equal(e.errno, -2)
214-
}
239+
assert.equal(process.env.ENV1, undefined)
240+
dotenv.config(file1)
241+
assert.equal(process.env.ENV1, 'value1')
242+
delete process.env.ENV1
243+
})
215244
})
216245
})
217246
})

0 commit comments

Comments
 (0)