Skip to content

Commit ad45f2c

Browse files
committed
Updated how the execute functions operate and made string commands better
Updated execute commands to use spawn-command making sure we run the command in the actual shell. This means that we do not need special handling for &&, & and cd anymore. String commands will now automatically be get access to node_modules/.bin when for the extension in question making it easier to proxy CLI commands from npm. Added support to pass extra arguments to proxied CLI tools by using --. These arguments will also be available to command functions through the new extraArguments property on the commandObject. Improved how string commands are managed for better error messages.
1 parent cac3fcd commit ad45f2c

File tree

19 files changed

+262
-292
lines changed

19 files changed

+262
-292
lines changed

docs/API.md

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,40 @@ Will deeply merge two objects together and return a new object.
122122
## `Execute`
123123
__These functions should be seen as experimental and might change without a mayor version change to Roc.__
124124
125-
Roc has a simple implementation of execute that makes it easy to invoke string commands as they would have been invoked through a npm script. Does not currently automatically bind `node_modules` to the _PATH_ meaning that correct paths needs to be used. Not a complete mapping to the shell, supports `&`, `&&` for running commands in sequence and `cd`.
125+
Roc has a simple implementation of execute that makes it easy to invoke string commands as they would have been invoked through a npm script. Supports binding of `node_modules` to the _$PATH_ using the options.
126+
127+
__`options`__
128+
All of the variants of execute takes in the same option object as the second argument.
129+
130+
```js
131+
{
132+
args: [], // Additional arguments as an array that should be used with the command.
133+
context: '/some/path', // A path that where a lookup will be done for node_modules/.bin and if found it will be added to the $PATH
134+
cwd: 'some/path/', // The directory the command should be invoked inside
135+
silent: true // A boolean that will enable and disable output from the command
136+
}
137+
```
138+
139+
__`ExecuteError`__
140+
If an error happens in one of the execute functions an `ExecuteError` will be thrown, or in the case of `execute` the promise will be rejected with the error.
141+
142+
It extends the normal `Error` with the following methods.
143+
144+
```
145+
error.getCommand() - The command that was used that caused the problem
146+
error.getExitCode() - The exit code from the process
147+
error.getStderr() - The result from stderr
148+
error.getStdout() - The result from stdout
149+
```
126150
127151
### `execute`
128152
```javascript
129153
import { execute } from 'roc';
130154

131-
execute('git log')
132-
.then(() => {
155+
execute('git log', options).then(() => {
133156
// Completed
134157
})
135-
.catch((exitStatus) => {
136-
// A problem happened, status from the process available
158+
.catch((executeError) => {
137159
});
138160
```
139161
Runs a string in the shell asynchronous and returns a promise that resolves when the command is completed or when a error happened.
@@ -142,36 +164,20 @@ Runs a string in the shell asynchronous and returns a promise that resolves when
142164
```javascript
143165
import { executeSync } from 'roc';
144166

145-
// Will no print anything to console, result available in return value
146-
const output = executeSync('git log', true);
147-
148-
// Will print to console
149-
executeSync('git log');
167+
const output = executeSync('git log', options);
150168
```
151-
Runs a string in the shell synchronous. If running in silent mode, the second argument set to true, the function will return the output.
169+
Runs a string in the shell synchronously.
152170
153-
The function will throw if an error happens. The error object will be extended with the following extra properties.
154-
155-
```
156-
error.command - The command that was used that caused the problem
157-
error.arguments - The arguments that where used that caused the problem
158-
error.exitStatus - The status from the process
159-
error.stderr - The result from stderr
160-
error.stdout - The result from stdout
161-
```
171+
The function will throw if an `ExecuteError` if an error happens.
162172
163173
### `executeSyncExit`
164174
```javascript
165175
import { executeSyncExit } from 'roc';
166176

167-
// Will no print anything to console, result available in return value
168-
const output = executeSyncExit('git log', true);
169-
170-
// Will print to console
171-
executeSyncExit('git log');
177+
const output = executeSyncExit('git log', options);
172178
```
173179
174-
A wrapper around `executeSync` with the difference that it will terminate the process if an error happens with the same status.
180+
A wrapper around `executeSync` with the difference that it will terminate the process if an error happens using the same exit code.
175181
176182
## Configuration
177183

docs/Commands.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ __Important!__
2020
The property name `command` is reserved and should not be used as the name for a command group or command.
2121

