@depup/env-cmd 11.0.0-depup.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ import type { Environment } from './types.ts';
2
+ /**
3
+ * Gets the environment vars from an env file
4
+ */
5
+ export declare function getEnvFileVars(envFilePath: string): Promise<Environment>;
6
+ /**
7
+ * Parse out all env vars from a given env file string and return an object
8
+ */
9
+ export declare function parseEnvString(envFileString: string): Environment;
10
+ /**
11
+ * Parse out all env vars from an env file string
12
+ */
13
+ export declare function parseEnvVars(envString: string): Environment;
14
+ /**
15
+ * Strips out comments from env file string
16
+ */
17
+ export declare function stripComments(envString: string): string;
18
+ /**
19
+ * Strips out newlines from env file string
20
+ */
21
+ export declare function stripEmptyLines(envString: string): string;
22
+ /**
23
+ * If we load data from a file like .js, the user
24
+ * might export something which is not an object.
25
+ *
26
+ * This function ensures that the input is valid,
27
+ * and converts the object's values to strings, for
28
+ * consistincy. See issue #125 for details.
29
+ */
30
+ export declare function normalizeEnvObject(input: unknown, absolutePath: string): Environment;
@@ -0,0 +1,119 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { extname } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
5
+ import { checkIfTypescriptSupported } from './loaders/typescript.js';
6
+ /**
7
+ * Gets the environment vars from an env file
8
+ */
9
+ export async function getEnvFileVars(envFilePath) {
10
+ const absolutePath = resolveEnvFilePath(envFilePath);
11
+ if (!existsSync(absolutePath)) {
12
+ const pathError = new Error(`Invalid env file path (${envFilePath}).`);
13
+ pathError.name = 'PathError';
14
+ throw pathError;
15
+ }
16
+ // Get the file extension
17
+ const ext = extname(absolutePath).toLowerCase();
18
+ let env;
19
+ if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
20
+ if (/tsx?$/.test(ext))
21
+ checkIfTypescriptSupported();
22
+ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
23
+ let attributeTypes = {};
24
+ if (ext === '.json') {
25
+ attributeTypes = { with: { type: 'json' } };
26
+ }
27
+ const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
28
+ if (typeof res === 'object' && res && 'default' in res) {
29
+ env = res.default;
30
+ }
31
+ else {
32
+ env = res;
33
+ }
34
+ // Check to see if the imported value is a promise
35
+ if (isPromise(env)) {
36
+ env = await env;
37
+ }
38
+ return normalizeEnvObject(env, absolutePath);
39
+ }
40
+ const file = readFileSync(absolutePath, { encoding: 'utf8' });
41
+ switch (ext) {
42
+ // other loaders can be added here
43
+ default:
44
+ return parseEnvString(file);
45
+ }
46
+ }
47
+ /**
48
+ * Parse out all env vars from a given env file string and return an object
49
+ */
50
+ export function parseEnvString(envFileString) {
51
+ // First thing we do is stripe out all comments
52
+ envFileString = stripComments(envFileString);
53
+ // Next we stripe out all the empty lines
54
+ envFileString = stripEmptyLines(envFileString);
55
+ // Merge the file env vars with the current process env vars (the file vars overwrite process vars)
56
+ return parseEnvVars(envFileString);
57
+ }
58
+ /**
59
+ * Parse out all env vars from an env file string
60
+ */
61
+ export function parseEnvVars(envString) {
62
+ const envParseRegex = /^((.+?)[=](.*))$/gim;
63
+ const matches = {};
64
+ let match;
65
+ while ((match = envParseRegex.exec(envString)) !== null) {
66
+ // Note: match[1] is the full env=var line
67
+ const key = match[2].trim();
68
+ let value = match[3].trim();
69
+ // if the string is quoted, remove everything after the final
70
+ // quote. This implicitly removes inline comments.
71
+ if (value.startsWith("'") || value.startsWith('"')) {
72
+ value = value.slice(1, value.lastIndexOf(value[0]));
73
+ }
74
+ else {
75
+ // if the string is not quoted, we need to explicitly remove
76
+ // inline comments.
77
+ value = value.split('#')[0].trim();
78
+ }
79
+ value = value.replace(/\\n/g, '\n');
80
+ matches[key] = value;
81
+ }
82
+ return JSON.parse(JSON.stringify(matches));
83
+ }
84
+ /**
85
+ * Strips out comments from env file string
86
+ */
87
+ export function stripComments(envString) {
88
+ const commentsRegex = /(^\s*#.*$)/gim;
89
+ return envString.replace(commentsRegex, '');
90
+ }
91
+ /**
92
+ * Strips out newlines from env file string
93
+ */
94
+ export function stripEmptyLines(envString) {
95
+ const emptyLinesRegex = /(^\n)/gim;
96
+ return envString.replace(emptyLinesRegex, '');
97
+ }
98
+ /**
99
+ * If we load data from a file like .js, the user
100
+ * might export something which is not an object.
101
+ *
102
+ * This function ensures that the input is valid,
103
+ * and converts the object's values to strings, for
104
+ * consistincy. See issue #125 for details.
105
+ */
106
+ export function normalizeEnvObject(input, absolutePath) {
107
+ if (typeof input !== 'object' || !input) {
108
+ throw new Error(`env-cmd cannot load “${absolutePath}” because it does not export an object.`);
109
+ }
110
+ const env = {};
111
+ for (const [key, value] of Object.entries(input)) {
112
+ // we're intentionally stringifying the value here, to
113
+ // match what `child_process.spawn` does when loading
114
+ // env variables.
115
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
116
+ env[key] = `${value}`;
117
+ }
118
+ return env;
119
+ }
@@ -0,0 +1,8 @@
1
+ import type { Environment } from './types.ts';
2
+ /**
3
+ * Gets the env vars from the rc file and rc environments
4
+ */
5
+ export declare function getRCFileVars({ environments, filePath }: {
6
+ environments: string[];
7
+ filePath: string;
8
+ }): Promise<Environment>;
@@ -0,0 +1,78 @@
1
+ import { stat, readFile } from 'node:fs';
2
+ import { promisify } from 'node:util';
3
+ import { extname } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { resolveEnvFilePath, IMPORT_HOOK_EXTENSIONS, isPromise } from './utils.js';
6
+ import { checkIfTypescriptSupported } from './loaders/typescript.js';
7
+ const statAsync = promisify(stat);
8
+ const readFileAsync = promisify(readFile);
9
+ /**
10
+ * Gets the env vars from the rc file and rc environments
11
+ */
12
+ export async function getRCFileVars({ environments, filePath }) {
13
+ const absolutePath = resolveEnvFilePath(filePath);
14
+ try {
15
+ await statAsync(absolutePath);
16
+ }
17
+ catch {
18
+ const pathError = new Error(`Failed to find .rc file at path: ${absolutePath}`);
19
+ pathError.name = 'PathError';
20
+ throw pathError;
21
+ }
22
+ // Get the file extension
23
+ const ext = extname(absolutePath).toLowerCase();
24
+ let parsedData = {};
25
+ try {
26
+ if (IMPORT_HOOK_EXTENSIONS.includes(ext)) {
27
+ if (/tsx?$/.test(ext))
28
+ checkIfTypescriptSupported();
29
+ // For some reason in ES Modules, only JSON file types need to be specifically delinated when importing them
30
+ let attributeTypes = {};
31
+ if (ext === '.json') {
32
+ attributeTypes = { with: { type: 'json' } };
33
+ }
34
+ const res = await import(pathToFileURL(absolutePath).href, attributeTypes);
35
+ if ('default' in res) {
36
+ parsedData = res.default;
37
+ }
38
+ else {
39
+ parsedData = res;
40
+ }
41
+ // Check to see if the imported value is a promise
42
+ if (isPromise(parsedData)) {
43
+ parsedData = await parsedData;
44
+ }
45
+ }
46
+ else {
47
+ const file = await readFileAsync(absolutePath, { encoding: 'utf8' });
48
+ parsedData = JSON.parse(file);
49
+ }
50
+ }
51
+ catch (e) {
52
+ const errorMessage = e instanceof Error ? e.message : 'Unknown error';
53
+ const parseError = new Error(`Failed to parse .rc file at path: ${absolutePath}.\n${errorMessage}`);
54
+ parseError.name = 'ParseError';
55
+ throw parseError;
56
+ }
57
+ // Parse and merge multiple rc environments together
58
+ let result = {};
59
+ let environmentFound = false;
60
+ for (const name of environments) {
61
+ if (name in parsedData) {
62
+ const envVars = parsedData[name];
63
+ if (envVars != null && typeof envVars === 'object') {
64
+ environmentFound = true;
65
+ result = {
66
+ ...result,
67
+ ...envVars,
68
+ };
69
+ }
70
+ }
71
+ }
72
+ if (!environmentFound) {
73
+ const environmentError = new Error(`Failed to find environments [${environments.join(',')}] at .rc file location: ${absolutePath}`);
74
+ environmentError.name = 'EnvironmentError';
75
+ throw environmentError;
76
+ }
77
+ return result;
78
+ }
@@ -0,0 +1,27 @@
1
+ import { ChildProcess } from 'child_process';
2
+ export declare class TermSignals {
3
+ private readonly terminateSpawnedProcessFuncHandlers;
4
+ private terminateSpawnedProcessFuncExitHandler?;
5
+ private readonly verbose;
6
+ _exitCalled: boolean;
7
+ constructor(options?: {
8
+ verbose?: boolean;
9
+ });
10
+ handleTermSignals(proc: ChildProcess): void;
11
+ /**
12
+ * Enables catching of unhandled exceptions
13
+ */
14
+ handleUncaughtExceptions(): void;
15
+ /**
16
+ * Terminate parent process helper
17
+ */
18
+ _terminateProcess(signal?: NodeJS.Signals | number): void;
19
+ /**
20
+ * Exit event listener clean up helper
21
+ */
22
+ _removeProcessListeners(): void;
23
+ /**
24
+ * General exception handler
25
+ */
26
+ _uncaughtExceptionHandler(e: Error): void;
27
+ }
@@ -0,0 +1,116 @@
1
+ const SIGNALS_TO_HANDLE = [
2
+ 'SIGINT', 'SIGTERM', 'SIGHUP',
3
+ ];
4
+ export class TermSignals {
5
+ terminateSpawnedProcessFuncHandlers = {};
6
+ terminateSpawnedProcessFuncExitHandler;
7
+ verbose = false;
8
+ _exitCalled = false;
9
+ constructor(options = {}) {
10
+ this.verbose = options.verbose === true;
11
+ }
12
+ handleTermSignals(proc) {
13
+ // Terminate child process if parent process receives termination events
14
+ const terminationFunc = (signal) => {
15
+ this._removeProcessListeners();
16
+ if (!this._exitCalled) {
17
+ if (this.verbose) {
18
+ console.info('Parent process exited with signal: '
19
+ + signal.toString()
20
+ + '. Terminating child process...');
21
+ }
22
+ // Mark shared state so we do not run into a signal/exit loop
23
+ this._exitCalled = true;
24
+ // Use the signal code if it is an error code
25
+ // let correctSignal: NodeJS.Signals | undefined
26
+ if (typeof signal === 'number') {
27
+ if (signal > 0) {
28
+ // code = signal
29
+ signal = 'SIGINT';
30
+ }
31
+ }
32
+ // else {
33
+ // correctSignal = signal
34
+ // }
35
+ // Kill the child process
36
+ proc.kill(signal);
37
+ // Terminate the parent process
38
+ this._terminateProcess(signal);
39
+ }
40
+ };
41
+ for (const signal of SIGNALS_TO_HANDLE) {
42
+ this.terminateSpawnedProcessFuncHandlers[signal] = terminationFunc;
43
+ process.once(signal, this.terminateSpawnedProcessFuncHandlers[signal]);
44
+ }
45
+ this.terminateSpawnedProcessFuncExitHandler = terminationFunc;
46
+ process.once('exit', this.terminateSpawnedProcessFuncExitHandler);
47
+ // Terminate parent process if child process receives termination events
48
+ proc.on('exit', (code, signal) => {
49
+ this._removeProcessListeners();
50
+ if (!this._exitCalled) {
51
+ if (this.verbose) {
52
+ console.info(`Child process exited with code: ${(code ?? '').toString()} and signal:`
53
+ + (signal ?? '').toString()
54
+ + '. Terminating parent process...');
55
+ }
56
+ // Mark shared state so we do not run into a signal/exit loop
57
+ this._exitCalled = true;
58
+ // Use the signal code if it is an error code
59
+ let correctSignal;
60
+ if (typeof signal === 'number') {
61
+ if (signal > (code ?? 0)) {
62
+ code = signal;
63
+ correctSignal = 'SIGINT';
64
+ }
65
+ }
66
+ else {
67
+ correctSignal = signal ?? undefined;
68
+ }
69
+ // Terminate the parent process
70
+ this._terminateProcess(correctSignal ?? code);
71
+ }
72
+ });
73
+ }
74
+ /**
75
+ * Enables catching of unhandled exceptions
76
+ */
77
+ handleUncaughtExceptions() {
78
+ process.on('uncaughtException', (e) => {
79
+ this._uncaughtExceptionHandler(e);
80
+ });
81
+ }
82
+ /**
83
+ * Terminate parent process helper
84
+ */
85
+ _terminateProcess(signal) {
86
+ if (signal != null) {
87
+ if (typeof signal === 'string') {
88
+ process.kill(process.pid, signal);
89
+ return;
90
+ }
91
+ if (typeof signal === 'number') {
92
+ process.exit(signal);
93
+ return;
94
+ }
95
+ }
96
+ throw new Error('Unable to terminate parent process successfully');
97
+ }
98
+ /**
99
+ * Exit event listener clean up helper
100
+ */
101
+ _removeProcessListeners() {
102
+ SIGNALS_TO_HANDLE.forEach((signal) => {
103
+ process.removeListener(signal, this.terminateSpawnedProcessFuncHandlers[signal]);
104
+ });
105
+ if (this.terminateSpawnedProcessFuncExitHandler != null) {
106
+ process.removeListener('exit', this.terminateSpawnedProcessFuncExitHandler);
107
+ }
108
+ }
109
+ /**
110
+ * General exception handler
111
+ */
112
+ _uncaughtExceptionHandler(e) {
113
+ console.error(e.message);
114
+ process.exit(1);
115
+ }
116
+ }
@@ -0,0 +1,39 @@
1
+ import type { Command } from '@commander-js/extra-typings';
2
+ export type Environment = Partial<Record<string, string>>;
3
+ export type RCEnvironment = Partial<Record<string, Environment>>;
4
+ export type CommanderOptions = Command<[], {
5
+ environments?: true | string[];
6
+ expandEnvs?: boolean;
7
+ recursive?: boolean;
8
+ fallback?: boolean;
9
+ file?: true | string;
10
+ override?: boolean;
11
+ silent?: boolean;
12
+ useShell?: boolean;
13
+ verbose?: boolean;
14
+ }>;
15
+ export interface RCFileOptions {
16
+ environments: string[];
17
+ filePath?: string;
18
+ }
19
+ export interface EnvFileOptions {
20
+ filePath?: string;
21
+ fallback?: boolean;
22
+ }
23
+ export interface GetEnvVarOptions {
24
+ envFile?: EnvFileOptions;
25
+ rc?: RCFileOptions;
26
+ verbose?: boolean;
27
+ }
28
+ export interface EnvCmdOptions extends GetEnvVarOptions {
29
+ command: string;
30
+ commandArgs: string[];
31
+ options?: {
32
+ expandEnvs?: boolean;
33
+ recursive?: boolean;
34
+ noOverride?: boolean;
35
+ silent?: boolean;
36
+ useShell?: boolean;
37
+ verbose?: boolean;
38
+ };
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ export declare const IMPORT_HOOK_EXTENSIONS: string[];
2
+ /**
3
+ * A simple function for resolving the path the user entered
4
+ */
5
+ export declare function resolveEnvFilePath(userPath: string): string;
6
+ /**
7
+ * A simple function that parses a comma separated string into an array of strings
8
+ */
9
+ export declare function parseArgList(list: string): string[];
10
+ /**
11
+ * A simple function to test if the value is a promise/thenable
12
+ */
13
+ export declare function isPromise<T>(value?: T | PromiseLike<T>): value is PromiseLike<T>;
14
+ /** @returns true if the error is `ERR_UNKNOWN_FILE_EXTENSION` */
15
+ export declare function isLoaderError(error: unknown): error is Error;
package/dist/utils.js ADDED
@@ -0,0 +1,46 @@
1
+ import { resolve } from 'node:path';
2
+ import { homedir } from 'node:os';
3
+ import { cwd } from 'node:process';
4
+ // Special file extensions that node can natively import
5
+ export const IMPORT_HOOK_EXTENSIONS = [
6
+ '.json',
7
+ '.js',
8
+ '.cjs',
9
+ '.mjs',
10
+ '.ts',
11
+ '.mts',
12
+ '.cts',
13
+ '.tsx',
14
+ ];
15
+ /**
16
+ * A simple function for resolving the path the user entered
17
+ */
18
+ export function resolveEnvFilePath(userPath) {
19
+ // Make sure a home directory exist
20
+ const home = homedir();
21
+ if (home != null) {
22
+ userPath = userPath.replace(/^~($|\/|\\)/, `${home}$1`);
23
+ }
24
+ return resolve(cwd(), userPath);
25
+ }
26
+ /**
27
+ * A simple function that parses a comma separated string into an array of strings
28
+ */
29
+ export function parseArgList(list) {
30
+ return list.split(',');
31
+ }
32
+ /**
33
+ * A simple function to test if the value is a promise/thenable
34
+ */
35
+ export function isPromise(value) {
36
+ return value != null
37
+ && typeof value === 'object'
38
+ && 'then' in value
39
+ && typeof value.then === 'function';
40
+ }
41
+ /** @returns true if the error is `ERR_UNKNOWN_FILE_EXTENSION` */
42
+ export function isLoaderError(error) {
43
+ return (error instanceof Error &&
44
+ 'code' in error &&
45
+ error.code === 'ERR_UNKNOWN_FILE_EXTENSION');
46
+ }
@@ -0,0 +1,38 @@
1
+ import { default as tseslint } from 'typescript-eslint'
2
+ import { default as globals } from 'globals'
3
+ import { default as eslint } from '@eslint/js'
4
+
5
+ export default tseslint.config(
6
+ {
7
+ // Ignore build folder
8
+ ignores: ['dist/*'],
9
+ },
10
+ eslint.configs.recommended,
11
+ tseslint.configs.strictTypeChecked,
12
+ tseslint.configs.stylisticTypeChecked,
13
+ {
14
+ // Enable Type generation
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ },
19
+ parserOptions: {
20
+ project: ['./tsconfig.json', './test/tsconfig.json'],
21
+ },
22
+ }
23
+ },
24
+ {
25
+ // For test files ignore some rules
26
+ files: ['test/**/*'],
27
+ rules: {
28
+ '@typescript-eslint/no-explicit-any': 'off',
29
+ '@typescript-eslint/no-unsafe-member-access': 'off',
30
+ '@typescript-eslint/no-unsafe-assignment': 'off'
31
+ },
32
+ },
33
+ // Disable Type Checking JS/CJS/MJS files
34
+ {
35
+ files: ['**/*.js', '**/*.cjs', '**/*.mjs'],
36
+ extends: [tseslint.configs.disableTypeChecked],
37
+ },
38
+ )
package/package.json ADDED
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "@depup/env-cmd",
3
+ "version": "11.0.0-depup.25",
4
+ "description": "Executes a command using the environment variables in an env file (with updated dependencies)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20.10.0"
10
+ },
11
+ "bin": {
12
+ "env-cmd": "bin/env-cmd.js"
13
+ },
14
+ "scripts": {
15
+ "test": "mocha",
16
+ "test-cover": "c8 npm test",
17
+ "coveralls": "coveralls < coverage/lcov.info",
18
+ "lint": "npx eslint .",
19
+ "build": "tsc",
20
+ "watch": "tsc -w"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+ssh://git@github.com/toddbluhm/env-cmd.git"
25
+ },
26
+ "keywords": [
27
+ "env-cmd",
28
+ "depup",
29
+ "updated-dependencies",
30
+ "security",
31
+ "latest",
32
+ "patched",
33
+ "env",
34
+ "environment",
35
+ "cli",
36
+ "command",
37
+ "cmd",
38
+ "execute",
39
+ "run",
40
+ "file",
41
+ "variables",
42
+ "config"
43
+ ],
44
+ "author": "Todd Bluhm",
45
+ "contributors": [
46
+ "Anton Versal <ant.ver@gmail.com>",
47
+ "Eric Lanehart <eric@pushred.co>",
48
+ "Jon Scheiding <jonscheiding@gmail.com>",
49
+ "Kyle Hensel <me@kyle.kiwi>",
50
+ "Nicholas Krul <nicholas.krul@gmail.com>",
51
+ "serapath (Alexander Praetorius) <dev@serapath.de>"
52
+ ],
53
+ "license": "MIT",
54
+ "bugs": {
55
+ "url": "https://github.com/toddbluhm/env-cmd/issues"
56
+ },
57
+ "homepage": "https://github.com/toddbluhm/env-cmd#readme",
58
+ "dependencies": {
59
+ "@commander-js/extra-typings": "^15.0.0",
60
+ "commander": "^15.0.0",
61
+ "cross-spawn": "^7.0.6"
62
+ },
63
+ "devDependencies": {
64
+ "@commitlint/cli": "^19.6.0",
65
+ "@commitlint/config-conventional": "^19.6.0",
66
+ "@eslint/js": "^9.16.0",
67
+ "@types/chai": "^5.0.1",
68
+ "@types/cross-spawn": "^6.0.6",
69
+ "@types/mocha": "^10.0.10",
70
+ "@types/node": "^24.0.10",
71
+ "@types/sinon": "^17.0.3",
72
+ "c8": "^10.1.2",
73
+ "chai": "^5.1.2",
74
+ "coveralls": "^3.0.0",
75
+ "esmock": "^2.6.9",
76
+ "globals": "^16.0.0",
77
+ "husky": "^9.1.7",
78
+ "mocha": "^11.0.0",
79
+ "sinon": "^21.0.0",
80
+ "tsx": "^4.19.2",
81
+ "typescript": "^5.7.2",
82
+ "typescript-eslint": "^8.15.0"
83
+ },
84
+ "commitlint": {
85
+ "extends": [
86
+ "@commitlint/config-conventional"
87
+ ]
88
+ },
89
+ "c8": {
90
+ "reporter": [
91
+ "text",
92
+ "lcov"
93
+ ]
94
+ },
95
+ "release": {
96
+ "branches": [
97
+ "master"
98
+ ],
99
+ "tagFormat": "${version}"
100
+ },
101
+ "depup": {
102
+ "changes": {
103
+ "@commander-js/extra-typings": {
104
+ "from": "^13.1.0",
105
+ "to": "^15.0.0"
106
+ },
107
+ "commander": {
108
+ "from": "^13.1.0",
109
+ "to": "^15.0.0"
110
+ }
111
+ },
112
+ "depsUpdated": 2,
113
+ "originalPackage": "env-cmd",
114
+ "originalVersion": "11.0.0",
115
+ "processedAt": "2026-07-21T17:22:46.794Z",
116
+ "smokeTest": "passed"
117
+ }
118
+ }