fix(commander)!: updated code for new version

This commit is contained in:
Todd Bluhm 2024-12-03 05:38:40 -09:00
parent 29c824674b
commit 9942f3c448
No known key found for this signature in database
GPG Key ID: 9CF312607477B8AB
8 changed files with 105 additions and 95 deletions

View File

@ -30,7 +30,7 @@ ENV3=THE FISH
```json ```json
{ {
"scripts": { "scripts": {
"test": "env-cmd mocha -R spec" "test": "env-cmd -- mocha -R spec"
} }
} }
``` ```
@ -38,7 +38,7 @@ ENV3=THE FISH
**Terminal** **Terminal**
```sh ```sh
./node_modules/.bin/env-cmd node index.js ./node_modules/.bin/env-cmd -- node index.js
``` ```
### Using custom env file path ### Using custom env file path
@ -48,13 +48,13 @@ To use a custom env filename or path, pass the `-f` flag. This is a major breaki
**Terminal** **Terminal**
```sh ```sh
./node_modules/.bin/env-cmd -f ./custom/path/.env node index.js ./node_modules/.bin/env-cmd -f ./custom/path/.env -- node index.js
``` ```
## 📜 Help ## 📜 Help
```text ```text
Usage: _ [options] <command> [...args] Usage: env-cmd [options] -- <command> [...args]
Options: Options:
-v, --version output the version number -v, --version output the version number
@ -101,10 +101,10 @@ are found.
**Terminal** **Terminal**
```sh ```sh
./node_modules/.bin/env-cmd -e production node index.js ./node_modules/.bin/env-cmd -e production -- node index.js
# Or for multiple environments (where `production` vars override `test` vars, # Or for multiple environments (where `production` vars override `test` vars,
# but both are included) # but both are included)
./node_modules/.bin/env-cmd -e test,production node index.js ./node_modules/.bin/env-cmd -e test,production -- node index.js
``` ```
### `--no-override` option ### `--no-override` option
@ -125,7 +125,7 @@ commands together that share the same environment variables.
**Terminal** **Terminal**
```sh ```sh
./node_modules/.bin/env-cmd -f ./test/.env --use-shell "npm run lint && npm test" ./node_modules/.bin/env-cmd -f ./test/.env --use-shell -- "npm run lint && npm test"
``` ```
### Asynchronous env file support ### Asynchronous env file support
@ -138,7 +138,7 @@ commands together that share the same environment variables.
**Terminal** **Terminal**
```sh ```sh
./node_modules/.bin/env-cmd -f ./async-file.js node index.js ./node_modules/.bin/env-cmd -f ./async-file.js -- node index.js
``` ```
### `-x` expands vars in arguments ### `-x` expands vars in arguments
@ -152,14 +152,14 @@ to provide arguments to a command that are based on environment variable values
```sh ```sh
# $VAR will be expanded into the env value it contains at runtime # $VAR will be expanded into the env value it contains at runtime
./node_modules/.bin/env-cmd -x node index.js --arg=\$VAR ./node_modules/.bin/env-cmd -x -- node index.js --arg=\$VAR
``` ```
or in `package.json` (use `\\` to insert a literal backslash) or in `package.json` (use `\\` to insert a literal backslash)
```json ```json
{ {
"script": { "script": {
"start": "env-cmd -x node index.js --arg=\\$VAR" "start": "env-cmd -x -- node index.js --arg=\\$VAR"
} }
} }
``` ```
@ -252,11 +252,6 @@ usually just easier to have a file with all the vars in them, especially for dev
[`cross-env`](https://github.com/kentcdodds/cross-env) - Cross platform setting of environment scripts [`cross-env`](https://github.com/kentcdodds/cross-env) - Cross platform setting of environment scripts
## 🎊 Special Thanks
Special thanks to [`cross-env`](https://github.com/kentcdodds/cross-env) for inspiration (uses the
same `cross-spawn` lib underneath too).
## 📋 Contributing Guide ## 📋 Contributing Guide
I welcome all pull requests. Please make sure you add appropriate test cases for any features I welcome all pull requests. Please make sure you add appropriate test cases for any features

53
dist/parse-args.js vendored
View File

@ -1,4 +1,4 @@
import * as commander from 'commander'; import { Command } from '@commander-js/extra-typings';
import { parseArgList } from './utils.js'; import { parseArgList } from './utils.js';
import { default as packageJson } from '../package.json' with { type: 'json' }; import { default as packageJson } from '../package.json' with { type: 'json' };
/** /**
@ -7,48 +7,55 @@ import { default as packageJson } from '../package.json' with { type: 'json' };
export function parseArgs(args) { export function parseArgs(args) {
// Run the initial arguments through commander in order to determine // Run the initial arguments through commander in order to determine
// which value in the args array is the `command` to execute // which value in the args array is the `command` to execute
let program = parseArgsUsingCommander(args); const program = parseArgsUsingCommander(args);
const command = program.args[0]; const command = program.args[0];
// Grab all arguments after the `command` in the args array // Grab all arguments after the `command` in the args array
const commandArgs = args.splice(args.indexOf(command) + 1); const commandArgs = args.splice(args.indexOf(command) + 1);
// Reprocess the args with the command and command arguments removed // Reprocess the args with the command and command arguments removed
program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))); // program = parseArgsUsingCommander(args.slice(0, args.indexOf(command)))
const parsedCmdOptions = program.opts();
// Set values for provided options // Set values for provided options
let noOverride = false; let noOverride = false;
// In commander `no-` negates the original value `override` // In commander `no-` negates the original value `override`
if (program.override === false) { if (parsedCmdOptions.override === false) {
noOverride = true; noOverride = true;
} }
let useShell = false; let useShell = false;
if (program.useShell === true) { if (parsedCmdOptions.useShell === true) {
useShell = true; useShell = true;
} }
let expandEnvs = false; let expandEnvs = false;
if (program.expandEnvs === true) { if (parsedCmdOptions.expandEnvs === true) {
expandEnvs = true; expandEnvs = true;
} }
let verbose = false; let verbose = false;
if (program.verbose === true) { if (parsedCmdOptions.verbose === true) {
verbose = true; verbose = true;
} }
let silent = false; let silent = false;
if (program.silent === true) { if (parsedCmdOptions.silent === true) {
silent = true; silent = true;
} }
let rc; let rc;
if (program.environments !== undefined if (parsedCmdOptions.environments !== undefined
&& Array.isArray(program.environments) && Array.isArray(parsedCmdOptions.environments)
&& program.environments.length !== 0) { && parsedCmdOptions.environments.length !== 0) {
rc = { rc = {
environments: program.environments, environments: parsedCmdOptions.environments,
filePath: program.rcFile, // if we get a boolean value assume not defined
filePath: parsedCmdOptions.rcFile === true ?
undefined :
parsedCmdOptions.rcFile,
}; };
} }
let envFile; let envFile;
if (program.file !== undefined) { if (parsedCmdOptions.file !== undefined) {
envFile = { envFile = {
filePath: program.file, // if we get a boolean value assume not defined
fallback: program.fallback, filePath: parsedCmdOptions.file === true ?
undefined :
parsedCmdOptions.file,
fallback: parsedCmdOptions.fallback,
}; };
} }
const options = { const options = {
@ -70,19 +77,19 @@ export function parseArgs(args) {
return options; return options;
} }
export function parseArgsUsingCommander(args) { export function parseArgsUsingCommander(args) {
const program = new commander.Command(); return new Command('env-cmd')
return program .description('CLI for executing commands using an environment from an env file.')
.version(packageJson.version, '-v, --version') .version(packageJson.version, '-v, --version')
.usage('[options] <command> [...args]') .usage('[options] -- <command> [...args]')
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList) .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList)
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)') .option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc.(js|cjs|mjs|json)')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--fallback', 'Fallback to default env file path, if custom env file path not found')
.option('--no-override', 'Do not override existing environment variables') .option('--no-override', 'Do not override existing environment variables')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)')
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.') .option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--use-shell', 'Execute the command in a new shell with the given environment')
.option('--verbose', 'Print helpful debugging information') .option('--verbose', 'Print helpful debugging information')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.allowUnknownOption(true) .allowUnknownOption(true)
.parse(['_', '_', ...args]); .parse(['_', '_', ...args], { from: 'node' });
} }

20
dist/types.d.ts vendored
View File

@ -1,17 +1,17 @@
import { Command } from 'commander'; import type { Command } from '@commander-js/extra-typings';
export type Environment = Partial<Record<string, string | number | boolean>>; export type Environment = Partial<Record<string, string | number | boolean>>;
export type RCEnvironment = Partial<Record<string, Environment>>; export type RCEnvironment = Partial<Record<string, Environment>>;
export interface CommanderOptions extends Command { export type CommanderOptions = Command<[], {
override?: boolean; environments?: true | string[];
useShell?: boolean;
expandEnvs?: boolean; expandEnvs?: boolean;
verbose?: boolean;
silent?: boolean;
fallback?: boolean; fallback?: boolean;
environments?: string[]; file?: true | string;
rcFile?: string; override?: boolean;
file?: string; rcFile?: true | string;
} silent?: boolean;
useShell?: boolean;
verbose?: boolean;
}>;
export interface RCFileOptions { export interface RCFileOptions {
environments: string[]; environments: string[];
filePath?: string; filePath?: string;

View File

@ -49,6 +49,7 @@
}, },
"homepage": "https://github.com/toddbluhm/env-cmd#readme", "homepage": "https://github.com/toddbluhm/env-cmd#readme",
"dependencies": { "dependencies": {
"@commander-js/extra-typings": "^12.1.0",
"commander": "^12.1.0", "commander": "^12.1.0",
"cross-spawn": "^7.0.6" "cross-spawn": "^7.0.6"
}, },

View File

@ -1,4 +1,4 @@
import * as commander from 'commander' import { Command } from '@commander-js/extra-typings'
import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts' import type { EnvCmdOptions, CommanderOptions, EnvFileOptions, RCFileOptions } from './types.ts'
import { parseArgList } from './utils.js' import { parseArgList } from './utils.js'
import { default as packageJson } from '../package.json' with { type: 'json' }; import { default as packageJson } from '../package.json' with { type: 'json' };
@ -9,54 +9,62 @@ import { default as packageJson } from '../package.json' with { type: 'json' };
export function parseArgs(args: string[]): EnvCmdOptions { export function parseArgs(args: string[]): EnvCmdOptions {
// Run the initial arguments through commander in order to determine // Run the initial arguments through commander in order to determine
// which value in the args array is the `command` to execute // which value in the args array is the `command` to execute
let program = parseArgsUsingCommander(args) const program = parseArgsUsingCommander(args)
const command = program.args[0] const command = program.args[0]
// Grab all arguments after the `command` in the args array // Grab all arguments after the `command` in the args array
const commandArgs = args.splice(args.indexOf(command) + 1) const commandArgs = args.splice(args.indexOf(command) + 1)
// Reprocess the args with the command and command arguments removed // Reprocess the args with the command and command arguments removed
program = parseArgsUsingCommander(args.slice(0, args.indexOf(command))) // program = parseArgsUsingCommander(args.slice(0, args.indexOf(command)))
const parsedCmdOptions = program.opts()
// Set values for provided options // Set values for provided options
let noOverride = false let noOverride = false
// In commander `no-` negates the original value `override` // In commander `no-` negates the original value `override`
if (program.override === false) { if (parsedCmdOptions.override === false) {
noOverride = true noOverride = true
} }
let useShell = false let useShell = false
if (program.useShell === true) { if (parsedCmdOptions.useShell === true) {
useShell = true useShell = true
} }
let expandEnvs = false let expandEnvs = false
if (program.expandEnvs === true) { if (parsedCmdOptions.expandEnvs === true) {
expandEnvs = true expandEnvs = true
} }
let verbose = false let verbose = false
if (program.verbose === true) { if (parsedCmdOptions.verbose === true) {
verbose = true verbose = true
} }
let silent = false let silent = false
if (program.silent === true) { if (parsedCmdOptions.silent === true) {
silent = true silent = true
} }
let rc: RCFileOptions | undefined let rc: RCFileOptions | undefined
if ( if (
program.environments !== undefined parsedCmdOptions.environments !== undefined
&& Array.isArray(program.environments) && Array.isArray(parsedCmdOptions.environments)
&& program.environments.length !== 0 && parsedCmdOptions.environments.length !== 0
) { ) {
rc = { rc = {
environments: program.environments, environments: parsedCmdOptions.environments,
filePath: program.rcFile, // if we get a boolean value assume not defined
filePath: parsedCmdOptions.rcFile === true ?
undefined :
parsedCmdOptions.rcFile,
} }
} }
let envFile: EnvFileOptions | undefined let envFile: EnvFileOptions | undefined
if (program.file !== undefined) { if (parsedCmdOptions.file !== undefined) {
envFile = { envFile = {
filePath: program.file, // if we get a boolean value assume not defined
fallback: program.fallback, filePath: parsedCmdOptions.file === true ?
undefined :
parsedCmdOptions.file,
fallback: parsedCmdOptions.fallback,
} }
} }
@ -80,19 +88,19 @@ export function parseArgs(args: string[]): EnvCmdOptions {
} }
export function parseArgsUsingCommander(args: string[]): CommanderOptions { export function parseArgsUsingCommander(args: string[]): CommanderOptions {
const program = new commander.Command() as CommanderOptions return new Command('env-cmd')
return program .description('CLI for executing commands using an environment from an env file.')
.version(packageJson.version, '-v, --version') .version(packageJson.version, '-v, --version')
.usage('[options] <command> [...args]') .usage('[options] -- <command> [...args]')
.option('-e, --environments [env1,env2,...]', 'The rc file environment(s) to use', parseArgList) .option('-e, --environments [envs...]', 'The rc file environment(s) to use', parseArgList)
.option('-f, --file [path]', 'Custom env file path (default path: ./.env)') .option('-f, --file [path]', 'Custom env file path (default path: ./.env)')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc.(js|cjs|mjs|json)')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.option('--fallback', 'Fallback to default env file path, if custom env file path not found') .option('--fallback', 'Fallback to default env file path, if custom env file path not found')
.option('--no-override', 'Do not override existing environment variables') .option('--no-override', 'Do not override existing environment variables')
.option('-r, --rc-file [path]', 'Custom rc file path (default path: ./.env-cmdrc(|.js|.json)')
.option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.') .option('--silent', 'Ignore any env-cmd errors and only fail on executed program failure.')
.option('--use-shell', 'Execute the command in a new shell with the given environment') .option('--use-shell', 'Execute the command in a new shell with the given environment')
.option('--verbose', 'Print helpful debugging information') .option('--verbose', 'Print helpful debugging information')
.option('-x, --expand-envs', 'Replace $var in args and command with environment variables')
.allowUnknownOption(true) .allowUnknownOption(true)
.parse(['_', '_', ...args]) .parse(['_', '_', ...args], { from: 'node' })
} }

View File

@ -1,21 +1,21 @@
import { Command } from 'commander' import type { Command } from '@commander-js/extra-typings'
// Define an export type // Define an export type
export type Environment = Partial<Record<string, string | number | boolean>> export type Environment = Partial<Record<string, string | number | boolean>>
export type RCEnvironment = Partial<Record<string, Environment>> export type RCEnvironment = Partial<Record<string, Environment>>
export interface CommanderOptions extends Command { export type CommanderOptions = Command<[], {
override?: boolean // Default: false environments?: true | string[]
useShell?: boolean // Default: false
expandEnvs?: boolean // Default: false expandEnvs?: boolean // Default: false
verbose?: boolean // Default: false
silent?: boolean // Default: false
fallback?: boolean // Default false fallback?: boolean // Default false
environments?: string[] file?: true | string
rcFile?: string override?: boolean // Default: false
file?: string rcFile?: true | string
} silent?: boolean // Default: false
useShell?: boolean // Default: false
verbose?: boolean // Default: false
}>
export interface RCFileOptions { export interface RCFileOptions {
environments: string[] environments: string[]

View File

@ -25,31 +25,31 @@ describe('parseArgs', (): void => {
}) })
it('should parse environment value', (): void => { it('should parse environment value', (): void => {
const res = parseArgs(['-e', environments[0], command]) const res = parseArgs(['-e', environments[0], '--', command])
assert.exists(res.rc) 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 => { it('should parse multiple environment values', (): void => {
const res = parseArgs(['-e', environments.join(','), command]) const res = parseArgs(['-e', environments.join(','), '--', command])
assert.exists(res.rc) assert.exists(res.rc)
assert.sameOrderedMembers(res.rc.environments, environments) assert.sameOrderedMembers(res.rc.environments, environments)
}) })
it('should parse command value', (): void => { it('should parse command value', (): void => {
const res = parseArgs(['-e', environments[0], command]) const res = parseArgs(['-e', environments[0], '--', command])
assert.equal(res.command, command) assert.equal(res.command, command)
}) })
it('should parse multiple command arguments', (): void => { it('should parse multiple command arguments', (): void => {
const res = parseArgs(['-e', environments[0], command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '--', command, ...commandArgs])
assert.sameOrderedMembers(res.commandArgs, commandArgs) assert.sameOrderedMembers(res.commandArgs, commandArgs)
}) })
it('should parse multiple command arguments even if they use the same options flags as env-cmd', it('should parse multiple command arguments even if they use the same options flags as env-cmd',
(): void => { (): void => {
const commandFlags = ['-f', './other-file', '--use-shell', '-r'] const commandFlags = ['-f', './other-file', '--use-shell', '-r']
const res = parseArgs(['-e', environments[0], command, ...commandFlags]) const res = parseArgs(['-e', environments[0], '--', command, ...commandFlags])
assert.sameOrderedMembers(res.commandArgs, commandFlags) assert.sameOrderedMembers(res.commandArgs, commandFlags)
assert.notOk(res.options!.useShell) assert.notOk(res.options!.useShell)
assert.notOk(res.envFile) assert.notOk(res.envFile)
@ -57,49 +57,49 @@ describe('parseArgs', (): void => {
) )
it('should parse override option', (): void => { it('should parse override option', (): void => {
const res = parseArgs(['-e', environments[0], '--no-override', command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '--no-override', '--', command, ...commandArgs])
assert.exists(res.options) assert.exists(res.options)
assert.isTrue(res.options.noOverride) assert.isTrue(res.options.noOverride)
}) })
it('should parse use shell option', (): void => { it('should parse use shell option', (): void => {
const res = parseArgs(['-e', environments[0], '--use-shell', command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '--use-shell', '--', command, ...commandArgs])
assert.exists(res.options) assert.exists(res.options)
assert.isTrue(res.options.useShell) assert.isTrue(res.options.useShell)
}) })
it('should parse rc file path', (): void => { it('should parse rc file path', (): void => {
const res = parseArgs(['-e', environments[0], '-r', rcFilePath, command, ...commandArgs]) const res = parseArgs(['-e', environments[0], '-r', rcFilePath, '--', command, ...commandArgs])
assert.exists(res.rc) assert.exists(res.rc)
assert.equal(res.rc.filePath, rcFilePath) assert.equal(res.rc.filePath, rcFilePath)
}) })
it('should parse env file path', (): void => { it('should parse env file path', (): void => {
const res = parseArgs(['-f', envFilePath, command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '--', command, ...commandArgs])
assert.exists(res.envFile) assert.exists(res.envFile)
assert.equal(res.envFile.filePath, envFilePath) assert.equal(res.envFile.filePath, envFilePath)
}) })
it('should parse fallback option', (): void => { it('should parse fallback option', (): void => {
const res = parseArgs(['-f', envFilePath, '--fallback', command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '--fallback', '--', command, ...commandArgs])
assert.exists(res.envFile) 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 => { it('should print to console.info if --verbose flag is passed', (): void => {
const res = parseArgs(['-f', envFilePath, '--verbose', command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '--verbose', '--', command, ...commandArgs])
assert.exists(res.options!.verbose) assert.exists(res.options!.verbose)
assert.equal(logInfoStub.callCount, 1) assert.equal(logInfoStub.callCount, 1)
}) })
it('should parse expandEnvs option', (): void => { it('should parse expandEnvs option', (): void => {
const res = parseArgs(['-f', envFilePath, '-x', command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '-x', '--', command, ...commandArgs])
assert.exists(res.envFile) assert.exists(res.envFile)
assert.isTrue(res.options!.expandEnvs) assert.isTrue(res.options!.expandEnvs)
}) })
it('should parse silent option', (): void => { it('should parse silent option', (): void => {
const res = parseArgs(['-f', envFilePath, '--silent', command, ...commandArgs]) const res = parseArgs(['-f', envFilePath, '--silent', '--', command, ...commandArgs])
assert.exists(res.envFile) assert.exists(res.envFile)
assert.isTrue(res.options!.silent) assert.isTrue(res.options!.silent)
}) })

View File

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