feat(esm): convert to using esm modules

This commit is contained in:
Todd Bluhm 2024-12-02 10:25:25 -09:00
parent ef9665c5fe
commit f8106ac44a
No known key found for this signature in database
GPG Key ID: 9CF312607477B8AB
33 changed files with 386 additions and 274 deletions

8
.mocharc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/mocharc.json",
"require": ["tsx/esm", "esmock"],
"extensions": ["ts"],
"spec": [
"test/**/*.ts"
]
}

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2019 Todd Bluhm Copyright (c) Todd Bluhm
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,2 +1,3 @@
#! /usr/bin/env node #! /usr/bin/env node
require('../dist').CLI(process.argv.slice(2)) import { CLI } from '../dist'
CLI(process.argv.slice(2))

View File

@ -1,43 +1,38 @@
const eslint = require('@eslint/js') import { default as tseslint } from 'typescript-eslint'
const tseslint = require('typescript-eslint') import { default as globals } from 'globals'
const globals = require('globals') import { default as eslint } from '@eslint/js'
const stylistic = require('@stylistic/eslint-plugin')
module.exports = tseslint.config( export default tseslint.config(
{ {
ignores: ['dist/*', 'bin/*'], // Ignore build folder
rules: { ignores: ['dist/*'],
'@typescript-eslint/no-require-imports': 'off', },
}, eslint.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
// Enable Type generation
languageOptions: { languageOptions: {
globals: { globals: {
...globals.node, ...globals.node,
}, },
parserOptions: { parserOptions: {
projectService: { project: ['./tsconfig.json', './test/tsconfig.json'],
allowDefaultProject: ['test/*.ts'],
},
}, },
}, }
extends: [
eslint.configs.recommended,
stylistic.configs['recommended-flat'],
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
],
},
// Disable Type Checking JS files
{
files: ['**/*.js'],
extends: [tseslint.configs.disableTypeChecked],
}, },
{ {
// For test files ignore some rules // For test files ignore some rules
files: ['test/*.ts'], files: ['test/**/*'],
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-assignment': 'off'
}, },
}, },
// Disable Type Checking JS/CJS/MJS files
{
files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
extends: [tseslint.configs.disableTypeChecked],
},
) )

View File

@ -1,26 +0,0 @@
module.exports = (async function config() {
const { default: love } = await import('eslint-config-love')
return [
love,
{
files: [
'src/**/*.[j|t]s',
// 'src/**/*.ts',
'test/**/*.[j|t]s',
// 'test/**/*.ts'
],
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['eslint.config.js', 'bin/env-cmd.js'],
defaultProject: './tsconfig.json',
},
},
},
},
{
ignores: ['dist/'],
}
]
})()

View File

@ -4,16 +4,17 @@
"description": "Executes a command using the environment variables in an env file", "description": "Executes a command using the environment variables in an env file",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "module",
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=18.0.0"
}, },
"bin": { "bin": {
"env-cmd": "bin/env-cmd.js" "env-cmd": "bin/env-cmd.js"
}, },
"scripts": { "scripts": {
"prepare": "husky", "prepare": "husky",
"test": "mocha -r ts-node/register ./test/**/*.ts", "test": "mocha",
"test-cover": "nyc npm test", "test-cover": "c8 npm test",
"coveralls": "coveralls < coverage/lcov.info", "coveralls": "coveralls < coverage/lcov.info",
"lint": "npx eslint .", "lint": "npx eslint .",
"build": "tsc", "build": "tsc",
@ -54,58 +55,33 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.6.0", "@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0", "@commitlint/config-conventional": "^19.6.0",
"@eslint/js": "^9.15.0", "@eslint/js": "^9.16.0",
"@stylistic/eslint-plugin": "^2.11.0", "@types/chai": "^5.0.1",
"@types/chai": "^4.0.0", "@types/cross-spawn": "^6.0.6",
"@types/cross-spawn": "^6.0.0", "@types/mocha": "^10.0.10",
"@types/mocha": "^7.0.0", "@types/node": "^22.10.1",
"@types/node": "^12.0.0",
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"c8": "^10.1.2",
"chai": "^5.1.2", "chai": "^5.1.2",
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"esmock": "^2.6.9",
"globals": "^15.12.0", "globals": "^15.12.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"mocha": "^10.8.2", "mocha": "^11.0.0",
"nyc": "^17.1.0",
"sinon": "^19.0.2", "sinon": "^19.0.2",
"ts-node": "^8.0.0", "tsx": "^4.19.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"typescript-eslint": "^8.15.0" "typescript-eslint": "^8.15.0"
}, },
"nyc": {
"include": [
"src/**/*.ts"
],
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"reporter": [
"text",
"lcov"
],
"sourceMap": true,
"instrument": true
},
"greenkeeper": {
"ignore": [
"@types/node"
],
"commitMessages": {
"initialBadge": "docs: add greenkeeper badge",
"initialDependencies": "chore: update dependencies",
"initialBranches": "chore: whitelist greenkeeper branches",
"dependencyUpdate": "chore: update dependency ${dependency}",
"devDependencyUpdate": "chore: update devDependecy ${dependency}",
"dependencyPin": "fix: pin dependency ${dependency}",
"devDependencyPin": "fix: pin devDependecy ${dependency}"
}
},
"commitlint": { "commitlint": {
"extends": [ "extends": [
"@commitlint/config-conventional" "@commitlint/config-conventional"
] ]
},
"c8": {
"reporter": [
"text",
"lcov"
]
} }
} }

