tspace-spear 1.0.0-rc
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 +19 -0
- package/README.md +320 -0
- package/build/lib/core/decorators/context.d.ts +5 -0
- package/build/lib/core/decorators/context.js +92 -0
- package/build/lib/core/decorators/controller.d.ts +1 -0
- package/build/lib/core/decorators/controller.js +9 -0
- package/build/lib/core/decorators/headers.d.ts +2 -0
- package/build/lib/core/decorators/headers.js +25 -0
- package/build/lib/core/decorators/index.d.ts +8 -0
- package/build/lib/core/decorators/index.js +24 -0
- package/build/lib/core/decorators/methods.d.ts +5 -0
- package/build/lib/core/decorators/methods.js +24 -0
- package/build/lib/core/decorators/middleware.d.ts +2 -0
- package/build/lib/core/decorators/middleware.js +23 -0
- package/build/lib/core/decorators/statusCode.d.ts +1 -0
- package/build/lib/core/decorators/statusCode.js +26 -0
- package/build/lib/core/server/index.d.ts +202 -0
- package/build/lib/core/server/index.js +868 -0
- package/build/lib/core/server/router.d.ts +18 -0
- package/build/lib/core/server/router.js +68 -0
- package/build/lib/index.d.ts +11 -0
- package/build/lib/index.js +30 -0
- package/build/lib/types/index.d.ts +79 -0
- package/build/lib/types/index.js +2 -0
- package/package.json +47 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.Application = exports.Spear = void 0;
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const find_my_way_1 = __importDefault(require("find-my-way"));
|
|
42
|
+
const formidable_1 = __importDefault(require("formidable"));
|
|
43
|
+
const url_1 = require("url");
|
|
44
|
+
const on_finished_1 = __importDefault(require("on-finished"));
|
|
45
|
+
const string_decoder_1 = require("string_decoder");
|
|
46
|
+
const http_1 = __importStar(require("http"));
|
|
47
|
+
/**
|
|
48
|
+
*
|
|
49
|
+
* The 'Spear' class is used to create a server and handle HTTP requests.
|
|
50
|
+
*
|
|
51
|
+
* @returns {Spear} application
|
|
52
|
+
* @example
|
|
53
|
+
* new Application()
|
|
54
|
+
* .get('/' , () => 'Hello world!')
|
|
55
|
+
* .get('/json' , () => {
|
|
56
|
+
* return {
|
|
57
|
+
* message : 'Hello world!'
|
|
58
|
+
* }
|
|
59
|
+
* })
|
|
60
|
+
* .listen(3000 , () => console.log('server listening on port : 3000'))
|
|
61
|
+
*
|
|
62
|
+
*/
|
|
63
|
+
class Spear {
|
|
64
|
+
constructor({ controllers, middlewares, globalPrefix, logger } = {}) {
|
|
65
|
+
this._router = (0, find_my_way_1.default)();
|
|
66
|
+
this._errorHandler = null;
|
|
67
|
+
this._globalMiddlewares = [];
|
|
68
|
+
this._formatResponse = null;
|
|
69
|
+
this._onListeners = [];
|
|
70
|
+
this._fileUploadOptions = {
|
|
71
|
+
limits: Infinity,
|
|
72
|
+
useTempFiles: false,
|
|
73
|
+
tempFileDir: 'tmp',
|
|
74
|
+
removeTempFile: {
|
|
75
|
+
remove: true,
|
|
76
|
+
ms: 1000 * 60 * 10
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
this._filesParser = (req) => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
try {
|
|
81
|
+
const temp = this._fileUploadOptions.tempFileDir;
|
|
82
|
+
if (!fs_1.default.existsSync(temp)) {
|
|
83
|
+
try {
|
|
84
|
+
fs_1.default.mkdirSync(temp, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
catch (err) { }
|
|
87
|
+
}
|
|
88
|
+
const form = (0, formidable_1.default)({ uploadDir: temp });
|
|
89
|
+
const [dataBody, dataFiles] = yield form.parse(req);
|
|
90
|
+
const files = {};
|
|
91
|
+
const body = {};
|
|
92
|
+
const removeTemp = (fileTemp) => {
|
|
93
|
+
if (!this._fileUploadOptions.removeTempFile.remove)
|
|
94
|
+
return;
|
|
95
|
+
const remove = () => fs_1.default.unlinkSync(fileTemp);
|
|
96
|
+
setTimeout(remove, this._fileUploadOptions.removeTempFile.ms);
|
|
97
|
+
};
|
|
98
|
+
for (const key in dataFiles) {
|
|
99
|
+
const v = dataFiles[key];
|
|
100
|
+
if (v == null)
|
|
101
|
+
continue;
|
|
102
|
+
const file = v[0];
|
|
103
|
+
if (file.size > this._fileUploadOptions.limits) {
|
|
104
|
+
fs_1.default.unlinkSync(file.filepath);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
files[key] = {
|
|
108
|
+
size: file.size,
|
|
109
|
+
tempFilePath: file.filepath,
|
|
110
|
+
tempFileName: file.newFilename,
|
|
111
|
+
mimetype: file.mimetype,
|
|
112
|
+
name: file.originalFilename
|
|
113
|
+
};
|
|
114
|
+
removeTemp(file.filepath);
|
|
115
|
+
}
|
|
116
|
+
for (const key in dataBody) {
|
|
117
|
+
const v = dataBody[key];
|
|
118
|
+
if (v == null)
|
|
119
|
+
continue;
|
|
120
|
+
body[key] = v[0];
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
body,
|
|
124
|
+
files
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return {
|
|
129
|
+
body: {},
|
|
130
|
+
files: {}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this._bodyParser = (req) => {
|
|
135
|
+
if ((req === null || req === void 0 ? void 0 : req._writeParserEnd) === true)
|
|
136
|
+
return req._bodyParser;
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const decoder = new string_decoder_1.StringDecoder('utf-8');
|
|
139
|
+
let payload = '';
|
|
140
|
+
req.on('data', (data) => {
|
|
141
|
+
payload += decoder.write(data);
|
|
142
|
+
});
|
|
143
|
+
req.on('end', () => {
|
|
144
|
+
try {
|
|
145
|
+
payload += decoder.end();
|
|
146
|
+
req._writeParserEnd = true;
|
|
147
|
+
req._bodyParser = JSON.parse(payload);
|
|
148
|
+
return resolve(JSON.parse(payload));
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
req._writeParserEnd = true;
|
|
152
|
+
req._bodyParser = {};
|
|
153
|
+
return resolve({});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
req.on('error', (err) => {
|
|
157
|
+
req._writeParserEnd = true;
|
|
158
|
+
req._bodyParser = {};
|
|
159
|
+
return reject(err);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
this._wrapHandlers = (...handlers) => {
|
|
164
|
+
return (req, res, params) => __awaiter(this, void 0, void 0, function* () {
|
|
165
|
+
const runHandler = (index = 0) => __awaiter(this, void 0, void 0, function* () {
|
|
166
|
+
const response = this._customizeResponse(req, res);
|
|
167
|
+
const request = req;
|
|
168
|
+
const body = request._bodyParser;
|
|
169
|
+
const files = request._filesParser;
|
|
170
|
+
const cookies = request._cookiesParser;
|
|
171
|
+
const query = Object.assign({}, (0, url_1.parse)(String(req.url), true).query);
|
|
172
|
+
const objectOrNull = (data) => {
|
|
173
|
+
if (data == null)
|
|
174
|
+
return null;
|
|
175
|
+
return Object.keys(data).length ? data : null;
|
|
176
|
+
};
|
|
177
|
+
const ctx = {
|
|
178
|
+
req: request,
|
|
179
|
+
res: response,
|
|
180
|
+
params: objectOrNull(params),
|
|
181
|
+
query: objectOrNull(query),
|
|
182
|
+
body: objectOrNull(body),
|
|
183
|
+
files: objectOrNull(files),
|
|
184
|
+
cookies: objectOrNull(cookies),
|
|
185
|
+
};
|
|
186
|
+
if (index === handlers.length - 1) {
|
|
187
|
+
return this._wrapResponse(handlers[index].bind(handlers[index]))(ctx, this._nextFunction(ctx));
|
|
188
|
+
}
|
|
189
|
+
return handlers[index](ctx, () => {
|
|
190
|
+
return runHandler(index + 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
yield runHandler()
|
|
194
|
+
.catch(err => {
|
|
195
|
+
const ctx = {
|
|
196
|
+
req,
|
|
197
|
+
res: this._customizeResponse(req, res),
|
|
198
|
+
params: Object.keys(params).length ? params : null,
|
|
199
|
+
query: null,
|
|
200
|
+
body: null,
|
|
201
|
+
files: null,
|
|
202
|
+
cookies: null
|
|
203
|
+
};
|
|
204
|
+
return this._nextFunction(ctx)(err);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
if (logger)
|
|
209
|
+
this._onListeners.push(() => this.use(this._logger));
|
|
210
|
+
this._controllers = controllers;
|
|
211
|
+
this._middlewares = middlewares;
|
|
212
|
+
this._globalPrefix = globalPrefix == null ? '' : globalPrefix;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The 'enableCors' is used to enable the cors origins on the server.
|
|
216
|
+
*
|
|
217
|
+
* @params {Object}
|
|
218
|
+
* @property {(string | RegExp)[]} origins
|
|
219
|
+
* @property {boolean} credentials
|
|
220
|
+
* @returns
|
|
221
|
+
*/
|
|
222
|
+
enableCors({ origins, credentials } = {}) {
|
|
223
|
+
this._globalMiddlewares.push(({ req, res }, next) => {
|
|
224
|
+
var _a;
|
|
225
|
+
const origin = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.origin;
|
|
226
|
+
if (origin == null)
|
|
227
|
+
return next();
|
|
228
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
229
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
230
|
+
if (origins == null) {
|
|
231
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
232
|
+
}
|
|
233
|
+
if (Array.isArray(origins) && origins.length) {
|
|
234
|
+
if (origins.includes(origin)) {
|
|
235
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (credentials) {
|
|
239
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
240
|
+
}
|
|
241
|
+
return next();
|
|
242
|
+
});
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The 'use' method is used to add the middleware into the request pipeline.
|
|
247
|
+
*
|
|
248
|
+
* @callback {Function} middleware
|
|
249
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
250
|
+
* @property {Function} next - go to next function
|
|
251
|
+
* @returns {this}
|
|
252
|
+
*/
|
|
253
|
+
use(middleware) {
|
|
254
|
+
this._globalMiddlewares.push(middleware);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The 'useRouter' method is used to add the router in the request context.
|
|
259
|
+
*
|
|
260
|
+
* @parms {Function} router
|
|
261
|
+
* @property {Function} router - get() , post() , put() , patch() , delete()
|
|
262
|
+
* @returns {this}
|
|
263
|
+
*/
|
|
264
|
+
useRouter(router) {
|
|
265
|
+
const routes = router.routes;
|
|
266
|
+
for (const { path, method, handlers } of routes) {
|
|
267
|
+
this[method](this._normalizePath(this._globalPrefix, path), ...handlers);
|
|
268
|
+
}
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The 'useBodyParser' method is a middleware used to parse the request body of incoming HTTP requests.
|
|
273
|
+
*
|
|
274
|
+
* @returns {this}
|
|
275
|
+
*/
|
|
276
|
+
useBodyParser() {
|
|
277
|
+
this._globalMiddlewares.push(({ req, res }, next) => __awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
const contentType = req === null || req === void 0 ? void 0 : req.headers['content-type'];
|
|
279
|
+
const isFileUpload = contentType && contentType.startsWith('multipart/form-data');
|
|
280
|
+
const isListMethods = ['POST', 'PATCH', 'PUT'].includes(String(req.method));
|
|
281
|
+
if (!isListMethods)
|
|
282
|
+
return next();
|
|
283
|
+
if (isFileUpload)
|
|
284
|
+
return next();
|
|
285
|
+
if ((req === null || req === void 0 ? void 0 : req._bodyParser) != null)
|
|
286
|
+
return next();
|
|
287
|
+
req._bodyParser = yield this._bodyParser(req);
|
|
288
|
+
return next();
|
|
289
|
+
}));
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The 'useCookiesParser' method is a middleware used to parses cookies attached to the client request object.
|
|
294
|
+
*
|
|
295
|
+
* @returns {this}
|
|
296
|
+
*/
|
|
297
|
+
useCookiesParser() {
|
|
298
|
+
this._globalMiddlewares.push(({ req }, next) => {
|
|
299
|
+
if ((req === null || req === void 0 ? void 0 : req._cookiesParser) != null)
|
|
300
|
+
return next();
|
|
301
|
+
req._cookiesParser = this._cookiesParser(req);
|
|
302
|
+
return next();
|
|
303
|
+
});
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The 'useFileUpload' method is a middleware used to handler file uploads. It adds a file upload of incoming HTTP requests.
|
|
308
|
+
*
|
|
309
|
+
* @param {?Object}
|
|
310
|
+
* @property {?number} limits
|
|
311
|
+
* @property {?boolean} useTempFiles
|
|
312
|
+
* @property {?string} tempFileDir
|
|
313
|
+
* @property {?Object} removeTempFile
|
|
314
|
+
* @property {boolean} removeTempFile.remove
|
|
315
|
+
* @property {number} removeTempFile.ms
|
|
316
|
+
* @returns
|
|
317
|
+
*/
|
|
318
|
+
useFileUpload({ limits, useTempFiles, tempFileDir, removeTempFile } = {}) {
|
|
319
|
+
if (limits != null) {
|
|
320
|
+
this._fileUploadOptions.limits = limits;
|
|
321
|
+
}
|
|
322
|
+
if (tempFileDir != null) {
|
|
323
|
+
this._fileUploadOptions.tempFileDir = tempFileDir;
|
|
324
|
+
}
|
|
325
|
+
if (useTempFiles != null) {
|
|
326
|
+
this._fileUploadOptions.useTempFiles = useTempFiles;
|
|
327
|
+
}
|
|
328
|
+
if (removeTempFile != null) {
|
|
329
|
+
this._fileUploadOptions.removeTempFile = removeTempFile;
|
|
330
|
+
}
|
|
331
|
+
this._globalMiddlewares.push(({ req }, next) => __awaiter(this, void 0, void 0, function* () {
|
|
332
|
+
const contentType = req === null || req === void 0 ? void 0 : req.headers['content-type'];
|
|
333
|
+
const isFileUpload = contentType && contentType.startsWith('multipart/form-data');
|
|
334
|
+
const isListMethods = ['POST', 'PATCH', 'PUT'].includes(String(req.method));
|
|
335
|
+
if (!isListMethods)
|
|
336
|
+
return next();
|
|
337
|
+
if (!isFileUpload)
|
|
338
|
+
return next();
|
|
339
|
+
if ((req === null || req === void 0 ? void 0 : req._filesParser) != null)
|
|
340
|
+
return next();
|
|
341
|
+
const { body, files } = yield this._filesParser(req);
|
|
342
|
+
req._filesParser = files;
|
|
343
|
+
req._bodyParser = body;
|
|
344
|
+
return next();
|
|
345
|
+
}));
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The 'formatResponse' method is used to format the response
|
|
350
|
+
*
|
|
351
|
+
* @param {function} format
|
|
352
|
+
* @returns
|
|
353
|
+
*/
|
|
354
|
+
formatResponse(format) {
|
|
355
|
+
this._formatResponse = format;
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The 'errorHandler' method is middleware that is specifically designed to handle errors that occur during the processing of requests
|
|
360
|
+
*
|
|
361
|
+
* @param {function} error
|
|
362
|
+
* @returns
|
|
363
|
+
*/
|
|
364
|
+
errorHandler(error) {
|
|
365
|
+
this._errorHandler = error;
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* The 'notFoundHandler' method is middleware that is specifically designed to handle errors notfound that occur during the processing of requests
|
|
370
|
+
*
|
|
371
|
+
* @param {function} notfound
|
|
372
|
+
* @returns
|
|
373
|
+
*/
|
|
374
|
+
notFoundHandler(notfound) {
|
|
375
|
+
const handler = ({ req, res }) => {
|
|
376
|
+
return notfound({
|
|
377
|
+
req,
|
|
378
|
+
res: this._customizeResponse(req, res),
|
|
379
|
+
query: null,
|
|
380
|
+
files: null,
|
|
381
|
+
body: null,
|
|
382
|
+
params: null,
|
|
383
|
+
cookies: null
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
this._onListeners.push(() => {
|
|
387
|
+
return this._router.all('*', this._wrapHandlers(...this._globalMiddlewares, handler));
|
|
388
|
+
});
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The 'get' method is used to add the request handler to the router for the 'GET' method.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} path
|
|
395
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
396
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
397
|
+
* @property {Function} next - go to next function
|
|
398
|
+
* @returns {this}
|
|
399
|
+
*/
|
|
400
|
+
get(path, ...handlers) {
|
|
401
|
+
this._onListeners.push(() => {
|
|
402
|
+
return this._router.get(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
403
|
+
});
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The 'post' method is used to add the request handler to the router for the 'POST' method.
|
|
408
|
+
*
|
|
409
|
+
* @param {string} path
|
|
410
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
411
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
412
|
+
* @property {Function} next - go to next function
|
|
413
|
+
* @returns {this}
|
|
414
|
+
*/
|
|
415
|
+
post(path, ...handlers) {
|
|
416
|
+
this._onListeners.push(() => {
|
|
417
|
+
return this._router.post(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
418
|
+
});
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* The 'put' method is used to add the request handler to the router for the 'PUT' method.
|
|
423
|
+
*
|
|
424
|
+
* @param {string} path
|
|
425
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
426
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
427
|
+
* @property {Function} next - go to next function
|
|
428
|
+
* @returns {this}
|
|
429
|
+
*/
|
|
430
|
+
put(path, ...handlers) {
|
|
431
|
+
this._onListeners.push(() => {
|
|
432
|
+
return this._router.put(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
433
|
+
});
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* The 'patch' method is used to add the request handler to the router for the 'PATCH' method.
|
|
438
|
+
*
|
|
439
|
+
* @param {string} path
|
|
440
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
441
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
442
|
+
* @property {Function} next - go to next function
|
|
443
|
+
* @returns {this}
|
|
444
|
+
*/
|
|
445
|
+
patch(path, ...handlers) {
|
|
446
|
+
this._onListeners.push(() => {
|
|
447
|
+
return this._router.patch(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
448
|
+
});
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The 'delete' method is used to add the request handler to the router for the 'DELETE' method.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} path
|
|
455
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
456
|
+
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
|
|
457
|
+
* @property {Function} next - go to next function
|
|
458
|
+
* @returns {this}
|
|
459
|
+
*/
|
|
460
|
+
delete(path, ...handlers) {
|
|
461
|
+
this._onListeners.push(() => {
|
|
462
|
+
return this._router.delete(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
463
|
+
});
|
|
464
|
+
return this;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The 'all' method is used to add the request handler to the router for the 'all' method.
|
|
468
|
+
*
|
|
469
|
+
* @param {string} path
|
|
470
|
+
* @callback {...Function[]} handlers of the middlewares
|
|
471
|
+
* @property {object} ctx - context { req , res , query , params , cookies , files , body}
|
|
472
|
+
* @property {function} next - go to next function
|
|
473
|
+
* @returns {this}
|
|
474
|
+
*/
|
|
475
|
+
all(path, ...handlers) {
|
|
476
|
+
this._onListeners.push(() => {
|
|
477
|
+
return this._router.all(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
|
|
478
|
+
});
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* The 'listen' method is used to bind and start a server to a particular port and optionally a hostname.
|
|
483
|
+
*
|
|
484
|
+
* @param {number} port
|
|
485
|
+
* @param {function} cb
|
|
486
|
+
* @returns
|
|
487
|
+
*/
|
|
488
|
+
listen(port = 3000, cb) {
|
|
489
|
+
return __awaiter(this, arguments, void 0, function* () {
|
|
490
|
+
if (arguments.length === 1 && typeof port === 'function') {
|
|
491
|
+
cb = port;
|
|
492
|
+
port = 3000;
|
|
493
|
+
}
|
|
494
|
+
const server = yield this._createServer();
|
|
495
|
+
server.listen(port == null ? 3000 : port, () => {
|
|
496
|
+
if (cb)
|
|
497
|
+
cb({ server, port });
|
|
498
|
+
});
|
|
499
|
+
server.on('listening', () => {
|
|
500
|
+
this._onListeners.forEach(listener => listener());
|
|
501
|
+
});
|
|
502
|
+
server.on('error', (_) => {
|
|
503
|
+
port = Math.floor(Math.random() * 8999) + 1000;
|
|
504
|
+
server.listen(port);
|
|
505
|
+
});
|
|
506
|
+
return;
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
_logger({ req, res }, next) {
|
|
510
|
+
const diffTime = (hrtime) => {
|
|
511
|
+
const MS = 1000;
|
|
512
|
+
if (hrtime == null)
|
|
513
|
+
return 0;
|
|
514
|
+
const [start, end] = process.hrtime(hrtime);
|
|
515
|
+
const time = +(((start * MS) + (end / 1e6)).toFixed(2));
|
|
516
|
+
return `${time > MS ? `${time / MS} s` : `${time} ms`}`;
|
|
517
|
+
};
|
|
518
|
+
const statusCode = (res) => {
|
|
519
|
+
const statusCode = res.statusCode == null ? 500 : Number(res.statusCode);
|
|
520
|
+
return statusCode < 400
|
|
521
|
+
? `\x1b[32m${statusCode}\x1b[0m`
|
|
522
|
+
: `\x1b[31m${statusCode}\x1b[0m`;
|
|
523
|
+
};
|
|
524
|
+
const startTime = process.hrtime();
|
|
525
|
+
(0, on_finished_1.default)(res, () => {
|
|
526
|
+
console.log([
|
|
527
|
+
`[\x1b[1m\x1b[34mINFO\x1b[0m]`,
|
|
528
|
+
`\x1b[34m${new Date().toJSON()}\x1b[0m`,
|
|
529
|
+
`\x1b[33m${req.method}\x1b[0m`,
|
|
530
|
+
`${decodeURIComponent(String(req.url))}`,
|
|
531
|
+
`${statusCode(res)}`,
|
|
532
|
+
`${diffTime(startTime)}`,
|
|
533
|
+
].join(" "));
|
|
534
|
+
});
|
|
535
|
+
return next();
|
|
536
|
+
}
|
|
537
|
+
_import(dir, pattern) {
|
|
538
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
539
|
+
const directories = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
540
|
+
const files = (yield Promise.all(directories.map((directory) => {
|
|
541
|
+
const newDir = path_1.default.resolve(String(dir), directory.name);
|
|
542
|
+
if (pattern == null) {
|
|
543
|
+
return directory.isDirectory() ? this._import(newDir) : newDir;
|
|
544
|
+
}
|
|
545
|
+
return directory.isDirectory()
|
|
546
|
+
? this._import(newDir)
|
|
547
|
+
: pattern.test(directory.name)
|
|
548
|
+
? newDir
|
|
549
|
+
: null;
|
|
550
|
+
}))).filter(d => d != null);
|
|
551
|
+
return [].concat(...files);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
_registerControllers() {
|
|
555
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
556
|
+
if (this._controllers == null)
|
|
557
|
+
return;
|
|
558
|
+
if (!Array.isArray(this._controllers)) {
|
|
559
|
+
const controllers = yield this._import(this._controllers.folder, this._controllers.name);
|
|
560
|
+
for (const file of controllers) {
|
|
561
|
+
const response = yield Promise.resolve(`${file}`).then(s => __importStar(require(s)));
|
|
562
|
+
const controller = response === null || response === void 0 ? void 0 : response.default;
|
|
563
|
+
const controllerInstance = new controller();
|
|
564
|
+
const prefixPath = Reflect.getMetadata("controllers", controller);
|
|
565
|
+
const routers = Reflect.getMetadata("routers", controller);
|
|
566
|
+
if (prefixPath == null)
|
|
567
|
+
continue;
|
|
568
|
+
for (const { method, path, handler } of routers) {
|
|
569
|
+
this[method](this._normalizePath(this._globalPrefix, prefixPath, path), this._wrapResponse(controllerInstance[String(handler)].bind(controllerInstance)));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
for (const controller of this._controllers) {
|
|
575
|
+
const controllerInstance = new controller();
|
|
576
|
+
const prefixPath = Reflect.getMetadata("controllers", controller);
|
|
577
|
+
const routers = Reflect.getMetadata("routers", controller);
|
|
578
|
+
if (prefixPath == null)
|
|
579
|
+
continue;
|
|
580
|
+
for (const { method, path, handler } of routers) {
|
|
581
|
+
this[method](this._normalizePath(this._globalPrefix, prefixPath, path), this._wrapResponse(controllerInstance[String(handler)].bind(controllerInstance)));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
_registerMiddlewares() {
|
|
587
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
588
|
+
if (this._middlewares == null)
|
|
589
|
+
return;
|
|
590
|
+
if (!Array.isArray(this._middlewares)) {
|
|
591
|
+
const middlewares = yield this._import(this._middlewares.folder, this._middlewares.name);
|
|
592
|
+
for (const file of middlewares) {
|
|
593
|
+
const response = yield Promise.resolve(`${file}`).then(s => __importStar(require(s)));
|
|
594
|
+
const middleware = response === null || response === void 0 ? void 0 : response.default;
|
|
595
|
+
this.use(middleware);
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const middlewares = this._middlewares;
|
|
600
|
+
for (const middleware of middlewares) {
|
|
601
|
+
this.use(middleware);
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
_cookiesParser(req) {
|
|
607
|
+
var _a;
|
|
608
|
+
const cookies = {};
|
|
609
|
+
const cookieString = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.cookie;
|
|
610
|
+
if (cookieString == null)
|
|
611
|
+
return null;
|
|
612
|
+
for (const cookie of cookieString.split(';')) {
|
|
613
|
+
const [name, value] = cookie.split('=').map(v => v.trim());
|
|
614
|
+
cookies[name] = decodeURIComponent(value);
|
|
615
|
+
}
|
|
616
|
+
for (const name of Object.keys(cookies)) {
|
|
617
|
+
const cookie = cookies[name];
|
|
618
|
+
if (!cookie.startsWith('Expires='))
|
|
619
|
+
continue;
|
|
620
|
+
const expiresString = cookie.replace('Expires=', '');
|
|
621
|
+
const expiresDate = new Date(expiresString);
|
|
622
|
+
if (isNaN(expiresDate.getTime()) || expiresDate < new Date()) {
|
|
623
|
+
delete cookies[name];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return cookies;
|
|
627
|
+
}
|
|
628
|
+
_customizeResponse(req, res) {
|
|
629
|
+
const response = res;
|
|
630
|
+
response.json = (results) => {
|
|
631
|
+
if (typeof results === 'string') {
|
|
632
|
+
if (!res.headersSent) {
|
|
633
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
634
|
+
}
|
|
635
|
+
return res.end(results);
|
|
636
|
+
}
|
|
637
|
+
if (!res.headersSent) {
|
|
638
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
639
|
+
}
|
|
640
|
+
if (results == null) {
|
|
641
|
+
if (this._formatResponse != null) {
|
|
642
|
+
return res.end(JSON.stringify(this._formatResponse(null, res.statusCode), null, 2));
|
|
643
|
+
}
|
|
644
|
+
return res.end();
|
|
645
|
+
}
|
|
646
|
+
if (this._formatResponse != null) {
|
|
647
|
+
return res.end(JSON.stringify(this._formatResponse(Object.assign({}, results), res.statusCode), null, 2));
|
|
648
|
+
}
|
|
649
|
+
return res.end(JSON.stringify(Object.assign({}, results), null, 2));
|
|
650
|
+
};
|
|
651
|
+
response.send = (results) => {
|
|
652
|
+
res.writeHead(res.statusCode, { 'Content-Type': 'text/plain' });
|
|
653
|
+
return res.end(results);
|
|
654
|
+
};
|
|
655
|
+
response.error = (err) => {
|
|
656
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
657
|
+
let code = +((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.code) ||
|
|
658
|
+
+err.code ||
|
|
659
|
+
+err.status ||
|
|
660
|
+
+err.statusCode ||
|
|
661
|
+
+((_d = (_c = err.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.statusCode) ||
|
|
662
|
+
500;
|
|
663
|
+
code = (code == null || typeof code !== 'number') ? 500 : Number.isNaN(code) ? 500 : code < 400 ? 500 : code;
|
|
664
|
+
const message = ((_f = (_e = err.response) === null || _e === void 0 ? void 0 : _e.data) === null || _f === void 0 ? void 0 : _f.errorMessage) ||
|
|
665
|
+
((_h = (_g = err.response) === null || _g === void 0 ? void 0 : _g.data) === null || _h === void 0 ? void 0 : _h.message) ||
|
|
666
|
+
err.message ||
|
|
667
|
+
`The url '${req.url}' resulted in a server error. Please investigate.`;
|
|
668
|
+
response.status(code);
|
|
669
|
+
if (this._formatResponse != null) {
|
|
670
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, code), null, 2));
|
|
671
|
+
}
|
|
672
|
+
return res.end(JSON.stringify({
|
|
673
|
+
message: message
|
|
674
|
+
}, null, 2));
|
|
675
|
+
};
|
|
676
|
+
response.ok = (results) => {
|
|
677
|
+
return response.json(results == null ? {} : results);
|
|
678
|
+
};
|
|
679
|
+
response.created = (results) => {
|
|
680
|
+
response.status(201);
|
|
681
|
+
return response.json(results == null ? {} : results);
|
|
682
|
+
};
|
|
683
|
+
response.accepted = (results) => {
|
|
684
|
+
response.status(202);
|
|
685
|
+
return response.json(results == null ? {} : results);
|
|
686
|
+
};
|
|
687
|
+
response.noContent = () => {
|
|
688
|
+
response.status(202);
|
|
689
|
+
return res.end();
|
|
690
|
+
};
|
|
691
|
+
response.badRequest = (message) => {
|
|
692
|
+
response.status(400);
|
|
693
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' resulted in a bad request. Please review the data and try again.`;
|
|
694
|
+
if (this._formatResponse != null) {
|
|
695
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 400), null, 2));
|
|
696
|
+
}
|
|
697
|
+
return res.end(JSON.stringify({
|
|
698
|
+
message: message
|
|
699
|
+
}, null, 2));
|
|
700
|
+
};
|
|
701
|
+
response.unauthorized = (message) => {
|
|
702
|
+
response.status(401);
|
|
703
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' is unauthorized. Please verify.`;
|
|
704
|
+
if (this._formatResponse != null) {
|
|
705
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 401), null, 2));
|
|
706
|
+
}
|
|
707
|
+
return res.end(JSON.stringify({
|
|
708
|
+
message
|
|
709
|
+
}, null, 2));
|
|
710
|
+
};
|
|
711
|
+
response.paymentRequired = (message) => {
|
|
712
|
+
response.status(402);
|
|
713
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' requires payment. Please proceed with payment.`;
|
|
714
|
+
if (this._formatResponse != null) {
|
|
715
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 402), null, 2));
|
|
716
|
+
}
|
|
717
|
+
return res.end(JSON.stringify({
|
|
718
|
+
message
|
|
719
|
+
}, null, 2));
|
|
720
|
+
};
|
|
721
|
+
response.forbidden = (message) => {
|
|
722
|
+
response.status(403);
|
|
723
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' is forbidden. Please check the permissions or access rights.`;
|
|
724
|
+
if (this._formatResponse != null) {
|
|
725
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 403), null, 2));
|
|
726
|
+
}
|
|
727
|
+
return res.end(JSON.stringify({
|
|
728
|
+
message
|
|
729
|
+
}, null, 2));
|
|
730
|
+
};
|
|
731
|
+
response.notFound = (message) => {
|
|
732
|
+
response.status(404);
|
|
733
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' was not found. Please re-check the your url again`;
|
|
734
|
+
if (this._formatResponse != null) {
|
|
735
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 404), null, 2));
|
|
736
|
+
}
|
|
737
|
+
return res.end(JSON.stringify({
|
|
738
|
+
message
|
|
739
|
+
}, null, 2));
|
|
740
|
+
};
|
|
741
|
+
response.serverError = (message) => {
|
|
742
|
+
response.status(500);
|
|
743
|
+
message = message !== null && message !== void 0 ? message : `The url '${req.url}' resulted in a server error. Please investigate.`;
|
|
744
|
+
if (this._formatResponse != null) {
|
|
745
|
+
return res.end(JSON.stringify(this._formatResponse({ message }, 500), null, 2));
|
|
746
|
+
}
|
|
747
|
+
return res.end(JSON.stringify({
|
|
748
|
+
message
|
|
749
|
+
}, null, 2));
|
|
750
|
+
};
|
|
751
|
+
response.status = (code) => {
|
|
752
|
+
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
753
|
+
return res;
|
|
754
|
+
};
|
|
755
|
+
response.setCookies = (cookies) => {
|
|
756
|
+
for (const [key, v] of Object.entries(cookies)) {
|
|
757
|
+
if (typeof v === 'string') {
|
|
758
|
+
res.setHeader('Set-Cookie', `${key}=${v}`);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (v.value === '' || v.value == null)
|
|
762
|
+
continue;
|
|
763
|
+
let str = `${key}=${v.value}`;
|
|
764
|
+
if (v.sameSite != null) {
|
|
765
|
+
str += ` ;SameSite=${v.sameSite}`;
|
|
766
|
+
}
|
|
767
|
+
if (v.domain != null) {
|
|
768
|
+
str += ` ;Domain=${v.domain}`;
|
|
769
|
+
}
|
|
770
|
+
if (v.httpOnly != null) {
|
|
771
|
+
str += ` ;HttpOnly`;
|
|
772
|
+
}
|
|
773
|
+
if (v.secure != null) {
|
|
774
|
+
str += ` ;Secure`;
|
|
775
|
+
}
|
|
776
|
+
if (v.expires != null) {
|
|
777
|
+
str += ` ;Expires=${v.expires.toUTCString()}`;
|
|
778
|
+
}
|
|
779
|
+
res.setHeader('Set-Cookie', str);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
return response;
|
|
783
|
+
}
|
|
784
|
+
_nextFunction(ctx) {
|
|
785
|
+
return (err) => __awaiter(this, void 0, void 0, function* () {
|
|
786
|
+
if (err != null) {
|
|
787
|
+
if (this._errorHandler != null) {
|
|
788
|
+
return this._errorHandler(err, ctx);
|
|
789
|
+
}
|
|
790
|
+
ctx.res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
791
|
+
if (this._formatResponse != null) {
|
|
792
|
+
return ctx.res.end(JSON.stringify(this._formatResponse({
|
|
793
|
+
message: err === null || err === void 0 ? void 0 : err.message,
|
|
794
|
+
}, ctx.res.statusCode), null, 2));
|
|
795
|
+
}
|
|
796
|
+
return ctx.res.end(JSON.stringify({
|
|
797
|
+
message: err === null || err === void 0 ? void 0 : err.message,
|
|
798
|
+
}, null, 2));
|
|
799
|
+
}
|
|
800
|
+
if (this._errorHandler != null) {
|
|
801
|
+
return this._errorHandler(new Error(`The 'next' function does not have any subsequent function.`), ctx);
|
|
802
|
+
}
|
|
803
|
+
ctx.res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
804
|
+
if (this._formatResponse != null) {
|
|
805
|
+
return ctx.res.end(JSON.stringify(this._formatResponse({
|
|
806
|
+
message: `The 'next' function does not have any subsequent function.`
|
|
807
|
+
}, ctx.res.statusCode), null, 2));
|
|
808
|
+
}
|
|
809
|
+
return ctx.res.end(JSON.stringify({
|
|
810
|
+
message: `The 'next' function does not have any subsequent function.`
|
|
811
|
+
}, null, 2));
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
_wrapResponse(handler) {
|
|
815
|
+
return (ctx, next) => __awaiter(this, void 0, void 0, function* () {
|
|
816
|
+
const result = yield handler(ctx, next);
|
|
817
|
+
if (result == null)
|
|
818
|
+
return;
|
|
819
|
+
if (result instanceof http_1.ServerResponse)
|
|
820
|
+
return;
|
|
821
|
+
if (typeof result === 'string') {
|
|
822
|
+
if (!ctx.res.headersSent) {
|
|
823
|
+
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
824
|
+
}
|
|
825
|
+
return ctx.res.end(result);
|
|
826
|
+
}
|
|
827
|
+
if (this._formatResponse != null) {
|
|
828
|
+
const formatResponse = this._formatResponse(Object.assign({}, result), ctx.res.statusCode);
|
|
829
|
+
if (typeof formatResponse === 'string') {
|
|
830
|
+
if (!ctx.res.headersSent) {
|
|
831
|
+
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
832
|
+
}
|
|
833
|
+
return ctx.res.end(formatResponse);
|
|
834
|
+
}
|
|
835
|
+
if (!ctx.res.headersSent) {
|
|
836
|
+
ctx.res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
837
|
+
}
|
|
838
|
+
return ctx.res.end(JSON.stringify(this._formatResponse(Object.assign({}, result), ctx.res.statusCode), null, 2));
|
|
839
|
+
}
|
|
840
|
+
if (!ctx.res.headersSent) {
|
|
841
|
+
ctx.res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
842
|
+
}
|
|
843
|
+
return ctx.res.end(JSON.stringify(Object.assign({}, result), null, 2));
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
_createServer() {
|
|
847
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
848
|
+
yield this._registerMiddlewares();
|
|
849
|
+
yield this._registerControllers();
|
|
850
|
+
const server = http_1.default.createServer((req, res) => {
|
|
851
|
+
return this._router.lookup(req, res);
|
|
852
|
+
});
|
|
853
|
+
return server;
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
_normalizePath(...paths) {
|
|
857
|
+
let path = paths
|
|
858
|
+
.join('/')
|
|
859
|
+
.replace(/\/+/g, '/')
|
|
860
|
+
.replace(/\/+$/, '');
|
|
861
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
exports.Spear = Spear;
|
|
865
|
+
class Application extends Spear {
|
|
866
|
+
}
|
|
867
|
+
exports.Application = Application;
|
|
868
|
+
exports.default = Spear;
|