@zhennann/common-bin 2.9.2
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.
- package/LICENSE +21 -0
- package/README.md +430 -0
- package/index.d.ts +188 -0
- package/index.js +3 -0
- package/lib/command.js +370 -0
- package/lib/helper.js +189 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-present node-modules and other contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# common-bin
|
|
2
|
+
|
|
3
|
+
[![NPM version][npm-image]][npm-url]
|
|
4
|
+
[![Test coverage][codecov-image]][codecov-url]
|
|
5
|
+
[![Known Vulnerabilities][snyk-image]][snyk-url]
|
|
6
|
+
[![npm download][download-image]][download-url]
|
|
7
|
+
|
|
8
|
+
[npm-image]: https://img.shields.io/npm/v/common-bin.svg?style=flat-square
|
|
9
|
+
[npm-url]: https://npmjs.org/package/common-bin
|
|
10
|
+
[codecov-image]: https://codecov.io/gh/node-modules/common-bin/branch/master/graph/badge.svg
|
|
11
|
+
[codecov-url]: https://codecov.io/gh/node-modules/common-bin
|
|
12
|
+
[snyk-image]: https://snyk.io/test/npm/common-bin/badge.svg?style=flat-square
|
|
13
|
+
[snyk-url]: https://snyk.io/test/npm/common-bin
|
|
14
|
+
[download-image]: https://img.shields.io/npm/dm/common-bin.svg?style=flat-square
|
|
15
|
+
[download-url]: https://npmjs.org/package/common-bin
|
|
16
|
+
|
|
17
|
+
Abstraction bin tool wrap [yargs](http://yargs.js.org/), to provide more convenient usage, support async / generator.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ npm i common-bin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Build a bin tool for your team
|
|
28
|
+
|
|
29
|
+
You maybe need a custom xxx-bin to implement more custom features.
|
|
30
|
+
|
|
31
|
+
Now you can implement a [Command](lib/command.js) sub class to do that.
|
|
32
|
+
|
|
33
|
+
### Example: Write your own `git` command
|
|
34
|
+
|
|
35
|
+
This example will show you how to create a new `my-git` tool.
|
|
36
|
+
|
|
37
|
+
- Full demo: [my-git](test/fixtures/my-git)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
test/fixtures/my-git
|
|
41
|
+
├── bin
|
|
42
|
+
│ └── my-git.js
|
|
43
|
+
├── command
|
|
44
|
+
│ ├── remote
|
|
45
|
+
│ │ ├── add.js
|
|
46
|
+
│ │ └── remove.js
|
|
47
|
+
│ ├── clone.js
|
|
48
|
+
│ └── remote.js
|
|
49
|
+
├── index.js
|
|
50
|
+
└── package.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### [my-git.js](test/fixtures/my-git/bin/my-git.js)
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
#!/usr/bin/env node
|
|
57
|
+
|
|
58
|
+
'use strict';
|
|
59
|
+
|
|
60
|
+
const Command = require('..');
|
|
61
|
+
new Command().start();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### [Main Command](test/fixtures/my-git/index.js)
|
|
65
|
+
|
|
66
|
+
Just extend `Command`, and use as your bin start point.
|
|
67
|
+
|
|
68
|
+
You can use `this.yargs` to custom yargs config, see http://yargs.js.org/docs for more detail.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const Command = require('common-bin');
|
|
72
|
+
const pkg = require('./package.json');
|
|
73
|
+
|
|
74
|
+
class MainCommand extends Command {
|
|
75
|
+
constructor(rawArgv) {
|
|
76
|
+
super(rawArgv);
|
|
77
|
+
this.usage = 'Usage: my-git <command> [options]';
|
|
78
|
+
|
|
79
|
+
// load entire command directory
|
|
80
|
+
this.load(path.join(__dirname, 'command'));
|
|
81
|
+
|
|
82
|
+
// or load special command file
|
|
83
|
+
// this.add(path.join(__dirname, 'test_command.js'));
|
|
84
|
+
|
|
85
|
+
// more custom with `yargs` api, such as you can use `my-git -V`
|
|
86
|
+
this.yargs.alias('V', 'version');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = MainCommand;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### [CloneCommand](test/fixtures/my-git/command/clone.js)
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const Command = require('common-bin');
|
|
97
|
+
class CloneCommand extends Command {
|
|
98
|
+
constructor(rawArgv) {
|
|
99
|
+
super(rawArgv);
|
|
100
|
+
|
|
101
|
+
this.options = {
|
|
102
|
+
depth: {
|
|
103
|
+
type: 'number',
|
|
104
|
+
description: 'Create a shallow clone with a history truncated to the specified number of commits',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
* run({ argv }) {
|
|
110
|
+
console.log('git clone %s to %s with depth %d', argv._[0], argv._[1], argv.depth);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get description() {
|
|
114
|
+
return 'Clone a repository into a new directory';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = CloneCommand;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Run result
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
$ my-git clone gh://node-modules/common-bin dist --depth=1
|
|
125
|
+
|
|
126
|
+
git clone gh://node-modules/common-bin to dist with depth 1
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Concept
|
|
130
|
+
|
|
131
|
+
### Command
|
|
132
|
+
|
|
133
|
+
Define the main logic of command
|
|
134
|
+
|
|
135
|
+
**Method:**
|
|
136
|
+
|
|
137
|
+
- `start()` - start your program, only use once in your bin file.
|
|
138
|
+
- `run(context)`
|
|
139
|
+
- should implement this to provide command handler, will exec when not found sub command.
|
|
140
|
+
- Support generator / async function / normal function which return promise.
|
|
141
|
+
- `context` is `{ cwd, env, argv, rawArgv }`
|
|
142
|
+
- `cwd` - `process.cwd()`
|
|
143
|
+
- `env` - clone env object from `process.env`
|
|
144
|
+
- `argv` - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
|
|
145
|
+
- `rawArgv` - the raw argv, `[ "--baseDir=simple" ]`
|
|
146
|
+
- `load(fullPath)` - register the entire directory to commands
|
|
147
|
+
- `add(name, target)` - register special command with command name, `target` could be full path of file or Class.
|
|
148
|
+
- `alias(alias, name)` - register a command with an existing command
|
|
149
|
+
- `showHelp()` - print usage message to console.
|
|
150
|
+
- `options=` - a setter, shortcut for `yargs.options`
|
|
151
|
+
- `usage=` - a setter, shortcut for `yargs.usage`
|
|
152
|
+
|
|
153
|
+
**Properties:**
|
|
154
|
+
|
|
155
|
+
- `description` - {String} a getter, only show this description when it's a sub command in help console
|
|
156
|
+
- `helper` - {Object} helper instance
|
|
157
|
+
- `yargs` - {Object} yargs instance for advanced custom usage
|
|
158
|
+
- `options` - {Object} a setter, set yargs' options
|
|
159
|
+
- `version` - {String} customize version, can be defined as a getter to support lazy load.
|
|
160
|
+
- `parserOptions` - {Object} control `context` parse rule.
|
|
161
|
+
- `execArgv` - {Boolean} whether extract `execArgv` to `context.execArgv`
|
|
162
|
+
- `removeAlias` - {Boolean} whether remove alias key from `argv`
|
|
163
|
+
- `removeCamelCase` - {Boolean} whether remove camel case key from `argv`
|
|
164
|
+
|
|
165
|
+
You can define options by set `this.options`
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
this.options = {
|
|
169
|
+
baseDir: {
|
|
170
|
+
alias: 'b',
|
|
171
|
+
demandOption: true,
|
|
172
|
+
description: 'the target directory',
|
|
173
|
+
coerce: str => path.resolve(process.cwd(), str),
|
|
174
|
+
},
|
|
175
|
+
depth: {
|
|
176
|
+
description: 'level to clone',
|
|
177
|
+
type: 'number',
|
|
178
|
+
default: 1,
|
|
179
|
+
},
|
|
180
|
+
size: {
|
|
181
|
+
description: 'choose a size',
|
|
182
|
+
choices: ['xs', 's', 'm', 'l', 'xl']
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
You can define version by define `this.version` getter:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
get version() {
|
|
191
|
+
return 'v1.0.0';
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Helper
|
|
196
|
+
|
|
197
|
+
- `forkNode(modulePath, args, opt)` - fork child process, wrap with promise and gracefull exit
|
|
198
|
+
- `spawn(cmd, args, opt)` - spawn a new process, wrap with promise and gracefull exit
|
|
199
|
+
- `npmInstall(npmCli, name, cwd)` - install node modules, wrap with promise
|
|
200
|
+
- `* callFn(fn, args, thisArg)` - call fn, support gernerator / async / normal function return promise
|
|
201
|
+
- `unparseArgv(argv, opts)` - unparse argv and change it to array style
|
|
202
|
+
|
|
203
|
+
**Extend Helper**
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// index.js
|
|
207
|
+
const Command = require('common-bin');
|
|
208
|
+
const helper = require('./helper');
|
|
209
|
+
class MainCommand extends Command {
|
|
210
|
+
constructor(rawArgv) {
|
|
211
|
+
super(rawArgv);
|
|
212
|
+
|
|
213
|
+
// load sub command
|
|
214
|
+
this.load(path.join(__dirname, 'command'));
|
|
215
|
+
|
|
216
|
+
// custom helper
|
|
217
|
+
Object.assign(this.helper, helper);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Advanced Usage
|
|
223
|
+
|
|
224
|
+
### Single Command
|
|
225
|
+
|
|
226
|
+
Just need to provide `options` and `run()`.
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
const Command = require('common-bin');
|
|
230
|
+
class MainCommand extends Command {
|
|
231
|
+
constructor(rawArgv) {
|
|
232
|
+
super(rawArgv);
|
|
233
|
+
this.options = {
|
|
234
|
+
baseDir: {
|
|
235
|
+
description: 'target directory',
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
* run(context) {
|
|
241
|
+
console.log('run default command at %s', context.argv.baseDir);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Sub Command
|
|
247
|
+
|
|
248
|
+
Also support sub command such as `my-git remote add <name> <url> --tags`.
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
// test/fixtures/my-git/command/remote.js
|
|
252
|
+
class RemoteCommand extends Command {
|
|
253
|
+
constructor(rawArgv) {
|
|
254
|
+
// DO NOT forgot to pass params to super
|
|
255
|
+
super(rawArgv);
|
|
256
|
+
// load sub command for directory
|
|
257
|
+
this.load(path.join(__dirname, 'remote'));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
* run({ argv }) {
|
|
261
|
+
console.log('run remote command with %j', argv._);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get description() {
|
|
265
|
+
return 'Manage set of tracked repositories';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// test/fixtures/my-git/command/remote/add.js
|
|
270
|
+
class AddCommand extends Command {
|
|
271
|
+
constructor(rawArgv) {
|
|
272
|
+
super(rawArgv);
|
|
273
|
+
|
|
274
|
+
this.options = {
|
|
275
|
+
tags: {
|
|
276
|
+
type: 'boolean',
|
|
277
|
+
default: false,
|
|
278
|
+
description: 'imports every tag from the remote repository',
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
* run({ argv }) {
|
|
285
|
+
console.log('git remote add %s to %s with tags=%s', argv.name, argv.url, argv.tags);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get description() {
|
|
289
|
+
return 'Adds a remote named <name> for the repository at <url>';
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
see [remote.js](test/fixtures/my-git/command/remote.js) for more detail.
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
### Async Support
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
class SleepCommand extends Command {
|
|
301
|
+
|
|
302
|
+
async run() {
|
|
303
|
+
await sleep('1s');
|
|
304
|
+
console.log('sleep 1s');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get description() {
|
|
308
|
+
return 'sleep showcase';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
see [async-bin](test/fixtures/async-bin) for more detail.
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
### Bash-Completions
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
$ # exec below will print usage for auto bash completion
|
|
324
|
+
$ my-git completion
|
|
325
|
+
$ # exec below will mount auto completion to your bash
|
|
326
|
+
$ my-git completion >> ~/.bashrc
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+

|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
## Migrating from v1 to v2
|
|
333
|
+
|
|
334
|
+
### bin
|
|
335
|
+
|
|
336
|
+
- `run` method is not longer exist.
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
// 1.x
|
|
340
|
+
const run = require('common-bin').run;
|
|
341
|
+
run(require('../lib/my_program'));
|
|
342
|
+
|
|
343
|
+
// 2.x
|
|
344
|
+
// require a main Command
|
|
345
|
+
const Command = require('..');
|
|
346
|
+
new Command().start();
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Program
|
|
350
|
+
|
|
351
|
+
- `Program` is just a `Command` sub class, you can call it `Main Command` now.
|
|
352
|
+
- `addCommand()` is replace with `add()`.
|
|
353
|
+
- Recommand to use `load()` to load the whole command directory.
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
// 1.x
|
|
357
|
+
this.addCommand('test', path.join(__dirname, 'test_command.js'));
|
|
358
|
+
|
|
359
|
+
// 2.x
|
|
360
|
+
const Command = require('common-bin');
|
|
361
|
+
const pkg = require('./package.json');
|
|
362
|
+
|
|
363
|
+
class MainCommand extends Command {
|
|
364
|
+
constructor() {
|
|
365
|
+
super();
|
|
366
|
+
|
|
367
|
+
this.add('test', path.join(__dirname, 'test_command.js'));
|
|
368
|
+
// or load the entire directory
|
|
369
|
+
this.load(path.join(__dirname, 'command'));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Command
|
|
375
|
+
|
|
376
|
+
- `help()` is not use anymore.
|
|
377
|
+
- should provide `name`, `description`, `options`.
|
|
378
|
+
- `* run()` arguments had change to object, recommand to use destructuring style - `{ cwd, env, argv, rawArgv }`
|
|
379
|
+
- `argv` is an object parse by `yargs`, **not `args`.**
|
|
380
|
+
- `rawArgv` is equivalent to old `args`
|
|
381
|
+
|
|
382
|
+
```js
|
|
383
|
+
// 1.x
|
|
384
|
+
class TestCommand extends Command {
|
|
385
|
+
* run(cwd, args) {
|
|
386
|
+
console.log('run mocha test at %s with %j', cwd, args);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 2.x
|
|
391
|
+
class TestCommand extends Command {
|
|
392
|
+
constructor() {
|
|
393
|
+
super();
|
|
394
|
+
// my-bin test --require=co-mocha
|
|
395
|
+
this.options = {
|
|
396
|
+
require: {
|
|
397
|
+
description: 'require module name',
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
* run({ cwd, env, argv, rawArgv }) {
|
|
403
|
+
console.log('run mocha test at %s with %j', cwd, argv);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
get description() {
|
|
407
|
+
return 'unit test';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### helper
|
|
413
|
+
|
|
414
|
+
- `getIronNodeBin` is remove.
|
|
415
|
+
- `child.kill` now support signal.
|
|
416
|
+
|
|
417
|
+
## License
|
|
418
|
+
|
|
419
|
+
[MIT](LICENSE)
|
|
420
|
+
<!-- GITCONTRIBUTOR_START -->
|
|
421
|
+
|
|
422
|
+
## Contributors
|
|
423
|
+
|
|
424
|
+
|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>|[<img src="https://avatars.githubusercontent.com/u/9692408?v=4" width="100px;"/><br/><sub><b>DiamondYuan</b></sub>](https://github.com/DiamondYuan)<br/>|
|
|
425
|
+
| :---: | :---: | :---: | :---: | :---: | :---: |
|
|
426
|
+
[<img src="https://avatars.githubusercontent.com/u/7477670?v=4" width="100px;"/><br/><sub><b>tenpend</b></sub>](https://github.com/tenpend)<br/>|[<img src="https://avatars.githubusercontent.com/u/6399899?v=4" width="100px;"/><br/><sub><b>hacke2</b></sub>](https://github.com/hacke2)<br/>|[<img src="https://avatars.githubusercontent.com/u/11896359?v=4" width="100px;"/><br/><sub><b>liuqipeng417</b></sub>](https://github.com/liuqipeng417)<br/>|[<img src="https://avatars.githubusercontent.com/u/36788851?v=4" width="100px;"/><br/><sub><b>Jarvis2018</b></sub>](https://github.com/Jarvis2018)<br/>
|
|
427
|
+
|
|
428
|
+
This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Feb 09 2022 22:35:03 GMT+0800`.
|
|
429
|
+
|
|
430
|
+
<!-- GITCONTRIBUTOR_END -->
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Arguments, Argv, Options } from 'yargs';
|
|
2
|
+
import { ForkOptions, SpawnOptions } from 'child_process';
|
|
3
|
+
import * as dargs from 'dargs';
|
|
4
|
+
|
|
5
|
+
interface PlainObject {
|
|
6
|
+
[key: string]: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// migrating to common-bin later
|
|
10
|
+
declare class CommonBin {
|
|
11
|
+
usage: string;
|
|
12
|
+
version: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* original argument
|
|
16
|
+
* @type {Array}
|
|
17
|
+
*/
|
|
18
|
+
rawArgv: string[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* yargs
|
|
22
|
+
* @type {Object}
|
|
23
|
+
*/
|
|
24
|
+
yargs: Argv;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* helper function
|
|
28
|
+
* @type {Object}
|
|
29
|
+
*/
|
|
30
|
+
helper: CommonBin.Helper;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* parserOptions
|
|
34
|
+
* @type {Object}
|
|
35
|
+
* @property {Boolean} execArgv - whether extract `execArgv` to `context.execArgv`
|
|
36
|
+
* @property {Boolean} removeAlias - whether remove alias key from `argv`
|
|
37
|
+
* @property {Boolean} removeCamelCase - whether remove camel case key from `argv`
|
|
38
|
+
*/
|
|
39
|
+
parserOptions: {
|
|
40
|
+
execArgv: boolean;
|
|
41
|
+
removeAlias: boolean;
|
|
42
|
+
removeCamelCase: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* getter of context, default behavior is remove `help` / `h` / `version`
|
|
47
|
+
* @return {Object} context - { cwd, env, argv, rawArgv }
|
|
48
|
+
* @protected
|
|
49
|
+
*/
|
|
50
|
+
protected context: CommonBin.Context;
|
|
51
|
+
|
|
52
|
+
constructor(rawArgv?: string[]);
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* shortcut for yargs.options
|
|
57
|
+
* @param {Object} opt - an object set to `yargs.options`
|
|
58
|
+
*/
|
|
59
|
+
set options(opt: { [key: string]: Options });
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* command handler, could be generator / async function / normal function which return promise
|
|
63
|
+
* @param {Object} context - context object
|
|
64
|
+
* @param {String} context.cwd - process.cwd()
|
|
65
|
+
* @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
|
|
66
|
+
* @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
|
|
67
|
+
* @protected
|
|
68
|
+
*/
|
|
69
|
+
protected run(context?: CommonBin.Context): any;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* load sub commands
|
|
73
|
+
* @param {String} fullPath - the command directory
|
|
74
|
+
* @example `load(path.join(__dirname, 'command'))`
|
|
75
|
+
*/
|
|
76
|
+
load(fullPath: string): void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* add sub command
|
|
80
|
+
* @param {String} name - a command name
|
|
81
|
+
* @param {String|Class} target - special file path (must contains ext) or Command Class
|
|
82
|
+
* @example `add('test', path.join(__dirname, 'test_command.js'))`
|
|
83
|
+
*/
|
|
84
|
+
add(name: string, target: string | CommonBin): void;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* alias an existing command
|
|
88
|
+
* @param {String} alias - alias command
|
|
89
|
+
* @param {String} name - exist command
|
|
90
|
+
*/
|
|
91
|
+
alias(alias: string, name: string): void;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* start point of bin process
|
|
95
|
+
*/
|
|
96
|
+
start(): void;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* default error hander
|
|
100
|
+
* @param {Error} err - error object
|
|
101
|
+
* @protected
|
|
102
|
+
*/
|
|
103
|
+
protected errorHandler(err: Error): void;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* print help message to console
|
|
107
|
+
* @param {String} [level=log] - console level
|
|
108
|
+
*/
|
|
109
|
+
showHelp(level?: string): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
declare namespace CommonBin {
|
|
113
|
+
export interface Helper {
|
|
114
|
+
/**
|
|
115
|
+
* fork child process, wrap with promise and gracefull exit
|
|
116
|
+
* @method helper#forkNode
|
|
117
|
+
* @param {String} modulePath - bin path
|
|
118
|
+
* @param {Array} [args] - arguments
|
|
119
|
+
* @param {Object} [options] - options
|
|
120
|
+
* @return {Promise} err or undefined
|
|
121
|
+
* @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
|
|
122
|
+
*/
|
|
123
|
+
forkNode(modulePath: string, args?: string[], options?: ForkOptions): Promise<void>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* spawn a new process, wrap with promise and gracefull exit
|
|
127
|
+
* @method helper#forkNode
|
|
128
|
+
* @param {String} cmd - command
|
|
129
|
+
* @param {Array} [args] - arguments
|
|
130
|
+
* @param {Object} [options] - options
|
|
131
|
+
* @return {Promise} err or undefined
|
|
132
|
+
* @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
|
|
133
|
+
*/
|
|
134
|
+
spawn(cmd: string, args?: string[], options?: SpawnOptions): Promise<void>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* exec npm install
|
|
138
|
+
* @method helper#npmInstall
|
|
139
|
+
* @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall`
|
|
140
|
+
* @param {String} name - node module name
|
|
141
|
+
* @param {String} cwd - target directory
|
|
142
|
+
* @return {Promise} err or undefined
|
|
143
|
+
*/
|
|
144
|
+
npmInstall(npmCli: string, name: string, cwd?: string): Promise<void>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* call fn
|
|
148
|
+
* @method helper#callFn
|
|
149
|
+
* @param {Function} fn - support generator / async / normal function return promise
|
|
150
|
+
* @param {Array} [args] - fn args
|
|
151
|
+
* @param {Object} [thisArg] - this
|
|
152
|
+
* @return {Object} result
|
|
153
|
+
*/
|
|
154
|
+
callFn<T = any, U extends any[] = any[]>(fn: (...args: U) => IterableIterator<T> | Promise<T> | T, args?: U, thisArg?: any): IterableIterator<T>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* unparse argv and change it to array style
|
|
158
|
+
* @method helper#unparseArgv
|
|
159
|
+
* @param {Object} argv - yargs style
|
|
160
|
+
* @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs
|
|
161
|
+
* @param {Array} [options.includes] - keys or regex of keys to include
|
|
162
|
+
* @param {Array} [options.excludes] - keys or regex of keys to exclude
|
|
163
|
+
* @return {Array} [ '--debug=7000', '--debug-brk' ]
|
|
164
|
+
*/
|
|
165
|
+
unparseArgv(argv: object, options?: dargs.Options): string[];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* extract execArgv from argv
|
|
169
|
+
* @method helper#extractExecArgv
|
|
170
|
+
* @param {Object} argv - yargs style
|
|
171
|
+
* @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} }
|
|
172
|
+
*/
|
|
173
|
+
extractExecArgv(argv: object): { debugPort?: number; debugOptions?: PlainObject; execArgvObj: PlainObject };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface Context extends PlainObject {
|
|
177
|
+
cwd: string;
|
|
178
|
+
rawArgv: string[];
|
|
179
|
+
env: PlainObject;
|
|
180
|
+
argv: Arguments<PlainObject>;
|
|
181
|
+
execArgvObj: PlainObject;
|
|
182
|
+
readonly execArgv: string[];
|
|
183
|
+
debugPort?: number;
|
|
184
|
+
debugOptions?: PlainObject;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export = CommonBin;
|
package/index.js
ADDED
package/lib/command.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('common-bin');
|
|
4
|
+
const co = require('co');
|
|
5
|
+
const yargs = require('yargs');
|
|
6
|
+
const parser = require('yargs-parser');
|
|
7
|
+
const helper = require('./helper');
|
|
8
|
+
const assert = require('assert');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const semver = require('semver');
|
|
12
|
+
const changeCase = require('change-case');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
|
|
15
|
+
const DISPATCH = Symbol.for('eb:Command#dispatch');
|
|
16
|
+
const PARSE = Symbol('Command#parse');
|
|
17
|
+
const COMMANDS = Symbol('Command#commands');
|
|
18
|
+
const VERSION = Symbol('Command#version');
|
|
19
|
+
|
|
20
|
+
class CommonBin {
|
|
21
|
+
constructor(rawArgv) {
|
|
22
|
+
/**
|
|
23
|
+
* original argument
|
|
24
|
+
* @type {Array}
|
|
25
|
+
*/
|
|
26
|
+
this.rawArgv = rawArgv || process.argv.slice(2);
|
|
27
|
+
debug('[%s] origin argument `%s`', this.constructor.name, this.rawArgv.join(' '));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* yargs
|
|
31
|
+
* @type {Object}
|
|
32
|
+
*/
|
|
33
|
+
this.yargs = yargs(this.rawArgv);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* helper function
|
|
37
|
+
* @type {Object}
|
|
38
|
+
*/
|
|
39
|
+
this.helper = helper;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* parserOptions
|
|
43
|
+
* @type {Object}
|
|
44
|
+
* @property {Boolean} execArgv - whether extract `execArgv` to `context.execArgv`
|
|
45
|
+
* @property {Boolean} removeAlias - whether remove alias key from `argv`
|
|
46
|
+
* @property {Boolean} removeCamelCase - whether remove camel case key from `argv`
|
|
47
|
+
*/
|
|
48
|
+
this.parserOptions = {
|
|
49
|
+
execArgv: false,
|
|
50
|
+
removeAlias: false,
|
|
51
|
+
removeCamelCase: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// <commandName, Command>
|
|
55
|
+
this[COMMANDS] = new Map();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* command handler, could be generator / async function / normal function which return promise
|
|
60
|
+
* @param {Object} context - context object
|
|
61
|
+
* @param {String} context.cwd - process.cwd()
|
|
62
|
+
* @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
|
|
63
|
+
* @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
|
|
64
|
+
* @protected
|
|
65
|
+
*/
|
|
66
|
+
run() {
|
|
67
|
+
this.showHelp();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* load sub commands
|
|
72
|
+
* @param {String} fullPath - the command directory
|
|
73
|
+
* @example `load(path.join(__dirname, 'command'))`
|
|
74
|
+
*/
|
|
75
|
+
load(fullPath) {
|
|
76
|
+
assert(fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory(),
|
|
77
|
+
`${fullPath} should exist and be a directory`);
|
|
78
|
+
|
|
79
|
+
// load entire directory
|
|
80
|
+
const files = fs.readdirSync(fullPath);
|
|
81
|
+
const names = [];
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
if (path.extname(file) === '.js') {
|
|
84
|
+
const name = path.basename(file).replace(/\.js$/, '');
|
|
85
|
+
names.push(name);
|
|
86
|
+
this.add(name, path.join(fullPath, file));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
debug('[%s] loaded command `%s` from directory `%s`',
|
|
91
|
+
this.constructor.name, names, fullPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* add sub command
|
|
96
|
+
* @param {String} name - a command name
|
|
97
|
+
* @param {String|Class} target - special file path (must contains ext) or Command Class
|
|
98
|
+
* @example `add('test', path.join(__dirname, 'test_command.js'))`
|
|
99
|
+
*/
|
|
100
|
+
add(name, target) {
|
|
101
|
+
assert(name, `${name} is required`);
|
|
102
|
+
if (!(target.prototype instanceof CommonBin)) {
|
|
103
|
+
assert(fs.existsSync(target) && fs.statSync(target).isFile(), `${target} is not a file.`);
|
|
104
|
+
debug('[%s] add command `%s` from `%s`', this.constructor.name, name, target);
|
|
105
|
+
target = require(target);
|
|
106
|
+
// try to require es module
|
|
107
|
+
if (target && target.__esModule && target.default) {
|
|
108
|
+
target = target.default;
|
|
109
|
+
}
|
|
110
|
+
assert(target.prototype instanceof CommonBin,
|
|
111
|
+
'command class should be sub class of common-bin');
|
|
112
|
+
}
|
|
113
|
+
this[COMMANDS].set(name, target);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* alias an existing command
|
|
118
|
+
* @param {String} alias - alias command
|
|
119
|
+
* @param {String} name - exist command
|
|
120
|
+
*/
|
|
121
|
+
alias(alias, name) {
|
|
122
|
+
assert(alias, 'alias command name is required');
|
|
123
|
+
assert(this[COMMANDS].has(name), `${name} should be added first`);
|
|
124
|
+
debug('[%s] set `%s` as alias of `%s`', this.constructor.name, alias, name);
|
|
125
|
+
this[COMMANDS].set(alias, this[COMMANDS].get(name));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* start point of bin process
|
|
130
|
+
*/
|
|
131
|
+
start() {
|
|
132
|
+
co(function* () {
|
|
133
|
+
// replace `--get-yargs-completions` to our KEY, so yargs will not block our DISPATCH
|
|
134
|
+
const index = this.rawArgv.indexOf('--get-yargs-completions');
|
|
135
|
+
if (index !== -1) {
|
|
136
|
+
// bash will request as `--get-yargs-completions my-git remote add`, so need to remove 2
|
|
137
|
+
this.rawArgv.splice(index, 2, `--AUTO_COMPLETIONS=${this.rawArgv.join(',')}`);
|
|
138
|
+
}
|
|
139
|
+
yield this[DISPATCH]();
|
|
140
|
+
}.bind(this)).catch(this.errorHandler.bind(this));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* default error hander
|
|
145
|
+
* @param {Error} err - error object
|
|
146
|
+
* @protected
|
|
147
|
+
*/
|
|
148
|
+
errorHandler(err) {
|
|
149
|
+
console.error(chalk.red(`⚠️ ${err.name}: ${err.message}`));
|
|
150
|
+
console.error(chalk.red('⚠️ Command Error, enable `DEBUG=common-bin` for detail'));
|
|
151
|
+
debug('args %s', process.argv.slice(3));
|
|
152
|
+
debug(err.stack);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* print help message to console
|
|
158
|
+
* @param {String} [level=log] - console level
|
|
159
|
+
*/
|
|
160
|
+
showHelp(level = 'log') {
|
|
161
|
+
this.yargs.showHelp(level);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* shortcut for yargs.options
|
|
166
|
+
* @param {Object} opt - an object set to `yargs.options`
|
|
167
|
+
*/
|
|
168
|
+
set options(opt) {
|
|
169
|
+
this.yargs.options(opt);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* shortcut for yargs.usage
|
|
174
|
+
* @param {String} usage - usage info
|
|
175
|
+
*/
|
|
176
|
+
set usage(usage) {
|
|
177
|
+
this.yargs.usage(usage);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
set version(ver) {
|
|
181
|
+
this[VERSION] = ver;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get version() {
|
|
185
|
+
return this[VERSION];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* instantiaze sub command
|
|
190
|
+
* @param {CommonBin} Clz - sub command class
|
|
191
|
+
* @param {Array} args - args
|
|
192
|
+
* @return {CommonBin} sub command instance
|
|
193
|
+
*/
|
|
194
|
+
getSubCommandInstance(Clz, ...args) {
|
|
195
|
+
return new Clz(...args);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* dispatch command, either `subCommand.exec` or `this.run`
|
|
200
|
+
* @param {Object} context - context object
|
|
201
|
+
* @param {String} context.cwd - process.cwd()
|
|
202
|
+
* @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
|
|
203
|
+
* @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
|
|
204
|
+
* @private
|
|
205
|
+
*/
|
|
206
|
+
* [DISPATCH]() {
|
|
207
|
+
// define --help and --version by default
|
|
208
|
+
this.yargs
|
|
209
|
+
// .reset()
|
|
210
|
+
.completion()
|
|
211
|
+
.help()
|
|
212
|
+
.version()
|
|
213
|
+
.wrap(120)
|
|
214
|
+
.alias('h', 'help')
|
|
215
|
+
.alias('v', 'version')
|
|
216
|
+
.group([ 'help', 'version' ], 'Global Options:');
|
|
217
|
+
|
|
218
|
+
// get parsed argument without handling helper and version
|
|
219
|
+
const parsed = yield this[PARSE](this.rawArgv);
|
|
220
|
+
const commandName = parsed._[0];
|
|
221
|
+
|
|
222
|
+
if (parsed.version && this.version) {
|
|
223
|
+
console.log(this.version);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// if sub command exist
|
|
228
|
+
if (this[COMMANDS].has(commandName)) {
|
|
229
|
+
const Command = this[COMMANDS].get(commandName);
|
|
230
|
+
const rawArgv = this.rawArgv.slice();
|
|
231
|
+
rawArgv.splice(rawArgv.indexOf(commandName), 1);
|
|
232
|
+
|
|
233
|
+
debug('[%s] dispatch to subcommand `%s` -> `%s` with %j', this.constructor.name, commandName, Command.name, rawArgv);
|
|
234
|
+
const command = this.getSubCommandInstance(Command, rawArgv);
|
|
235
|
+
yield command[DISPATCH]();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// register command for printing
|
|
240
|
+
for (const [ name, Command ] of this[COMMANDS].entries()) {
|
|
241
|
+
this.yargs.command(name, Command.prototype.description || '');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
debug('[%s] exec run command', this.constructor.name);
|
|
245
|
+
const context = this.context;
|
|
246
|
+
|
|
247
|
+
// print completion for bash
|
|
248
|
+
if (context.argv.AUTO_COMPLETIONS) {
|
|
249
|
+
// slice to remove `--AUTO_COMPLETIONS=` which we append
|
|
250
|
+
this.yargs.getCompletion(this.rawArgv.slice(1), completions => {
|
|
251
|
+
// console.log('%s', completions)
|
|
252
|
+
completions.forEach(x => console.log(x));
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
// handle by self
|
|
256
|
+
yield this.helper.callFn(this.run, [ context ], this);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* getter of context, default behavior is remove `help` / `h` / `version`
|
|
262
|
+
* @return {Object} context - { cwd, env, argv, rawArgv }
|
|
263
|
+
* @protected
|
|
264
|
+
*/
|
|
265
|
+
get context() {
|
|
266
|
+
const argv = this.yargs.argv;
|
|
267
|
+
const context = {
|
|
268
|
+
argv,
|
|
269
|
+
cwd: process.cwd(),
|
|
270
|
+
env: Object.assign({}, process.env),
|
|
271
|
+
rawArgv: this.rawArgv,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
argv.help = undefined;
|
|
275
|
+
argv.h = undefined;
|
|
276
|
+
argv.version = undefined;
|
|
277
|
+
argv.v = undefined;
|
|
278
|
+
|
|
279
|
+
// remove alias result
|
|
280
|
+
if (this.parserOptions.removeAlias) {
|
|
281
|
+
const aliases = this.yargs.getOptions().alias;
|
|
282
|
+
for (const key of Object.keys(aliases)) {
|
|
283
|
+
aliases[key].forEach(item => {
|
|
284
|
+
argv[item] = undefined;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// remove camel case result
|
|
290
|
+
if (this.parserOptions.removeCamelCase) {
|
|
291
|
+
for (const key of Object.keys(argv)) {
|
|
292
|
+
if (key.includes('-')) {
|
|
293
|
+
argv[changeCase.camel(key)] = undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// extract execArgv
|
|
299
|
+
if (this.parserOptions.execArgv) {
|
|
300
|
+
// extract from command argv
|
|
301
|
+
let { debugPort, debugOptions, execArgvObj } = this.helper.extractExecArgv(argv);
|
|
302
|
+
|
|
303
|
+
// extract from WebStorm env `$NODE_DEBUG_OPTION`
|
|
304
|
+
// Notice: WebStorm 2019 won't export the env, instead, use `env.NODE_OPTIONS="--require="`, but we can't extract it.
|
|
305
|
+
if (context.env.NODE_DEBUG_OPTION) {
|
|
306
|
+
console.log('Use $NODE_DEBUG_OPTION: %s', context.env.NODE_DEBUG_OPTION);
|
|
307
|
+
const argvFromEnv = parser(context.env.NODE_DEBUG_OPTION);
|
|
308
|
+
const obj = this.helper.extractExecArgv(argvFromEnv);
|
|
309
|
+
debugPort = obj.debugPort || debugPort;
|
|
310
|
+
Object.assign(debugOptions, obj.debugOptions);
|
|
311
|
+
Object.assign(execArgvObj, obj.execArgvObj);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// `--expose_debug_as` is not supported by 7.x+
|
|
315
|
+
if (execArgvObj.expose_debug_as && semver.gte(process.version, '7.0.0')) {
|
|
316
|
+
console.warn(chalk.yellow(`Node.js runtime is ${process.version}, and inspector protocol is not support --expose_debug_as`));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// remove from origin argv
|
|
320
|
+
for (const key of Object.keys(execArgvObj)) {
|
|
321
|
+
argv[key] = undefined;
|
|
322
|
+
argv[changeCase.camel(key)] = undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// exports execArgv
|
|
326
|
+
const self = this;
|
|
327
|
+
context.execArgvObj = execArgvObj;
|
|
328
|
+
|
|
329
|
+
// convert execArgvObj to execArgv
|
|
330
|
+
// `--require` should be `--require abc --require 123`, not allow `=`
|
|
331
|
+
// `--debug` should be `--debug=9999`, only allow `=`
|
|
332
|
+
Object.defineProperty(context, 'execArgv', {
|
|
333
|
+
get() {
|
|
334
|
+
const lazyExecArgvObj = context.execArgvObj;
|
|
335
|
+
const execArgv = self.helper.unparseArgv(lazyExecArgvObj, { excludes: [ 'require' ] });
|
|
336
|
+
// convert require to execArgv
|
|
337
|
+
let requireArgv = lazyExecArgvObj.require;
|
|
338
|
+
if (requireArgv) {
|
|
339
|
+
if (!Array.isArray(requireArgv)) requireArgv = [ requireArgv ];
|
|
340
|
+
requireArgv.forEach(item => {
|
|
341
|
+
execArgv.push('--require');
|
|
342
|
+
execArgv.push(item.startsWith('./') || item.startsWith('.\\') ? path.resolve(context.cwd, item) : item);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return execArgv;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// only exports debugPort when any match
|
|
350
|
+
if (Object.keys(debugOptions).length) {
|
|
351
|
+
context.debugPort = debugPort;
|
|
352
|
+
context.debugOptions = debugOptions;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return context;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[PARSE](rawArgv) {
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
this.yargs.parse(rawArgv, (err, argv) => {
|
|
362
|
+
/* istanbul ignore next */
|
|
363
|
+
if (err) return reject(err);
|
|
364
|
+
resolve(argv);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = CommonBin;
|
package/lib/helper.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('common-bin');
|
|
4
|
+
const cp = require('child_process');
|
|
5
|
+
const is = require('is-type-of');
|
|
6
|
+
const unparse = require('dargs');
|
|
7
|
+
|
|
8
|
+
// only hook once and only when ever start any child.
|
|
9
|
+
const childs = new Set();
|
|
10
|
+
let hadHook = false;
|
|
11
|
+
function gracefull(proc) {
|
|
12
|
+
// save child ref
|
|
13
|
+
childs.add(proc);
|
|
14
|
+
|
|
15
|
+
// only hook once
|
|
16
|
+
/* istanbul ignore else */
|
|
17
|
+
if (!hadHook) {
|
|
18
|
+
hadHook = true;
|
|
19
|
+
let signal;
|
|
20
|
+
[ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
|
|
21
|
+
process.once(event, () => {
|
|
22
|
+
signal = event;
|
|
23
|
+
process.exit(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
process.once('exit', () => {
|
|
28
|
+
// had test at my-helper.test.js, but coffee can't collect coverage info.
|
|
29
|
+
for (const child of childs) {
|
|
30
|
+
debug('kill child %s with %s', child.pid, signal);
|
|
31
|
+
child.kill(signal);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* fork child process, wrap with promise and gracefull exit
|
|
39
|
+
* @function helper#forkNode
|
|
40
|
+
* @param {String} modulePath - bin path
|
|
41
|
+
* @param {Array} [args] - arguments
|
|
42
|
+
* @param {Object} [options] - options
|
|
43
|
+
* @return {Promise} err or undefined
|
|
44
|
+
* @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
|
|
45
|
+
*/
|
|
46
|
+
exports.forkNode = (modulePath, args = [], options = {}) => {
|
|
47
|
+
options.stdio = options.stdio || 'inherit';
|
|
48
|
+
debug('Run fork `%s %s %s`', process.execPath, modulePath, args.join(' '));
|
|
49
|
+
const proc = cp.fork(modulePath, args, options);
|
|
50
|
+
gracefull(proc);
|
|
51
|
+
|
|
52
|
+
const promise = new Promise((resolve, reject) => {
|
|
53
|
+
proc.once('exit', code => {
|
|
54
|
+
childs.delete(proc);
|
|
55
|
+
if (code !== 0) {
|
|
56
|
+
const err = new Error(modulePath + ' ' + args + ' exit with code ' + code);
|
|
57
|
+
err.code = code;
|
|
58
|
+
reject(err);
|
|
59
|
+
} else {
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
promise.proc = proc;
|
|
66
|
+
|
|
67
|
+
return promise;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* spawn a new process, wrap with promise and gracefull exit
|
|
72
|
+
* @function helper#forkNode
|
|
73
|
+
* @param {String} cmd - command
|
|
74
|
+
* @param {Array} [args] - arguments
|
|
75
|
+
* @param {Object} [options] - options
|
|
76
|
+
* @return {Promise} err or undefined
|
|
77
|
+
* @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
|
|
78
|
+
*/
|
|
79
|
+
exports.spawn = (cmd, args = [], options = {}) => {
|
|
80
|
+
options.stdio = options.stdio || 'inherit';
|
|
81
|
+
debug('Run spawn `%s %s`', cmd, args.join(' '));
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const proc = cp.spawn(cmd, args, options);
|
|
85
|
+
gracefull(proc);
|
|
86
|
+
proc.once('error', err => {
|
|
87
|
+
/* istanbul ignore next */
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
proc.once('exit', code => {
|
|
91
|
+
childs.delete(proc);
|
|
92
|
+
|
|
93
|
+
if (code !== 0) {
|
|
94
|
+
return reject(new Error(`spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`));
|
|
95
|
+
}
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* exec npm install
|
|
103
|
+
* @function helper#npmInstall
|
|
104
|
+
* @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall`
|
|
105
|
+
* @param {String} name - node module name
|
|
106
|
+
* @param {String} cwd - target directory
|
|
107
|
+
* @return {Promise} err or undefined
|
|
108
|
+
*/
|
|
109
|
+
exports.npmInstall = (npmCli, name, cwd) => {
|
|
110
|
+
const options = {
|
|
111
|
+
stdio: 'inherit',
|
|
112
|
+
env: process.env,
|
|
113
|
+
cwd,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const args = [ 'i', name ];
|
|
117
|
+
console.log('[common-bin] `%s %s` to %s ...', npmCli, args.join(' '), options.cwd);
|
|
118
|
+
|
|
119
|
+
return exports.spawn(npmCli, args, options);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* call fn
|
|
124
|
+
* @function helper#callFn
|
|
125
|
+
* @param {Function} fn - support generator / async / normal function return promise
|
|
126
|
+
* @param {Array} [args] - fn args
|
|
127
|
+
* @param {Object} [thisArg] - this
|
|
128
|
+
* @return {Object} result
|
|
129
|
+
*/
|
|
130
|
+
exports.callFn = function* (fn, args = [], thisArg) {
|
|
131
|
+
if (!is.function(fn)) return;
|
|
132
|
+
if (is.generatorFunction(fn)) {
|
|
133
|
+
return yield fn.apply(thisArg, args);
|
|
134
|
+
}
|
|
135
|
+
const r = fn.apply(thisArg, args);
|
|
136
|
+
if (is.promise(r)) {
|
|
137
|
+
return yield r;
|
|
138
|
+
}
|
|
139
|
+
return r;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* unparse argv and change it to array style
|
|
144
|
+
* @function helper#unparseArgv
|
|
145
|
+
* @param {Object} argv - yargs style
|
|
146
|
+
* @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs
|
|
147
|
+
* @param {Array} [options.includes] - keys or regex of keys to include
|
|
148
|
+
* @param {Array} [options.excludes] - keys or regex of keys to exclude
|
|
149
|
+
* @return {Array} [ '--debug=7000', '--debug-brk' ]
|
|
150
|
+
*/
|
|
151
|
+
exports.unparseArgv = (argv, options = {}) => {
|
|
152
|
+
// revert argv object to array
|
|
153
|
+
// yargs will paser `debug-brk` to `debug-brk` and `debugBrk`, so we need to filter
|
|
154
|
+
return [ ...new Set(unparse(argv, options)) ];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* extract execArgv from argv
|
|
159
|
+
* @function helper#extractExecArgv
|
|
160
|
+
* @param {Object} argv - yargs style
|
|
161
|
+
* @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} }
|
|
162
|
+
*/
|
|
163
|
+
exports.extractExecArgv = argv => {
|
|
164
|
+
const debugOptions = {};
|
|
165
|
+
const execArgvObj = {};
|
|
166
|
+
let debugPort;
|
|
167
|
+
|
|
168
|
+
for (const key of Object.keys(argv)) {
|
|
169
|
+
const value = argv[key];
|
|
170
|
+
// skip undefined set uppon (camel etc.)
|
|
171
|
+
if (value === undefined) continue;
|
|
172
|
+
// debug / debug-brk / debug-port / inspect / inspect-brk / inspect-port
|
|
173
|
+
if ([ 'debug', 'debug-brk', 'debug-port', 'inspect', 'inspect-brk', 'inspect-port' ].includes(key)) {
|
|
174
|
+
if (typeof value === 'number') debugPort = value;
|
|
175
|
+
debugOptions[key] = argv[key];
|
|
176
|
+
execArgvObj[key] = argv[key];
|
|
177
|
+
} else if (match(key, [ 'es_staging', 'expose_debug_as', /^harmony.*/ ])) {
|
|
178
|
+
execArgvObj[key] = argv[key];
|
|
179
|
+
} else if (key.startsWith('node-options--')) {
|
|
180
|
+
// support node options, like: commond --node-options--trace-warnings => execArgv.push('--trace-warnings')
|
|
181
|
+
execArgvObj[key.replace('node-options--', '')] = argv[key];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { debugPort, debugOptions, execArgvObj };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function match(key, arr) {
|
|
188
|
+
return arr.some(x => x instanceof RegExp ? x.test(key) : x === key); // eslint-disable-line no-confusing-arrow
|
|
189
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhennann/common-bin",
|
|
3
|
+
"version": "2.9.2",
|
|
4
|
+
"description": "Abstraction bin tool",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@types/dargs": "^5.1.0",
|
|
8
|
+
"@types/node": "^10.12.18",
|
|
9
|
+
"@types/yargs": "^12.0.4",
|
|
10
|
+
"chalk": "^2.4.1",
|
|
11
|
+
"change-case": "^3.0.2",
|
|
12
|
+
"co": "^4.6.0",
|
|
13
|
+
"dargs": "^6.0.0",
|
|
14
|
+
"debug": "^4.1.0",
|
|
15
|
+
"is-type-of": "^1.2.1",
|
|
16
|
+
"semver": "^5.5.1",
|
|
17
|
+
"yargs": "^13.3.0",
|
|
18
|
+
"yargs-parser": "^13.1.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"autod": "^3.0.1",
|
|
22
|
+
"coffee": "^5.1.0",
|
|
23
|
+
"egg-bin": "^4.17.0",
|
|
24
|
+
"egg-ci": "^1.19.0",
|
|
25
|
+
"eslint": "^5.6.1",
|
|
26
|
+
"eslint-config-egg": "^7.1.0",
|
|
27
|
+
"git-contributor": "^1.0.10",
|
|
28
|
+
"mm": "^2.4.1",
|
|
29
|
+
"rimraf": "^2.6.2",
|
|
30
|
+
"typescript": "^3.2.2"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/node-modules/common-bin.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/node-modules/common-bin",
|
|
37
|
+
"author": "fengmk2 <fengmk2@gmail.com> (https://github.com/fengmk2)",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"contributor": "git-contributor",
|
|
41
|
+
"autod": "autod",
|
|
42
|
+
"clean": "rimraf coverage",
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"test": "npm run lint -- --fix && npm run test-local",
|
|
45
|
+
"test-local": "egg-bin test",
|
|
46
|
+
"cov": "egg-bin cov",
|
|
47
|
+
"ci": "npm run clean && npm run lint && egg-bin cov"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">= 6.0.0"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"lib",
|
|
54
|
+
"index.d.ts",
|
|
55
|
+
"index.js"
|
|
56
|
+
],
|
|
57
|
+
"types": "index.d.ts",
|
|
58
|
+
"ci": {
|
|
59
|
+
"version": "8, 10, 12, 14, 16",
|
|
60
|
+
"type": "github",
|
|
61
|
+
"license": {
|
|
62
|
+
"year": "2017",
|
|
63
|
+
"fullname": "node-modules and other contributors"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|