25
src/cli.ts Normal file
View File

@ -0,0 +1,25 @@
import * as processLib from 'node:process'
import type { Environment } from './types.ts'
import { EnvCmd } from './env-cmd.js'
import { parseArgs } from './parse-args.js'
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<Environment>}
*/
export async function CLI(args: string[]): Promise<Environment> {
// Parse the args from the command line
const parsedArgs = parseArgs(args)
// Run EnvCmd
try {
return await EnvCmd(parsedArgs)
}
catch (e) {
console.error(e)
return processLib.exit(1)
}
}

View File

@ -1,29 +1,9 @@
import { spawn } from './spawn' import { default as spawn } from 'cross-spawn'
import { EnvCmdOptions, Environment } from './types' import type { EnvCmdOptions, Environment } from './types.ts'
import { TermSignals } from './signal-termination' import { TermSignals } from './signal-termination.js'
import { parseArgs } from './parse-args' import { getEnvVars } from './get-env-vars.js'
import { getEnvVars } from './get-env-vars' import { expandEnvs } from './expand-envs.js'
import { expandEnvs } from './expand-envs' import * as processLib from 'node:process'
/**
* Executes env - cmd using command line arguments
* @export
* @param {string[]} args Command line argument to pass in ['-f', './.env']
* @returns {Promise<Environment>}
*/
export async function CLI(args: string[]): Promise<Environment> {
// Parse the args from the command line
const parsedArgs = parseArgs(args)
// Run EnvCmd
try {
return await (exports as { EnvCmd: typeof EnvCmd }).EnvCmd(parsedArgs)
}
catch (e) {
console.error(e)
return process.exit(1)
}
}
/** /**
* The main env-cmd program. This will spawn a new process and run the given command using * The main env-cmd program. This will spawn a new process and run the given command using
@ -53,11 +33,11 @@ export async function EnvCmd(
} }
// Override the merge order if --no-override flag set // Override the merge order if --no-override flag set
if (options.noOverride === true) { if (options.noOverride === true) {
env = Object.assign({}, env, process.env) env = Object.assign({}, env, processLib.env)
} }
else { else {
// Add in the system environment variables to our environment list // Add in the system environment variables to our environment list
env = Object.assign({}, process.env, env) env = Object.assign({}, processLib.env, env)
} }
if (options.expandEnvs === true) { if (options.expandEnvs === true) {

View File

@ -1,4 +1,4 @@
import { Environment } from './types' import type { Environment } from './types.ts'
/** /**
* expandEnvs Replaces $var in args and command with environment variables * expandEnvs Replaces $var in args and command with environment variables

View File

@ -1,6 +1,6 @@
import { GetEnvVarOptions, Environment } from './types' import type { GetEnvVarOptions, Environment } from './types.ts'
import { getRCFileVars } from './parse-rc-file' import { getRCFileVars } from './parse-rc-file.js'
import { getEnvFileVars } from './parse-env-file' import { getEnvFileVars } from './parse-env-file.js'
const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'] const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json']
const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'] const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json']

View File

@ -1,6 +1,7 @@
import { getEnvVars } from './get-env-vars' import { getEnvVars } from './get-env-vars.js'
// Export the core env-cmd API // Export the core env-cmd API
export * from './types' export * from './types.js'
export * from './env-cmd' export * from './cli.js'
export * from './env-cmd.js'
export const GetEnvVars = getEnvVars export const GetEnvVars = getEnvVars

View File

@ -1,9 +1,9 @@
import * as commander from 'commander' import * as commander from 'commander'
import { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types' import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts'
import { parseArgList } from './utils' import { parseArgList } from './utils.js'
// Use commonjs require to prevent a weird folder hierarchy in dist // Use commonjs require to prevent a weird folder hierarchy in dist
const packageJson: { version: string } = require('../package.json') /* eslint-disable-line */ const packageJson = (await import('../package.json')).default
/** /**
* Parses the arguments passed into the cli * Parses the arguments passed into the cli

View File

@ -1,9 +1,7 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import { resolveEnvFilePath, isPromise } from './utils' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import { Environment } from './types' import type { Environment } from './types.ts'
const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs']
/** /**
* Gets the environment vars from an env file * Gets the environment vars from an env file
@ -19,9 +17,17 @@ export async function getEnvFileVars(envFilePath: string): Promise<Environment>
// Get the file extension // Get the file extension
const ext = path.extname(absolutePath).toLowerCase() const ext = path.extname(absolutePath).toLowerCase()
let env: Environment = {} let env: Environment = {}
if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise: Environment | Promise<Environment> = require(absolutePath) /* eslint-disable-line */ const res = await import(absolutePath) as Environment | { default: Environment }
env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise if ('default' in res) {
env = res.default as Environment
} else {
env = res
}
// Check to see if the imported value is a promise
if (isPromise(env)) {
env = await env
}
} }
else { else {
const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) const file = fs.readFileSync(absolutePath, { encoding: 'utf8' })

View File

@ -1,8 +1,8 @@
import { stat, readFile } from 'fs' import { stat, readFile } from 'fs'
import { promisify } from 'util' import { promisify } from 'util'
import { extname } from 'path' import { extname } from 'path'
import { resolveEnvFilePath, isPromise } from './utils' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'
import { Environment, RCEnvironment } from './types' import type { Environment, RCEnvironment } from './types.ts'
const statAsync = promisify(stat) const statAsync = promisify(stat)
const readFileAsync = promisify(readFile) const readFileAsync = promisify(readFile)
@ -26,11 +26,19 @@ export async function getRCFileVars(
// Get the file extension // Get the file extension
const ext = extname(absolutePath).toLowerCase() const ext = extname(absolutePath).toLowerCase()
let parsedData: Partial<RCEnvironment> let parsedData: Partial<RCEnvironment> = {}
try { try {
if (ext === '.json' || ext === '.js' || ext === '.cjs') { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
const possiblePromise = require(absolutePath) as PromiseLike<RCEnvironment> | RCEnvironment const res = await import(absolutePath) as RCEnvironment | { default: RCEnvironment }
parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise if ('default' in res) {
parsedData = res.default as RCEnvironment
} else {
parsedData = res
}
// Check to see if the imported value is a promise
if (isPromise(parsedData)) {
parsedData = await parsedData
}
} }
else { else {
const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) const file = await readFileAsync(absolutePath, { encoding: 'utf8' })

View File

@ -1,4 +0,0 @@
import * as spawn from 'cross-spawn'
export {
spawn,
}

View File

@ -1,16 +1,20 @@
import * as path from 'path' import { resolve } from 'node:path'
import * as os from 'os' import { homedir } from 'node:os'
import { cwd } from 'node:process'
// Special file extensions that node can natively import
export const IMPORT_HOOK_EXTENSIONS = ['.json', '.js', '.cjs', '.mjs']
/** /**
* A simple function for resolving the path the user entered * A simple function for resolving the path the user entered
*/ */
export function resolveEnvFilePath(userPath: string): string { export function resolveEnvFilePath(userPath: string): string {
// Make sure a home directory exist // Make sure a home directory exist
const home = os.homedir() as string | undefined const home = homedir() as string | undefined
if (home != null) { if (home != null) {
userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`) userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`)
} }
return path.resolve(process.cwd(), userPath) return resolve(cwd(), userPath)
} }
/** /**
* A simple function that parses a comma separated string into an array of strings * A simple function that parses a comma separated string into an array of strings

57
test/cli.spec.ts Normal file
View File

@ -0,0 +1,57 @@
import { default as sinon } from 'sinon'
import { assert } from 'chai'
import { default as esmock } from 'esmock'
import type { CLI } from '../src/cli.ts'
describe('CLI', (): void => {
let sandbox: sinon.SinonSandbox
let parseArgsStub: sinon.SinonStub<any>
let envCmdStub: sinon.SinonStub<any>
let processExitStub: sinon.SinonStub<any>
let cliLib: { CLI: typeof CLI }
before(async (): Promise<void> => {
sandbox = sinon.createSandbox()
envCmdStub = sandbox.stub()
parseArgsStub = sandbox.stub()
processExitStub = sandbox.stub()
cliLib = await esmock('../src/cli.ts', {
'../src/env-cmd': {
EnvCmd: envCmdStub,
},
'../src/parse-args': {
parseArgs: parseArgsStub,
},
'node:process': {
exit: processExitStub,
},
})
})
after((): void => {
sandbox.restore()
})
afterEach((): void => {
sandbox.resetHistory()
sandbox.resetBehavior()
})
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
parseArgsStub.returns({})
await cliLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 0)
})
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
parseArgsStub.returns({})
envCmdStub.throwsException('Error')
await cliLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 1)
assert.equal(processExitStub.args[0][0], 1)
})
})

View File

@ -1,68 +1,44 @@
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import * as signalTermLib from '../src/signal-termination' import { default as esmock } from 'esmock'
import * as parseArgsLib from '../src/parse-args' import { expandEnvs } from '../src/expand-envs.js'
import * as getEnvVarsLib from '../src/get-env-vars' import type { EnvCmd } from '../src/env-cmd.ts'
import * as expandEnvsLib from '../src/expand-envs'
import * as spawnLib from '../src/spawn'
import * as envCmdLib from '../src/env-cmd'
describe('CLI', (): void => { let envCmdLib: { EnvCmd: typeof EnvCmd }
let sandbox: sinon.SinonSandbox
let parseArgsStub: sinon.SinonStub<any>
let envCmdStub: sinon.SinonStub<any>
let processExitStub: sinon.SinonStub<any>
before((): void => {
sandbox = sinon.createSandbox()
parseArgsStub = sandbox.stub(parseArgsLib, 'parseArgs')
envCmdStub = sandbox.stub(envCmdLib, 'EnvCmd')
processExitStub = sandbox.stub(process, 'exit')
})
after((): void => {
sandbox.restore()
})
afterEach((): void => {
sandbox.resetHistory()
sandbox.resetBehavior()
})
it('should parse the provided args and execute the EnvCmd', async (): Promise<void> => {
parseArgsStub.returns({})
await envCmdLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 0)
})
it('should catch exception if EnvCmd throws an exception', async (): Promise<void> => {
parseArgsStub.returns({})
envCmdStub.throwsException('Error')
await envCmdLib.CLI(['node', './env-cmd', '-v'])
assert.equal(parseArgsStub.callCount, 1)
assert.equal(envCmdStub.callCount, 1)
assert.equal(processExitStub.callCount, 1)
assert.equal(processExitStub.args[0][0], 1)
})
})
describe('EnvCmd', (): void => { describe('EnvCmd', (): void => {
let sandbox: sinon.SinonSandbox let sandbox: sinon.SinonSandbox
let getEnvVarsStub: sinon.SinonStub<any> let getEnvVarsStub: sinon.SinonStub<any>
let spawnStub: sinon.SinonStub<any> let spawnStub: sinon.SinonStub<any>
let expandEnvsSpy: sinon.SinonSpy<any> let expandEnvsSpy: sinon.SinonSpy<any>
before((): void => { before(async (): Promise<void> => {
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars') getEnvVarsStub = sandbox.stub()
spawnStub = sandbox.stub(spawnLib, 'spawn') spawnStub = sandbox.stub()
spawnStub.returns({ spawnStub.returns({
on: (): void => { /* Fake the on method */ }, on: sinon.stub(),
kill: (): void => { /* Fake the kill method */ }, kill: sinon.stub(),
})
expandEnvsSpy = sandbox.spy(expandEnvs)
const TermSignals = sandbox.stub()
TermSignals.prototype.handleTermSignals = sandbox.stub()
TermSignals.prototype.handleUncaughtExceptions = sandbox.stub()
envCmdLib = await esmock('../src/env-cmd.ts', {
'../src/get-env-vars': {
getEnvVars: getEnvVarsStub,
},
'cross-spawn': {
default: spawnStub,
},
'../src/expand-envs': {
expandEnvs: expandEnvsSpy,
},
'../src/signal-termination': {
TermSignals,
},
}) })
expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals')
sandbox.stub(signalTermLib.TermSignals.prototype, 'handleUncaughtExceptions')
}) })
after((): void => { after((): void => {

View File

@ -1,6 +1,6 @@
/* eslint @typescript-eslint/no-non-null-assertion: 0 */ /* eslint @typescript-eslint/no-non-null-assertion: 0 */
import { assert } from 'chai' import { assert } from 'chai'
import { expandEnvs } from '../src/expand-envs' import { expandEnvs } from '../src/expand-envs.js'
describe('expandEnvs', (): void => { describe('expandEnvs', (): void => {
const envs = { const envs = {

View File

@ -1,17 +1,26 @@
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import { getEnvVars } from '../src/get-env-vars' import { default as esmock } from 'esmock'
import * as rcFile from '../src/parse-rc-file' import type { getEnvVars } from '../src/get-env-vars.ts'
import * as envFile from '../src/parse-env-file'
let getEnvVarsLib: { getEnvVars: typeof getEnvVars }
describe('getEnvVars', (): void => { describe('getEnvVars', (): void => {
let getRCFileVarsStub: sinon.SinonStub<any> let getRCFileVarsStub: sinon.SinonStub<any>
let getEnvFileVarsStub: sinon.SinonStub<any> let getEnvFileVarsStub: sinon.SinonStub<any>
let logInfoStub: sinon.SinonStub<any> | undefined let logInfoStub: sinon.SinonStub<any> | undefined
before((): void => { before(async (): Promise<void> => {
getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') getRCFileVarsStub = sinon.stub()
getEnvFileVarsStub = sinon.stub(envFile, 'getEnvFileVars') getEnvFileVarsStub = sinon.stub()
getEnvVarsLib = await esmock('../src/get-env-vars.ts', {
'../src/parse-rc-file': {
getRCFileVars: getRCFileVarsStub
},
'../src/parse-env-file': {
getEnvFileVars: getEnvFileVarsStub
}
})
}) })
after((): void => { after((): void => {
@ -27,7 +36,7 @@ describe('getEnvVars', (): void => {
it('should parse the json .rc file from the default path with the given environment', it('should parse the json .rc file from the default path with the given environment',
async (): Promise<void> => { async (): Promise<void> => {
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ rc: { environments: ['production'] } }) const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -42,7 +51,7 @@ describe('getEnvVars', (): void => {
async (): Promise<void> => { async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}, },
) )
@ -52,7 +61,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ rc: { environments: ['production'] } }) const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -67,7 +76,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ rc: { environments: ['production'] } }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch (e) { catch (e) {
@ -84,7 +93,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch { catch {
@ -97,7 +106,7 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ rc: { environments: ['bad'] } }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] } })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch (e) { catch (e) {
@ -113,7 +122,7 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch { catch {
@ -123,7 +132,7 @@ describe('getEnvVars', (): void => {
it('should find .rc file at custom path path', async (): Promise<void> => { it('should find .rc file at custom path path', async (): Promise<void> => {
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ const envs = await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
}) })
assert.isOk(envs) assert.isOk(envs)
@ -138,7 +147,7 @@ describe('getEnvVars', (): void => {
it('should print custom .rc file path to info for verbose', async (): Promise<void> => { it('should print custom .rc file path to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
verbose: true, verbose: true,
}) })
@ -150,7 +159,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
}) })
assert.fail('should not get here.') assert.fail('should not get here.')
@ -168,7 +177,7 @@ describe('getEnvVars', (): void => {
pathError.name = 'PathError' pathError.name = 'PathError'
getRCFileVarsStub.rejects(pathError) getRCFileVarsStub.rejects(pathError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['production'], filePath: '../.custom-rc' }, rc: { environments: ['production'], filePath: '../.custom-rc' },
verbose: true, verbose: true,
}) })
@ -184,7 +193,7 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['bad'], filePath: '../.custom-rc' }, rc: { environments: ['bad'], filePath: '../.custom-rc' },
}) })
assert.fail('should not get here.') assert.fail('should not get here.')
@ -203,7 +212,7 @@ describe('getEnvVars', (): void => {
environmentError.name = 'EnvironmentError' environmentError.name = 'EnvironmentError'
getRCFileVarsStub.rejects(environmentError) getRCFileVarsStub.rejects(environmentError)
try { try {
await getEnvVars({ await getEnvVarsLib.getEnvVars({
rc: { environments: ['bad'], filePath: '../.custom-rc' }, rc: { environments: ['bad'], filePath: '../.custom-rc' },
verbose: true, verbose: true,
}) })
@ -217,7 +226,7 @@ describe('getEnvVars', (): void => {
it('should parse the env file from a custom path', async (): Promise<void> => { it('should parse the env file from a custom path', async (): Promise<void> => {
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ envFile: { filePath: '../.env-file' } }) const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -228,14 +237,14 @@ describe('getEnvVars', (): void => {
it('should print path of .env file to info for verbose', async (): Promise<void> => { it('should print path of .env file to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should fail to find env file at custom path', async (): Promise<void> => { it('should fail to find env file at custom path', async (): Promise<void> => {
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ envFile: { filePath: '../.env-file' } }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch (e) { catch (e) {
@ -249,7 +258,7 @@ describe('getEnvVars', (): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch { catch {
@ -263,7 +272,7 @@ describe('getEnvVars', (): void => {
async (): Promise<void> => { async (): Promise<void> => {
getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.onFirstCall().rejects('File not found.')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } }) const envs = await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true } })
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -279,14 +288,14 @@ describe('getEnvVars', (): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.onFirstCall().rejects('File not found.')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true }) await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file', fallback: true }, verbose: true })
assert.equal(logInfoStub.callCount, 2) assert.equal(logInfoStub.callCount, 2)
}, },
) )
it('should parse the env file from the default path', async (): Promise<void> => { it('should parse the env file from the default path', async (): Promise<void> => {
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars() const envs = await getEnvVarsLib.getEnvVars()
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -297,14 +306,14 @@ describe('getEnvVars', (): void => {
it('should print path of .env file to info for verbose', async (): Promise<void> => { it('should print path of .env file to info for verbose', async (): Promise<void> => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' })
await getEnvVars({ verbose: true }) await getEnvVarsLib.getEnvVars({ verbose: true })
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should search all default env file paths', async (): Promise<void> => { it('should search all default env file paths', async (): Promise<void> => {
getEnvFileVarsStub.throws('Not found.') getEnvFileVarsStub.throws('Not found.')
getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' }) getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' })
const envs = await getEnvVars() const envs = await getEnvVarsLib.getEnvVars()
assert.isOk(envs) assert.isOk(envs)
assert.lengthOf(Object.keys(envs), 1) assert.lengthOf(Object.keys(envs), 1)
assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(envs.THANKS, 'FOR ALL THE FISH')
@ -315,7 +324,7 @@ describe('getEnvVars', (): void => {
it('should fail to find env file at default path', async (): Promise<void> => { it('should fail to find env file at default path', async (): Promise<void> => {
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars() await getEnvVarsLib.getEnvVars()
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch (e) { catch (e) {
@ -332,7 +341,7 @@ describe('getEnvVars', (): void => {
logInfoStub = sinon.stub(console, 'info') logInfoStub = sinon.stub(console, 'info')
getEnvFileVarsStub.rejects('Not found.') getEnvFileVarsStub.rejects('Not found.')
try { try {
await getEnvVars({ verbose: true }) await getEnvVarsLib.getEnvVars({ verbose: true })
assert.fail('should not get here.') assert.fail('should not get here.')
} }
catch { catch {

View File

@ -1,7 +1,7 @@
/* eslint @typescript-eslint/no-non-null-assertion: 0 */ /* eslint @typescript-eslint/no-non-null-assertion: 0 */
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { assert } from 'chai' import { assert } from 'chai'
import { parseArgs } from '../src/parse-args' import { parseArgs } from '../src/parse-args.js'
describe('parseArgs', (): void => { describe('parseArgs', (): void => {
const command = 'command' const command = 'command'

View File

@ -2,7 +2,7 @@ import { assert } from 'chai'
import { import {
stripEmptyLines, stripComments, parseEnvVars, stripEmptyLines, stripComments, parseEnvVars,
parseEnvString, getEnvFileVars, parseEnvString, getEnvFileVars,
} from '../src/parse-env-file' } from '../src/parse-env-file.js'
describe('stripEmptyLines', (): void => { describe('stripEmptyLines', (): void => {
it('should strip out all empty lines', (): void => { it('should strip out all empty lines', (): void => {
@ -125,8 +125,8 @@ describe('getEnvFileVars', (): void => {
}) })
}) })
it('should parse a js file', async (): Promise<void> => { it('should parse a js/cjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test.js') const env = await getEnvFileVars('./test/test-files/test.cjs')
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0, ANSWER: 0,
@ -134,8 +134,25 @@ describe('getEnvFileVars', (): void => {
}) })
}) })
it('should parse an async js file', async (): Promise<void> => { it('should parse an async js/cjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test-async.js') const env = await getEnvFileVars('./test/test-files/test-async.cjs')
assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
})
})
it('should parse a mjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test.mjs')
assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
GALAXY: 'hitch\nhiking',
})
})
it('should parse an async mjs file', async (): Promise<void> => {
const env = await getEnvFileVars('./test/test-files/test-async.mjs')
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',
ANSWER: 0, ANSWER: 0,

View File

@ -1,5 +1,5 @@
import { assert } from 'chai' import { assert } from 'chai'
import { getRCFileVars } from '../src/parse-rc-file' import { getRCFileVars } from '../src/parse-rc-file.js'
const rcFilePath = './test/test-files/.rc-test' const rcFilePath = './test/test-files/.rc-test'
const rcJSONFilePath = './test/test-files/.rc-test.json' const rcJSONFilePath = './test/test-files/.rc-test.json'
@ -58,10 +58,23 @@ describe('getRCFileVars', (): void => {
} }
}) })
it('should parse an async js .rc file', async (): Promise<void> => { it('should parse an async js/cjs .rc file', async (): Promise<void> => {
const env = await getRCFileVars({ const env = await getRCFileVars({
environments: ['production'], environments: ['production'],
filePath: './test/test-files/.rc-test-async.js', filePath: './test/test-files/.rc-test-async.cjs',
})
assert.deepEqual(env, {
THANKS: 'FOR WHAT?!',
ANSWER: 42,
ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
})
})
it('should parse an async mjs .rc file', async (): Promise<void> => {
const env = await getRCFileVars({
environments: ['production'],
filePath: './test/test-files/.rc-test-async.mjs',
}) })
assert.deepEqual(env, { assert.deepEqual(env, {
THANKS: 'FOR WHAT?!', THANKS: 'FOR WHAT?!',

View File

@ -1,6 +1,6 @@
import { assert } from 'chai' import { assert } from 'chai'
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { TermSignals } from '../src/signal-termination' import { TermSignals } from '../src/signal-termination.js'
import { ChildProcess } from 'child_process' import { ChildProcess } from 'child_process'
type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void

View File

@ -1,5 +1,6 @@
module.exports = new Promise((resolve) => { module.exports = new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
console.log('resolved')
resolve({ resolve({
development: { development: {
THANKS: 'FOR ALL THE FISH', THANKS: 'FOR ALL THE FISH',

View File

@ -0,0 +1,20 @@
export default new Promise((resolve) => {
setTimeout(() => {
resolve({
development: {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
},
test: {
THANKS: 'FOR MORE FISHIES',
ANSWER: 21,
},
production: {
THANKS: 'FOR WHAT?!',
ANSWER: 42,
ONLY: 'IN PRODUCTION',
BRINGATOWEL: true,
},
})
}, 200)
})

View File

@ -0,0 +1,8 @@
export default new Promise((resolve) => {
setTimeout(() => {
resolve({
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
})
}, 200)
})

5
test/test-files/test.mjs Normal file
View File

@ -0,0 +1,5 @@
export default {
THANKS: 'FOR ALL THE FISH',
ANSWER: 0,
GALAXY: 'hitch\nhiking',
}

18
test/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"declaration": true,
"esModuleInterop": false,
"lib": ["es2023"],
"module": "Node16",
"moduleDetection": "force",
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"target": "ES2022",
},
"include": [
"./**/*",
"./test-files/.rc-test-async.cjs",
"./test-files/.rc-test-async.mjs",
]
}

View File

@ -1,14 +1,31 @@
import * as os from 'os' import { homedir } from 'node:os'
import * as process from 'process' import { cwd } from 'node:process'
import * as path from 'path' import { normalize } from 'node:path'
import { assert } from 'chai' import { assert } from 'chai'
import * as sinon from 'sinon' import { default as sinon } from 'sinon'
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils' import { default as esmock } from 'esmock'
import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils.js'
let utilsLib: {
resolveEnvFilePath: typeof resolveEnvFilePath,
parseArgList: typeof parseArgList,
isPromise: typeof isPromise
}
describe('utils', (): void => { describe('utils', (): void => {
describe('resolveEnvFilePath', (): void => { describe('resolveEnvFilePath', (): void => {
const homePath = os.homedir() const homePath = homedir()
const currentDir = process.cwd() const currentDir = cwd()
let homedirStub: sinon.SinonStub<any>
before(async (): Promise<void> => {
homedirStub = sinon.stub()
utilsLib = await esmock('../src/utils.js', {
'node:os': {
homedir: homedirStub
},
})
})
afterEach((): void => { afterEach((): void => {
sinon.restore() sinon.restore()
@ -16,18 +33,17 @@ describe('utils', (): void => {
it('should return an absolute path, given a relative path', (): void => { it('should return an absolute path, given a relative path', (): void => {
const res = resolveEnvFilePath('./bob') const res = resolveEnvFilePath('./bob')
assert.equal(res, path.normalize(`${currentDir}/bob`)) assert.equal(res, normalize(`${currentDir}/bob`))
}) })
it('should return an absolute path, given a path with ~ for home directory', (): void => { it('should return an absolute path, given a path with ~ for home directory', (): void => {
const res = resolveEnvFilePath('~/bob') const res = resolveEnvFilePath('~/bob')
assert.equal(res, path.normalize(`${homePath}/bob`)) assert.equal(res, normalize(`${homePath}/bob`))
}) })
it('should not attempt to replace ~ if home dir does not exist', (): void => { it('should not attempt to replace ~ if home dir does not exist', (): void => {
sinon.stub(os, 'homedir') const res = utilsLib.resolveEnvFilePath('~/bob')
const res = resolveEnvFilePath('~/bob') assert.equal(res, normalize(`${currentDir}/~/bob`))
assert.equal(res, path.normalize(`${currentDir}/~/bob`))
}) })
}) })

View File

@ -1,16 +1,14 @@
{ {
"compilerOptions": { "compilerOptions": {
"declaration": true,
"esModuleInterop": false,
"lib": ["es2023"],
"module": "Node16",
"moduleDetection": "force",
"outDir": "./dist", "outDir": "./dist",
"target": "es2017",
"module": "commonjs",
"resolveJsonModule": true, "resolveJsonModule": true,
"strict": true, "strict": true,
"declaration": true, "target": "ES2022",
"lib": [
"es2018",
"es2019",
"es2020"
]
}, },
"include": [ "include": [
"./src/**/*" "./src/**/*"