vanilla-jet 1.0.0
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/.grunt/build_styles_task.js +19 -0
- package/.grunt/compile_html.js +212 -0
- package/Gruntfile.js +147 -0
- package/README.md +6 -0
- package/framework/dipper.js +522 -0
- package/framework/request.js +147 -0
- package/framework/response.js +102 -0
- package/framework/router.js +235 -0
- package/framework/server.js +116 -0
- package/index.js +5 -0
- package/package.json +53 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Version 3.5
|
|
3
|
+
Created by: nalancer08 <https://github.com/nalancer08>
|
|
4
|
+
**/
|
|
5
|
+
|
|
6
|
+
const nunjucks = require('nunjucks');
|
|
7
|
+
var _ = require('underscore');
|
|
8
|
+
|
|
9
|
+
function Response(res) {
|
|
10
|
+
|
|
11
|
+
this.res = null;
|
|
12
|
+
this.body = '';
|
|
13
|
+
this.status = 200;
|
|
14
|
+
this.headers = [];
|
|
15
|
+
this.autoRespond = true;
|
|
16
|
+
// Call initialization callback
|
|
17
|
+
this.init(res);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Response.prototype.init = function(res) {
|
|
21
|
+
|
|
22
|
+
var obj = this;
|
|
23
|
+
obj.res = res;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Response.prototype.setBody = function(body) {
|
|
27
|
+
|
|
28
|
+
var obj = this;
|
|
29
|
+
obj.body = body;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Response.prototype.setStatus = function(status) {
|
|
33
|
+
|
|
34
|
+
var obj = this;
|
|
35
|
+
obj.status = status;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Response.prototype.setHeader = function(name, value) {
|
|
39
|
+
|
|
40
|
+
var obj = this;
|
|
41
|
+
obj.headers.push({
|
|
42
|
+
name: name,
|
|
43
|
+
value: value
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Response.prototype.getBody = function() {
|
|
48
|
+
|
|
49
|
+
var obj = this;
|
|
50
|
+
return obj.body;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Response.prototype.getStatus = function() {
|
|
54
|
+
|
|
55
|
+
var obj = this;
|
|
56
|
+
return obj.status;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Response.prototype.getHeader = function(name) {
|
|
60
|
+
|
|
61
|
+
var obj = this,
|
|
62
|
+
ret = null;
|
|
63
|
+
ret = _.find(obj.headers, function(header) {
|
|
64
|
+
return header.name == name;
|
|
65
|
+
});
|
|
66
|
+
return ret;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
Response.prototype.respond = function() {
|
|
70
|
+
|
|
71
|
+
var obj = this;
|
|
72
|
+
this.setHeader('Access-Control-Allow-Origin', '*');
|
|
73
|
+
|
|
74
|
+
_.each(obj.headers, function(header) {
|
|
75
|
+
obj.res.setHeader(header.name, header.value);
|
|
76
|
+
});
|
|
77
|
+
obj.res.writeHead(obj.status);
|
|
78
|
+
obj.res.end(obj.body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Response.prototype.error404 = function() {
|
|
82
|
+
|
|
83
|
+
var obj = this;
|
|
84
|
+
obj.res.writeHead(404, {"Content-Type": "text/plain"});
|
|
85
|
+
obj.res.write("404 Not Found\n");
|
|
86
|
+
obj.res.end();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Response.prototype.render = function(template) {
|
|
90
|
+
|
|
91
|
+
var obj = this,
|
|
92
|
+
path = require("path"),
|
|
93
|
+
fs = require("fs"),
|
|
94
|
+
template = 'pages/' + template;
|
|
95
|
+
|
|
96
|
+
const filename = path.join(process.cwd(), 'public/' + template);
|
|
97
|
+
const fileStream = fs.createReadStream(filename);
|
|
98
|
+
obj.res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
99
|
+
fileStream.pipe(obj.res);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = Response;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Version 3.5
|
|
3
|
+
Created by: nalancer08 <https://github.com/nalancer08>
|
|
4
|
+
**/
|
|
5
|
+
|
|
6
|
+
var Request = require('./request.js');
|
|
7
|
+
var Response = require('./response.js');
|
|
8
|
+
var _ = require('underscore');
|
|
9
|
+
|
|
10
|
+
function Router(server) {
|
|
11
|
+
|
|
12
|
+
this.routes = {
|
|
13
|
+
'*': [],
|
|
14
|
+
'get': [],
|
|
15
|
+
'post': []
|
|
16
|
+
};
|
|
17
|
+
this.defaultRoute = '';
|
|
18
|
+
this.server = server;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Router.prototype.routeToRegExp = function(route) {
|
|
22
|
+
|
|
23
|
+
var optionalParam = /\((.*?)\)/g,
|
|
24
|
+
namedParam = /(\(\?)?:\w+/g,
|
|
25
|
+
splatParam = /\*\w+/g,
|
|
26
|
+
escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
|
27
|
+
// Convert route to regular expression, this was taken from Backbone's router
|
|
28
|
+
route = route.replace(escapeRegExp, '\\$&')
|
|
29
|
+
.replace(optionalParam, '(?:$1)?')
|
|
30
|
+
.replace(namedParam, function(match, optional) {
|
|
31
|
+
return optional ? match : '([^/?]+)';
|
|
32
|
+
})
|
|
33
|
+
.replace(splatParam, '([^?]*?)');
|
|
34
|
+
return new RegExp(`^${route}(?:\\?([\\s\\S]*))?$`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
Router.prototype.addRoute = function(method, route, handler, insert) {
|
|
38
|
+
|
|
39
|
+
var obj = this,
|
|
40
|
+
insert = insert || false,
|
|
41
|
+
method = method.toLowerCase(),
|
|
42
|
+
prev = (obj.server.options.base_url && obj.server.options.base_url != '' && obj.server.options.base_url != '/') ? obj.server.options.base_url : '',
|
|
43
|
+
instance = {
|
|
44
|
+
regexp: obj.routeToRegExp(prev + route),
|
|
45
|
+
handler: handler
|
|
46
|
+
};
|
|
47
|
+
// Add the route, may be at the beginning or at the end
|
|
48
|
+
if (insert) { // Adding the route at the beginning of the route's array
|
|
49
|
+
obj.routes[method].unshift(instance);
|
|
50
|
+
} else { // Adding the route at the end of the route's array
|
|
51
|
+
obj.routes[method].push(instance);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
Router.prototype.removeRoute = function(method, route) {};
|
|
56
|
+
|
|
57
|
+
Router.prototype.onRequest = function(req, res) {
|
|
58
|
+
|
|
59
|
+
var obj = this;
|
|
60
|
+
var isMatch = false;
|
|
61
|
+
var zlib = require('zlib');
|
|
62
|
+
var response = new Response(res);
|
|
63
|
+
var request = new Request(req, {
|
|
64
|
+
onDataReceived: function() {
|
|
65
|
+
|
|
66
|
+
//console.log(request.path);
|
|
67
|
+
if (request.path == obj.server.options.base_url) {
|
|
68
|
+
request.path = obj.server.options.base_url + obj.defaultRoute;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try with the routes for the current method (get or post)
|
|
72
|
+
_.each(obj.routes[request.type], function(route) {
|
|
73
|
+
if ( request.path.match(route.regexp) ) {
|
|
74
|
+
|
|
75
|
+
var parts = route.handler.split('.'),
|
|
76
|
+
clazz = parts[0],
|
|
77
|
+
method = parts[1],
|
|
78
|
+
callback = obj.validateCallback(clazz, method);
|
|
79
|
+
|
|
80
|
+
if (callback && callback != undefined && callback != '') {
|
|
81
|
+
|
|
82
|
+
isMatch = true;
|
|
83
|
+
handled = callback(request, response, obj.server);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// If not handled yet, try with the wildcard ones
|
|
90
|
+
if (!handled) {
|
|
91
|
+
_.each(obj.routes['*'], function(route) {
|
|
92
|
+
|
|
93
|
+
if ( request.path.match(route.regexp) ) {
|
|
94
|
+
|
|
95
|
+
var parts = route.handler.split('.'),
|
|
96
|
+
clazz = parts[0],
|
|
97
|
+
method = parts[1],
|
|
98
|
+
callback = obj.validateCallback(clazz, method);
|
|
99
|
+
|
|
100
|
+
if (callback && callback != undefined && callback != '') {
|
|
101
|
+
|
|
102
|
+
isMatch = true;
|
|
103
|
+
handled = callback(request, response, obj.server);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// No route catched, maybe it's a static content
|
|
111
|
+
// or not handled? Well, at this point we call 404
|
|
112
|
+
if (handled == false && isMatch == false ) {
|
|
113
|
+
|
|
114
|
+
var path = require('path'),
|
|
115
|
+
ext = path.extname(req.url).replace('.', ''),
|
|
116
|
+
extHandled = false,
|
|
117
|
+
extHeader = {};
|
|
118
|
+
var mimes = {
|
|
119
|
+
|
|
120
|
+
'png' : 'image/png',
|
|
121
|
+
'webp': 'image/webp',
|
|
122
|
+
'jpg' : 'image/jpg',
|
|
123
|
+
'css' : 'text/css',
|
|
124
|
+
'gz' : 'application/x-gzip',
|
|
125
|
+
'gif' : 'image/gif',
|
|
126
|
+
'js' : 'text/javascript',
|
|
127
|
+
'svg' : 'image/svg+xml',
|
|
128
|
+
'ttf' : 'application/x-font-ttf',
|
|
129
|
+
'otf' : 'application/x-font-opentype',
|
|
130
|
+
'pdf' : 'application/pdf',
|
|
131
|
+
'json' : 'application/json'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
var compressionMimes = {
|
|
135
|
+
'css' : 'text/css',
|
|
136
|
+
'js' : 'text/javascript',
|
|
137
|
+
'gz' : 'application/x-gzip'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (mimes[ext] != undefined && mimes[ext] != 'undefined') {
|
|
141
|
+
|
|
142
|
+
extHandled = true;
|
|
143
|
+
extHeader = {'Content-Type': mimes[ext]};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (extHandled) {
|
|
147
|
+
|
|
148
|
+
var fs = require('fs'),
|
|
149
|
+
rep = __dirname.replace('core/framework', ''),
|
|
150
|
+
route = request.path.replace(obj.server.options.base_url, ''),
|
|
151
|
+
filename = path.join(rep, route),
|
|
152
|
+
filePrivate = obj.isProtectedFile(route);
|
|
153
|
+
|
|
154
|
+
fs.exists(filename, function(exists) {
|
|
155
|
+
|
|
156
|
+
if (exists && !filePrivate) {
|
|
157
|
+
|
|
158
|
+
var acceptEncoding = (req.headers['accept-encoding'] != undefined) ? req.headers['accept-encoding'] : '';
|
|
159
|
+
var fileStream = fs.createReadStream(filename);
|
|
160
|
+
if (acceptEncoding.match(/\bgzip\b/) && compressionMimes[ext] != undefined) {
|
|
161
|
+
extHeader['Content-Encoding'] = 'gzip';
|
|
162
|
+
res.writeHead(200, extHeader);
|
|
163
|
+
fileStream.pipe(zlib.createGzip()).pipe(res);
|
|
164
|
+
} else {
|
|
165
|
+
res.writeHead(200, extHeader);
|
|
166
|
+
fileStream.pipe(res);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
|
|
170
|
+
} else {
|
|
171
|
+
|
|
172
|
+
// Return 404
|
|
173
|
+
obj.onNotFound(response);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
handled = false;
|
|
181
|
+
isMatch = false;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
Router.prototype.isProtectedFile = function(route) {
|
|
185
|
+
|
|
186
|
+
let protectedDirs = ['framework', 'external', 'node_mudules'];
|
|
187
|
+
var routeParts = route.split('/');
|
|
188
|
+
if (routeParts[1] != undefined && routeParts.length > 2) {
|
|
189
|
+
return protectedDirs.includes(routeParts[1]);
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
Router.prototype.validateExtension = function(route) {};
|
|
195
|
+
|
|
196
|
+
Router.prototype.validateCallback = function(clazz, method) {
|
|
197
|
+
|
|
198
|
+
var obj = this,
|
|
199
|
+
endpoints = obj.server.functions.endpoints;
|
|
200
|
+
|
|
201
|
+
if (endpoints[clazz] != undefined) {
|
|
202
|
+
|
|
203
|
+
clazz = endpoints[clazz];
|
|
204
|
+
|
|
205
|
+
if (typeof clazz[method] === 'function') {
|
|
206
|
+
return clazz[method];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return '';
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* This method allows to set the default route for the api
|
|
213
|
+
* @param route: String name for the route
|
|
214
|
+
**/
|
|
215
|
+
Router.prototype.setDefaultRoute = function(route) {
|
|
216
|
+
|
|
217
|
+
var obj = this;
|
|
218
|
+
obj.defaultRoute = route;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
Router.prototype.getDefaultRoute = function() {
|
|
222
|
+
|
|
223
|
+
var obj = this,
|
|
224
|
+
prev = (obj.server.options.base_url && obj.server.options.base_url != '' && obj.server.options.base_url != '/') ? obj.server.options.base_url : '';
|
|
225
|
+
|
|
226
|
+
return (prev + obj.defaultRoute);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
Router.prototype.onNotFound = function(response) {
|
|
230
|
+
|
|
231
|
+
response.setStatus(404);
|
|
232
|
+
response.respond();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
module.exports = Router;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Version 3.5
|
|
3
|
+
Created by: nalancer08 <https://github.com/nalancer08>
|
|
4
|
+
**/
|
|
5
|
+
|
|
6
|
+
const _ = require('underscore');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const http2 = require('http2');
|
|
9
|
+
const url = require('url') ;
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const nunjucks = require('nunjucks');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
var Router = require('./router.js');
|
|
15
|
+
var Dipper = require('./dipper.js');
|
|
16
|
+
var Functions = require('../external/functions.js');
|
|
17
|
+
|
|
18
|
+
function Server(options) {
|
|
19
|
+
|
|
20
|
+
this.httpx = null;
|
|
21
|
+
this.verbose = true;
|
|
22
|
+
this.router = null;
|
|
23
|
+
this.functions = null;
|
|
24
|
+
this.dipper = null;
|
|
25
|
+
this.init(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Server.prototype.init = function(options) {
|
|
29
|
+
|
|
30
|
+
var obj = this,
|
|
31
|
+
settings = options.settings,
|
|
32
|
+
opts = settings[options.profile] || {},
|
|
33
|
+
shared = settings['shared'] || {},
|
|
34
|
+
security = settings['security'] || {};
|
|
35
|
+
|
|
36
|
+
_.defaults(opts, {
|
|
37
|
+
base_url: '',
|
|
38
|
+
site_url: '',
|
|
39
|
+
wsServer: false,
|
|
40
|
+
onNotFound: obj.onNotFound
|
|
41
|
+
});
|
|
42
|
+
obj.options = opts;
|
|
43
|
+
|
|
44
|
+
_.defaults(security, {
|
|
45
|
+
pass_salt: '1234567890',
|
|
46
|
+
token_salt: '0987654321',
|
|
47
|
+
version: '1.0'
|
|
48
|
+
});
|
|
49
|
+
obj.security = security;
|
|
50
|
+
|
|
51
|
+
// Render
|
|
52
|
+
global.render = nunjucks.configure(path.join(process.cwd(), 'assets'), {
|
|
53
|
+
autoescape: false,
|
|
54
|
+
throwOnUndefined: true,
|
|
55
|
+
noCache: true,
|
|
56
|
+
});
|
|
57
|
+
global.dipper = new Dipper(opts, shared);
|
|
58
|
+
|
|
59
|
+
// Cheking for http or https
|
|
60
|
+
if (/^((http):\/\/)/.test(obj.options.site_url) || /^((localhost))/.test(obj.options.site_url)) {
|
|
61
|
+
|
|
62
|
+
obj.httpx = http.createServer((req, res) => {
|
|
63
|
+
//console.log(req.url);
|
|
64
|
+
obj.router.onRequest.call(obj.router, req, res);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
} else if (/^((https):\/\/)/.test(obj.options.site_url)) {
|
|
68
|
+
|
|
69
|
+
certs = {
|
|
70
|
+
key: fs.readFileSync(obj.security.key, 'utf8'),
|
|
71
|
+
cert: fs.readFileSync(obj.security.cert, 'utf8'),
|
|
72
|
+
allowHTTP1: true
|
|
73
|
+
};
|
|
74
|
+
obj.httpx = http2.createSecureServer(certs, (req, res) => {
|
|
75
|
+
obj.router.onRequest.call(obj.router, req, res);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
obj.onNotFound = opts.onNotFound;
|
|
80
|
+
|
|
81
|
+
// Initialize router
|
|
82
|
+
obj.router = new Router(obj);
|
|
83
|
+
|
|
84
|
+
// Setting endpoints
|
|
85
|
+
obj.functions = new Functions(obj);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
Server.prototype.start = function() {
|
|
89
|
+
|
|
90
|
+
var obj = this;
|
|
91
|
+
var port = (obj.options.port && obj.options.port != '') ? obj.options.port : 8080;
|
|
92
|
+
|
|
93
|
+
// Listen on the specified port
|
|
94
|
+
obj.httpx.listen(port);
|
|
95
|
+
|
|
96
|
+
// Logging
|
|
97
|
+
obj.log('__________________ Dragonfly server started ___________________');
|
|
98
|
+
obj.log(` > Listening on ${obj.options.site_url}:${obj.options.port}${obj.options.base_url} < `);
|
|
99
|
+
obj.log('-----------------------------------------------------------------');
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
Server.prototype.log = function(value) {
|
|
103
|
+
|
|
104
|
+
var obj = this;
|
|
105
|
+
if (obj.verbose) {
|
|
106
|
+
console.log(value);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
Server.prototype.onNotFound = function(request, response) {
|
|
111
|
+
|
|
112
|
+
response.setStatus(404);
|
|
113
|
+
response.respond();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = Server;
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vanilla-jet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "VannilaJet framework",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "grunt --env=development & nodemon index.js --ignore '**/public/**'",
|
|
8
|
+
"build:qa": "grunt build --env=qa",
|
|
9
|
+
"build:prod": "grunt build --env=production",
|
|
10
|
+
"start": "pm2 start index.js -- --p production",
|
|
11
|
+
"test": "npm run test"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/nalancer08/VanillaJet.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"webapp",
|
|
19
|
+
"spa"
|
|
20
|
+
],
|
|
21
|
+
"author": "Erick Sanchez",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/nalancer08/VanillaJet/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/nalancer08/VanillaJet#readme",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"blueimp-md5": "2.8.0",
|
|
29
|
+
"html-minifier-terser": "7.2.0",
|
|
30
|
+
"js-beautify": "1.14.8",
|
|
31
|
+
"jsrsasign": "11.0.0",
|
|
32
|
+
"jwt-simple": "0.5.3",
|
|
33
|
+
"minimist": "1.2.6",
|
|
34
|
+
"nunjucks": "3.2.4",
|
|
35
|
+
"underscore": ">= 1.12.x",
|
|
36
|
+
"zlib": "1.0.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"grunt": "1.6.1",
|
|
40
|
+
"grunt-cli": "1.4.3",
|
|
41
|
+
"grunt-contrib-clean": "2.0.1",
|
|
42
|
+
"grunt-contrib-compress": "2.0.0",
|
|
43
|
+
"grunt-contrib-concat": "2.1.0",
|
|
44
|
+
"grunt-contrib-cssmin": "5.0.0",
|
|
45
|
+
"grunt-contrib-less": "3.0.0",
|
|
46
|
+
"grunt-contrib-uglify": "5.0.1",
|
|
47
|
+
"grunt-contrib-watch": "1.1.0",
|
|
48
|
+
"grunt-run": "0.8.1",
|
|
49
|
+
"grunt-shell": "4.0.0",
|
|
50
|
+
"jit-grunt": "0.10.0",
|
|
51
|
+
"nodemon": "3.0.1"
|
|
52
|
+
}
|
|
53
|
+
}
|