diff --git a/README.md b/README.md index d511102..37b0200 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ I use this in my npm scripts: } ``` -Ultimately, the command that is executed (using `cross-spawn`) is: +Ultimately, the command that is executed (using `spawn-command`) is: ``` webpack --config build/webpack.config.js @@ -86,6 +86,19 @@ the parent. This is quite useful for launching the same command with different env variables or when the environment variables are too long to have everything in one line. +## Gotchas + +If you want to have the environment variable apply to several commands in series +then you will need to wrap those in quotes in your script. For example: + +```json +{ + "scripts": { + "greet": "cross-env GREETING=Hi NAME=Joe \"echo $GREETING && echo $NAME\"" + } +} +``` + ## Inspiration I originally created this to solve a problem I was having with my npm scripts in diff --git a/__mocks__/cross-spawn.js b/__mocks__/cross-spawn.js deleted file mode 100644 index 167027a..0000000 --- a/__mocks__/cross-spawn.js +++ /dev/null @@ -1,17 +0,0 @@ -const __mock = { - reset() { - __mock.spawned = null - Object.assign(module.exports, { - __mock, - spawn: jest.fn(() => { - __mock.spawned = { - on: jest.fn(), - kill: jest.fn(), - } - return __mock.spawned - }), - }) - }, -} - -__mock.reset() diff --git a/__mocks__/spawn-command.js b/__mocks__/spawn-command.js new file mode 100644 index 0000000..4ec204e --- /dev/null +++ b/__mocks__/spawn-command.js @@ -0,0 +1,17 @@ +const __mock = { + reset() { + __mock.spawned = null + module.exports.__mock = __mock + module.exports.mockClear() + }, +} + +module.exports = jest.fn(() => { + __mock.spawned = { + on: jest.fn(), + kill: jest.fn(), + } + return __mock.spawned +}) + +__mock.reset() diff --git a/package.json b/package.json index 0695643..bb7ad6f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "author": "Kent C. Dodds (http://kentcdodds.com/)", "license": "MIT", "dependencies": { - "cross-spawn": "^5.1.0", "is-windows": "^1.0.0" }, "devDependencies": { @@ -46,6 +45,7 @@ "opt-cli": "^1.5.1", "prettier-eslint-cli": "^3.1.2", "semantic-release": "^6.3.6", + "spawn-command": "^0.0.2-1", "validate-commit-msg": "^2.11.1" }, "eslintConfig": { diff --git a/src/index.js b/src/index.js index 6a299ae..1a03b77 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import {spawn} from 'cross-spawn' +import spawn from 'spawn-command' import commandConvert from './command' export default crossEnv @@ -6,9 +6,9 @@ export default crossEnv const envSetterRegex = /(\w+)=('(.+)'|"(.+)"|(.+))/ function crossEnv(args) { - const [command, commandArgs, env] = getCommandArgsAndEnvVars(args) + const [command, env] = getCommandArgsAndEnvVars(args) if (command) { - const proc = spawn(command, commandArgs, {stdio: 'inherit', env}) + const proc = spawn(command, {stdio: 'inherit', env}) process.on('SIGTERM', () => proc.kill('SIGTERM')) process.on('SIGINT', () => proc.kill('SIGINT')) process.on('SIGBREAK', () => proc.kill('SIGBREAK')) @@ -23,7 +23,10 @@ function getCommandArgsAndEnvVars(args) { const envVars = getEnvVars() const commandArgs = args.map(commandConvert) const command = getCommand(commandArgs, envVars) - return [command, commandArgs, envVars] + if (!command) { + return [] + } + return [`${command} ${commandArgs.join(' ')}`, envVars] } function getCommand(commandArgs, envVars) { diff --git a/src/index.test.js b/src/index.test.js index 0f0e2f3..3825f1f 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -1,19 +1,19 @@ -import crossSpawnMock from 'cross-spawn' +import spawnCommand from 'spawn-command' import crossEnv from '.' beforeEach(() => { - crossSpawnMock.__mock.reset() + spawnCommand.__mock.reset() }) -it(`should set environment variables and run the remaining command`, () => { +test(`sets environment variables and run the remaining command`, () => { testEnvSetting({FOO_ENV: 'production'}, 'FOO_ENV=production') }) -it(`should APPDATA be undefined and not string`, () => { +test(`APPDATAs be undefined and not string`, () => { testEnvSetting({FOO_ENV: 'production', APPDATA: 2}, 'FOO_ENV=production') }) -it(`should handle multiple env variables`, () => { +test(`handles multiple env variables`, () => { testEnvSetting( { FOO_ENV: 'production', @@ -26,38 +26,47 @@ it(`should handle multiple env variables`, () => { ) }) -it(`should handle special characters`, () => { +test(`handles special characters`, () => { testEnvSetting({FOO_ENV: './!?'}, 'FOO_ENV=./!?') }) -it(`should handle single-quoted strings`, () => { +test(`handles single-quoted strings`, () => { testEnvSetting({FOO_ENV: 'bar env'}, "FOO_ENV='bar env'") }) -it(`should handle double-quoted strings`, () => { +test(`handles double-quoted strings`, () => { testEnvSetting({FOO_ENV: 'bar env'}, 'FOO_ENV="bar env"') }) -it(`should handle equality signs in quoted strings`, () => { +test(`handles equality signs in quoted strings`, () => { testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"') }) -it(`should do nothing given no command`, () => { +test(`does nothing given no command`, () => { crossEnv([]) - expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(0) + expect(spawnCommand).toHaveBeenCalledTimes(0) }) -it(`should propagate kill signals`, () => { +test(`propagates kill signals`, () => { testEnvSetting({FOO_ENV: 'foo=bar'}, 'FOO_ENV="foo=bar"') process.emit('SIGTERM') process.emit('SIGINT') process.emit('SIGHUP') process.emit('SIGBREAK') - expect(crossSpawnMock.__mock.spawned.kill).toHaveBeenCalledWith('SIGTERM') - expect(crossSpawnMock.__mock.spawned.kill).toHaveBeenCalledWith('SIGINT') - expect(crossSpawnMock.__mock.spawned.kill).toHaveBeenCalledWith('SIGHUP') - expect(crossSpawnMock.__mock.spawned.kill).toHaveBeenCalledWith('SIGBREAK') + expect(spawnCommand.__mock.spawned.kill).toHaveBeenCalledWith('SIGTERM') + expect(spawnCommand.__mock.spawned.kill).toHaveBeenCalledWith('SIGINT') + expect(spawnCommand.__mock.spawned.kill).toHaveBeenCalledWith('SIGHUP') + expect(spawnCommand.__mock.spawned.kill).toHaveBeenCalledWith('SIGBREAK') +}) + +test('can spawn a group of scripts in a string', () => { + crossEnv(['FOO_ENV=baz', '"echo $baz && echo $baz"']) + expect(spawnCommand).toHaveBeenCalledTimes(1) + expect(spawnCommand).toHaveBeenCalledWith('"echo $baz && echo $baz" ', { + stdio: 'inherit', + env: expect.any(Object), + }) }) function testEnvSetting(expected, ...envSettings) { @@ -75,15 +84,15 @@ function testEnvSetting(expected, ...envSettings) { env.APPDATA = process.env.APPDATA } Object.assign(env, expected) - expect(ret, 'returns what spawn returns').toBe(crossSpawnMock.__mock.spawned) - expect(crossSpawnMock.spawn).toHaveBeenCalledTimes(1) - expect(crossSpawnMock.spawn).toHaveBeenCalledWith('echo', ['hello world'], { + expect(ret, 'returns what spawn returns').toBe(spawnCommand.__mock.spawned) + expect(spawnCommand).toHaveBeenCalledTimes(1) + expect(spawnCommand).toHaveBeenCalledWith('echo hello world', { stdio: 'inherit', env: Object.assign({}, process.env, env), }) - expect(crossSpawnMock.__mock.spawned.on).toHaveBeenCalledTimes(1) - expect(crossSpawnMock.__mock.spawned.on).toHaveBeenCalledWith( + expect(spawnCommand.__mock.spawned.on).toHaveBeenCalledTimes(1) + expect(spawnCommand.__mock.spawned.on).toHaveBeenCalledWith( 'exit', expect.any(Function), ) diff --git a/yarn.lock b/yarn.lock index 65dd4fd..720f101 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1492,7 +1492,7 @@ cross-spawn@^3.0.1: lru-cache "^4.0.1" which "^1.2.9" -cross-spawn@^5.0.1, cross-spawn@^5.1.0: +cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" dependencies: