From fb917920d4d88ea11b58d0abb8bf46b1e000f309 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 29 Apr 2021 21:25:50 +0000 Subject: [PATCH 01/22] Upgrade to GitHub-native Dependabot --- .github/dependabot.yml | 67 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..190fc94 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,67 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "@types/node" + versions: + - ">= 13.a, < 14" + - dependency-name: "@types/node" + versions: + - 14.14.22 + - 14.14.25 + - 14.14.27 + - 14.14.28 + - 14.14.30 + - 14.14.31 + - 14.14.32 + - 14.14.33 + - 14.14.34 + - 14.14.35 + - 14.14.36 + - 14.14.37 + - 14.14.39 + - 14.14.41 + - 15.0.0 + - dependency-name: husky + versions: + - 5.0.9 + - 5.1.0 + - 5.1.1 + - 5.1.2 + - 5.1.3 + - 5.2.0 + - dependency-name: mocha + versions: + - 8.2.1 + - 8.3.0 + - 8.3.1 + - dependency-name: typescript + versions: + - 4.1.3 + - 4.1.4 + - 4.1.5 + - 4.2.2 + - 4.2.3 + - dependency-name: "@commitlint/cli" + versions: + - 11.0.0 + - 12.0.0 + - 12.0.1 + - dependency-name: "@commitlint/config-conventional" + versions: + - 11.0.0 + - 12.0.0 + - 12.0.1 + - dependency-name: "@types/mocha" + versions: + - 8.2.0 + - 8.2.1 + - dependency-name: commander + versions: + - 7.0.0 + - 7.1.0 From 853c0006ad284d134f76416aae816c221b89e749 Mon Sep 17 00:00:00 2001 From: Nick Darvey Date: Thu, 20 Jan 2022 16:38:35 +1100 Subject: [PATCH 02/22] chore: Include parse error message in error --- src/parse-rc-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index b348532..eff5a9e 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -34,7 +34,7 @@ export async function getRCFileVars ( parsedData = JSON.parse(file) } } catch (e) { - const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}`) + const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${e.message}`) parseError.name = 'ParseError' throw parseError } From 66f3fa398685b83ef089532af3de67cc82d7b3ac Mon Sep 17 00:00:00 2001 From: Nick Darvey Date: Thu, 20 Jan 2022 16:40:00 +1100 Subject: [PATCH 03/22] chore: Throw parse error instead of continuing --- src/get-env-vars.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index f89f847..69953b3 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -103,6 +103,12 @@ export async function getRCFile ( } throw new Error(errorText) } + if (e.name === 'ParseError') { + if (verbose === true) { + console.info(e.message); + } + throw new Error(e.message); + } } } From bc942236740134c8b54f8d43ce4fe32eeeecb56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Tue, 15 Feb 2022 17:40:57 +0100 Subject: [PATCH 04/22] fix: returning "true" instead of "void" Apparently process.kill() returns a Boolean, so the early return will return a true||false rather than the void as per the func sig. This patch ensures we return void regardless of what process.kill() and process.exit() return. What they return may be dependent on node version and/or platform. --- src/signal-termination.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/signal-termination.ts b/src/signal-termination.ts index db8b6fc..84c2c26 100644 --- a/src/signal-termination.ts +++ b/src/signal-termination.ts @@ -89,10 +89,11 @@ export class TermSignals { */ public _terminateProcess (code?: number, signal?: NodeJS.Signals): void { if (signal !== undefined) { - return process.kill(process.pid, signal) + process.kill(process.pid, signal) + return } if (code !== undefined) { - return process.exit(code) + process.exit(code) } throw new Error('Unable to terminate parent process successfully') } From 33b3367cff88a236b9e79e4fd2990bdcd06ae47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Wed, 16 Feb 2022 10:38:46 +0100 Subject: [PATCH 05/22] fix: add an unreachable return, to test signal termination --- src/signal-termination.ts | 1 + test/signal-termination.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/signal-termination.ts b/src/signal-termination.ts index 84c2c26..5e4120f 100644 --- a/src/signal-termination.ts +++ b/src/signal-termination.ts @@ -94,6 +94,7 @@ export class TermSignals { } if (code !== undefined) { process.exit(code) + return // eslint-disable-line no-unreachable } throw new Error('Unable to terminate parent process successfully') } diff --git a/test/signal-termination.spec.ts b/test/signal-termination.spec.ts index f203ff9..96ad89c 100644 --- a/test/signal-termination.spec.ts +++ b/test/signal-termination.spec.ts @@ -71,6 +71,7 @@ describe('signal-termination', (): void => { it('should call exit method on parent process if no signal provided', (): void => { term._terminateProcess(0) + // We here test code that in reality is unreachable. assert.equal(exitStub.callCount, 1) }) From ab91b2fc52f87a345b679998f913a9aed14cde11 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Sat, 23 Nov 2024 01:42:13 -0900 Subject: [PATCH 06/22] chore(ci): remove node 8.x version, add node versions 18, 20, 22 --- .github/workflows/linux-tests.yml | 31 ++++++++++++++++------------- .github/workflows/windows-tests.yml | 30 ++++++++++++++++------------ 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 030231d..436040c 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -3,17 +3,16 @@ name: linux tests on: [push, pull_request] jobs: - build: - + test: runs-on: ubuntu-latest strategy: matrix: - node-version: [8.x, 10.x, 12.x] + node-version: [10.x, 12.x, 18.x, 20.x, 22.x] steps: - - name: Checkout project - uses: actions/checkout@v2 + - name: Checkout Project + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -23,7 +22,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -33,20 +32,24 @@ jobs: - name: Lint Files run: npm run lint - - name: Run Tests and Converage + - name: Run Tests and Coverage env: CI: true run: npm run test-cover - - name: Coveralls Parallel - uses: coverallsapp/github-action@master + - name: Send Coverage to Coveralls + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true path-to-lcov: ./coverage/lcov.info - - name: Coveralls Finished - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Close Coveralls Parallel Build + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index 1bc7140..be4526b 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -3,22 +3,22 @@ name: windows tests on: [push, pull_request] jobs: - build: + test: runs-on: windows-latest strategy: matrix: - node-version: [8.x, 10.x, 12.x] + node-version: [10.x, 12.x, 18.x, 20.x, 22.x] steps: - - name: Checkout project - uses: actions/checkout@v2 + - name: Checkout Project + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -28,20 +28,24 @@ jobs: - name: Lint Files run: npm run lint - - name: Run Tests and Converage + - name: Run Tests and Coverage env: CI: true run: npm run test-cover - - name: Coveralls Parallel - uses: coverallsapp/github-action@master + - name: Send Coverage to Coveralls + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true path-to-lcov: ./coverage/lcov.info - - name: Coveralls Finished - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Close Coveralls Parallel Build + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true From fd86da6734b56d873f2093d0a1c3f6ea85f78101 Mon Sep 17 00:00:00 2001 From: Ross Hill Date: Sat, 23 Nov 2024 22:38:41 -0500 Subject: [PATCH 07/22] fix: resolve lint issue in src/parse-rc-file.ts Corrected linting errors in src/parse-rc-file.ts to ensure compliance with code style guidelines. Also resolved other auto-fixable lint errors. --- src/get-env-vars.ts | 4 ++-- src/parse-rc-file.ts | 5 ++++- test/parse-args.spec.ts | 14 +++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index 69953b3..cd5db92 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -105,9 +105,9 @@ export async function getRCFile ( } if (e.name === 'ParseError') { if (verbose === true) { - console.info(e.message); + console.info(e.message) } - throw new Error(e.message); + throw new Error(e.message) } } } diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index eff5a9e..3f1e036 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -34,7 +34,10 @@ export async function getRCFileVars ( parsedData = JSON.parse(file) } } catch (e) { - const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${e.message}`) + 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 } diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index 79accab..b6fced0 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -27,13 +27,13 @@ describe('parseArgs', (): void => { it('should parse environment value', (): void => { const res = parseArgs(['-e', environments[0], command]) assert.exists(res.rc) - assert.sameOrderedMembers(res.rc!.environments, [environments[0]]) + assert.sameOrderedMembers(res.rc.environments, [environments[0]]) }) it('should parse multiple environment values', (): void => { const res = parseArgs(['-e', environments.join(','), command]) assert.exists(res.rc) - assert.sameOrderedMembers(res.rc!.environments, environments) + assert.sameOrderedMembers(res.rc.environments, environments) }) it('should parse command value', (): void => { @@ -59,31 +59,31 @@ describe('parseArgs', (): void => { it('should parse override option', (): void => { const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs]) assert.exists(res.options) - assert.isTrue(res.options!.noOverride) + assert.isTrue(res.options.noOverride) }) it('should parse use shell option', (): void => { const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs]) assert.exists(res.options) - assert.isTrue(res.options!.useShell) + assert.isTrue(res.options.useShell) }) it('should parse rc file path', (): void => { const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs]) assert.exists(res.rc) - assert.equal(res.rc!.filePath, rcFilePath) + assert.equal(res.rc.filePath, rcFilePath) }) it('should parse env file path', (): void => { const res = parseArgs(['-f', envFilePath, command, ...commandArgs]) assert.exists(res.envFile) - assert.equal(res.envFile!.filePath, envFilePath) + assert.equal(res.envFile.filePath, envFilePath) }) it('should parse fallback option', (): void => { const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs]) assert.exists(res.envFile) - assert.isTrue(res.envFile!.fallback) + assert.isTrue(res.envFile.fallback) }) it('should print to console.info if --verbose flag is passed', (): void => { From 6211bb098ddb3f76abf05767b279dad02b522662 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Nov 2024 11:53:40 +0000 Subject: [PATCH 08/22] chore(deps-dev): bump typescript from 3.9.10 to 5.7.2 Bumps [typescript](https://github.com/microsoft/TypeScript) from 3.9.10 to 5.7.2. - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml) - [Commits](https://github.com/microsoft/TypeScript/compare/v3.9.10...v5.7.2) --- updated-dependencies: - dependency-name: typescript 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 104b957..99ef782 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "sinon": "^9.0.0", "ts-node": "^8.0.0", "ts-standard": "^8.0.0", - "typescript": "^3.7.0" + "typescript": "^5.7.2" }, "nyc": { "include": [ From 26f4dfb2f69c42b0d830d48a531d74757a0c35e5 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Sat, 30 Nov 2024 04:28:44 -0900 Subject: [PATCH 09/22] chore(pkg): new linter and updated typescript support --- dist/env-cmd.d.ts | 8 +- dist/expand-envs.d.ts | 4 +- dist/get-env-vars.d.ts | 12 +-- dist/parse-args.js | 2 +- dist/parse-env-file.d.ts | 12 +-- dist/parse-env-file.js | 2 +- dist/parse-rc-file.d.ts | 4 +- dist/parse-rc-file.js | 2 +- dist/utils.d.ts | 2 +- eslint.config.js | 43 +++++++++++ eslint.config.js.test | 26 +++++++ package.json | 15 ++-- src/env-cmd.ts | 31 ++++---- src/expand-envs.ts | 10 ++- src/get-env-vars.ts | 75 +++++++++++-------- src/parse-args.ts | 30 ++++---- src/parse-env-file.ts | 40 +++++++--- src/parse-rc-file.ts | 48 ++++++------ src/signal-termination.ts | 113 ++++++++++++++++------------- src/spawn.ts | 2 +- src/types.ts | 41 ++++++++--- src/utils.ts | 17 +++-- test/env-cmd.spec.ts | 78 ++++++++++---------- test/expand-envs.spec.ts | 7 +- test/get-env-vars.spec.ts | 84 ++++++++++++--------- test/parse-args.spec.ts | 4 +- test/parse-env-file.spec.ts | 27 ++++--- test/parse-rc-file.spec.ts | 22 ++++-- test/signal-termination.spec.ts | 102 +++++++++++++------------- test/test-files/.rc-test | 5 +- test/test-files/.rc-test-async.js | 23 +++--- test/test-files/.rc-test.json | 5 +- test/test-files/test | 3 +- test/test-files/test-async.js | 2 +- test/test-files/test-newlines.json | 5 +- test/test-files/test.js | 2 +- test/test-files/test.json | 5 +- test/utils.spec.ts | 8 ++ tsconfig.eslint.json | 12 --- 39 files changed, 550 insertions(+), 383 deletions(-) create mode 100644 eslint.config.js create mode 100644 eslint.config.js.test delete mode 100644 tsconfig.eslint.json diff --git a/dist/env-cmd.d.ts b/dist/env-cmd.d.ts index 859a7da..5141864 100644 --- a/dist/env-cmd.d.ts +++ b/dist/env-cmd.d.ts @@ -5,9 +5,7 @@ import { EnvCmdOptions } from './types'; * @param {string[]} args Command line argument to pass in ['-f', './.env'] * @returns {Promise<{ [key: string]: any }>} */ -export declare function CLI(args: string[]): Promise<{ - [key: string]: any; -}>; +export declare function CLI(args: string[]): Promise>; /** * The main env-cmd program. This will spawn a new process and run the given command using * various environment file solutions. @@ -16,6 +14,4 @@ export declare function CLI(args: string[]): Promise<{ * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options } * @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value */ -export declare function EnvCmd({ command, commandArgs, envFile, rc, options }: EnvCmdOptions): Promise<{ - [key: string]: any; -}>; +export declare function EnvCmd({ command, commandArgs, envFile, rc, options }: EnvCmdOptions): Promise>; diff --git a/dist/expand-envs.d.ts b/dist/expand-envs.d.ts index cd06ffa..5a68b32 100644 --- a/dist/expand-envs.d.ts +++ b/dist/expand-envs.d.ts @@ -2,6 +2,4 @@ * expandEnvs Replaces $var in args and command with environment variables * the environment variable doesn't exist, it leaves it as is. */ -export declare function expandEnvs(str: string, envs: { - [key: string]: any; -}): string; +export declare function expandEnvs(str: string, envs: Record): string; diff --git a/dist/get-env-vars.d.ts b/dist/get-env-vars.d.ts index dc7ea2c..aaf968e 100644 --- a/dist/get-env-vars.d.ts +++ b/dist/get-env-vars.d.ts @@ -1,18 +1,12 @@ import { GetEnvVarOptions } from './types'; -export declare function getEnvVars(options?: GetEnvVarOptions): Promise<{ - [key: string]: any; -}>; +export declare function getEnvVars(options?: GetEnvVarOptions): Promise>; export declare function getEnvFile({ filePath, fallback, verbose }: { filePath?: string; fallback?: boolean; verbose?: boolean; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; export declare function getRCFile({ environments, filePath, verbose }: { environments: string[]; filePath?: string; verbose?: boolean; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; diff --git a/dist/parse-args.js b/dist/parse-args.js index 5e1f894..c650ea5 100644 --- a/dist/parse-args.js +++ b/dist/parse-args.js @@ -3,7 +3,7 @@ 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'); /* eslint-disable-line */ +const packageJson = require('../package.json'); /** * Parses the arguments passed into the cli */ diff --git a/dist/parse-env-file.d.ts b/dist/parse-env-file.d.ts index 530f487..c298868 100644 --- a/dist/parse-env-file.d.ts +++ b/dist/parse-env-file.d.ts @@ -1,21 +1,15 @@ /** * Gets the environment vars from an env file */ -export declare function getEnvFileVars(envFilePath: string): Promise<{ - [key: string]: any; -}>; +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): { - [key: string]: string; -}; +export declare function parseEnvString(envFileString: string): Record; /** * Parse out all env vars from an env file string */ -export declare function parseEnvVars(envString: string): { - [key: string]: string; -}; +export declare function parseEnvVars(envString: string): Record; /** * Strips out comments from env file string */ diff --git a/dist/parse-env-file.js b/dist/parse-env-file.js index 6b6dafe..a4370ce 100644 --- a/dist/parse-env-file.js +++ b/dist/parse-env-file.js @@ -18,7 +18,7 @@ async function getEnvFileVars(envFilePath) { const ext = path.extname(absolutePath).toLowerCase(); let env = {}; if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise = require(absolutePath); /* eslint-disable-line */ + const possiblePromise = require(absolutePath); env = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; } else { diff --git a/dist/parse-rc-file.d.ts b/dist/parse-rc-file.d.ts index 053adf2..bd193e1 100644 --- a/dist/parse-rc-file.d.ts +++ b/dist/parse-rc-file.d.ts @@ -4,6 +4,4 @@ export declare function getRCFileVars({ environments, filePath }: { environments: string[]; filePath: string; -}): Promise<{ - [key: string]: any; -}>; +}): Promise>; diff --git a/dist/parse-rc-file.js b/dist/parse-rc-file.js index f3df690..07bb65e 100644 --- a/dist/parse-rc-file.js +++ b/dist/parse-rc-file.js @@ -24,7 +24,7 @@ async function getRCFileVars({ environments, filePath }) { let parsedData; try { if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath); /* eslint-disable-line */ + const possiblePromise = require(absolutePath); parsedData = utils_1.isPromise(possiblePromise) ? await possiblePromise : possiblePromise; } else { diff --git a/dist/utils.d.ts b/dist/utils.d.ts index d3714a7..9a731d9 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -9,4 +9,4 @@ export declare function parseArgList(list: string): string[]; /** * A simple function to test if the value is a promise */ -export declare function isPromise(value: any | PromiseLike): value is Promise; +export declare function isPromise(value: any | PromiseLike): value is Promise; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9edb080 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,43 @@ +const eslint = require('@eslint/js') +const tseslint = require('typescript-eslint') +const globals = require('globals') +const stylistic = require('@stylistic/eslint-plugin') + +module.exports = tseslint.config( + { + ignores: ['dist/*', 'bin/*'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + languageOptions: { + globals: { + ...globals.node, + }, + parserOptions: { + projectService: { + allowDefaultProject: ['test/*.ts'], + }, + }, + }, + extends: [ + eslint.configs.recommended, + stylistic.configs['recommended-flat'], + tseslint.configs.strictTypeChecked, + tseslint.configs.stylisticTypeChecked, + ], + }, + // Disable Type Checking JS files + { + files: ['**/*.js'], + extends: [tseslint.configs.disableTypeChecked], + }, + { + // For test files ignore some rules + files: ['test/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + }, + }, +) diff --git a/eslint.config.js.test b/eslint.config.js.test new file mode 100644 index 0000000..00e63e2 --- /dev/null +++ b/eslint.config.js.test @@ -0,0 +1,26 @@ +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 99ef782..a973ff4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "mocha -r ts-node/register ./test/**/*.ts", "test-cover": "nyc npm test", "coveralls": "coveralls < coverage/lcov.info", - "lint": "ts-standard --fix && tsc --noEmit", + "lint": "npx eslint .", "build": "tsc", "watch": "tsc -w" }, @@ -53,6 +53,8 @@ "devDependencies": { "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.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", @@ -60,13 +62,14 @@ "@types/sinon": "^9.0.0", "chai": "^4.0.0", "coveralls": "^3.0.0", + "globals": "^15.12.0", "husky": "^4.0.0", "mocha": "^7.0.0", "nyc": "^15.0.0", "sinon": "^9.0.0", "ts-node": "^8.0.0", - "ts-standard": "^8.0.0", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "typescript-eslint": "^8.15.0" }, "nyc": { "include": [ @@ -85,12 +88,6 @@ "sourceMap": true, "instrument": true }, - "ts-standard": { - "project": "./tsconfig.eslint.json", - "ignore": [ - "dist" - ] - }, "greenkeeper": { "ignore": [ "@types/node" diff --git a/src/env-cmd.ts b/src/env-cmd.ts index cc3179a..424998b 100644 --- a/src/env-cmd.ts +++ b/src/env-cmd.ts @@ -1,5 +1,5 @@ import { spawn } from './spawn' -import { EnvCmdOptions } from './types' +import { EnvCmdOptions, Environment } from './types' import { TermSignals } from './signal-termination' import { parseArgs } from './parse-args' import { getEnvVars } from './get-env-vars' @@ -9,16 +9,17 @@ 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<{ [key: string]: any }>} + * @returns {Promise} */ -export async function CLI (args: string[]): Promise<{ [key: string]: any }> { +export async function CLI(args: string[]): Promise { // Parse the args from the command line const parsedArgs = parseArgs(args) // Run EnvCmd try { - return await (exports.EnvCmd(parsedArgs) as Promise<{ [key: string]: any }>) - } catch (e) { + return await (exports as { EnvCmd: typeof EnvCmd }).EnvCmd(parsedArgs) + } + catch (e) { console.error(e) return process.exit(1) } @@ -30,21 +31,22 @@ export async function CLI (args: string[]): Promise<{ [key: string]: any }> { * * @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 async function EnvCmd ( +export async function EnvCmd( { command, commandArgs, envFile, rc, - options = {} - }: EnvCmdOptions -): Promise<{ [key: string]: any }> { - let env: { [name: string]: string } = {} + options = {}, + }: EnvCmdOptions, +): Promise { + let env: Environment = {} try { env = await getEnvVars({ envFile, rc, verbose: options.verbose }) - } catch (e) { + } + catch (e) { if (!(options.silent ?? false)) { throw e } @@ -52,7 +54,8 @@ export async function EnvCmd ( // Override the merge order if --no-override flag set if (options.noOverride === true) { env = Object.assign({}, env, process.env) - } else { + } + else { // Add in the system environment variables to our environment list env = Object.assign({}, process.env, env) } @@ -66,7 +69,7 @@ export async function EnvCmd ( const proc = spawn(command, commandArgs, { stdio: 'inherit', shell: options.useShell, - env + env: env as Record, }) // Handle any termination signals for parent and child proceses diff --git a/src/expand-envs.ts b/src/expand-envs.ts index cd77317..f3c3b3a 100644 --- a/src/expand-envs.ts +++ b/src/expand-envs.ts @@ -1,11 +1,13 @@ +import { Environment } from './types' /** * 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 function expandEnvs (str: string, envs: { [key: string]: any }): string { - return str.replace(/(? { +export function expandEnvs(str: string, envs: Environment): string { + return str.replace(/(? { const varValue = envs[varName.slice(1)] - return varValue === undefined ? varName : varValue + // const test = 42; + return varValue === undefined ? varName : varValue.toString() }) } diff --git a/src/get-env-vars.ts b/src/get-env-vars.ts index cd5db92..e4e6b8d 100644 --- a/src/get-env-vars.ts +++ b/src/get-env-vars.ts @@ -1,30 +1,30 @@ -import { GetEnvVarOptions } from './types' +import { GetEnvVarOptions, Environment } from './types' import { getRCFileVars } from './parse-rc-file' import { getEnvFileVars } from './parse-env-file' const RC_FILE_DEFAULT_LOCATIONS = ['./.env-cmdrc', './.env-cmdrc.js', './.env-cmdrc.json'] const ENV_FILE_DEFAULT_LOCATIONS = ['./.env', './.env.js', './.env.json'] -export async function getEnvVars (options: GetEnvVarOptions = {}): Promise<{ [key: string]: any }> { - options.envFile = options.envFile !== undefined ? options.envFile : {} +export async function getEnvVars(options: GetEnvVarOptions = {}): Promise { + 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, }) } -export async function getEnvFile ( - { filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean } -): Promise<{ [key: string]: any }> { +export async function getEnvFile( + { filePath, fallback, verbose }: { filePath?: string, fallback?: boolean, verbose?: boolean }, +): Promise { // Use env file if (filePath !== undefined) { try { @@ -33,10 +33,12 @@ export async function getEnvFile ( 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}`) @@ -51,7 +53,10 @@ export async function getEnvFile ( 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(',')}]` @@ -61,9 +66,9 @@ export async function getEnvFile ( throw new Error(error) } -export async function getRCFile ( - { environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean } -): Promise<{ [key: string]: any }> { +export async function getRCFile( + { environments, filePath, verbose }: { environments: string[], filePath?: string, verbose?: boolean }, +): Promise { // User provided an .rc file path if (filePath !== undefined) { try { @@ -72,15 +77,18 @@ export async function getRCFile ( 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}`) + } + catch (e) { + 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 @@ -95,19 +103,22 @@ export async function getRCFile ( 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) + } + catch (e) { + 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) } - throw new Error(errorText) - } - if (e.name === 'ParseError') { - if (verbose === true) { - console.info(e.message) + if (e.name === 'ParseError') { + if (verbose === true) { + console.info(e.message) + } + throw new Error(e.message) } - throw new Error(e.message) } } } diff --git a/src/parse-args.ts b/src/parse-args.ts index bbd4c56..cdf0927 100644 --- a/src/parse-args.ts +++ b/src/parse-args.ts @@ -1,14 +1,14 @@ import * as commander from 'commander' -import { EnvCmdOptions } from './types' +import { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types' import { parseArgList } from './utils' // Use commonjs require to prevent a weird folder hierarchy in dist -const packageJson = require('../package.json') /* eslint-disable-line */ +const packageJson: { version: string } = require('../package.json') /* eslint-disable-line */ /** * Parses the arguments passed into the cli */ -export function parseArgs (args: string[]): EnvCmdOptions { +export function parseArgs(args: string[]): EnvCmdOptions { // 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) @@ -42,23 +42,27 @@ export function parseArgs (args: string[]): EnvCmdOptions { silent = true } - let rc: any - if (program.environments !== undefined && program.environments.length !== 0) { + let rc: RCFileOptions | undefined + if ( + program.environments !== undefined + && Array.isArray(program.environments) + && program.environments.length !== 0 + ) { rc = { environments: program.environments, - filePath: program.rcFile + filePath: program.rcFile, } } - let envFile: any + let envFile: EnvFileOptions | undefined if (program.file !== undefined) { envFile = { filePath: program.file, - fallback: program.fallback + fallback: program.fallback, } } - const options = { + const options: EnvCmdOptions = { command, commandArgs, envFile, @@ -68,8 +72,8 @@ export function parseArgs (args: string[]): EnvCmdOptions { noOverride, silent, useShell, - verbose - } + verbose, + }, } if (verbose) { console.info(`Options: ${JSON.stringify(options, null, 0)}`) @@ -77,8 +81,8 @@ export function parseArgs (args: string[]): EnvCmdOptions { return options } -export function parseArgsUsingCommander (args: string[]): commander.Command { - const program = new commander.Command() +export function parseArgsUsingCommander(args: string[]): CommanderOptions { + const program = new commander.Command() as CommanderOptions return program .version(packageJson.version, '-v, --version') .usage('[options] [...args]') diff --git a/src/parse-env-file.ts b/src/parse-env-file.ts index bbd6334..488da52 100644 --- a/src/parse-env-file.ts +++ b/src/parse-env-file.ts @@ -1,13 +1,14 @@ 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'] /** * Gets the environment vars from an env file */ -export async function getEnvFileVars (envFilePath: string): Promise<{ [key: string]: any }> { +export async function getEnvFileVars(envFilePath: string): Promise { const absolutePath = resolveEnvFilePath(envFilePath) if (!fs.existsSync(absolutePath)) { const pathError = new Error(`Invalid env file path (${envFilePath}).`) @@ -17,11 +18,12 @@ export async function getEnvFileVars (envFilePath: string): Promise<{ [key: stri // Get the file extension const ext = path.extname(absolutePath).toLowerCase() - let env = {} + let env: Environment = {} if (REQUIRE_HOOK_EXTENSIONS.includes(ext)) { - const possiblePromise = require(absolutePath) /* eslint-disable-line */ + const possiblePromise: Environment | Promise = require(absolutePath) /* eslint-disable-line */ env = isPromise(possiblePromise) ? await possiblePromise : possiblePromise - } else { + } + else { const file = fs.readFileSync(absolutePath, { encoding: 'utf8' }) env = parseEnvString(file) } @@ -31,7 +33,7 @@ export async function getEnvFileVars (envFilePath: string): Promise<{ [key: stri /** * Parse out all env vars from a given env file string and return an object */ -export function parseEnvString (envFileString: string): { [key: string]: string } { +export function parseEnvString(envFileString: string): Environment { // First thing we do is stripe out all comments envFileString = stripComments(envFileString.toString()) @@ -45,27 +47,41 @@ export function parseEnvString (envFileString: string): { [key: string]: string /** * Parse out all env vars from an env file string */ -export function parseEnvVars (envString: string): { [key: string]: string } { +export function parseEnvVars(envString: string): Environment { const envParseRegex = /^((.+?)[=](.*))$/gim - const matches: { [key: string]: string } = {} + const matches: Environment = {} 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: string | number | boolean = 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)) as Environment } /** * Strips out comments from env file string */ -export function stripComments (envString: string): string { +export function stripComments(envString: string): string { const commentsRegex = /(^#.*$)/gim let match = commentsRegex.exec(envString) let newString = envString @@ -79,7 +95,7 @@ export function stripComments (envString: string): string { /** * Strips out newlines from env file string */ -export function stripEmptyLines (envString: string): string { +export function stripEmptyLines(envString: string): string { const emptyLinesRegex = /(^\n)/gim return envString.replace(emptyLinesRegex, '') } diff --git a/src/parse-rc-file.ts b/src/parse-rc-file.ts index 3f1e036..1c0c43b 100644 --- a/src/parse-rc-file.ts +++ b/src/parse-rc-file.ts @@ -2,6 +2,7 @@ import { stat, readFile } from 'fs' import { promisify } from 'util' import { extname } from 'path' import { resolveEnvFilePath, isPromise } from './utils' +import { Environment, RCEnvironment } from './types' const statAsync = promisify(stat) const readFileAsync = promisify(readFile) @@ -9,14 +10,15 @@ const readFileAsync = promisify(readFile) /** * Gets the env vars from the rc file and rc environments */ -export async function getRCFileVars ( +export async function getRCFileVars( { environments, filePath }: - { environments: string[], filePath: string } -): Promise<{ [key: string]: any }> { + { environments: string[], filePath: string }, +): Promise { 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 @@ -24,41 +26,45 @@ export async function getRCFileVars ( // Get the file extension const ext = extname(absolutePath).toLowerCase() - let parsedData: { [key: string]: any } + let parsedData: Partial try { if (ext === '.json' || ext === '.js' || ext === '.cjs') { - const possiblePromise = require(absolutePath) /* eslint-disable-line */ + const possiblePromise = require(absolutePath) as PromiseLike | RCEnvironment parsedData = isPromise(possiblePromise) ? await possiblePromise : possiblePromise - } else { - const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) - parsedData = JSON.parse(file) } - } catch (e) { + else { + const file = await readFileAsync(absolutePath, { encoding: 'utf8' }) + parsedData = JSON.parse(file) as Partial + } + } + catch (e) { const errorMessage = e instanceof Error ? e.message : 'Unknown error' const parseError = new Error( - `Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}` + `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 result: Environment = {} let environmentFound = false - environments.forEach((name): void => { - const envVars = parsedData[name] - if (envVars !== undefined) { - environmentFound = true - result = { - ...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}` + `Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`, ) environmentError.name = 'EnvironmentError' throw environmentError diff --git a/src/signal-termination.ts b/src/signal-termination.ts index 5e4120f..73f45e4 100644 --- a/src/signal-termination.ts +++ b/src/signal-termination.ts @@ -1,52 +1,56 @@ import { ChildProcess } from 'child_process' const SIGNALS_TO_HANDLE: NodeJS.Signals[] = [ - 'SIGINT', 'SIGTERM', 'SIGHUP' + 'SIGINT', 'SIGTERM', 'SIGHUP', ] export class TermSignals { - private readonly terminateSpawnedProcessFuncHandlers: { [key: string]: any } = {} + private readonly terminateSpawnedProcessFuncHandlers: Record = {} + private terminateSpawnedProcessFuncExitHandler?: NodeJS.ExitListener private readonly verbose: boolean = false public _exitCalled = false - constructor (options: { verbose?: boolean } = {}) { + constructor(options: { verbose?: boolean } = {}) { this.verbose = options.verbose === true } - public handleTermSignals (proc: ChildProcess): void { + public handleTermSignals(proc: ChildProcess): void { // Terminate child process if parent process receives termination events - SIGNALS_TO_HANDLE.forEach((signal): void => { - this.terminateSpawnedProcessFuncHandlers[signal] = - (signal: NodeJS.Signals | number, code: number): void => { - 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 > (code ?? 0)) { - code = signal - correctSignal = 'SIGINT' - } - } else { - correctSignal = signal - } - // Kill the child process - proc.kill(correctSignal ?? code) - // Terminate the parent process - this._terminateProcess(code, correctSignal) + const terminationFunc = (signal: NodeJS.Signals | number): void => { + 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: number | undefined, signal: NodeJS.Signals | number | null): void => { @@ -54,9 +58,9 @@ export class TermSignals { if (!this._exitCalled) { if (this.verbose) { console.info( - `Child process exited with code: ${(code ?? '').toString()} and signal:` + - (signal ?? '').toString() + - '. Terminating parent process...' + `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 @@ -68,11 +72,12 @@ export class TermSignals { code = signal correctSignal = 'SIGINT' } - } else { + } + else { correctSignal = signal ?? undefined } // Terminate the parent process - this._terminateProcess(code, correctSignal) + this._terminateProcess(correctSignal ?? code) } }) } @@ -80,21 +85,25 @@ export class TermSignals { /** * Enables catching of unhandled exceptions */ - public handleUncaughtExceptions (): void { - process.on('uncaughtException', (e): void => this._uncaughtExceptionHandler(e)) + public handleUncaughtExceptions(): void { + process.on('uncaughtException', (e): void => { + this._uncaughtExceptionHandler(e) + }) } /** * Terminate parent process helper */ - public _terminateProcess (code?: number, signal?: NodeJS.Signals): void { - if (signal !== undefined) { - process.kill(process.pid, signal) - return - } - if (code !== undefined) { - process.exit(code) - return // eslint-disable-line no-unreachable + public _terminateProcess(signal?: NodeJS.Signals | number): void { + 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') } @@ -102,17 +111,19 @@ export class TermSignals { /** * Exit event listener clean up helper */ - public _removeProcessListeners (): void { + public _removeProcessListeners(): void { SIGNALS_TO_HANDLE.forEach((signal): void => { process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]) }) - process.removeListener('exit', this.terminateSpawnedProcessFuncHandlers.SIGTERM) + if (this.terminateSpawnedProcessFuncExitHandler != null) { + process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler) + } } /** * General exception handler */ - public _uncaughtExceptionHandler (e: Error): void { + public _uncaughtExceptionHandler(e: Error): void { console.error(e.message) process.exit(1) } diff --git a/src/spawn.ts b/src/spawn.ts index 2da09b0..b4d9d5f 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -1,4 +1,4 @@ import * as spawn from 'cross-spawn' export { - spawn + spawn, } diff --git a/src/types.ts b/src/types.ts index a1c1486..092c637 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,39 @@ +import { Command } from 'commander' + +// Define an export type +export type Environment = Partial> + +export type RCEnvironment = Partial> + +export interface CommanderOptions extends Command { + override?: boolean // Default: false + useShell?: boolean // Default: false + expandEnvs?: boolean // Default: false + verbose?: boolean // Default: false + silent?: boolean // Default: false + fallback?: boolean // Default false + 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/src/utils.ts b/src/utils.ts index 12b6d3c..e5c6e50 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,10 +4,10 @@ import * as os from 'os' /** * A simple function for resolving the path the user entered */ -export function resolveEnvFilePath (userPath: string): string { +export function resolveEnvFilePath(userPath: string): string { // Make sure a home directory exist - const home = os.homedir() - if (home !== undefined) { + const home = os.homedir() as string | undefined + if (home != null) { userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`) } return path.resolve(process.cwd(), userPath) @@ -15,13 +15,16 @@ export function resolveEnvFilePath (userPath: string): string { /** * A simple function that parses a comma separated string into an array of strings */ -export function parseArgList (list: string): string[] { +export function parseArgList(list: string): string[] { return list.split(',') } /** - * A simple function to test if the value is a promise + * A simple function to test if the value is a promise/thenable */ -export function isPromise (value: any | PromiseLike): value is Promise { - return value != null && typeof value.then === 'function' +export function isPromise(value?: T | PromiseLike): value is PromiseLike { + return value != null + && typeof value === 'object' + && 'then' in value + && typeof value.then === 'function' } diff --git a/test/env-cmd.spec.ts b/test/env-cmd.spec.ts index 2db408e..3e08264 100644 --- a/test/env-cmd.spec.ts +++ b/test/env-cmd.spec.ts @@ -9,9 +9,9 @@ import * as envCmdLib from '../src/env-cmd' describe('CLI', (): void => { let sandbox: sinon.SinonSandbox - let parseArgsStub: sinon.SinonStub - let envCmdStub: sinon.SinonStub - let processExitStub: sinon.SinonStub + let parseArgsStub: sinon.SinonStub + let envCmdStub: sinon.SinonStub + let processExitStub: sinon.SinonStub before((): void => { sandbox = sinon.createSandbox() parseArgsStub = sandbox.stub(parseArgsLib, 'parseArgs') @@ -49,16 +49,16 @@ describe('CLI', (): void => { describe('EnvCmd', (): void => { let sandbox: sinon.SinonSandbox - let getEnvVarsStub: sinon.SinonStub - let spawnStub: sinon.SinonStub - let expandEnvsSpy: sinon.SinonSpy + let getEnvVarsStub: sinon.SinonStub + let spawnStub: sinon.SinonStub + let expandEnvsSpy: sinon.SinonSpy before((): void => { sandbox = sinon.createSandbox() getEnvVarsStub = sandbox.stub(getEnvVarsLib, 'getEnvVars') spawnStub = sandbox.stub(spawnLib, 'spawn') spawnStub.returns({ on: (): void => { /* Fake the on method */ }, - kill: (): void => { /* Fake the kill method */ } + kill: (): void => { /* Fake the kill method */ }, }) expandEnvsSpy = sandbox.spy(expandEnvsLib, 'expandEnvs') sandbox.stub(signalTermLib.TermSignals.prototype, 'handleTermSignals') @@ -80,12 +80,12 @@ describe('EnvCmd', (): void => { commandArgs: ['-v'], envFile: { filePath: './.env', - fallback: true + fallback: true, }, rc: { environments: ['dev'], - filePath: './.rc' - } + filePath: './.rc', + }, }) assert.equal(getEnvVarsStub.callCount, 1) assert.equal(spawnStub.callCount, 1) @@ -100,17 +100,17 @@ describe('EnvCmd', (): void => { commandArgs: ['-v'], envFile: { filePath: './.env', - fallback: true + fallback: true, }, rc: { environments: ['dev'], - filePath: './.rc' - } + filePath: './.rc', + }, }) assert.equal(getEnvVarsStub.callCount, 1) assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.args[0][2].env.BOB, 'test') - } + }, ) it('should not override existing env vars if noOverride option is true', @@ -122,20 +122,20 @@ describe('EnvCmd', (): void => { commandArgs: ['-v'], envFile: { filePath: './.env', - fallback: true + fallback: true, }, rc: { environments: ['dev'], - filePath: './.rc' + filePath: './.rc', }, options: { - noOverride: true - } + noOverride: true, + }, }) assert.equal(getEnvVarsStub.callCount, 1) assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.args[0][2].env.BOB, 'cool') - } + }, ) it('should spawn process with shell option if useShell option is true', @@ -147,20 +147,20 @@ describe('EnvCmd', (): void => { commandArgs: ['-v'], envFile: { filePath: './.env', - fallback: true + fallback: true, }, rc: { environments: ['dev'], - filePath: './.rc' + filePath: './.rc', }, options: { - useShell: true - } + useShell: true, + }, }) assert.equal(getEnvVarsStub.callCount, 1) assert.equal(spawnStub.callCount, 1) assert.equal(spawnStub.args[0][2].shell, true) - } + }, ) it('should spawn process with command and args expanded if expandEnvs option is true', @@ -171,15 +171,15 @@ describe('EnvCmd', (): void => { commandArgs: ['$PING', '\\$IP'], envFile: { filePath: './.env', - fallback: true + fallback: true, }, rc: { environments: ['dev'], - filePath: './.rc' + filePath: './.rc', }, options: { - expandEnvs: true - } + expandEnvs: true, + }, }) const spawnArgs = spawnStub.args[0] @@ -188,9 +188,9 @@ describe('EnvCmd', (): void => { assert.equal(spawnStub.callCount, 1) assert.equal(expandEnvsSpy.callCount, 3, 'command + number of args') assert.equal(spawnArgs[0], 'node') - assert.sameOrderedMembers(spawnArgs[1], ['PONG', '\\$IP']) + assert.sameOrderedMembers(spawnArgs[1] as string[], ['PONG', '\\$IP']) assert.equal(spawnArgs[2].env.PING, 'PONG') - } + }, ) it('should ignore errors if silent flag provided', @@ -201,16 +201,16 @@ describe('EnvCmd', (): void => { command: 'node', commandArgs: ['-v'], envFile: { - filePath: './.env' + filePath: './.env', }, options: { - silent: true - } + silent: true, + }, }) assert.equal(getEnvVarsStub.callCount, 1) assert.equal(spawnStub.callCount, 1) assert.isUndefined(spawnStub.args[0][2].env.BOB) - } + }, ) it('should allow errors if silent flag not provided', @@ -221,14 +221,16 @@ describe('EnvCmd', (): void => { command: 'node', commandArgs: ['-v'], envFile: { - filePath: './.env' - } + filePath: './.env', + }, }) - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.equal(e.name, 'MissingFile') return } assert.fail('Should not get here.') - } + }, ) }) diff --git a/test/expand-envs.spec.ts b/test/expand-envs.spec.ts index e998409..7474831 100644 --- a/test/expand-envs.spec.ts +++ b/test/expand-envs.spec.ts @@ -7,7 +7,9 @@ describe('expandEnvs', (): void => { notvar: 'this is not used', dollar: 'money', PING: 'PONG', - IP1: '127.0.0.1' + IP1: '127.0.0.1', + THANKSFORALLTHEFISH: 42, + BRINGATOWEL: true, } const args = ['notvar', '$dollar', '\\$notvar', '-4', '$PING', '$IP1', '\\$IP1', '$NONEXIST'] const argsExpanded = ['notvar', 'money', '\\$notvar', '-4', 'PONG', '127.0.0.1', '\\$IP1', '$NONEXIST'] @@ -15,5 +17,8 @@ describe('expandEnvs', (): void => { it('should replace environment variables in args', (): void => { const res = args.map(arg => expandEnvs(arg, envs)) assert.sameOrderedMembers(res, argsExpanded) + for (const arg of args) { + assert.typeOf(arg, 'string') + } }) }) diff --git a/test/get-env-vars.spec.ts b/test/get-env-vars.spec.ts index 59649b8..d1401ea 100644 --- a/test/get-env-vars.spec.ts +++ b/test/get-env-vars.spec.ts @@ -5,9 +5,9 @@ import * as rcFile from '../src/parse-rc-file' import * as envFile from '../src/parse-env-file' describe('getEnvVars', (): void => { - let getRCFileVarsStub: sinon.SinonStub - let getEnvFileVarsStub: sinon.SinonStub - let logInfoStub: sinon.SinonStub + let getRCFileVarsStub: sinon.SinonStub + let getEnvFileVarsStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub | undefined before((): void => { getRCFileVarsStub = sinon.stub(rcFile, 'getRCFileVars') @@ -21,9 +21,7 @@ describe('getEnvVars', (): void => { afterEach((): void => { sinon.resetHistory() sinon.resetBehavior() - if (logInfoStub !== undefined) { - logInfoStub.restore() - } + logInfoStub?.restore() }) it('should parse the json .rc file from the default path with the given environment', @@ -37,7 +35,7 @@ describe('getEnvVars', (): void => { assert.lengthOf(getRCFileVarsStub.args[0][0].environments, 1) assert.equal(getRCFileVarsStub.args[0][0].environments[0], 'production') assert.equal(getRCFileVarsStub.args[0][0].filePath, './.env-cmdrc') - } + }, ) it('should print path of custom .rc file and environments to info for verbose', @@ -46,7 +44,7 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.equal(logInfoStub.callCount, 1) - } + }, ) it('should search all default .rc file paths', async (): Promise => { @@ -71,7 +69,9 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['production'] } }) assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find/gi) assert.match(e.message, /\.rc file/gi) assert.match(e.message, /default paths/gi) @@ -86,7 +86,8 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['production'] }, verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) @@ -98,7 +99,9 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['bad'] } }) assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find environments/gi) assert.match(e.message, /\.rc file at path/gi) } @@ -112,7 +115,8 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['bad'] }, verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) @@ -120,7 +124,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({ - rc: { environments: ['production'], filePath: '../.custom-rc' } + rc: { environments: ['production'], filePath: '../.custom-rc' }, }) assert.isOk(envs) assert.lengthOf(Object.keys(envs), 1) @@ -136,7 +140,7 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) await getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, - verbose: true + verbose: true, }) assert.equal(logInfoStub.callCount, 1) }) @@ -147,10 +151,12 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.rejects(pathError) try { await getEnvVars({ - rc: { environments: ['production'], filePath: '../.custom-rc' } + rc: { environments: ['production'], filePath: '../.custom-rc' }, }) assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find/gi) assert.match(e.message, /\.rc file at path/gi) } @@ -164,10 +170,11 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['production'], filePath: '../.custom-rc' }, - verbose: true + verbose: true, }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) @@ -178,10 +185,12 @@ describe('getEnvVars', (): void => { getRCFileVarsStub.rejects(environmentError) try { await getEnvVars({ - rc: { environments: ['bad'], filePath: '../.custom-rc' } + rc: { environments: ['bad'], filePath: '../.custom-rc' }, }) assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find environments/gi) assert.match(e.message, /\.rc file at path/gi) } @@ -196,13 +205,14 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ rc: { environments: ['bad'], filePath: '../.custom-rc' }, - verbose: true + verbose: true, }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } - } + }, ) it('should parse the env file from a custom path', async (): Promise => { @@ -227,7 +237,9 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ envFile: { filePath: '../.env-file' } }) assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find/gi) assert.match(e.message, /\.env file at path/gi) } @@ -239,14 +251,15 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ envFile: { filePath: '../.env-file' }, verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } }) it( - 'should parse the env file from the default path if custom ' + - 'path not found and fallback option provided', + 'should parse the env file from the default path if custom ' + + 'path not found and fallback option provided', async (): Promise => { getEnvFileVarsStub.onFirstCall().rejects('File not found.') getEnvFileVarsStub.returns({ THANKS: 'FOR ALL THE FISH' }) @@ -256,19 +269,19 @@ describe('getEnvVars', (): void => { assert.equal(envs.THANKS, 'FOR ALL THE FISH') assert.equal(getEnvFileVarsStub.callCount, 2) assert.equal(getEnvFileVarsStub.args[1][0], './.env') - } + }, ) it( - 'should print multiple times for failure to find .env file and ' + - 'failure to find fallback file to infor for verbose', + 'should print multiple times for failure to find .env file and ' + + 'failure to find fallback file to infor for verbose', async (): Promise => { 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 }) assert.equal(logInfoStub.callCount, 2) - } + }, ) it('should parse the env file from the default path', async (): Promise => { @@ -304,7 +317,9 @@ describe('getEnvVars', (): void => { try { await getEnvVars() assert.fail('should not get here.') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /failed to find/gi) assert.match(e.message, /\.env file/gi) assert.match(e.message, /default paths/gi) @@ -319,9 +334,10 @@ describe('getEnvVars', (): void => { try { await getEnvVars({ verbose: true }) assert.fail('should not get here.') - } catch (e) { + } + catch { assert.equal(logInfoStub.callCount, 1) } - } + }, ) }) diff --git a/test/parse-args.spec.ts b/test/parse-args.spec.ts index b6fced0..e8443d2 100644 --- a/test/parse-args.spec.ts +++ b/test/parse-args.spec.ts @@ -9,7 +9,7 @@ describe('parseArgs', (): void => { const environments = ['development', 'production'] const rcFilePath = './.env-cmdrc' const envFilePath = './.env' - let logInfoStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub before((): void => { logInfoStub = sinon.stub(console, 'info') @@ -53,7 +53,7 @@ describe('parseArgs', (): void => { assert.sameOrderedMembers(res.commandArgs, commandFlags) assert.notOk(res.options!.useShell) assert.notOk(res.envFile) - } + }, ) it('should parse override option', (): void => { diff --git a/test/parse-env-file.spec.ts b/test/parse-env-file.spec.ts index 0ddcdcc..22043ca 100644 --- a/test/parse-env-file.spec.ts +++ b/test/parse-env-file.spec.ts @@ -1,7 +1,7 @@ import { assert } from 'chai' import { stripEmptyLines, stripComments, parseEnvVars, - parseEnvString, getEnvFileVars + parseEnvString, getEnvFileVars, } from '../src/parse-env-file' describe('stripEmptyLines', (): void => { @@ -20,10 +20,12 @@ describe('stripComments', (): void => { describe('parseEnvVars', (): void => { it('should parse out all env vars in string when not ending with \'\\n\'', (): void => { - const envVars = parseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING') + const envVars = parseEnvVars('BOB=COOL\nNODE_ENV=dev\nANSWER=42 AND COUNTING\nNUMBER=42\nBOOLEAN=true') assert(envVars.BOB === 'COOL') assert(envVars.NODE_ENV === 'dev') assert(envVars.ANSWER === '42 AND COUNTING') + assert(envVars.NUMBER === 42) + assert(envVars.BOOLEAN === true) }) it('should parse out all env vars in string with format \'key=value\'', (): void => { @@ -96,7 +98,7 @@ describe('parseEnvString', (): void => { const env = parseEnvString('BOB=COOL\nNODE_ENV=dev\nANSWER=42\n') assert(env.BOB === 'COOL') assert(env.NODE_ENV === 'dev') - assert(env.ANSWER === '42') + assert(env.ANSWER === 42) }) }) @@ -107,7 +109,8 @@ describe('getEnvFileVars', (): void => { THANKS: 'FOR WHAT?!', ANSWER: 42, ONLY: 'IN PRODUCTION', - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', + BRINGATOWEL: true, }) }) @@ -117,7 +120,8 @@ describe('getEnvFileVars', (): void => { THANKS: 'FOR WHAT?!', ANSWER: 42, ONLY: 'IN\n PRODUCTION', - GALAXY: 'hitch\nhiking\n\n' + GALAXY: 'hitch\nhiking\n\n', + BRINGATOWEL: true, }) }) @@ -126,7 +130,7 @@ describe('getEnvFileVars', (): void => { assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', ANSWER: 0, - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', }) }) @@ -134,7 +138,7 @@ describe('getEnvFileVars', (): void => { const env = await getEnvFileVars('./test/test-files/test-async.js') assert.deepEqual(env, { THANKS: 'FOR ALL THE FISH', - ANSWER: 0 + ANSWER: 0, }) }) @@ -142,9 +146,10 @@ describe('getEnvFileVars', (): void => { const env = await getEnvFileVars('./test/test-files/test') assert.deepEqual(env, { THANKS: 'FOR WHAT?!', - ANSWER: '42', + ANSWER: 42, ONLY: 'IN=PRODUCTION', - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', + BRINGATOWEL: true, }) }) @@ -152,7 +157,9 @@ describe('getEnvFileVars', (): void => { try { await getEnvFileVars('./test/test-files/non-existent-file') assert.fail('Should not get here!') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /file path/gi) } }) diff --git a/test/parse-rc-file.spec.ts b/test/parse-rc-file.spec.ts index 2b74213..1f61a12 100644 --- a/test/parse-rc-file.spec.ts +++ b/test/parse-rc-file.spec.ts @@ -11,7 +11,8 @@ describe('getRCFileVars', (): void => { assert.deepEqual(res, { THANKS: 'FOR WHAT?!', ANSWER: 42, - ONLY: 'IN PRODUCTION' + ONLY: 'IN PRODUCTION', + BRINGATOWEL: true, }) }) @@ -20,7 +21,7 @@ describe('getRCFileVars', (): void => { assert.exists(res) assert.deepEqual(res, { THANKS: 'FOR MORE FISHIES', - ANSWER: 21 + ANSWER: 21, }) }) @@ -28,7 +29,9 @@ describe('getRCFileVars', (): void => { try { await getRCFileVars({ environments: ['bad'], filePath: 'bad-path' }) assert.fail('Should not get here!') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /\.rc file at path/gi) } }) @@ -37,7 +40,9 @@ describe('getRCFileVars', (): void => { try { await getRCFileVars({ environments: ['bad'], filePath: rcFilePath }) assert.fail('Should not get here!') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /environments/gi) } }) @@ -46,7 +51,9 @@ describe('getRCFileVars', (): void => { try { await getRCFileVars({ environments: ['bad'], filePath: './test/test-files/.rc-test-bad-format' }) assert.fail('Should not get here!') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /parse/gi) } }) @@ -54,12 +61,13 @@ describe('getRCFileVars', (): void => { it('should parse an async js .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.js', }) assert.deepEqual(env, { THANKS: 'FOR WHAT?!', ANSWER: 42, - ONLY: 'IN PRODUCTION' + ONLY: 'IN PRODUCTION', + BRINGATOWEL: true, }) }) }) diff --git a/test/signal-termination.spec.ts b/test/signal-termination.spec.ts index 96ad89c..4657eed 100644 --- a/test/signal-termination.spec.ts +++ b/test/signal-termination.spec.ts @@ -1,6 +1,9 @@ import { assert } from 'chai' import * as sinon from 'sinon' import { TermSignals } from '../src/signal-termination' +import { ChildProcess } from 'child_process' + +type ChildExitListener = (code: number | null, signal: NodeJS.Signals | null | number) => void describe('signal-termination', (): void => { let sandbox: sinon.SinonSandbox @@ -15,8 +18,8 @@ describe('signal-termination', (): void => { describe('TermSignals', (): void => { describe('_uncaughtExceptionHandler', (): void => { const term = new TermSignals() - let logStub: sinon.SinonStub - let processStub: sinon.SinonStub + let logStub: sinon.SinonStub + let processStub: sinon.SinonStub beforeEach((): void => { logStub = sandbox.stub(console, 'error') @@ -39,7 +42,7 @@ describe('signal-termination', (): void => { describe('_removeProcessListeners', (): void => { const term = new TermSignals() - let removeListenerStub: sinon.SinonStub + let removeListenerStub: sinon.SinonStub before((): void => { removeListenerStub = sandbox.stub(process, 'removeListener') }) @@ -50,15 +53,15 @@ describe('signal-termination', (): void => { it('should remove all listeners from default signals and exit signal', (): void => { term._removeProcessListeners() - assert.equal(removeListenerStub.callCount, 4) - assert.equal(removeListenerStub.args[3][0], 'exit') + assert.equal(removeListenerStub.callCount, 3) + assert.oneOf(removeListenerStub.args[2][0], ['SIGTERM', 'SIGINT', 'SIGHUP']) }) }) describe('_terminateProcess', (): void => { const term = new TermSignals() - let exitStub: sinon.SinonStub - let killStub: sinon.SinonStub + let exitStub: sinon.SinonStub + let killStub: sinon.SinonStub beforeEach((): void => { exitStub = sandbox.stub(process, 'exit') @@ -83,7 +86,7 @@ describe('signal-termination', (): void => { }) it('should call kill method with correct kill signal', (): void => { - term._terminateProcess(1, 'SIGINT') + term._terminateProcess('SIGINT') assert.equal(killStub.callCount, 1) assert.equal(exitStub.callCount, 0) assert.equal(killStub.args[0][1], 'SIGINT') @@ -93,7 +96,9 @@ describe('signal-termination', (): void => { try { term._terminateProcess() assert.fail('should not get here') - } catch (e) { + } + catch (e) { + assert.instanceOf(e, Error) assert.match(e.message, /unable to terminate parent process/gi) } }) @@ -101,8 +106,8 @@ describe('signal-termination', (): void => { describe('handleUncaughtExceptions', (): void => { const term = new TermSignals() - let processOnStub: sinon.SinonStub - let _uncaughtExceptionHandlerStub: sinon.SinonStub + let processOnStub: sinon.SinonStub + let _uncaughtExceptionHandlerStub: sinon.SinonStub before((): void => { processOnStub = sandbox.stub(process, 'on') @@ -117,22 +122,22 @@ describe('signal-termination', (): void => { term.handleUncaughtExceptions() assert.equal(processOnStub.callCount, 1) assert.equal(_uncaughtExceptionHandlerStub.callCount, 0) - processOnStub.args[0][1]() + ;(processOnStub.args[0][1] as () => void)() assert.equal(_uncaughtExceptionHandlerStub.callCount, 1) }) }) describe('handleTermSignals', (): void => { let term: TermSignals - let procKillStub: sinon.SinonStub - let procOnStub: sinon.SinonStub - let processOnceStub: sinon.SinonStub - let _removeProcessListenersStub: sinon.SinonStub - let _terminateProcessStub: sinon.SinonStub - let logInfoStub: sinon.SinonStub - let proc: any + let procKillStub: sinon.SinonStub + let procOnStub: sinon.SinonStub + let processOnceStub: sinon.SinonStub + let _removeProcessListenersStub: sinon.SinonStub + let _terminateProcessStub: sinon.SinonStub + let logInfoStub: sinon.SinonStub + let proc: ChildProcess - function setup (verbose: boolean = false): void { + function setup(verbose = false): void { term = new TermSignals({ verbose }) procKillStub = sandbox.stub() procOnStub = sandbox.stub() @@ -141,8 +146,8 @@ describe('signal-termination', (): void => { _terminateProcessStub = sandbox.stub(term, '_terminateProcess') proc = { kill: procKillStub, - on: procOnStub - } + on: procOnStub, + } as unknown as ChildProcess } beforeEach((): void => { @@ -163,7 +168,7 @@ describe('signal-termination', (): void => { it('should terminate child process if parent process terminated', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1]('SIGTERM', 1) + ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM') assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(procKillStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) @@ -176,17 +181,17 @@ describe('signal-termination', (): void => { logInfoStub = sandbox.stub(console, 'info') assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1]('SIGTERM', 1) + ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM') assert.equal(logInfoStub.callCount, 1) }) - it('should not terminate child process if child process termination ' + - 'has already been called by parent', (): void => { + it('should not terminate child process if child process termination ' + + 'has already been called by parent', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1]('SIGINT', 1) + ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGINT') assert.isOk(term._exitCalled) - processOnceStub.args[0][1]('SIGTERM', 1) + ;(processOnceStub.args[0][1] as NodeJS.SignalsListener)('SIGTERM') assert.equal(_removeProcessListenersStub.callCount, 2) assert.equal(procKillStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) @@ -196,7 +201,7 @@ describe('signal-termination', (): void => { it('should convert and use number signal as code', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1](4, 1) + ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(1) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.args[0][0], 'SIGINT') @@ -204,13 +209,13 @@ describe('signal-termination', (): void => { assert.isOk(term._exitCalled) }) - it('should not use signal number as code if value is 0', (): void => { + it('should not use default signal if code is 0', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1](0, 1) + ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(0) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(procKillStub.callCount, 1) - assert.equal(procKillStub.args[0], 1) + assert.equal(procKillStub.args[0], 0) assert.equal(_terminateProcessStub.callCount, 1) assert.isOk(term._exitCalled) }) @@ -218,7 +223,7 @@ describe('signal-termination', (): void => { it('should use signal value and default SIGINT signal if code is undefined', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - processOnceStub.args[0][1](4, undefined) + ;(processOnceStub.args[0][1] as NodeJS.ExitListener)(4) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(procKillStub.callCount, 1) assert.equal(procKillStub.args[0][0], 'SIGINT') @@ -229,7 +234,7 @@ describe('signal-termination', (): void => { it('should terminate parent process if child process terminated', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](1, 'SIGTERM') + ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM') assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) assert.isOk(term._exitCalled) @@ -241,31 +246,31 @@ describe('signal-termination', (): void => { logInfoStub = sandbox.stub(console, 'info') assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](1, 'SIGTERM') + ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM') assert.equal(logInfoStub.callCount, 1) }) it( - 'should print parent process terminated to info for verbose when ' + - 'code and signal are undefined', + 'should print parent process terminated to info for verbose when ' + + 'code and signal are undefined', (): void => { sandbox.restore() setup(true) logInfoStub = sandbox.stub(console, 'info') assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](undefined, null) + ;(procOnStub.args[0][1] as ChildExitListener)(null, null) assert.equal(logInfoStub.callCount, 1) - } + }, ) it('should not terminate parent process if parent process already terminating', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](1, 'SIGINT') + ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGINT') assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) - procOnStub.args[0][1](1, 'SIGTERM') + ;(procOnStub.args[0][1] as ChildExitListener)(1, 'SIGTERM') assert.equal(_removeProcessListenersStub.callCount, 2) assert.equal(_terminateProcessStub.callCount, 1) assert.isOk(term._exitCalled) @@ -274,7 +279,7 @@ describe('signal-termination', (): void => { it('should convert null signal value to undefined', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](0, null) + ;(procOnStub.args[0][1] as ChildExitListener)(0, null) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) assert.strictEqual(_terminateProcessStub.firstCall.args[1], undefined) @@ -284,33 +289,30 @@ describe('signal-termination', (): void => { it('should convert and use number signal as code', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](1, 4) + ;(procOnStub.args[0][1] as ChildExitListener)(1, 4) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) - assert.strictEqual(_terminateProcessStub.firstCall.args[0], 4) - assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT') + assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT') assert.isOk(term._exitCalled) }) it('should not use signal number as code if value is 0', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](1, 0) + ;(procOnStub.args[0][1] as ChildExitListener)(1, 0) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1) - assert.isUndefined(_terminateProcessStub.firstCall.args[1]) assert.isOk(term._exitCalled) }) it('should use signal value and default SIGINT signal if code is undefined', (): void => { assert.notOk(term._exitCalled) term.handleTermSignals(proc) - procOnStub.args[0][1](null, 1) + ;(procOnStub.args[0][1] as ChildExitListener)(null, 1) assert.equal(_removeProcessListenersStub.callCount, 1) assert.equal(_terminateProcessStub.callCount, 1) - assert.strictEqual(_terminateProcessStub.firstCall.args[0], 1) - assert.strictEqual(_terminateProcessStub.firstCall.args[1], 'SIGINT') + assert.strictEqual(_terminateProcessStub.firstCall.args[0], 'SIGINT') assert.isOk(term._exitCalled) }) }) diff --git a/test/test-files/.rc-test b/test/test-files/.rc-test index 57aea54..d76f2e2 100644 --- a/test/test-files/.rc-test +++ b/test/test-files/.rc-test @@ -10,6 +10,7 @@ "production": { "THANKS": "FOR WHAT?!", "ANSWER": 42, - "ONLY": "IN PRODUCTION" + "ONLY": "IN PRODUCTION", + "BRINGATOWEL": true } -} \ No newline at end of file +} diff --git a/test/test-files/.rc-test-async.js b/test/test-files/.rc-test-async.js index a3fb453..356914f 100644 --- a/test/test-files/.rc-test-async.js +++ b/test/test-files/.rc-test-async.js @@ -1,19 +1,20 @@ module.exports = new Promise((resolve) => { setTimeout(() => { resolve({ - 'development': { - 'THANKS': 'FOR ALL THE FISH', - 'ANSWER': 0 + development: { + THANKS: 'FOR ALL THE FISH', + ANSWER: 0, }, - 'test': { - 'THANKS': 'FOR MORE FISHIES', - 'ANSWER': 21 + test: { + THANKS: 'FOR MORE FISHIES', + ANSWER: 21, + }, + production: { + THANKS: 'FOR WHAT?!', + ANSWER: 42, + ONLY: 'IN PRODUCTION', + BRINGATOWEL: true, }, - 'production': { - 'THANKS': 'FOR WHAT?!', - 'ANSWER': 42, - 'ONLY': 'IN PRODUCTION' - } }) }, 200) }) diff --git a/test/test-files/.rc-test.json b/test/test-files/.rc-test.json index 57aea54..d76f2e2 100644 --- a/test/test-files/.rc-test.json +++ b/test/test-files/.rc-test.json @@ -10,6 +10,7 @@ "production": { "THANKS": "FOR WHAT?!", "ANSWER": 42, - "ONLY": "IN PRODUCTION" + "ONLY": "IN PRODUCTION", + "BRINGATOWEL": true } -} \ No newline at end of file +} diff --git a/test/test-files/test b/test/test-files/test index bee4cae..bd9a1d2 100644 --- a/test/test-files/test +++ b/test/test-files/test @@ -1,4 +1,5 @@ THANKS = FOR WHAT?! ANSWER=42 ONLY= "IN=PRODUCTION" -GALAXY="hitch\nhiking" \ No newline at end of file +GALAXY="hitch\nhiking" +BRINGATOWEL=true diff --git a/test/test-files/test-async.js b/test/test-files/test-async.js index ba95d4d..7ba707d 100644 --- a/test/test-files/test-async.js +++ b/test/test-files/test-async.js @@ -2,7 +2,7 @@ module.exports = new Promise((resolve) => { setTimeout(() => { resolve({ THANKS: 'FOR ALL THE FISH', - ANSWER: 0 + ANSWER: 0, }) }, 200) }) diff --git a/test/test-files/test-newlines.json b/test/test-files/test-newlines.json index 912f322..52d404b 100644 --- a/test/test-files/test-newlines.json +++ b/test/test-files/test-newlines.json @@ -2,5 +2,6 @@ "THANKS": "FOR WHAT?!", "ANSWER": 42, "ONLY": "IN\n PRODUCTION", - "GALAXY": "hitch\nhiking\n\n" -} \ No newline at end of file + "GALAXY": "hitch\nhiking\n\n", + "BRINGATOWEL": true +} diff --git a/test/test-files/test.js b/test/test-files/test.js index 0ef99e6..5e25974 100644 --- a/test/test-files/test.js +++ b/test/test-files/test.js @@ -1,5 +1,5 @@ module.exports = { THANKS: 'FOR ALL THE FISH', ANSWER: 0, - GALAXY: 'hitch\nhiking' + GALAXY: 'hitch\nhiking', } diff --git a/test/test-files/test.json b/test/test-files/test.json index a90c062..379f9c6 100644 --- a/test/test-files/test.json +++ b/test/test-files/test.json @@ -2,5 +2,6 @@ "THANKS": "FOR WHAT?!", "ANSWER": 42, "ONLY": "IN PRODUCTION", - "GALAXY": "hitch\nhiking" -} \ No newline at end of file + "GALAXY": "hitch\nhiking", + "BRINGATOWEL": true +} diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 479ad3d..8afac0e 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -48,5 +48,13 @@ describe('utils', (): void => { const res = isPromise({}) assert.isFalse(res) }) + it('should return false for string', (): void => { + const res = isPromise('test') + assert.isFalse(res) + }) + it('should return false for undefined', (): void => { + const res = isPromise(undefined) + assert.isFalse(res) + }) }) }) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index 17ae7bb..0000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true - }, - "include": [ - "src/**/*", - "test/**/*", - "bin/**/*" - ] -} \ No newline at end of file From 7c53224b2c61b4c254d9f0dd90d6d84cb671d435 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Sat, 30 Nov 2024 04:43:25 -0900 Subject: [PATCH 10/22] chore(support): drop nodejs 10x, 12x support --- .github/workflows/linux-tests.yml | 2 +- .github/workflows/windows-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 436040c..1aa2c9e 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - name: Checkout Project diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index be4526b..8bcb1b4 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 18.x, 20.x, 22.x] + node-version: [18.x, 20.x, 22.x] steps: - name: Checkout Project From 05df88702ab82de23d11a4ed6f596d505fb00dbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 13:46:36 +0000 Subject: [PATCH 11/22] chore(deps-dev): bump husky from 4.3.8 to 9.1.7 Bumps [husky](https://github.com/typicode/husky) from 4.3.8 to 9.1.7. - [Release notes](https://github.com/typicode/husky/releases) - [Commits](https://github.com/typicode/husky/compare/v4.3.8...v9.1.7) --- updated-dependencies: - dependency-name: husky 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 a973ff4..4d54d41 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "chai": "^4.0.0", "coveralls": "^3.0.0", "globals": "^15.12.0", - "husky": "^4.0.0", + "husky": "^9.1.7", "mocha": "^7.0.0", "nyc": "^15.0.0", "sinon": "^9.0.0", From 96b3f428a291c43f891484e77a298a989e7d4ed4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 13:46:42 +0000 Subject: [PATCH 12/22] chore(deps-dev): bump @types/sinon from 9.0.11 to 17.0.3 Bumps [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) from 9.0.11 to 17.0.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon) --- updated-dependencies: - dependency-name: "@types/sinon" 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 a973ff4..0eed6e1 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@types/cross-spawn": "^6.0.0", "@types/mocha": "^7.0.0", "@types/node": "^12.0.0", - "@types/sinon": "^9.0.0", + "@types/sinon": "^17.0.3", "chai": "^4.0.0", "coveralls": "^3.0.0", "globals": "^15.12.0", From a0c8f2fad04b2c85231bd0776b8b77b32396b5e5 Mon Sep 17 00:00:00 2001 From: Todd Bluhm Date: Sun, 1 Dec 2024 03:28:33 -0900 Subject: [PATCH 13/22] chore(readme): fix readme file, update husky config --- .github/workflows/linux-tests.yml | 3 +++ .github/workflows/windows-tests.yml | 3 +++ .husky/commit-msg | 1 + README.md | 3 +-- package.json | 6 +----- 5 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .husky/commit-msg diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 1aa2c9e..5a5c6d0 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -6,6 +6,9 @@ jobs: test: runs-on: ubuntu-latest + env: + HUSKY: 0 + strategy: matrix: node-version: [18.x, 20.x, 22.x] diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index 8bcb1b4..574e919 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -7,6 +7,9 @@ jobs: runs-on: windows-latest + env: + HUSKY: 0 + strategy: matrix: node-version: [18.x, 20.x, 22.x] diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..a4dc436 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx commitlint --edit diff --git a/README.md b/README.md index b4cbc3c..19d915f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![npm](https://badgen.net/npm/v/env-cmd)](https://www.npmjs.com/package/env-cmd) [![npm](https://badgen.net/npm/dm/env-cmd)](https://www.npmjs.com/package/env-cmd) [![License](https://badgen.net/github/license/toddbluhm/env-cmd)](https://github.com/toddbluhm/env-cmd/blob/master/LICENSE) -[![TS-Standard - Typescript Standard Style Guide](https://badgen.net/badge/code%20style/ts-standard/blue?icon=typescript)](https://github.com/toddbluhm/ts-standard) -[![Dependabot badge](https://badgen.net/dependabot/toddbluhm/env-cmd?icon=dependabot)](https://dependabot.com/) +[![Typescript-ESLint](https://badgen.net/badge/code%20style/typescript-eslint/blue?icon=typescript)](https://github.com/typescript-eslint/typescript-eslint) # env-cmd diff --git a/package.json b/package.json index 4d54d41..3385e76 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "env-cmd": "bin/env-cmd.js" }, "scripts": { + "prepare": "husky", "test": "mocha -r ts-node/register ./test/**/*.ts", "test-cover": "nyc npm test", "coveralls": "coveralls < coverage/lcov.info", @@ -102,11 +103,6 @@ "devDependencyPin": "fix: pin devDependecy ${dependency}" } }, - "husky": { - "hooks": { - "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" - } - }, "commitlint": { "extends": [ "@commitlint/config-conventional" From 69053ab6e9837b8aad33ea34f9dbe08ad3ee4542 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:38:33 +0000 Subject: [PATCH 14/22] chore(deps-dev): bump @commitlint/config-conventional Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 8.3.6 to 19.6.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.6.0/@commitlint/config-conventional) --- updated-dependencies: - dependency-name: "@commitlint/config-conventional" 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 3385e76..ddf22cd 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@commitlint/cli": "^8.0.0", - "@commitlint/config-conventional": "^8.0.0", + "@commitlint/config-conventional": "^19.6.0", "@eslint/js": "^9.15.0", "@stylistic/eslint-plugin": "^2.11.0", "@types/chai": "^4.0.0", From 8814eddb10b5f7ce36211001a11fe69cfb00119c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:38:35 +0000 Subject: [PATCH 15/22] chore(deps-dev): bump nyc from 15.1.0 to 17.1.0 Bumps [nyc](https://github.com/istanbuljs/nyc) from 15.1.0 to 17.1.0. - [Release notes](https://github.com/istanbuljs/nyc/releases) - [Changelog](https://github.com/istanbuljs/nyc/blob/main/CHANGELOG.md) - [Commits](https://github.com/istanbuljs/nyc/compare/v15.1.0...nyc-v17.1.0) --- updated-dependencies: - dependency-name: nyc 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 3385e76..167a374 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "globals": "^15.12.0", "husky": "^9.1.7", "mocha": "^7.0.0", - "nyc": "^15.0.0", + "nyc": "^17.1.0", "sinon": "^9.0.0", "ts-node": "^8.0.0", "typescript": "^5.7.2", From aaa9139305f52ce25e11a59eebb6d270e65e69a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:44:34 +0000 Subject: [PATCH 16/22] chore(deps-dev): bump @commitlint/cli from 8.3.6 to 19.6.0 Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 8.3.6 to 19.6.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.6.0/@commitlint/cli) --- updated-dependencies: - dependency-name: "@commitlint/cli" 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 ddf22cd..c25a84f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "cross-spawn": "^7.0.0" }, "devDependencies": { - "@commitlint/cli": "^8.0.0", + "@commitlint/cli": "^19.6.0", "@commitlint/config-conventional": "^19.6.0", "@eslint/js": "^9.15.0", "@stylistic/eslint-plugin": "^2.11.0", From 9debd7bebf97f9dd600bfefaacd1e800bc363f64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:49:38 +0000 Subject: [PATCH 17/22] chore(deps-dev): bump mocha from 7.2.0 to 10.8.2 Bumps [mocha](https://github.com/mochajs/mocha) from 7.2.0 to 10.8.2. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v7.2.0...v10.8.2) --- updated-dependencies: - dependency-name: mocha 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 98e0a99..618fe3e 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "coveralls": "^3.0.0", "globals": "^15.12.0", "husky": "^9.1.7", - "mocha": "^7.0.0", + "mocha": "^10.8.2", "nyc": "^17.1.0", "sinon": "^9.0.0", "ts-node": "^8.0.0", From b3663b427d7e559ba302ff4faf74a8180c83081a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:54:51 +0000 Subject: [PATCH 18/22] chore(deps-dev): bump sinon from 9.2.4 to 19.0.2 Bumps [sinon](https://github.com/sinonjs/sinon) from 9.2.4 to 19.0.2. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v9.2.4...v19.0.2) --- updated-dependencies: - dependency-name: sinon 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 618fe3e..fef12a1 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "husky": "^9.1.7", "mocha": "^10.8.2", "nyc": "^17.1.0", - "sinon": "^9.0.0", + "sinon": "^19.0.2", "ts-node": "^8.0.0", "typescript": "^5.7.2", "typescript-eslint": "^8.15.0" 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 19/22] 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 20/22] 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 21/22] 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 22/22] 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 {