jsir 2.1.5 → 2.1.6
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/README.md +49 -0
- package/cmd/oaa.js +35 -51
- package/deps/evalCode.js +1 -2
- package/deps/example.js +25 -0
- package/deps/util.js +13 -12
- package/index.js +0 -4
- package/package.json +8 -3
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# jsir: JavaScript Script Management Tool
|
|
2
|
+
|
|
3
|
+
**jsir** is a command line tool designed to help manage and organize your JavaScript files. With **jsir**, you can edit, run, and compare JavaScript files, view file dependencies, and switch between different workspaces.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can install **jsir** globally using npm:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install -g jsir
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
To start using **jsir**, simply run the command:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
jsir
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This will open the **jsir** REPL interface where you can start managing your files.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
Here is a list of available commands in **jsir**:
|
|
26
|
+
|
|
27
|
+
| Keyword | Short | Comment |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| .add | .a | Add file |
|
|
30
|
+
| .clear | .C | Clear task |
|
|
31
|
+
| .compare | .c | Compare files |
|
|
32
|
+
| .deps | .d | View file dependencies |
|
|
33
|
+
| .edit | .e | Edit file |
|
|
34
|
+
| .file | .f | Working file mode |
|
|
35
|
+
| .get | .g | Get file |
|
|
36
|
+
| .help | .h | Help documentation |
|
|
37
|
+
| .list | .l | View file list |
|
|
38
|
+
| .quit | .q | Exit |
|
|
39
|
+
| .rename | .n | Rename file |
|
|
40
|
+
| .repl | .p | Interactive interface |
|
|
41
|
+
| .rm | .R | Remove file |
|
|
42
|
+
| .run | .r | Quick execution |
|
|
43
|
+
| .see | .s | View file |
|
|
44
|
+
| .switch | .S | Switch workspace |
|
|
45
|
+
| .uninstall | .u | Uninstall workspace |
|
|
46
|
+
| .version | .v | View version |
|
|
47
|
+
| .workspace | .w | Workspace Management |
|
|
48
|
+
|
|
49
|
+
For detailed information on how to use each command, use the `.help` command followed by the short keyword of the command.
|
package/cmd/oaa.js
CHANGED
|
@@ -3,10 +3,10 @@ const $lib = require('../deps/util');
|
|
|
3
3
|
const {
|
|
4
4
|
getLibDataDir, trim, regEach, getConfig, mkdir, reget,
|
|
5
5
|
getCbText, e, sleep, objDataFile, vl, md5, BigNumber,
|
|
6
|
-
|
|
6
|
+
arrayDataFile, infoStr, warnStr, errorStack,
|
|
7
7
|
getInfo, ei, pad, msgStr, getType,
|
|
8
8
|
errorTag, isArgsMatch, draftQuery, setConfig,
|
|
9
|
-
$log, $draft, getTextComments, getOr, importG
|
|
9
|
+
$log, $draft, getTextComments, getOr, importG, requireG
|
|
10
10
|
} = $lib;
|
|
11
11
|
const _args = process.argv.slice(2).map(trim);
|
|
12
12
|
const evalCode = require('../deps/evalCode')
|
|
@@ -15,8 +15,8 @@ const _chokidar = require('chokidar');
|
|
|
15
15
|
const setting = require('../deps/setting')
|
|
16
16
|
const _fs = require('fs')
|
|
17
17
|
const readline = require("readline");
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const packageJson = require("../package.json");
|
|
19
|
+
const example = require("../deps/example");
|
|
20
20
|
const _workspaceConfigFile = 'workspace.json';
|
|
21
21
|
const _libDataDir = getLibDataDir()
|
|
22
22
|
const _tipsOnRm = {}
|
|
@@ -30,6 +30,10 @@ const $config = {
|
|
|
30
30
|
get: getConfig,
|
|
31
31
|
set: setConfig
|
|
32
32
|
}
|
|
33
|
+
const CONSOLE = Object.assign({}, console);
|
|
34
|
+
const info = (msg) => $lib.info(msg, CONSOLE);
|
|
35
|
+
const warn = (msg) => $lib.warn(msg, CONSOLE);
|
|
36
|
+
const error = (msg, tag) => $lib.error(msg, tag, CONSOLE);
|
|
33
37
|
|
|
34
38
|
let _cmdMapFile = setting.name + 'CmdMap.json'
|
|
35
39
|
let _cmdMap = {}
|
|
@@ -40,14 +44,14 @@ let _noAppendNextLine = true
|
|
|
40
44
|
|
|
41
45
|
const _onLazyGetMap = {}
|
|
42
46
|
const _data = {}
|
|
43
|
-
const
|
|
47
|
+
const _lazyTime = 5 * 60 * 1000
|
|
44
48
|
const _dealOnLazyGet = (key) => {
|
|
45
49
|
if (_noAppendNextLine || !_onLazyGetMap[key]) {
|
|
46
50
|
return _data[key]
|
|
47
51
|
}
|
|
48
52
|
let item = _onLazyGetMap[key]
|
|
49
53
|
if (Date.now() <= item.startTime) {
|
|
50
|
-
item.startTime = Date.now() +
|
|
54
|
+
item.startTime = Date.now() + _lazyTime
|
|
51
55
|
return _data[key]
|
|
52
56
|
}
|
|
53
57
|
let flag = _onLazyGetMap[key].fn()
|
|
@@ -55,14 +59,14 @@ const _dealOnLazyGet = (key) => {
|
|
|
55
59
|
return new Promise(async (resolve, reject) => {
|
|
56
60
|
try {
|
|
57
61
|
await flag
|
|
58
|
-
item.startTime = Date.now() +
|
|
62
|
+
item.startTime = Date.now() + _lazyTime
|
|
59
63
|
resolve(_data[key])
|
|
60
64
|
} catch (e) {
|
|
61
65
|
reject(e)
|
|
62
66
|
}
|
|
63
67
|
})
|
|
64
68
|
} else {
|
|
65
|
-
item.startTime = Date.now() +
|
|
69
|
+
item.startTime = Date.now() + _lazyTime
|
|
66
70
|
return _data[key]
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -70,7 +74,7 @@ const _dataSet = (key, val, onLazyGet) => {
|
|
|
70
74
|
if (onLazyGet) {
|
|
71
75
|
_onLazyGetMap[key] = {
|
|
72
76
|
fn: onLazyGet,
|
|
73
|
-
startTime: Date.now() +
|
|
77
|
+
startTime: Date.now() + _lazyTime
|
|
74
78
|
}
|
|
75
79
|
}
|
|
76
80
|
return _data[key] = val
|
|
@@ -209,7 +213,7 @@ async function watchFile(uniqueName) {
|
|
|
209
213
|
text = newText;
|
|
210
214
|
if (trim(exeStr)) {
|
|
211
215
|
try {
|
|
212
|
-
|
|
216
|
+
CONSOLE.log("\n" + infoStr("------ workFile run ------"))
|
|
213
217
|
await wrapperInput("# " + exeStr)
|
|
214
218
|
} catch (e) {
|
|
215
219
|
error(e)
|
|
@@ -422,30 +426,9 @@ async function save(args) {
|
|
|
422
426
|
}
|
|
423
427
|
let resp;
|
|
424
428
|
if (args[0] === 'e') {
|
|
425
|
-
resp =
|
|
426
|
-
A test exe script
|
|
427
|
-
*/
|
|
428
|
-
|
|
429
|
-
$defArgs({
|
|
430
|
-
arg: 'required argument',
|
|
431
|
-
_arg: 'optional argument'
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
let {} = $lib // Reference to built-in resources
|
|
435
|
-
|
|
436
|
-
console.log($args)
|
|
437
|
-
|
|
438
|
-
return {}
|
|
439
|
-
`;
|
|
429
|
+
resp = example.exeFile;
|
|
440
430
|
} else if (args[0] === 'i') {
|
|
441
|
-
resp =
|
|
442
|
-
A test init script
|
|
443
|
-
*/
|
|
444
|
-
|
|
445
|
-
let {} = $lib // Reference to built-in resources
|
|
446
|
-
|
|
447
|
-
return {}
|
|
448
|
-
`;
|
|
431
|
+
resp = example.initFile;
|
|
449
432
|
} else {
|
|
450
433
|
resp = await getCbText();
|
|
451
434
|
}
|
|
@@ -534,7 +517,7 @@ function listCmd(tmpMap) {
|
|
|
534
517
|
item.name = pair[0] + '/' + item.name
|
|
535
518
|
}
|
|
536
519
|
})
|
|
537
|
-
|
|
520
|
+
CONSOLE.table(items)
|
|
538
521
|
}
|
|
539
522
|
}
|
|
540
523
|
|
|
@@ -576,7 +559,7 @@ async function _wrapperInput(str) {
|
|
|
576
559
|
} else {
|
|
577
560
|
let fLine = trim(str.substr(1))
|
|
578
561
|
if (fLine) {
|
|
579
|
-
|
|
562
|
+
CONSOLE.log(draftQuery(fLine).join("\n"))
|
|
580
563
|
} else {
|
|
581
564
|
let text = str.substr(1) + "\n" + await nextText(line => line, fstr)
|
|
582
565
|
$draft(text)
|
|
@@ -587,14 +570,14 @@ async function _wrapperInput(str) {
|
|
|
587
570
|
let isStar = str.startsWith('*')
|
|
588
571
|
let text = trim(str.replace(/^[$#*]/, ''))
|
|
589
572
|
if (is$) {
|
|
590
|
-
|
|
573
|
+
CONSOLE.log(await evalText('return ' + text))
|
|
591
574
|
} else if (isStar) {
|
|
592
575
|
if (!text) {
|
|
593
576
|
text = '$context';
|
|
594
577
|
}
|
|
595
578
|
let result = await evalText('return ' + text)
|
|
596
579
|
if (!result || typeof result === 'string' || Object.keys(result).length === 0) {
|
|
597
|
-
|
|
580
|
+
CONSOLE.nable([getInfo(result)])
|
|
598
581
|
return
|
|
599
582
|
}
|
|
600
583
|
let rows = []
|
|
@@ -604,7 +587,7 @@ async function _wrapperInput(str) {
|
|
|
604
587
|
if (rows.length === 0) {
|
|
605
588
|
warn("no items")
|
|
606
589
|
} else {
|
|
607
|
-
|
|
590
|
+
CONSOLE.nable(rows)
|
|
608
591
|
}
|
|
609
592
|
} else {
|
|
610
593
|
await evalText(text)
|
|
@@ -624,7 +607,7 @@ async function _wrapperInput(str) {
|
|
|
624
607
|
if (firstName === 'f') {
|
|
625
608
|
await workFile(uniqueName)
|
|
626
609
|
} else if (firstName !== 'e') {
|
|
627
|
-
|
|
610
|
+
CONSOLE.log(String(_fs.readFileSync(path)));
|
|
628
611
|
} else {
|
|
629
612
|
await runCmd(str)
|
|
630
613
|
}
|
|
@@ -643,7 +626,7 @@ function arrayToCmdMap(cmds) {
|
|
|
643
626
|
}
|
|
644
627
|
|
|
645
628
|
function help() {
|
|
646
|
-
|
|
629
|
+
CONSOLE.nable(Object.keys(keywordDef).map(key => {
|
|
647
630
|
let item = keywordDef[key];
|
|
648
631
|
return {
|
|
649
632
|
keyword: ' ' + warnStr(`.${key}`),
|
|
@@ -759,7 +742,7 @@ const keywordDef = {
|
|
|
759
742
|
version: {
|
|
760
743
|
comment: 'View version',
|
|
761
744
|
exeFn: (args) => {
|
|
762
|
-
|
|
745
|
+
CONSOLE.log(packageJson.version)
|
|
763
746
|
},
|
|
764
747
|
short: 'v'
|
|
765
748
|
},
|
|
@@ -795,7 +778,7 @@ const keywordDef = {
|
|
|
795
778
|
} else {
|
|
796
779
|
let path = getFullPath(uniqueName)
|
|
797
780
|
let sourceStr = String(_fs.readFileSync(path))
|
|
798
|
-
|
|
781
|
+
CONSOLE.log(sourceStr)
|
|
799
782
|
}
|
|
800
783
|
},
|
|
801
784
|
args: {
|
|
@@ -888,7 +871,7 @@ const keywordDef = {
|
|
|
888
871
|
let bSpace = args[1]
|
|
889
872
|
|
|
890
873
|
if (aUniqueName && bUniqueName) {
|
|
891
|
-
await e(
|
|
874
|
+
await e(`${getConfig("compareExe", "idea diff")} "${getFullPath(aUniqueName)}" "${getFullPath(bUniqueName)}"`)
|
|
892
875
|
} else if (aSpace && bSpace) {
|
|
893
876
|
if (!args[2]) {
|
|
894
877
|
warn('require mode')
|
|
@@ -984,7 +967,7 @@ const keywordDef = {
|
|
|
984
967
|
path
|
|
985
968
|
}
|
|
986
969
|
})
|
|
987
|
-
|
|
970
|
+
CONSOLE.table(items)
|
|
988
971
|
},
|
|
989
972
|
args: {
|
|
990
973
|
workspacePath: 'New workspace path'
|
|
@@ -1002,7 +985,7 @@ const keywordDef = {
|
|
|
1002
985
|
path
|
|
1003
986
|
}
|
|
1004
987
|
})
|
|
1005
|
-
|
|
988
|
+
CONSOLE.table(items)
|
|
1006
989
|
let index = await nextLine(line => line, "index: ")
|
|
1007
990
|
let workspace = workspaces[index - 1]
|
|
1008
991
|
if (workspace) {
|
|
@@ -1027,7 +1010,7 @@ const keywordDef = {
|
|
|
1027
1010
|
path
|
|
1028
1011
|
}
|
|
1029
1012
|
})
|
|
1030
|
-
|
|
1013
|
+
CONSOLE.table(items)
|
|
1031
1014
|
let index = trim(await nextLine(line => line, "switch workspace: "));
|
|
1032
1015
|
if (index && workspaces[index - 1]) {
|
|
1033
1016
|
let workspace = workspaces[index - 1];
|
|
@@ -1066,7 +1049,9 @@ const keywordDef = {
|
|
|
1066
1049
|
}
|
|
1067
1050
|
}
|
|
1068
1051
|
if (cmds.length === 1) {
|
|
1069
|
-
|
|
1052
|
+
if (_noAppendNextLine) {
|
|
1053
|
+
listCmd(arrayToCmdMap(cmds))
|
|
1054
|
+
}
|
|
1070
1055
|
await runCmd(args.slice(1).join(' '), cmds[0])
|
|
1071
1056
|
} else {
|
|
1072
1057
|
warn(cmds.length > 1 ? "multiple match" : "no match")
|
|
@@ -1081,7 +1066,7 @@ const keywordDef = {
|
|
|
1081
1066
|
comment: 'Exit',
|
|
1082
1067
|
exeFn: (args) => {
|
|
1083
1068
|
delTips();
|
|
1084
|
-
|
|
1069
|
+
CONSOLE.log(infoStr("Bye!"))
|
|
1085
1070
|
process.exit(0)
|
|
1086
1071
|
},
|
|
1087
1072
|
short: 'q'
|
|
@@ -1091,7 +1076,7 @@ const keywordDef = {
|
|
|
1091
1076
|
exeFn: (args) => {
|
|
1092
1077
|
_noAppendNextLine = false
|
|
1093
1078
|
_cmdMap = {}
|
|
1094
|
-
|
|
1079
|
+
CONSOLE.log(warnStr("You can start with .help, use * to expand context"))
|
|
1095
1080
|
},
|
|
1096
1081
|
short: 'p'
|
|
1097
1082
|
}
|
|
@@ -1158,7 +1143,7 @@ async function _dealKeyword(keyword, args) {
|
|
|
1158
1143
|
}
|
|
1159
1144
|
}
|
|
1160
1145
|
if (unMatched) {
|
|
1161
|
-
|
|
1146
|
+
await keywordDef.run.exeFn([keyword, ...args])
|
|
1162
1147
|
}
|
|
1163
1148
|
}
|
|
1164
1149
|
|
|
@@ -1714,7 +1699,6 @@ async function evalText($text = '', $cmdName = '', $args = []) {
|
|
|
1714
1699
|
setTips, delTips,
|
|
1715
1700
|
wrapperInput, filterCmd,
|
|
1716
1701
|
currSpace,
|
|
1717
|
-
$log, $draft,
|
|
1718
1702
|
$homeDir, $lib)
|
|
1719
1703
|
} catch(e) {
|
|
1720
1704
|
throw errorTag(e, $cmdName);
|
package/deps/evalCode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {info: $info, msg: $msg, warn: $warn, error: $error, importG: $import} = require("./util");
|
|
1
|
+
const {info: $info, msg: $msg, warn: $warn, error: $error, importG: $import, $log, $draft} = require("./util");
|
|
2
2
|
require = require("./util").requireG
|
|
3
3
|
module.exports = async ($text = '', $cmdName = '', $args = [],
|
|
4
4
|
$data, $config,
|
|
@@ -7,7 +7,6 @@ module.exports = async ($text = '', $cmdName = '', $args = [],
|
|
|
7
7
|
$setTips, $delTips,
|
|
8
8
|
$enter, $filterCmd,
|
|
9
9
|
$currentSpace,
|
|
10
|
-
$log, $draft,
|
|
11
10
|
$homeDir, $lib) => {
|
|
12
11
|
const $defArgs = () => $args;
|
|
13
12
|
const $context = {
|
package/deps/example.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
exeFile: `/*
|
|
3
|
+
A test exe script
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
$defArgs({
|
|
7
|
+
arg: 'required argument',
|
|
8
|
+
_arg: 'optional argument'
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
let {} = $lib // Reference to built-in resources
|
|
12
|
+
|
|
13
|
+
console.log($args)
|
|
14
|
+
|
|
15
|
+
return {}
|
|
16
|
+
`,
|
|
17
|
+
initFile: `/*
|
|
18
|
+
A test init script
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
let {} = $lib // Reference to built-in resources
|
|
22
|
+
|
|
23
|
+
return {}
|
|
24
|
+
`
|
|
25
|
+
}
|
package/deps/util.js
CHANGED
|
@@ -13,6 +13,7 @@ const globalDirectories = require('global-dirs');
|
|
|
13
13
|
const emptyFn = ()=>{}
|
|
14
14
|
const dayJs = require('dayjs')
|
|
15
15
|
const table = require('console.table')
|
|
16
|
+
const {log: _log} = console;
|
|
16
17
|
const _fs = require("fs");
|
|
17
18
|
if (module.paths.indexOf(globalDirectories.npm.packages) === -1) {
|
|
18
19
|
module.paths.push(globalDirectories.npm.packages)
|
|
@@ -126,10 +127,10 @@ function nableStr(rows) {
|
|
|
126
127
|
return tableStr(wrapRows(rows))
|
|
127
128
|
}
|
|
128
129
|
console.table = (...args) => {
|
|
129
|
-
|
|
130
|
+
_log(tableStr(...args))
|
|
130
131
|
}
|
|
131
132
|
console.nable = (rows) => {
|
|
132
|
-
|
|
133
|
+
_log(nableStr(rows))
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
function timeStr(fmt, date) {
|
|
@@ -344,7 +345,7 @@ function requireG(moduleName){
|
|
|
344
345
|
} catch (e) {}
|
|
345
346
|
let path = globalDirectories.npm.packages + '/' + moduleName;
|
|
346
347
|
if (!fs.existsSync(path)) {
|
|
347
|
-
|
|
348
|
+
_log(warnStr(`npm install -g ${moduleName}`))
|
|
348
349
|
throw `${moduleName} not found, use above cmd to install it`;
|
|
349
350
|
}
|
|
350
351
|
return require(path);
|
|
@@ -359,7 +360,7 @@ async function importG(moduleName) {
|
|
|
359
360
|
} catch (e) {}
|
|
360
361
|
let path = globalDirectories.npm.packages + '/' + moduleName;
|
|
361
362
|
if (!fs.existsSync(path)) {
|
|
362
|
-
|
|
363
|
+
_log(warnStr(`npm install -g ${moduleName}`))
|
|
363
364
|
throw `${moduleName} not found, use above cmd to install it`;
|
|
364
365
|
}
|
|
365
366
|
return await import(path);
|
|
@@ -953,32 +954,32 @@ function wrapRows(rows) {
|
|
|
953
954
|
return result
|
|
954
955
|
}
|
|
955
956
|
|
|
956
|
-
function info(msg) {
|
|
957
|
+
function info(msg, _console = console) {
|
|
957
958
|
if (typeof msg === 'string' && msg && msg.indexOf('\n') === -1) {
|
|
958
959
|
msg = infoStr(msg)
|
|
959
960
|
}
|
|
960
|
-
|
|
961
|
+
_console.log(infoStr('[info]'), msg)
|
|
961
962
|
}
|
|
962
963
|
|
|
963
|
-
function msg(msg) {
|
|
964
|
+
function msg(msg, _console = console) {
|
|
964
965
|
if (typeof msg === 'string' && msg && msg.indexOf('\n') === -1) {
|
|
965
966
|
msg = msgStr(msg)
|
|
966
967
|
}
|
|
967
|
-
|
|
968
|
+
_console.log(msgStr('[msg]'), msg)
|
|
968
969
|
}
|
|
969
970
|
|
|
970
|
-
function warn(msg) {
|
|
971
|
+
function warn(msg, _console = console) {
|
|
971
972
|
if (typeof msg === 'string' && msg && msg.indexOf('\n') === -1) {
|
|
972
973
|
msg = warnStr(msg)
|
|
973
974
|
}
|
|
974
|
-
|
|
975
|
+
_console.warn(warnStr('[warn]'), msg)
|
|
975
976
|
}
|
|
976
977
|
|
|
977
|
-
function error(msg, tag) {
|
|
978
|
+
function error(msg, tag, _console = console) {
|
|
978
979
|
if (typeof msg === 'string' && msg && msg.indexOf('\n') === -1) {
|
|
979
980
|
msg = errorStr(msg)
|
|
980
981
|
}
|
|
981
|
-
|
|
982
|
+
_console.error(errorStr(`[${tag || 'error'}]`), msg)
|
|
982
983
|
}
|
|
983
984
|
|
|
984
985
|
function infoStr(str) {
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsir",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.6",
|
|
4
|
+
"description": "JavaScript Script Management Tool",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "cmd/oaa.js"
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"jsir": "cmd/oaa.js"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"javascript",
|
|
14
|
+
"scripts",
|
|
15
|
+
"manager",
|
|
16
|
+
"executor"
|
|
17
|
+
],
|
|
13
18
|
"author": "",
|
|
14
19
|
"license": "ISC",
|
|
15
20
|
"dependencies": {
|