From ef9665c5fefa0f9d434b3850f0f5c2b623d4b4f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:05:40 +0000 Subject: [PATCH 1/4] chore(deps-dev): bump chai from 4.5.0 to 5.1.2 Bumps [chai](https://github.com/chaijs/chai) from 4.5.0 to 5.1.2. - [Release notes](https://github.com/chaijs/chai/releases) - [Changelog](https://github.com/chaijs/chai/blob/main/History.md) - [Commits](https://github.com/chaijs/chai/compare/v4.5.0...v5.1.2) --- updated-dependencies: - dependency-name: chai dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7eb7dbd..131bfa6 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@types/mocha": "^7.0.0", "@types/node": "^12.0.0", "@types/sinon": "^17.0.3", - "chai": "^4.0.0", + "chai": "^5.1.2", "coveralls": "^3.0.0", "globals": "^15.12.0", "husky": "^9.1.7", From f8106ac44ad049ef773c22168f1c9218d044fb8d Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Mon, 2 Dec 2024 10:25:25 -0900 Subject: [PATCH 2/4] feat(esm): convert to using esm modules --- .mocharc.json | 8 ++ LICENSE | 2 +- bin/env-cmd.js | 3 +- eslint.config.js | 47 +++++------ eslint.config.js.test | 26 ------ package.json | 62 +++++--------- src/cli.ts | 25 ++++++ src/env-cmd.ts | 36 ++------ src/expand-envs.ts | 2 +- src/get-env-vars.ts | 6 +- src/index.ts | 7 +- src/parse-args.ts | 6 +- src/parse-env-file.ts | 20 +++-- src/parse-rc-file.ts | 20 +++-- src/spawn.ts | 4 - src/utils.ts | 12 ++- test/cli.spec.ts | 57 +++++++++++++ test/env-cmd.spec.ts | 84 +++++++------------ test/expand-envs.spec.ts | 2 +- test/get-env-vars.spec.ts | 71 +++++++++------- test/parse-args.spec.ts | 4 +- test/parse-env-file.spec.ts | 27 ++++-- test/parse-rc-file.spec.ts | 19 ++++- test/signal-termination.spec.ts | 4 +- .../{.rc-test-async.js => .rc-test-async.cjs} | 1 + test/test-files/.rc-test-async.mjs | 20 +++++ .../{test-async.js => test-async.cjs} | 0 test/test-files/test-async.mjs | 8 ++ test/test-files/{test.js => test.cjs} | 0 test/test-files/test.mjs | 5 ++ test/tsconfig.json | 18 ++++ test/utils.spec.ts | 40 ++++++--- tsconfig.json | 14 ++-- 33 files changed, 386 insertions(+), 274 deletions(-) create mode 100644 .mocharc.json delete mode 100644 eslint.config.js.test create mode 100644 src/cli.ts delete mode 100644 src/spawn.ts create mode 100644 test/cli.spec.ts rename test/test-files/{.rc-test-async.js => .rc-test-async.cjs} (93%) create mode 100644 test/test-files/.rc-test-async.mjs rename test/test-files/{test-async.js => test-async.cjs} (100%) create mode 100644 test/test-files/test-async.mjs rename test/test-files/{test.js => test.cjs} (100%) create mode 100644 test/test-files/test.mjs create mode 100644 test/tsconfig.json diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..57bec6e --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/mocharc.json", + "require": ["tsx/esm", "esmock"], + "extensions": ["ts"], + "spec": [ + "test/**/*.ts" + ] +} diff --git a/LICENSE b/LICENSE index a384282..cceb893 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Todd Bluhm +Copyright (c) Todd Bluhm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/env-cmd.js b/bin/env-cmd.js index 18745fe..6a7f73f 100755 --- a/bin/env-cmd.js +++ b/bin/env-cmd.js @@ -1,2 +1,3 @@ #! /usr/bin/env node -require('../dist').CLI(process.argv.slice(2)) +import { CLI } from '../dist' +CLI(process.argv.slice(2)) diff --git a/eslint.config.js b/eslint.config.js index 9edb080..eeb2419 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,43 +1,38 @@ -const eslint = require('@eslint/js') -const tseslint = require('typescript-eslint') -const globals = require('globals') -const stylistic = require('@stylistic/eslint-plugin') +import { default as tseslint } from 'typescript-eslint' +import { default as globals } from 'globals' +import { default as eslint } from '@eslint/js' -module.exports = tseslint.config( +export default tseslint.config( { - ignores: ['dist/*', 'bin/*'], - rules: { - '@typescript-eslint/no-require-imports': 'off', - }, + // Ignore build folder + ignores: ['dist/*'], + }, + eslint.configs.recommended, + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + // Enable Type generation languageOptions: { globals: { ...globals.node, }, parserOptions: { - projectService: { - allowDefaultProject: ['test/*.ts'], - }, + project: ['./tsconfig.json', './test/tsconfig.json'], }, - }, - 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 - files: ['test/*.ts'], + files: ['test/**/*'], rules: { '@typescript-eslint/no-explicit-any': '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], + }, ) diff --git a/eslint.config.js.test b/eslint.config.js.test deleted file mode 100644 index 00e63e2..0000000 --- a/eslint.config.js.test +++ /dev/null @@ -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/'], - } - ] -})() diff --git a/package.json b/package.json index 131bfa6..d9eb091 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,17 @@ "description": "Executes a command using the environment variables in an env file", "main": "dist/index.js", "types": "dist/index.d.ts", + "type": "module", "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" }, "scripts": { "prepare": "husky", - "test": "mocha -r ts-node/register ./test/**/*.ts", - "test-cover": "nyc npm test", + "test": "mocha", + "test-cover": "c8 npm test", "coveralls": "coveralls < coverage/lcov.info", "lint": "npx eslint .", "build": "tsc", @@ -54,58 +55,33 @@ "devDependencies": { "@commitlint/cli": "^19.6.0", "@commitlint/config-conventional": "^19.6.0", - "@eslint/js": "^9.15.0", - "@stylistic/eslint-plugin": "^2.11.0", - "@types/chai": "^4.0.0", - "@types/cross-spawn": "^6.0.0", - "@types/mocha": "^7.0.0", - "@types/node": "^12.0.0", + "@eslint/js": "^9.16.0", + "@types/chai": "^5.0.1", + "@types/cross-spawn": "^6.0.6", + "@types/mocha": "^10.0.10", + "@types/node": "^22.10.1", "@types/sinon": "^17.0.3", + "c8": "^10.1.2", "chai": "^5.1.2", "coveralls": "^3.0.0", + "esmock": "^2.6.9", "globals": "^15.12.0", "husky": "^9.1.7", - "mocha": "^10.8.2", - "nyc": "^17.1.0", + "mocha": "^11.0.0", "sinon": "^19.0.2", - "ts-node": "^8.0.0", + "tsx": "^4.19.2", "typescript": "^5.7.2", "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": { "extends": [ "@commitlint/config-conventional" ] + }, + "c8": { + "reporter": [ + "text", + "lcov" + ] } } diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..9381026 --- /dev/null +++ b/src/cli.ts @@ -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} + */ +export async function CLI(args: string[]): Promise { + + // 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) + } +} diff --git a/src/env-cmd.ts b/src/env-cmd.ts index 424998b..814659f 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -1,29 +1,9 @@ -import { spawn } from './spawn' -import { EnvCmdOptions, Environment } from './types' -import { TermSignals } from './signal-termination' -import { parseArgs } from './parse-args' -import { getEnvVars } from './get-env-vars' -import { expandEnvs } from './expand-envs' - -/** - * Executes env - cmd using command line arguments - * @export - * @param {string[]} args Command line argument to pass in ['-f', './.env'] - * @returns {Promise} - */ -export async function CLI(args: string[]): Promise { - // 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) - } -} +import { default as spawn } from 'cross-spawn' +import type { EnvCmdOptions, Environment } from './types.ts' +import { TermSignals } from './signal-termination.js' +import { getEnvVars } from './get-env-vars.js' +import { expandEnvs } from './expand-envs.js' +import * as processLib from 'node:process' /** * 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 if (options.noOverride === true) { - env = Object.assign({}, env, process.env) + env = Object.assign({}, env, processLib.env) } else { // 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) { diff --git a/src/expand-envs.ts b/src/expand-envs.ts index f3c3b3a..47a56d5 100644 --- a/src/expand-envs.ts +++ b/src/expand-envs.ts @@ -1,4 +1,4 @@ -import { Environment } from './types' +import type { Environment } from './types.ts' /** * expandEnvs Replaces $var in args and command with environment variables diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index e4e6b8d..89f2f47 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -1,6 +1,6 @@ -import { GetEnvVarOptions, Environment } from './types' -import { getRCFileVars } from './parse-rc-file' -import { getEnvFileVars } from './parse-env-file' +import type { GetEnvVarOptions, Environment } from './types.ts' +import { getRCFileVars } from './parse-rc-file.js' +import { getEnvFileVars } from './parse-env-file.js' const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'] const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'] diff --git a/src/index.ts b/src/index.ts index 5004d6a..5bbaf63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ -import { getEnvVars } from './get-env-vars' +import { getEnvVars } from './get-env-vars.js' // Export the core env-cmd API -export * from './types' -export * from './env-cmd' +export * from './types.js' +export * from './cli.js' +export * from './env-cmd.js' export const GetEnvVars = getEnvVars diff --git a/src/parse-args.ts b/src/parse-args.ts index cdf0927..5f56d82 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,9 +1,9 @@ import * as commander from 'commander' -import { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types' -import { parseArgList } from './utils' +import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts' +import { parseArgList } from './utils.js' // 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 diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index 488da52..bc803bd 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -1,9 +1,7 @@ import * as fs from 'fs' import * as path from 'path' -import { resolveEnvFilePath, isPromise } from './utils' -import { Environment } from './types' - -const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs'] +import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js' +import type { Environment } from './types.ts' /** * Gets the environment vars from an env file @@ -19,9 +17,17 @@ export async function getEnvFileVars(envFilePath: string): Promise // Get the file extension const ext = path.extname(absolutePath).toLowerCase() let env: Environment = {} - if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise: Environment | Promise = require(absolutePath) /* eslint-disable-line */ - env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise + if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + const res = await import(absolutePath) as Environment | { default: Environment } + 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 { const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 1c0c43b..8fc720a 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -1,8 +1,8 @@ import { stat, readFile } from 'fs' import { promisify } from 'util' import { extname } from 'path' -import { resolveEnvFilePath, isPromise } from './utils' -import { Environment, RCEnvironment } from './types' +import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js' +import type { Environment, RCEnvironment } from './types.ts' const statAsync = promisify(stat) const readFileAsync = promisify(readFile) @@ -26,11 +26,19 @@ export async function getRCFileVars( // Get the file extension const ext = extname(absolutePath).toLowerCase() - let parsedData: Partial + let parsedData: Partial = {} try { - if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath) as PromiseLike | RCEnvironment - parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise + if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + const res = await import(absolutePath) as RCEnvironment | { default: RCEnvironment } + 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 { const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) diff --git a/src/spawn.ts b/src/spawn.ts deleted file mode 100644 index b4d9d5f..0000000 --- a/src/spawn.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as spawn from 'cross-spawn' -export { - spawn, -} diff --git a/src/utils.ts b/src/utils.ts index e5c6e50..466c483 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,16 +1,20 @@ -import * as path from 'path' -import * as os from 'os' +import { resolve } from 'node:path' +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 */ export function resolveEnvFilePath(userPath: string): string { // Make sure a home directory exist - const home = os.homedir() as string | undefined + const home = homedir() as string | undefined if (home != null) { 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 diff --git a/test/cli.spec.ts b/test/cli.spec.ts new file mode 100644 index 0000000..786df16 --- /dev/null +++ b/test/cli.spec.ts @@ -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 + let envCmdStub: sinon.SinonStub + let processExitStub: sinon.SinonStub + let cliLib: { CLI: typeof CLI } + + before(async (): Promise => { + 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 => { + 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 => { + 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) + }) +}) diff --git a/test/env-cmd.spec.ts b/test/env-cmd.spec.ts index 3e08264..b8887b6 100644 --- a/test/env-cmd.spec.ts +++ b/test/env-cmd.spec.ts @@ -1,68 +1,44 @@ -import * as sinon from 'sinon' +import { default as sinon } from 'sinon' import { assert } from 'chai' -import * as signalTermLib from '../src/signal-termination' -import * as parseArgsLib from '../src/parse-args' -import * as getEnvVarsLib from '../src/get-env-vars' -import * as expandEnvsLib from '../src/expand-envs' -import * as spawnLib from '../src/spawn' -import * as envCmdLib from '../src/env-cmd' +import { default as esmock } from 'esmock' +import { expandEnvs } from '../src/expand-envs.js' +import type { EnvCmd } from '../src/env-cmd.ts' -describe('CLI', (): void => { - let sandbox: sinon.SinonSandbox - let parseArgsStub: sinon.SinonStub - let envCmdStub: sinon.SinonStub - let processExitStub: sinon.SinonStub - 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 => { - 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 => { - 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) - }) -}) +let envCmdLib: { EnvCmd: typeof EnvCmd } describe('EnvCmd', (): void => { let sandbox: sinon.SinonSandbox let getEnvVarsStub: sinon.SinonStub let spawnStub: sinon.SinonStub let expandEnvsSpy: sinon.SinonSpy - before((): void => { + before(async (): Promise => { sandbox = sinon.createSandbox() - getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars') - spawnStub = sandbox.stub(spawnLib, 'spawn') + getEnvVarsStub = sandbox.stub() + spawnStub = sandbox.stub() spawnStub.returns({ - on: (): void => { /* Fake the on method */ }, - kill: (): void => { /* Fake the kill method */ }, + on: sinon.stub(), + 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 => { diff --git a/test/expand-envs.spec.ts b/test/expand-envs.spec.ts index 7474831..fb9c605 100644 --- a/test/expand-envs.spec.ts +++ b/test/expand-envs.spec.ts @@ -1,6 +1,6 @@ /* eslint @typescript-eslint/no-non-null-assertion: 0 */ import { assert } from 'chai' -import { expandEnvs } from '../src/expand-envs' +import { expandEnvs } from '../src/expand-envs.js' describe('expandEnvs', (): void => { const envs = { diff --git a/test/get-env-vars.spec.ts b/test/get-env-vars.spec.ts index d1401ea..f60aaba 100644 --- a/test/get-env-vars.spec.ts +++ b/test/get-env-vars.spec.ts @@ -1,17 +1,26 @@ -import * as sinon from 'sinon' +import { default as sinon } from 'sinon' import { assert } from 'chai' -import { getEnvVars } from '../src/get-env-vars' -import * as rcFile from '../src/parse-rc-file' -import * as envFile from '../src/parse-env-file' +import { default as esmock } from 'esmock' +import type { getEnvVars } from '../src/get-env-vars.ts' + +let getEnvVarsLib: { getEnvVars: typeof getEnvVars } describe('getEnvVars', (): void => { let getRCFileVarsStub: sinon.SinonStub let getEnvFileVarsStub: sinon.SinonStub let logInfoStub: sinon.SinonStub | undefined - before((): void => { - getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') - getEnvFileVarsStub = sinon.stub(envFile, 'getEnvFileVars') + before(async (): Promise => { + getRCFileVarsStub = sinon.stub() + 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 => { @@ -27,7 +36,7 @@ describe('getEnvVars', (): void => { it('should parse the json .rc file from the default path with the given environment', async (): Promise => { 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.lengthOf(Object.keys(envs), 1) assert.equal(envs.THANKS, 'FOR ALL THE FISH') @@ -42,7 +51,7 @@ describe('getEnvVars', (): void => { async (): Promise => { logInfoStub = sinon.stub(console, 'info') 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) }, ) @@ -52,7 +61,7 @@ describe('getEnvVars', (): void => { pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) 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.lengthOf(Object.keys(envs), 1) assert.equal(envs.THANKS, 'FOR ALL THE FISH') @@ -67,7 +76,7 @@ describe('getEnvVars', (): void => { pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { - await getEnvVars({ rc: { environments: ['production'] } }) + await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] } }) assert.fail('should not get here.') } catch (e) { @@ -84,7 +93,7 @@ describe('getEnvVars', (): void => { pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { - await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) + await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.fail('should not get here.') } catch { @@ -97,7 +106,7 @@ describe('getEnvVars', (): void => { environmentError.name = 'EnvironmentError' getRCFileVarsStub.rejects(environmentError) try { - await getEnvVars({ rc: { environments: ['bad'] } }) + await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] } }) assert.fail('should not get here.') } catch (e) { @@ -113,7 +122,7 @@ describe('getEnvVars', (): void => { environmentError.name = 'EnvironmentError' getRCFileVarsStub.rejects(environmentError) try { - await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) + await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) assert.fail('should not get here.') } catch { @@ -123,7 +132,7 @@ describe('getEnvVars', (): void => { it('should find .rc file at custom path path', async (): Promise => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) - const envs = await getEnvVars({ + const envs = await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, }) assert.isOk(envs) @@ -138,7 +147,7 @@ describe('getEnvVars', (): void => { it('should print custom .rc file path to info for verbose', async (): Promise => { logInfoStub = sinon.stub(console, 'info') getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) - await getEnvVars({ + await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, verbose: true, }) @@ -150,7 +159,7 @@ describe('getEnvVars', (): void => { pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { - await getEnvVars({ + await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, }) assert.fail('should not get here.') @@ -168,7 +177,7 @@ describe('getEnvVars', (): void => { pathError.name = 'PathError' getRCFileVarsStub.rejects(pathError) try { - await getEnvVars({ + await getEnvVarsLib.getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, verbose: true, }) @@ -184,7 +193,7 @@ describe('getEnvVars', (): void => { environmentError.name = 'EnvironmentError' getRCFileVarsStub.rejects(environmentError) try { - await getEnvVars({ + await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'], filePath: '../.custom-rc' }, }) assert.fail('should not get here.') @@ -203,7 +212,7 @@ describe('getEnvVars', (): void => { environmentError.name = 'EnvironmentError' getRCFileVarsStub.rejects(environmentError) try { - await getEnvVars({ + await getEnvVarsLib.getEnvVars({ rc: { environments: ['bad'], filePath: '../.custom-rc' }, verbose: true, }) @@ -217,7 +226,7 @@ describe('getEnvVars', (): void => { it('should parse the env file from a custom path', async (): Promise => { 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.lengthOf(Object.keys(envs), 1) 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 => { logInfoStub = sinon.stub(console, 'info') 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) }) it('should fail to find env file at custom path', async (): Promise => { getEnvFileVarsStub.rejects('Not found.') try { - await getEnvVars({ envFile: { filePath: '../.env-file' } }) + await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' } }) assert.fail('should not get here.') } catch (e) { @@ -249,7 +258,7 @@ describe('getEnvVars', (): void => { logInfoStub = sinon.stub(console, 'info') getEnvFileVarsStub.rejects('Not found.') try { - await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) + await getEnvVarsLib.getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) assert.fail('should not get here.') } catch { @@ -263,7 +272,7 @@ describe('getEnvVars', (): void => { async (): Promise => { getEnvFileVarsStub.onFirstCall().rejects('File not found.') 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.lengthOf(Object.keys(envs), 1) assert.equal(envs.THANKS, 'FOR ALL THE FISH') @@ -279,14 +288,14 @@ describe('getEnvVars', (): void => { logInfoStub = sinon.stub(console, 'info') getEnvFileVarsStub.onFirstCall().rejects('File not found.') 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) }, ) it('should parse the env file from the default path', async (): Promise => { getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) - const envs = await getEnvVars() + const envs = await getEnvVarsLib.getEnvVars() assert.isOk(envs) assert.lengthOf(Object.keys(envs), 1) 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 => { logInfoStub = sinon.stub(console, 'info') getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) - await getEnvVars({ verbose: true }) + await getEnvVarsLib.getEnvVars({ verbose: true }) assert.equal(logInfoStub.callCount, 1) }) it('should search all default env file paths', async (): Promise => { getEnvFileVarsStub.throws('Not found.') getEnvFileVarsStub.onThirdCall().returns({ THANKS: 'FOR ALL THE FISH' }) - const envs = await getEnvVars() + const envs = await getEnvVarsLib.getEnvVars() assert.isOk(envs) assert.lengthOf(Object.keys(envs), 1) 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 => { getEnvFileVarsStub.rejects('Not found.') try { - await getEnvVars() + await getEnvVarsLib.getEnvVars() assert.fail('should not get here.') } catch (e) { @@ -332,7 +341,7 @@ describe('getEnvVars', (): void => { logInfoStub = sinon.stub(console, 'info') getEnvFileVarsStub.rejects('Not found.') try { - await getEnvVars({ verbose: true }) + await getEnvVarsLib.getEnvVars({ verbose: true }) assert.fail('should not get here.') } catch { diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index e8443d2..bed6479 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -1,7 +1,7 @@ /* eslint @typescript-eslint/no-non-null-assertion: 0 */ -import * as sinon from 'sinon' +import { default as sinon } from 'sinon' import { assert } from 'chai' -import { parseArgs } from '../src/parse-args' +import { parseArgs } from '../src/parse-args.js' describe('parseArgs', (): void => { const command = 'command' diff --git a/test/parse-env-file.spec.ts b/test/parse-env-file.spec.ts index 22043ca..4478d7a 100644 --- a/test/parse-env-file.spec.ts +++ b/test/parse-env-file.spec.ts @@ -2,7 +2,7 @@ import { assert } from 'chai' import { stripEmptyLines, stripComments, parseEnvVars, parseEnvString, getEnvFileVars, -} from '../src/parse-env-file' +} from '../src/parse-env-file.js' describe('stripEmptyLines', (): void => { it('should strip out all empty lines', (): void => { @@ -125,8 +125,8 @@ describe('getEnvFileVars', (): void => { }) }) - it('should parse a js file', async (): Promise => { - const env = await getEnvFileVars('./test/test-files/test.js') + it('should parse a js/cjs file', async (): Promise => { + const env = await getEnvFileVars('./test/test-files/test.cjs') assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', ANSWER: 0, @@ -134,8 +134,25 @@ describe('getEnvFileVars', (): void => { }) }) - it('should parse an async js file', async (): Promise => { - const env = await getEnvFileVars('./test/test-files/test-async.js') + it('should parse an async js/cjs file', async (): Promise => { + 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 => { + 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 => { + const env = await getEnvFileVars('./test/test-files/test-async.mjs') assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', ANSWER: 0, diff --git a/test/parse-rc-file.spec.ts b/test/parse-rc-file.spec.ts index 1f61a12..b87f102 100644 --- a/test/parse-rc-file.spec.ts +++ b/test/parse-rc-file.spec.ts @@ -1,5 +1,5 @@ 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 rcJSONFilePath = './test/test-files/.rc-test.json' @@ -58,10 +58,23 @@ describe('getRCFileVars', (): void => { } }) - it('should parse an async js .rc file', async (): Promise => { + it('should parse an async js/cjs .rc file', async (): Promise => { const env = await getRCFileVars({ 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 => { + const env = await getRCFileVars({ + environments: ['production'], + filePath: './test/test-files/.rc-test-async.mjs', }) assert.deepEqual(env, { THANKS: 'FOR WHAT?!', diff --git a/test/signal-termination.spec.ts b/test/signal-termination.spec.ts index 4657eed..fbf9ec6 100644 --- a/test/signal-termination.spec.ts +++ b/test/signal-termination.spec.ts @@ -1,6 +1,6 @@ import { assert } from 'chai' -import * as sinon from 'sinon' -import { TermSignals } from '../src/signal-termination' +import { default as sinon } from 'sinon' +import { TermSignals } from '../src/signal-termination.js' import { ChildProcess } from 'child_process' type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void diff --git a/test/test-files/.rc-test-async.js b/test/test-files/.rc-test-async.cjs similarity index 93% rename from test/test-files/.rc-test-async.js rename to test/test-files/.rc-test-async.cjs index 356914f..7baa971 100644 --- a/test/test-files/.rc-test-async.js +++ b/test/test-files/.rc-test-async.cjs @@ -1,5 +1,6 @@ module.exports = new Promise((resolve) => { setTimeout(() => { + console.log('resolved') resolve({ development: { THANKS: 'FOR ALL THE FISH', diff --git a/test/test-files/.rc-test-async.mjs b/test/test-files/.rc-test-async.mjs new file mode 100644 index 0000000..33d9cc5 --- /dev/null +++ b/test/test-files/.rc-test-async.mjs @@ -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) +}) diff --git a/test/test-files/test-async.js b/test/test-files/test-async.cjs similarity index 100% rename from test/test-files/test-async.js rename to test/test-files/test-async.cjs diff --git a/test/test-files/test-async.mjs b/test/test-files/test-async.mjs new file mode 100644 index 0000000..5f7e957 --- /dev/null +++ b/test/test-files/test-async.mjs @@ -0,0 +1,8 @@ +export default new Promise((resolve) => { + setTimeout(() => { + resolve({ + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, + }) + }, 200) +}) diff --git a/test/test-files/test.js b/test/test-files/test.cjs similarity index 100% rename from test/test-files/test.js rename to test/test-files/test.cjs diff --git a/test/test-files/test.mjs b/test/test-files/test.mjs new file mode 100644 index 0000000..96a41ab --- /dev/null +++ b/test/test-files/test.mjs @@ -0,0 +1,5 @@ +export default { + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, + GALAXY: 'hitch\nhiking', +} diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..72425c7 --- /dev/null +++ b/test/tsconfig.json @@ -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", + ] +} diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 8afac0e..5fc8b1b 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -1,14 +1,31 @@ -import * as os from 'os' -import * as process from 'process' -import * as path from 'path' +import { homedir } from 'node:os' +import { cwd } from 'node:process' +import { normalize } from 'node:path' import { assert } from 'chai' -import * as sinon from 'sinon' -import { resolveEnvFilePath, parseArgList, isPromise } from '../src/utils' +import { default as sinon } from 'sinon' +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('resolveEnvFilePath', (): void => { - const homePath = os.homedir() - const currentDir = process.cwd() + const homePath = homedir() + const currentDir = cwd() + let homedirStub: sinon.SinonStub + + before(async (): Promise => { + homedirStub = sinon.stub() + utilsLib = await esmock('../src/utils.js', { + 'node:os': { + homedir: homedirStub + }, + }) + }) afterEach((): void => { sinon.restore() @@ -16,18 +33,17 @@ describe('utils', (): void => { it('should return an absolute path, given a relative path', (): void => { 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 => { 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 => { - sinon.stub(os, 'homedir') - const res = resolveEnvFilePath('~/bob') - assert.equal(res, path.normalize(`${currentDir}/~/bob`)) + const res = utilsLib.resolveEnvFilePath('~/bob') + assert.equal(res, normalize(`${currentDir}/~/bob`)) }) }) diff --git a/tsconfig.json b/tsconfig.json index f2b1805..1708f4c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,14 @@ { "compilerOptions": { + "declaration": true, + "esModuleInterop": false, + "lib": ["es2023"], + "module": "Node16", + "moduleDetection": "force", "outDir": "./dist", - "target": "es2017", - "module": "commonjs", "resolveJsonModule": true, "strict": true, - "declaration": true, - "lib": [ - "es2018", - "es2019", - "es2020" - ] + "target": "ES2022", }, "include": [ "./src/**/*" From 7eae0dae5411b561435f6c6b073024083098e037 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Tue, 3 Dec 2024 02:36:17 -0900 Subject: [PATCH 3/4] fix(tsc): fix tsc build process and file importing --- bin/env-cmd.js | 2 +- dist/cli.d.ts | 8 +++ dist/cli.js | 21 +++++++ dist/env-cmd.d.ts | 13 +--- dist/env-cmd.js | 55 +++++------------ dist/expand-envs.d.ts | 5 +- dist/expand-envs.js | 12 ++-- dist/get-env-vars.d.ts | 8 +-- dist/get-env-vars.js | 70 ++++++++++++---------- dist/index.d.ts | 7 ++- dist/index.js | 14 ++--- dist/parse-args.d.ts | 5 +- dist/parse-args.js | 29 ++++----- dist/parse-env-file.d.ts | 7 ++- dist/parse-env-file.js | 65 ++++++++++++-------- dist/parse-rc-file.d.ts | 3 +- dist/parse-rc-file.js | 65 ++++++++++++-------- dist/signal-termination.d.ts | 4 +- dist/signal-termination.js | 111 +++++++++++++++++++---------------- dist/spawn.d.ts | 2 - dist/spawn.js | 4 -- dist/types.d.ts | 34 ++++++++--- dist/types.js | 3 +- dist/utils.d.ts | 5 +- dist/utils.js | 31 +++++----- src/parse-args.ts | 4 +- src/parse-env-file.ts | 7 ++- src/parse-rc-file.ts | 7 ++- tsconfig.json | 5 +- 29 files changed, 336 insertions(+), 270 deletions(-) create mode 100644 dist/cli.d.ts create mode 100644 dist/cli.js delete mode 100644 dist/spawn.d.ts delete mode 100644 dist/spawn.js diff --git a/bin/env-cmd.js b/bin/env-cmd.js index 6a7f73f..b133f8a 100755 --- a/bin/env-cmd.js +++ b/bin/env-cmd.js @@ -1,3 +1,3 @@ #! /usr/bin/env node -import { CLI } from '../dist' +import { CLI } from '../dist/index.js' CLI(process.argv.slice(2)) diff --git a/dist/cli.d.ts b/dist/cli.d.ts new file mode 100644 index 0000000..5012bcc --- /dev/null +++ b/dist/cli.d.ts @@ -0,0 +1,8 @@ +import type { Environment } from './types.ts'; +/** + * Executes env - cmd using command line arguments + * @export + * @param {string[]} args Command line argument to pass in ['-f', './.env'] + * @returns {Promise} + */ +export declare function CLI(args: string[]): Promise; diff --git a/dist/cli.js b/dist/cli.js new file mode 100644 index 0000000..01984d4 --- /dev/null +++ b/dist/cli.js @@ -0,0 +1,21 @@ +import * as processLib from 'node:process'; +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} + */ +export async function CLI(args) { + // 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); + } +} diff --git a/dist/env-cmd.d.ts b/dist/env-cmd.d.ts index 5141864..b28c35a 100644 --- a/dist/env-cmd.d.ts +++ b/dist/env-cmd.d.ts @@ -1,17 +1,10 @@ -import { EnvCmdOptions } from './types'; -/** - * Executes env - cmd using command line arguments - * @export - * @param {string[]} args Command line argument to pass in ['-f', './.env'] - * @returns {Promise<{ [key: string]: any }>} - */ -export declare function CLI(args: string[]): Promise>; +import type { EnvCmdOptions, Environment } from './types.ts'; /** * The main env-cmd program. This will spawn a new process and run the given command using * various environment file solutions. * * @export * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } - * @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value + * @returns {Promise} Returns an object containing [environment variable name]: value */ -export declare function EnvCmd({ command, commandArgs, envFile, rc, options }: EnvCmdOptions): Promise>; +export declare function EnvCmd({ command, commandArgs, envFile, rc, options, }: EnvCmdOptions): Promise; diff --git a/dist/env-cmd.js b/dist/env-cmd.js index a499ed5..e02865f 100644 --- a/dist/env-cmd.js +++ b/dist/env-cmd.js @@ -1,70 +1,47 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const spawn_1 = require("./spawn"); -const signal_termination_1 = require("./signal-termination"); -const parse_args_1 = require("./parse-args"); -const get_env_vars_1 = require("./get-env-vars"); -const expand_envs_1 = require("./expand-envs"); -/** - * Executes env - cmd using command line arguments - * @export - * @param {string[]} args Command line argument to pass in ['-f', './.env'] - * @returns {Promise<{ [key: string]: any }>} - */ -async function CLI(args) { - // Parse the args from the command line - const parsedArgs = parse_args_1.parseArgs(args); - // Run EnvCmd - try { - return await exports.EnvCmd(parsedArgs); - } - catch (e) { - console.error(e); - return process.exit(1); - } -} -exports.CLI = CLI; +import { default as spawn } from 'cross-spawn'; +import { TermSignals } from './signal-termination.js'; +import { getEnvVars } from './get-env-vars.js'; +import { expandEnvs } from './expand-envs.js'; +import * as processLib from 'node:process'; /** * The main env-cmd program. This will spawn a new process and run the given command using * various environment file solutions. * * @export * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } - * @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value + * @returns {Promise} Returns an object containing [environment variable name]: value */ -async function EnvCmd({ command, commandArgs, envFile, rc, options = {} }) { - var _a; +export async function EnvCmd({ command, commandArgs, envFile, rc, options = {}, }) { let env = {}; try { - env = await get_env_vars_1.getEnvVars({ envFile, rc, verbose: options.verbose }); + env = await getEnvVars({ envFile, rc, verbose: options.verbose }); } catch (e) { - if (!((_a = options.silent) !== null && _a !== void 0 ? _a : false)) { + if (!(options.silent ?? false)) { throw e; } } // Override the merge order if --no-override flag set if (options.noOverride === true) { - env = Object.assign({}, env, process.env); + env = Object.assign({}, env, processLib.env); } else { // 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) { - command = expand_envs_1.expandEnvs(command, env); - commandArgs = commandArgs.map(arg => expand_envs_1.expandEnvs(arg, env)); + command = expandEnvs(command, env); + commandArgs = commandArgs.map(arg => expandEnvs(arg, env)); } // Execute the command with the given environment variables - const proc = spawn_1.spawn(command, commandArgs, { + const proc = spawn(command, commandArgs, { stdio: 'inherit', shell: options.useShell, - env + env: env, }); // Handle any termination signals for parent and child proceses - const signals = new signal_termination_1.TermSignals({ verbose: options.verbose }); + const signals = new TermSignals({ verbose: options.verbose }); signals.handleUncaughtExceptions(); signals.handleTermSignals(proc); return env; } -exports.EnvCmd = EnvCmd; diff --git a/dist/expand-envs.d.ts b/dist/expand-envs.d.ts index 5a68b32..7706ca7 100644 --- a/dist/expand-envs.d.ts +++ b/dist/expand-envs.d.ts @@ -1,5 +1,6 @@ +import type { Environment } from './types.ts'; /** * expandEnvs Replaces $var in args and command with environment variables - * the environment variable doesn't exist, it leaves it as is. + * if the environment variable doesn't exist, it leaves it as is. */ -export declare function expandEnvs(str: string, envs: Record): string; +export declare function expandEnvs(str: string, envs: Environment): string; diff --git a/dist/expand-envs.js b/dist/expand-envs.js index b46324a..ccb4853 100644 --- a/dist/expand-envs.js +++ b/dist/expand-envs.js @@ -1,13 +1,11 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); /** * expandEnvs Replaces $var in args and command with environment variables - * the environment variable doesn't exist, it leaves it as is. + * if the environment variable doesn't exist, it leaves it as is. */ -function expandEnvs(str, envs) { - return str.replace(/(? { +export function expandEnvs(str, envs) { + return str.replace(/(? { const varValue = envs[varName.slice(1)]; - return varValue === undefined ? varName : varValue; + // const test = 42; + return varValue === undefined ? varName : varValue.toString(); }); } -exports.expandEnvs = expandEnvs; diff --git a/dist/get-env-vars.d.ts b/dist/get-env-vars.d.ts index aaf968e..209e284 100644 --- a/dist/get-env-vars.d.ts +++ b/dist/get-env-vars.d.ts @@ -1,12 +1,12 @@ -import { GetEnvVarOptions } from './types'; -export declare function getEnvVars(options?: GetEnvVarOptions): Promise>; +import type { GetEnvVarOptions, Environment } from './types.ts'; +export declare function getEnvVars(options?: GetEnvVarOptions): Promise; export declare function getEnvFile({ filePath, fallback, verbose }: { filePath?: string; fallback?: boolean; verbose?: boolean; -}): Promise>; +}): Promise; export declare function getRCFile({ environments, filePath, verbose }: { environments: string[]; filePath?: string; verbose?: boolean; -}): Promise>; +}): Promise; diff --git a/dist/get-env-vars.js b/dist/get-env-vars.js index 4bd3c02..5c056ef 100644 --- a/dist/get-env-vars.js +++ b/dist/get-env-vars.js @@ -1,40 +1,38 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const parse_rc_file_1 = require("./parse-rc-file"); -const parse_env_file_1 = require("./parse-env-file"); +import { getRCFileVars } from './parse-rc-file.js'; +import { getEnvFileVars } from './parse-env-file.js'; const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json']; const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json']; -async function getEnvVars(options = {}) { - options.envFile = options.envFile !== undefined ? options.envFile : {}; +export async function getEnvVars(options = {}) { + options.envFile = options.envFile ?? {}; // Check for rc file usage if (options.rc !== undefined) { return await getRCFile({ environments: options.rc.environments, filePath: options.rc.filePath, - verbose: options.verbose + verbose: options.verbose, }); } return await getEnvFile({ filePath: options.envFile.filePath, fallback: options.envFile.fallback, - verbose: options.verbose + verbose: options.verbose, }); } -exports.getEnvVars = getEnvVars; -async function getEnvFile({ filePath, fallback, verbose }) { +export async function getEnvFile({ filePath, fallback, verbose }) { // Use env file if (filePath !== undefined) { try { - const env = await parse_env_file_1.getEnvFileVars(filePath); + const env = await getEnvFileVars(filePath); if (verbose === true) { console.info(`Found .env file at path: ${filePath}`); } return env; } - catch (e) { + catch { if (verbose === true) { console.info(`Failed to find .env file at path: ${filePath}`); } + // Ignore error as we are just trying this location } if (fallback !== true) { throw new Error(`Failed to find .env file at path: ${filePath}`); @@ -43,13 +41,15 @@ async function getEnvFile({ filePath, fallback, verbose }) { // Use the default env file locations for (const path of ENV_FILE_DEFAULT_LOCATIONS) { try { - const env = await parse_env_file_1.getEnvFileVars(path); + const env = await getEnvFileVars(path); if (verbose === true) { console.info(`Found .env file at default path: ${path}`); } return env; } - catch (e) { } + catch { + // Ignore error because we are just trying this location + } } const error = `Failed to find .env file at default paths: [${ENV_FILE_DEFAULT_LOCATIONS.join(',')}]`; if (verbose === true) { @@ -57,26 +57,27 @@ async function getEnvFile({ filePath, fallback, verbose }) { } throw new Error(error); } -exports.getEnvFile = getEnvFile; -async function getRCFile({ environments, filePath, verbose }) { +export async function getRCFile({ environments, filePath, verbose }) { // User provided an .rc file path if (filePath !== undefined) { try { - const env = await parse_rc_file_1.getRCFileVars({ environments, filePath }); + const env = await getRCFileVars({ environments, filePath }); if (verbose === true) { console.info(`Found environments: [${environments.join(',')}] for .rc file at path: ${filePath}`); } return env; } catch (e) { - if (e.name === 'PathError') { - if (verbose === true) { - console.info(`Failed to find .rc file at path: ${filePath}`); + if (e instanceof Error) { + if (e.name === 'PathError') { + if (verbose === true) { + console.info(`Failed to find .rc file at path: ${filePath}`); + } } - } - if (e.name === 'EnvironmentError') { - if (verbose === true) { - console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`); + if (e.name === 'EnvironmentError') { + if (verbose === true) { + console.info(`Failed to find environments: [${environments.join(',')}] for .rc file at path: ${filePath}`); + } } } throw e; @@ -85,19 +86,27 @@ async function getRCFile({ environments, filePath, verbose }) { // Use the default .rc file locations for (const path of RC_FILE_DEFAULT_LOCATIONS) { try { - const env = await parse_rc_file_1.getRCFileVars({ environments, filePath: path }); + const env = await getRCFileVars({ environments, filePath: path }); if (verbose === true) { console.info(`Found environments: [${environments.join(',')}] for default .rc file at path: ${path}`); } return env; } catch (e) { - if (e.name === 'EnvironmentError') { - const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`; - if (verbose === true) { - console.info(errorText); + if (e instanceof Error) { + if (e.name === 'EnvironmentError') { + const errorText = `Failed to find environments: [${environments.join(',')}] for .rc file at path: ${path}`; + if (verbose === true) { + console.info(errorText); + } + throw new Error(errorText); + } + if (e.name === 'ParseError') { + if (verbose === true) { + console.info(e.message); + } + throw new Error(e.message); } - throw new Error(errorText); } } } @@ -107,4 +116,3 @@ async function getRCFile({ environments, filePath, verbose }) { } throw new Error(errorText); } -exports.getRCFile = getRCFile; diff --git a/dist/index.d.ts b/dist/index.d.ts index 39037f2..e0c3b73 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,4 +1,5 @@ -import { getEnvVars } from './get-env-vars'; -export * from './types'; -export * from './env-cmd'; +import { getEnvVars } from './get-env-vars.js'; +export * from './types.js'; +export * from './cli.js'; +export * from './env-cmd.js'; export declare const GetEnvVars: typeof getEnvVars; diff --git a/dist/index.js b/dist/index.js index 6009b62..812f058 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,8 +1,6 @@ -"use strict"; -function __export(m) { - for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -Object.defineProperty(exports, "__esModule", { value: true }); -const get_env_vars_1 = require("./get-env-vars"); -__export(require("./env-cmd")); -exports.GetEnvVars = get_env_vars_1.getEnvVars; +import { getEnvVars } from './get-env-vars.js'; +// Export the core env-cmd API +export * from './types.js'; +export * from './cli.js'; +export * from './env-cmd.js'; +export const GetEnvVars = getEnvVars; diff --git a/dist/parse-args.d.ts b/dist/parse-args.d.ts index 0c68100..15394f5 100644 --- a/dist/parse-args.d.ts +++ b/dist/parse-args.d.ts @@ -1,7 +1,6 @@ -import * as commander from 'commander'; -import { EnvCmdOptions } from './types'; +import type { EnvCmdOptions, CommanderOptions } from './types.ts'; /** * Parses the arguments passed into the cli */ export declare function parseArgs(args: string[]): EnvCmdOptions; -export declare function parseArgsUsingCommander(args: string[]): commander.Command; +export declare function parseArgsUsingCommander(args: string[]): CommanderOptions; diff --git a/dist/parse-args.js b/dist/parse-args.js index c650ea5..4494073 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -1,13 +1,10 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const commander = require("commander"); -const utils_1 = require("./utils"); -// Use commonjs require to prevent a weird folder hierarchy in dist -const packageJson = require('../package.json'); +import * as commander from 'commander'; +import { parseArgList } from './utils.js'; +import { default as packageJson } from '../package.json' with { type: 'json' }; /** * Parses the arguments passed into the cli */ -function parseArgs(args) { +export function parseArgs(args) { // Run the initial arguments through commander in order to determine // which value in the args array is the `command` to execute let program = parseArgsUsingCommander(args); @@ -39,17 +36,19 @@ function parseArgs(args) { silent = true; } let rc; - if (program.environments !== undefined && program.environments.length !== 0) { + if (program.environments !== undefined + && Array.isArray(program.environments) + && program.environments.length !== 0) { rc = { environments: program.environments, - filePath: program.rcFile + filePath: program.rcFile, }; } let envFile; if (program.file !== undefined) { envFile = { filePath: program.file, - fallback: program.fallback + fallback: program.fallback, }; } const options = { @@ -62,21 +61,20 @@ function parseArgs(args) { noOverride, silent, useShell, - verbose - } + verbose, + }, }; if (verbose) { console.info(`Options: ${JSON.stringify(options, null, 0)}`); } return options; } -exports.parseArgs = parseArgs; -function parseArgsUsingCommander(args) { +export function parseArgsUsingCommander(args) { const program = new commander.Command(); return program .version(packageJson.version, '-v, --version') .usage('[options] [...args]') - .option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', utils_1.parseArgList) + .option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList) .option('-f, --file [path]', 'Custom env file path (default path: ./.env)') .option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--no-override', 'Do not override existing environment variables') @@ -88,4 +86,3 @@ function parseArgsUsingCommander(args) { .allowUnknownOption(true) .parse(['_', '_', ...args]); } -exports.parseArgsUsingCommander = parseArgsUsingCommander; diff --git a/dist/parse-env-file.d.ts b/dist/parse-env-file.d.ts index c298868..299c2f9 100644 --- a/dist/parse-env-file.d.ts +++ b/dist/parse-env-file.d.ts @@ -1,15 +1,16 @@ +import type { Environment } from './types.ts'; /** * Gets the environment vars from an env file */ -export declare function getEnvFileVars(envFilePath: string): Promise>; +export declare function getEnvFileVars(envFilePath: string): Promise; /** * Parse out all env vars from a given env file string and return an object */ -export declare function parseEnvString(envFileString: string): Record; +export declare function parseEnvString(envFileString: string): Environment; /** * Parse out all env vars from an env file string */ -export declare function parseEnvVars(envString: string): Record; +export declare function parseEnvVars(envString: string): Environment; /** * Strips out comments from env file string */ diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index a4370ce..f7c07df 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -1,14 +1,11 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -const utils_1 = require("./utils"); -const REQUIRE_HOOK_EXTENSIONS = ['.json', '.js', '.cjs']; +import * as fs from 'fs'; +import * as path from 'path'; +import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'; /** * Gets the environment vars from an env file */ -async function getEnvFileVars(envFilePath) { - const absolutePath = utils_1.resolveEnvFilePath(envFilePath); +export async function getEnvFileVars(envFilePath) { + const absolutePath = resolveEnvFilePath(envFilePath); if (!fs.existsSync(absolutePath)) { const pathError = new Error(`Invalid env file path (${envFilePath}).`); pathError.name = 'PathError'; @@ -17,9 +14,23 @@ async function getEnvFileVars(envFilePath) { // Get the file extension const ext = path.extname(absolutePath).toLowerCase(); let env = {}; - if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise = require(absolutePath); - env = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; + if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them + let attributeTypes = {}; + if (ext === '.json') { + attributeTypes = { with: { type: 'json' } }; + } + const res = await import(absolutePath, attributeTypes); + if ('default' in res) { + env = res.default; + } + else { + env = res; + } + // Check to see if the imported value is a promise + if (isPromise(env)) { + env = await env; + } } else { const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }); @@ -27,11 +38,10 @@ async function getEnvFileVars(envFilePath) { } return env; } -exports.getEnvFileVars = getEnvFileVars; /** * Parse out all env vars from a given env file string and return an object */ -function parseEnvString(envFileString) { +export function parseEnvString(envFileString) { // First thing we do is stripe out all comments envFileString = stripComments(envFileString.toString()); // Next we stripe out all the empty lines @@ -39,30 +49,41 @@ function parseEnvString(envFileString) { // Merge the file env vars with the current process env vars (the file vars overwrite process vars) return parseEnvVars(envFileString); } -exports.parseEnvString = parseEnvString; /** * Parse out all env vars from an env file string */ -function parseEnvVars(envString) { +export function parseEnvVars(envString) { const envParseRegex = /^((.+?)[=](.*))$/gim; const matches = {}; let match; while ((match = envParseRegex.exec(envString)) !== null) { // Note: match[1] is the full env=var line const key = match[2].trim(); - const value = match[3].trim(); + let value = match[3].trim(); // remove any surrounding quotes - matches[key] = value + value = value .replace(/(^['"]|['"]$)/g, '') .replace(/\\n/g, '\n'); + // Convert string to JS type if appropriate + if (value !== '' && !isNaN(+value)) { + matches[key] = +value; + } + else if (value === 'true') { + matches[key] = true; + } + else if (value === 'false') { + matches[key] = false; + } + else { + matches[key] = value; + } } - return matches; + return JSON.parse(JSON.stringify(matches)); } -exports.parseEnvVars = parseEnvVars; /** * Strips out comments from env file string */ -function stripComments(envString) { +export function stripComments(envString) { const commentsRegex = /(^#.*$)/gim; let match = commentsRegex.exec(envString); let newString = envString; @@ -72,12 +93,10 @@ function stripComments(envString) { } return newString; } -exports.stripComments = stripComments; /** * Strips out newlines from env file string */ -function stripEmptyLines(envString) { +export function stripEmptyLines(envString) { const emptyLinesRegex = /(^\n)/gim; return envString.replace(emptyLinesRegex, ''); } -exports.stripEmptyLines = stripEmptyLines; diff --git a/dist/parse-rc-file.d.ts b/dist/parse-rc-file.d.ts index bd193e1..05b37c7 100644 --- a/dist/parse-rc-file.d.ts +++ b/dist/parse-rc-file.d.ts @@ -1,7 +1,8 @@ +import type { Environment } from './types.ts'; /** * Gets the env vars from the rc file and rc environments */ export declare function getRCFileVars({ environments, filePath }: { environments: string[]; filePath: string; -}): Promise>; +}): Promise; diff --git a/dist/parse-rc-file.js b/dist/parse-rc-file.js index 07bb65e..1c80be6 100644 --- a/dist/parse-rc-file.js +++ b/dist/parse-rc-file.js @@ -1,31 +1,43 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = require("fs"); -const util_1 = require("util"); -const path_1 = require("path"); -const utils_1 = require("./utils"); -const statAsync = util_1.promisify(fs_1.stat); -const readFileAsync = util_1.promisify(fs_1.readFile); +import { stat, readFile } from 'fs'; +import { promisify } from 'util'; +import { extname } from 'path'; +import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'; +const statAsync = promisify(stat); +const readFileAsync = promisify(readFile); /** * Gets the env vars from the rc file and rc environments */ -async function getRCFileVars({ environments, filePath }) { - const absolutePath = utils_1.resolveEnvFilePath(filePath); +export async function getRCFileVars({ environments, filePath }) { + const absolutePath = resolveEnvFilePath(filePath); try { await statAsync(absolutePath); } - catch (e) { + catch { const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`); pathError.name = 'PathError'; throw pathError; } // Get the file extension - const ext = path_1.extname(absolutePath).toLowerCase(); - let parsedData; + const ext = extname(absolutePath).toLowerCase(); + let parsedData = {}; try { - if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath); - parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; + if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them + let attributeTypes = {}; + if (ext === '.json') { + attributeTypes = { with: { type: 'json' } }; + } + const res = await import(absolutePath, attributeTypes); + if ('default' in res) { + parsedData = res.default; + } + else { + parsedData = res; + } + // Check to see if the imported value is a promise + if (isPromise(parsedData)) { + parsedData = await parsedData; + } } else { const file = await readFileAsync(absolutePath, { encoding: 'utf8' }); @@ -33,20 +45,26 @@ async function getRCFileVars({ environments, filePath }) { } } catch (e) { - const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`); + const errorMessage = e instanceof Error ? e.message : 'Unknown error'; + const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`); parseError.name = 'ParseError'; throw parseError; } // Parse and merge multiple rc environments together let result = {}; let environmentFound = false; - environments.forEach((name) => { - const envVars = parsedData[name]; - if (envVars !== undefined) { - environmentFound = true; - result = Object.assign(Object.assign({}, result), envVars); + for (const name of environments) { + if (name in parsedData) { + const envVars = parsedData[name]; + if (envVars != null && typeof envVars === 'object') { + environmentFound = true; + result = { + ...result, + ...envVars, + }; + } } - }); + } if (!environmentFound) { const environmentError = new Error(`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`); environmentError.name = 'EnvironmentError'; @@ -54,4 +72,3 @@ async function getRCFileVars({ environments, filePath }) { } return result; } -exports.getRCFileVars = getRCFileVars; diff --git a/dist/signal-termination.d.ts b/dist/signal-termination.d.ts index 1a44663..1583776 100644 --- a/dist/signal-termination.d.ts +++ b/dist/signal-termination.d.ts @@ -1,7 +1,7 @@ -/// import { ChildProcess } from 'child_process'; export declare class TermSignals { private readonly terminateSpawnedProcessFuncHandlers; + private terminateSpawnedProcessFuncExitHandler?; private readonly verbose; _exitCalled: boolean; constructor(options?: { @@ -15,7 +15,7 @@ export declare class TermSignals { /** * Terminate parent process helper */ - _terminateProcess(code?: number, signal?: NodeJS.Signals): void; + _terminateProcess(signal?: NodeJS.Signals | number): void; /** * Exit event listener clean up helper */ diff --git a/dist/signal-termination.js b/dist/signal-termination.js index 136d7d6..230ec28 100644 --- a/dist/signal-termination.js +++ b/dist/signal-termination.js @@ -1,73 +1,73 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); const SIGNALS_TO_HANDLE = [ - 'SIGINT', 'SIGTERM', 'SIGHUP' + 'SIGINT', 'SIGTERM', 'SIGHUP', ]; -class TermSignals { +export class TermSignals { + terminateSpawnedProcessFuncHandlers = {}; + terminateSpawnedProcessFuncExitHandler; + verbose = false; + _exitCalled = false; constructor(options = {}) { - this.terminateSpawnedProcessFuncHandlers = {}; - this.verbose = false; - this._exitCalled = false; this.verbose = options.verbose === true; } handleTermSignals(proc) { // Terminate child process if parent process receives termination events - SIGNALS_TO_HANDLE.forEach((signal) => { - this.terminateSpawnedProcessFuncHandlers[signal] = - (signal, code) => { - this._removeProcessListeners(); - if (!this._exitCalled) { - if (this.verbose) { - console.info('Parent process exited with signal: ' + - signal.toString() + - '. Terminating child process...'); - } - // Mark shared state so we do not run into a signal/exit loop - this._exitCalled = true; - // Use the signal code if it is an error code - let correctSignal; - if (typeof signal === 'number') { - if (signal > (code !== null && code !== void 0 ? code : 0)) { - code = signal; - correctSignal = 'SIGINT'; - } - } - else { - correctSignal = signal; - } - // Kill the child process - proc.kill(correctSignal !== null && correctSignal !== void 0 ? correctSignal : code); - // Terminate the parent process - this._terminateProcess(code, correctSignal); + const terminationFunc = (signal) => { + this._removeProcessListeners(); + if (!this._exitCalled) { + if (this.verbose) { + console.info('Parent process exited with signal: ' + + signal.toString() + + '. Terminating child process...'); + } + // Mark shared state so we do not run into a signal/exit loop + this._exitCalled = true; + // Use the signal code if it is an error code + // let correctSignal: NodeJS.Signals | undefined + if (typeof signal === 'number') { + if (signal > 0) { + // code = signal + signal = 'SIGINT'; } - }; + } + // else { + // correctSignal = signal + // } + // Kill the child process + proc.kill(signal); + // Terminate the parent process + this._terminateProcess(signal); + } + }; + for (const signal of SIGNALS_TO_HANDLE) { + this.terminateSpawnedProcessFuncHandlers[signal] = terminationFunc; process.once(signal, this.terminateSpawnedProcessFuncHandlers[signal]); - }); - process.once('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM); + } + this.terminateSpawnedProcessFuncExitHandler = terminationFunc; + process.once('exit', this.terminateSpawnedProcessFuncExitHandler); // Terminate parent process if child process receives termination events proc.on('exit', (code, signal) => { this._removeProcessListeners(); if (!this._exitCalled) { if (this.verbose) { - console.info(`Child process exited with code: ${(code !== null && code !== void 0 ? code : '').toString()} and signal:` + - (signal !== null && signal !== void 0 ? signal : '').toString() + - '. Terminating parent process...'); + console.info(`Child process exited with code: ${(code ?? '').toString()} and signal:` + + (signal ?? '').toString() + + '. Terminating parent process...'); } // Mark shared state so we do not run into a signal/exit loop this._exitCalled = true; // Use the signal code if it is an error code let correctSignal; if (typeof signal === 'number') { - if (signal > (code !== null && code !== void 0 ? code : 0)) { + if (signal > (code ?? 0)) { code = signal; correctSignal = 'SIGINT'; } } else { - correctSignal = signal !== null && signal !== void 0 ? signal : undefined; + correctSignal = signal ?? undefined; } // Terminate the parent process - this._terminateProcess(code, correctSignal); + this._terminateProcess(correctSignal ?? code); } }); } @@ -75,17 +75,23 @@ class TermSignals { * Enables catching of unhandled exceptions */ handleUncaughtExceptions() { - process.on('uncaughtException', (e) => this._uncaughtExceptionHandler(e)); + process.on('uncaughtException', (e) => { + this._uncaughtExceptionHandler(e); + }); } /** * Terminate parent process helper */ - _terminateProcess(code, signal) { - if (signal !== undefined) { - return process.kill(process.pid, signal); - } - if (code !== undefined) { - return process.exit(code); + _terminateProcess(signal) { + if (signal != null) { + if (typeof signal === 'string') { + process.kill(process.pid, signal); + return; + } + if (typeof signal === 'number') { + process.exit(signal); + return; + } } throw new Error('Unable to terminate parent process successfully'); } @@ -96,7 +102,9 @@ class TermSignals { SIGNALS_TO_HANDLE.forEach((signal) => { process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]); }); - process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM); + if (this.terminateSpawnedProcessFuncExitHandler != null) { + process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler); + } } /** * General exception handler @@ -106,4 +114,3 @@ class TermSignals { process.exit(1); } } -exports.TermSignals = TermSignals; diff --git a/dist/spawn.d.ts b/dist/spawn.d.ts deleted file mode 100644 index cabd0a7..0000000 --- a/dist/spawn.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import * as spawn from 'cross-spawn'; -export { spawn }; diff --git a/dist/spawn.js b/dist/spawn.js deleted file mode 100644 index e83cc2c..0000000 --- a/dist/spawn.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const spawn = require("cross-spawn"); -exports.spawn = spawn; diff --git a/dist/types.d.ts b/dist/types.d.ts index a037458..ca2795d 100644 --- a/dist/types.d.ts +++ b/dist/types.d.ts @@ -1,15 +1,31 @@ +import { Command } from 'commander'; +export type Environment = Partial>; +export type RCEnvironment = Partial>; +export interface CommanderOptions extends Command { + override?: boolean; + useShell?: boolean; + expandEnvs?: boolean; + verbose?: boolean; + silent?: boolean; + fallback?: boolean; + environments?: string[]; + rcFile?: string; + file?: string; +} +export interface RCFileOptions { + environments: string[]; + filePath?: string; +} +export interface EnvFileOptions { + filePath?: string; + fallback?: boolean; +} export interface GetEnvVarOptions { - envFile?: { - filePath?: string; - fallback?: boolean; - }; - rc?: { - environments: string[]; - filePath?: string; - }; + envFile?: EnvFileOptions; + rc?: RCFileOptions; verbose?: boolean; } -export interface EnvCmdOptions extends Pick { +export interface EnvCmdOptions extends GetEnvVarOptions { command: string; commandArgs: string[]; options?: { diff --git a/dist/types.js b/dist/types.js index c8ad2e5..cb0ff5c 100644 --- a/dist/types.js +++ b/dist/types.js @@ -1,2 +1 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); +export {}; diff --git a/dist/utils.d.ts b/dist/utils.d.ts index 9a731d9..f8e85a2 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -1,3 +1,4 @@ +export declare const IMPORT_HOOK_EXTENSIONS: string[]; /** * A simple function for resolving the path the user entered */ @@ -7,6 +8,6 @@ export declare function resolveEnvFilePath(userPath: string): string; */ export declare function parseArgList(list: string): string[]; /** - * A simple function to test if the value is a promise + * A simple function to test if the value is a promise/thenable */ -export declare function isPromise(value: any | PromiseLike): value is Promise; +export declare function isPromise(value?: T | PromiseLike): value is PromiseLike; diff --git a/dist/utils.js b/dist/utils.js index 1c7aa4f..7535a34 100644 --- a/dist/utils.js +++ b/dist/utils.js @@ -1,30 +1,31 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const os = require("os"); +import { resolve } from 'node:path'; +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 */ -function resolveEnvFilePath(userPath) { +export function resolveEnvFilePath(userPath) { // Make sure a home directory exist - const home = os.homedir(); - if (home !== undefined) { + const home = homedir(); + if (home != null) { userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`); } - return path.resolve(process.cwd(), userPath); + return resolve(cwd(), userPath); } -exports.resolveEnvFilePath = resolveEnvFilePath; /** * A simple function that parses a comma separated string into an array of strings */ -function parseArgList(list) { +export function parseArgList(list) { return list.split(','); } -exports.parseArgList = parseArgList; /** - * A simple function to test if the value is a promise + * A simple function to test if the value is a promise/thenable */ -function isPromise(value) { - return value != null && typeof value.then === 'function'; +export function isPromise(value) { + return value != null + && typeof value === 'object' + && 'then' in value + && typeof value.then === 'function'; } -exports.isPromise = isPromise; diff --git a/src/parse-args.ts b/src/parse-args.ts index 5f56d82..4bbe764 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,9 +1,7 @@ import * as commander from 'commander' import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts' import { parseArgList } from './utils.js' - -// Use commonjs require to prevent a weird folder hierarchy in dist -const packageJson = (await import('../package.json')).default +import { default as packageJson } from '../package.json' with { type: 'json' }; /** * Parses the arguments passed into the cli diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index bc803bd..41cabb8 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -18,7 +18,12 @@ export async function getEnvFileVars(envFilePath: string): Promise const ext = path.extname(absolutePath).toLowerCase() let env: Environment = {} if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { - const res = await import(absolutePath) as Environment | { default: Environment } + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them + let attributeTypes = {} + if (ext === '.json') { + attributeTypes = { with: { type: 'json' } } + } + const res = await import(absolutePath, attributeTypes) as Environment | { default: Environment } if ('default' in res) { env = res.default as Environment } else { diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 8fc720a..7cc02a1 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -29,7 +29,12 @@ export async function getRCFileVars( let parsedData: Partial = {} try { if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { - const res = await import(absolutePath) as RCEnvironment | { default: RCEnvironment } + // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them + let attributeTypes = {} + if (ext === '.json') { + attributeTypes = { with: { type: 'json' } } + } + const res = await import(absolutePath, attributeTypes) as RCEnvironment | { default: RCEnvironment } if ('default' in res) { parsedData = res.default as RCEnvironment } else { diff --git a/tsconfig.json b/tsconfig.json index 1708f4c..8bade5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,14 +3,15 @@ "declaration": true, "esModuleInterop": false, "lib": ["es2023"], - "module": "Node16", + "module": "NodeNext", "moduleDetection": "force", "outDir": "./dist", "resolveJsonModule": true, "strict": true, "target": "ES2022", + "rootDir": "src" }, "include": [ - "./src/**/*" + "src/**/*" ] } From a3c908bf6bc1856ccc130949b244f1910365dc11 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Tue, 3 Dec 2024 02:54:08 -0900 Subject: [PATCH 4/4] fix(file-url): fix file path to conform to ES module requirements --- dist/parse-env-file.js | 13 +++++++------ dist/parse-rc-file.js | 9 +++++---- src/parse-env-file.ts | 13 +++++++------ src/parse-rc-file.ts | 9 +++++---- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index f7c07df..457b661 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -1,18 +1,19 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import { existsSync, readFileSync } from 'node:fs'; +import { extname } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'; /** * Gets the environment vars from an env file */ export async function getEnvFileVars(envFilePath) { const absolutePath = resolveEnvFilePath(envFilePath); - if (!fs.existsSync(absolutePath)) { + if (!existsSync(absolutePath)) { const pathError = new Error(`Invalid env file path (${envFilePath}).`); pathError.name = 'PathError'; throw pathError; } // Get the file extension - const ext = path.extname(absolutePath).toLowerCase(); + const ext = extname(absolutePath).toLowerCase(); let env = {}; if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them @@ -20,7 +21,7 @@ export async function getEnvFileVars(envFilePath) { if (ext === '.json') { attributeTypes = { with: { type: 'json' } }; } - const res = await import(absolutePath, attributeTypes); + const res = await import(pathToFileURL(absolutePath).href, attributeTypes); if ('default' in res) { env = res.default; } @@ -33,7 +34,7 @@ export async function getEnvFileVars(envFilePath) { } } else { - const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }); + const file = readFileSync(absolutePath, { encoding: 'utf8' }); env = parseEnvString(file); } return env; diff --git a/dist/parse-rc-file.js b/dist/parse-rc-file.js index 1c80be6..fd63997 100644 --- a/dist/parse-rc-file.js +++ b/dist/parse-rc-file.js @@ -1,6 +1,7 @@ -import { stat, readFile } from 'fs'; -import { promisify } from 'util'; -import { extname } from 'path'; +import { stat, readFile } from 'node:fs'; +import { promisify } from 'node:util'; +import { extname } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js'; const statAsync = promisify(stat); const readFileAsync = promisify(readFile); @@ -27,7 +28,7 @@ export async function getRCFileVars({ environments, filePath }) { if (ext === '.json') { attributeTypes = { with: { type: 'json' } }; } - const res = await import(absolutePath, attributeTypes); + const res = await import(pathToFileURL(absolutePath).href, attributeTypes); if ('default' in res) { parsedData = res.default; } diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index 41cabb8..b0fb1a2 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -1,5 +1,6 @@ -import * as fs from 'fs' -import * as path from 'path' +import { existsSync, readFileSync } from 'node:fs' +import { extname } from 'node:path' +import { pathToFileURL } from 'node:url' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js' import type { Environment } from './types.ts' @@ -8,14 +9,14 @@ import type { Environment } from './types.ts' */ export async function getEnvFileVars(envFilePath: string): Promise { const absolutePath = resolveEnvFilePath(envFilePath) - if (!fs.existsSync(absolutePath)) { + if (!existsSync(absolutePath)) { const pathError = new Error(`Invalid env file path (${envFilePath}).`) pathError.name = 'PathError' throw pathError } // Get the file extension - const ext = path.extname(absolutePath).toLowerCase() + const ext = extname(absolutePath).toLowerCase() let env: Environment = {} if (IMPORT_HOOK_EXTENSIONS.includes(ext)) { // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them @@ -23,7 +24,7 @@ export async function getEnvFileVars(envFilePath: string): Promise if (ext === '.json') { attributeTypes = { with: { type: 'json' } } } - const res = await import(absolutePath, attributeTypes) as Environment | { default: Environment } + const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as Environment | { default: Environment } if ('default' in res) { env = res.default as Environment } else { @@ -35,7 +36,7 @@ export async function getEnvFileVars(envFilePath: string): Promise } } else { - const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) + const file = readFileSync(absolutePath, { encoding: 'utf8' }) env = parseEnvString(file) } return env diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 7cc02a1..09ea9dd 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -1,6 +1,7 @@ -import { stat, readFile } from 'fs' -import { promisify } from 'util' -import { extname } from 'path' +import { stat, readFile } from 'node:fs' +import { promisify } from 'node:util' +import { extname } from 'node:path' +import { pathToFileURL } from 'node:url' import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js' import type { Environment, RCEnvironment } from './types.ts' @@ -34,7 +35,7 @@ export async function getRCFileVars( if (ext === '.json') { attributeTypes = { with: { type: 'json' } } } - const res = await import(absolutePath, attributeTypes) as RCEnvironment | { default: RCEnvironment } + const res = await import(pathToFileURL(absolutePath).href, attributeTypes) as RCEnvironment | { default: RCEnvironment } if ('default' in res) { parsedData = res.default as RCEnvironment } else {