Skip to content
This repository has been archived by the owner on Jan 6, 2021. It is now read-only.

Support quoting scripts to run multiple scripts with the same process.env #77

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
17 changes: 0 additions & 17 deletions __mocks__/cross-spawn.js

This file was deleted.

17 changes: 17 additions & 0 deletions __mocks__/spawn-command.js
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"author": "Kent C. Dodds <[email protected]> (http://kentcdodds.com/)",
"license": "MIT",
"dependencies": {
"cross-spawn": "^5.1.0",
"is-windows": "^1.0.0"
},
"devDependencies": {
Expand All @@ -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": {
Expand Down
11 changes: 7 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {spawn} from 'cross-spawn'
import spawn from 'spawn-command'
import commandConvert from './command'

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'))
Expand All @@ -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) {
Expand Down
51 changes: 30 additions & 21 deletions src/index.test.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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) {
Expand All @@ -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),
)
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down