2222
### String command
23-
A string command is a string that will managed as if it was typed into the terminal directly. Great for creating aliases to other commands. Does not work exactly as one would run things in a terminal but `&` and `&&` can be used to chain commands together. This feature is experimental in it’s current state and will be improved going forward.
23+
A string command is a string that will managed as if it was typed into the terminal directly. Great for creating aliases to other commands. Uses [`spawn-command`](https://github.com/mmalecki/spawn-command) internally meaning that the command will be launched in either `cmd.exe` or `/bin/sh`. By default will the `node_modules/.bin` for a extension be added making it easy to proxy CLI tools to Roc projects.
24+
25+
This feature is experimental in it’s current state and will be improved going forward.
2426

2527
String commands uses [execute](/docs/API.md#execute) internally.
2628

29+
Tip: You can provide additional arguments to a command in Roc that is using a string command by using `--`.
30+
2731
### Function command
2832
The function will be invoked with an object called `commandObject`.
2933

@@ -52,7 +56,8 @@ __Object structure__
5256
options: {
5357
managed: {}
5458
unmanaged: {}
55-
}
59+
},
60+
extraArguments: []
5661
}
5762
```
5863

@@ -82,6 +87,9 @@ An object with options matching the defined options from the commands meta infor
8287
#### `unmanaged`
8388
An object with options that was not managed by Roc.
8489

90+
### `extraArguments`
91+
An array with the arguments that are defined after `--` on the command line.
92+
8593
## Define new commands & groups
8694
To define new commands that extension will need to either use the [`command`](/docs/RocObject.md#commands) property on the [Roc object](/docs/RocObject.md) or return a command property form the [`init`](/docs/RocObject.md#init) function.
8795

docs/Context.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ actions Action objects.
6565
### `commands`
6666
An object with the merged commands.
6767

68-
The structure of the object is the same as the one used in the [Roc object](#commands) with the exception for `__extensions` that Roc will add when building the context to all the groups and commands. This property is an array with strings that list all of the extensions that have modified it in some way. This is used in the core together with `override` to make sure that extensions knowingly override groups and commands defined by other extensions.
68+
The structure of the object is the same as the one used in the [Roc object](#commands) with the exception for `__extensions` and `__context` that Roc will add when building the context to the groups and the commands.
69+
70+
__`__extensions`__
71+
This property is an array with strings that list all of the extensions that have modified it in some way. This is used in the core together with `override` to make sure that extensions knowingly override groups and commands defined by other extensions.
72+
73+
__`__context`__
74+
This is the path to the extension that registered the command. This is used internally for giving access to `node_modules/.bin` when invoking a string command.
6975

7076
### `config`
7177
An object containing the final configuration. This means that the project configuration will have been merged with the configuration from the packages as well as the settings that was defined in the cli at runtime and the `__raw` values added to their properties in `settings`.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"mocha": "~3.0.2",
8282
"npm-check-updates": "~2.5.6",
8383
"nyc": "7.1.0",
84+
"proxyquire": "1.7.10",
8485
"rimraf": "~2.5.0"
8586
},
8687
"dependencies": {
@@ -96,13 +97,16 @@
9697
"leven": "~2.0.0",
9798
"lodash": "~4.13.1",
9899
"loud-rejection": "~1.3.0",
100+
"manage-path": "2.0.0",
99101
"merge-options": "0.0.64",
100102
"minimist": "~1.2.0",
101103
"redent": "~1.0.0",
102104
"replace": "~0.3.0",
103105
"resolve": "~1.1.6",
104106
"semver": "5.1.0",
107+
"shell-escape": "0.2.0",
105108
"source-map-support": "~0.4.0",
109+
"spawn-command": "0.0.2-1",
106110
"strip-ansi": "~3.0.0",
107111
"tar": "~2.2.1",
108112
"temp": "~0.8.3",

src/cli/runCli.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import execute from '../execute';
1010
import getAbsolutePath from '../helpers/getAbsolutePath';
1111
import getSuggestions from '../helpers/getSuggestions';
1212
import initContext from '../context/initContext';
13-
import log from '../log/default/large';
13+
import log from '../log/default';
1414
import merge from '../helpers/merge';
1515
import runHook from '../hooks/runHook';
1616
import validateSettingsWrapper from '../validation/validateSettingsWrapper';
@@ -36,8 +36,8 @@ export default function runCli({
3636
}) {
3737
const {
3838
_, h, help, V, verbose, v, version, c, config, d, directory, b, 'better-feedback': betterFeedback,
39-
...restOptions,
40-
} = minimist(argv.slice(2));
39+
'--': extraArguments, ...restOptions,
40+
} = minimist(argv.slice(2), { '--': true });
4141

4242
// The first should be our command if there is one
4343
const [groupOrCommand, ...args] = _;
@@ -110,7 +110,7 @@ export default function runCli({
110110
}
111111

112112
if (!commands[command]) {
113-
log.error(
113+
log.large.error(
114114
getSuggestions([command], suggestions),
115115
'Invalid command'
116116
);
@@ -164,15 +164,22 @@ export default function runCli({
164164
if (invoke) {
165165
// If string run as shell command
166166
if (isString(commands[command].command)) {
167-
return execute(commands[command].command)
168-
.catch(process.exit);
167+
return execute(commands[command].command, {
168+
context: commands[command].__context,
169+
args: extraArguments,
170+
cwd: directory,
171+
}).catch((error) => {
172+
process.exitCode = error.getCode ? error.getCode() : 1;
173+
log.small.error('An error happened when running the Roc command', error);
174+
});
169175
}
170176

171177
// Run the command
172178
return commands[command].command({
173179
info,
174180
arguments: parsedArguments,
175181
options: parsedOptions,
182+
extraArguments,
176183

177184
// Roc Context
178185
context,

src/context/extensions/helpers/processCommands.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import merge from '../../../helpers/merge';
88
import buildList from './buildList';
99

1010
// Updates the command object and validates it
11-
export default function processCommands(name, extensionCommands, stateCommands) {
11+
export default function processCommands(name, path, extensionCommands, stateCommands) {
1212
return validateCommands(
1313
name,
14-
normalizeCommands(name, extensionCommands, stateCommands),
14+
normalizeCommands(name, path, extensionCommands, stateCommands),
1515
stateCommands,
1616
true
1717
);
1818
}
1919

2020
// Updated the command object
21-
export function normalizeCommands(name, extensionCommands, stateCommands = {}) {
21+
export function normalizeCommands(name, path, extensionCommands, stateCommands = {}) {
2222
const normalizeCommandsHelper = (newCommands, existingCommands = {}, oldPath = '') => {
2323
const localCommands = { ...newCommands };
2424
Object.keys(localCommands).forEach((command) => {
@@ -30,14 +30,19 @@ export function normalizeCommands(name, extensionCommands, stateCommands = {}) {
3030
localCommands[command] = {
3131
command: localCommands[command],
3232
__extensions: [name],
33+
__context: path,
3334
};
35+
} else if (localCommands[command].command) {
36+
// If the command has been changed we would like to update the context for it
37+
localCommands[command].__context = path;
3438
}
3539

3640
localCommands[command].__extensions = union(existingExtensions, [name]);
3741

3842
// If it was a command group and now is a command
3943
if (isCommandGroup(existingCommands)(command) && isCommand(localCommands)(command)) {
4044
localCommands[command].__extensions = [name];
45+
localCommands[command].__context = path;
4146
}
4247
} else if (isCommand(localCommands)(command)) {
4348
if (!isPlainObject(localCommands[command])) {
@@ -47,6 +52,7 @@ export function normalizeCommands(name, extensionCommands, stateCommands = {}) {
4752
}
4853

4954
localCommands[command].__extensions = [name];
55+
localCommands[command].__context = path;
5056
}
5157

5258
if (

src/context/extensions/helpers/processRocObject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default function processRocObject(
9696
if (roc.commands) {
9797
state.context.commands = merge(
9898
state.context.commands,
99-
processCommands(roc.name, roc.commands, state.context.commands)
99+
processCommands(roc.name, roc.path, roc.commands, state.context.commands)
100100
);
101101
}
102102

src/context/helpers/getDefaults.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function getDefaults(context, name = 'roc', directory) {
1616
commands: getDefaultCommands(directory) || {},
1717
});
1818

19-
newContext.commands = normalizeCommands(name, newContext.commands);
19+
newContext.commands = normalizeCommands(name, null, newContext.commands);
2020

2121
newContext.hooks = registerHooks(getDefaultHooks(), 'roc', newContext.hooks);
2222

src/execute/executeSync.js

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,24 @@
1-
import { join } from 'path';
2-
3-
import { sync } from 'cross-spawn';
4-
5-
import getParts from './helpers/getParts';
6-
7-
/**
8-
* Executes a command string in a synchronous manner.
9-
*
10-
* Quite simple in its current state and should be expected to change in the future.
11-
* Can manage multiple commands if they are divided by either & or &&. Important that there is spacing on both sides.
12-
*
13-
* @param {string} command - A command string that should run.
14-
* @param {boolean} [silent=false] - If the command should run in silent mode, no output.
15-
*
16-
* @returns {string[]} - The output to stdout if silent was used.
17-
*/
18-
export default function executeSync(command, silent = false) {
19-
// Will run them in parallel anyway, nothing we can do about it currently
20-
const parallelCommands = command.split(/ & /);
21-
return parallelCommands.map((syncCommand) => {
22-
const syncCommands = syncCommand.split(/ && /);
23-
return runCommandSync(syncCommands, silent);
1+
import spawnCommandSync from './helpers/spawnCommandSync';
2+
import getEnv from './helpers/getEnv';
3+
import getCommand from './helpers/getCommand';
4+
import ExecuteError from './helpers/ExecuteError';
5+
6+
export default function executeSync(command, { context, cwd, args, silent } = {}) {
7+
const cmd = getCommand(command, args);
8+
const { status, stdout, stderr } = spawnCommandSync(cmd, {
9+
stdio: silent ? undefined : 'inherit',
10+
env: getEnv(context),
11+
cwd,
2412
});
25-
}
26-
27-
function runCommandSync(syncCommands, silent, path = process.cwd(), results = []) {
28-
const command = syncCommands.shift();
29-
30-
if (command) {
31-
const parts = getParts(command);
32-
const cmd = parts[0];
33-
const args = parts.slice(1);
34-
35-
// If the command is to change directory we will carry the path over to the next command.
36-
if (cmd === 'cd') {
37-
// If the path is absolute, starts with a /, we will not join in with the previous
38-
const newPath = args[0].charAt(0) === '/' ?
39-
args[0] : join(path, args[0]);
40-
return runCommandSync(syncCommands, silent, newPath, results);
41-
}
42-
43-
const { status, stdout, stderr } = sync(cmd, args, { cwd: path, stdio: silent ? undefined : 'inherit' });
44-
45-
const newResults = [...results, stdout && stdout.toString()];
46-
47-
if (status) {
48-
const error = new Error(`The following command returned exit status [${status}]: ${cmd} ${args.join(' ')}`);
49-
50-
error.command = cmd;
51-
error.arguments = args;
52-
error.exitStatus = status;
53-
error.stderr = stderr && stderr.toString();
54-
error.stdout = newResults;
55-
56-
throw error;
57-
}
5813

59-
return runCommandSync(syncCommands, silent, path, newResults);
14+
if (status) {
15+
throw new ExecuteError(`The command "${cmd}" failed with error code ${status}`,
16+
cmd,
17+
status,
18+
stderr && stderr.toString(),
19+
stdout && stdout.toString()
20+
);
6021
}
6122

62-
return results;
23+
return stdout && stdout.toString();
6324
}

src/execute/executeSyncExit.js

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,16 @@ import executeSync from './executeSync';
55
* without throwing an exception.
66
*
77
* This can be useful for when the command that is running is handling the error output itself.
8-
*
9-
* @param {string} command - A command string that should run.
10-
* @param {boolean} [silent=false] - If the command should run in silent mode, no output.
11-
*
12-
* @returns {string[]} - The output to stdout if silent was used.
138
*/
14-
export default function executeSyncExit(command, silent = false) {
9+
export default function executeSyncExit(command, options) {
1510
try {
16-
return executeSync(command, silent);
17-
} catch (err) {
18-
// Only process if we got the error from below that sets the exitStatus.
19-
if (!err.exitStatus) {
20-
throw err;
11+
return executeSync(command, options);
12+
} catch (error) {
13+
// Only process if we got an error that have getCode
14+
if (!error.getExitCode) {
15+
throw error;
2116
}
2217

23-
return process.exit(err.exitStatus); // eslint-disable-line
18+
return process.exit(error.getExitCode()); // eslint-disable-line
2419
}
2520
}

0 commit comments

Comments
 (0)