From f8106ac44ad049ef773c22168f1c9218d044fb8d Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Mon, 2 Dec 2024 10:25:25 -0900 Subject: [PATCH] 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/**/*"