Commit f1ca807
Changed files (9)
.github
workflows
.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}}'
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
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
+```
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
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<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,
+ }
+})()
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<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),
- }
-}
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')
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
+ })
})
})
})
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')
- })
-})