@superhero/http-server 4.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/LICENCE +21 -0
- package/README.md +451 -0
- package/config.json +18 -0
- package/index.js +424 -0
- package/index.test.js +464 -0
- package/middleware/upstream/header/accept.js +52 -0
- package/middleware/upstream/header/content-type/application/json.js +29 -0
- package/middleware/upstream/header/content-type.js +45 -0
- package/middleware/upstream/method.js +38 -0
- package/package.json +42 -0
- package/view.js +285 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Erik Landvall
|
|
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,451 @@
|
|
|
1
|
+
|
|
2
|
+
# HTTP-server
|
|
3
|
+
|
|
4
|
+
An HTTP server module for Node.js that supports both HTTP/1.1 and HTTP/2 protocols, with built-in routing, HTTPS support, and stream support that defaults to server-sent events (SSE). Designed to be robust, flexible and extendible, while easy to work with.
|
|
5
|
+
|
|
6
|
+
## Table of Contents
|
|
7
|
+
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Getting Started](#getting-started)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [Basic Example](#basic-example)
|
|
12
|
+
- [HTTPS Setup with Self-Signed Certificate](#https-setup-with-self-signed-certificate)
|
|
13
|
+
- [Altering Response Body, Headers, and Status](#altering-response-body-headers-and-status)
|
|
14
|
+
- [Handling Aborted Requests](#handling-aborted-requests)
|
|
15
|
+
- [Streaming Server-Sent Events (SSE)](#streaming-server-sent-events-sse)
|
|
16
|
+
- [Custom Logging](#custom-logging)
|
|
17
|
+
- [API](#api)
|
|
18
|
+
- [`HttpServer`](#httpserver)
|
|
19
|
+
- [`session.view`](#sessionview)
|
|
20
|
+
- [`session.abortion`](#sessionabortion)
|
|
21
|
+
- [Testing](#testing)
|
|
22
|
+
- [Coverage Report](#coverage-report)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install the package using npm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @superhero/http-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Getting Started
|
|
35
|
+
|
|
36
|
+
The `@superhero/http-server` module integrates with the `@superhero/locator` and `@superhero/router` modules to provide a flexible and modular HTTP server.
|
|
37
|
+
|
|
38
|
+
To get started, you'll need to set up a `Locator` instance, register your dispatchers, and then locate the `HttpServer` module using the `locator`.
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Basic Example
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
import HttpServer from '@superhero/http-server';
|
|
46
|
+
import Locator from '@superhero/locator';
|
|
47
|
+
import Router from '@superhero/router';
|
|
48
|
+
|
|
49
|
+
// Instantiate the service locator
|
|
50
|
+
const locator = new Locator();
|
|
51
|
+
|
|
52
|
+
// Instantiate the router
|
|
53
|
+
const router = new Router(locator);
|
|
54
|
+
|
|
55
|
+
// Instantiate the server
|
|
56
|
+
const server = HttpServer(route);
|
|
57
|
+
|
|
58
|
+
// Register the route dispatcher service
|
|
59
|
+
locator.set('hello-dispatcher', {
|
|
60
|
+
dispatch: (request, session) => {
|
|
61
|
+
session.view.body.message = 'Hello, World!';
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Routes
|
|
66
|
+
const settings = {
|
|
67
|
+
router: {
|
|
68
|
+
routes: {
|
|
69
|
+
hello: {
|
|
70
|
+
criteria: '/hello',
|
|
71
|
+
dispatcher: 'hello-dispatcher',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Bootstrap and start the server
|
|
78
|
+
await server.bootstrap(settings);
|
|
79
|
+
await server.listen(3000);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Explanation:**
|
|
83
|
+
|
|
84
|
+
- **Import Statements**: We instantiate the required components `locator`, `router` and `server`.
|
|
85
|
+
- **Dispatcher Registration**: Register a dispatcher called `'hello-dispatcher'` in the locator.
|
|
86
|
+
- **Server Settings**: Define the routes, and possible other server configurations, in the `settings` object.
|
|
87
|
+
- **Bootstrap and Listen**: Bootstrap the server with the settings and start listening on port `3000`.
|
|
88
|
+
- **Ready to serve requests**: Request to `http://localhost:3000/hello` will reply `{ "message": "Hello, World!" }`.
|
|
89
|
+
|
|
90
|
+
### HTTPS Setup with Self-Signed Certificate
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
import fs from 'node:fs';
|
|
94
|
+
import Locator from '@superhero/locator';
|
|
95
|
+
|
|
96
|
+
// Instantiate the service locator
|
|
97
|
+
const locator = new Locator();
|
|
98
|
+
|
|
99
|
+
// Locate the server
|
|
100
|
+
const server = await locator.lazyload('@superhero/http-server');
|
|
101
|
+
|
|
102
|
+
// Register necessary services
|
|
103
|
+
locator.set('secure-dispatcher', {
|
|
104
|
+
dispatch: (request, session) => {
|
|
105
|
+
session.view.body = { message: 'Secure Hello, World!' };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Server settings and routes
|
|
110
|
+
const serverSettings = {
|
|
111
|
+
server: {
|
|
112
|
+
key: fs.readFileSync('path/to/private.key'),
|
|
113
|
+
cert: fs.readFileSync('path/to/server.cert'),
|
|
114
|
+
},
|
|
115
|
+
router: {
|
|
116
|
+
routes: {
|
|
117
|
+
secure: {
|
|
118
|
+
criteria: '/secure',
|
|
119
|
+
dispatcher: 'secure-dispatcher',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await server.bootstrap(serverSettings);
|
|
126
|
+
await server.listen(443);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> [!NOTE]
|
|
130
|
+
> Replace `'path/to/private.key'` and `'path/to/server.cert'` with the actual paths to your SSL key and certificate files.
|
|
131
|
+
|
|
132
|
+
### Altering Response Body, Headers, and Status
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
locator.set('custom-dispatcher', {
|
|
136
|
+
dispatch: (request, session) => {
|
|
137
|
+
session.view.body = { data: 'Custom Data' };
|
|
138
|
+
session.view.headers['Custom-Header'] = 'CustomValue';
|
|
139
|
+
session.view.status = 201; // HTTP 201 Created
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Update the routes in the settings
|
|
144
|
+
const settings = {
|
|
145
|
+
router: {
|
|
146
|
+
routes: {
|
|
147
|
+
custom: {
|
|
148
|
+
criteria: '/custom',
|
|
149
|
+
dispatcher: 'custom-dispatcher',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Bootstrap and start the server
|
|
156
|
+
await server.bootstrap(settings);
|
|
157
|
+
await server.listen(3000);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Handling Aborted Requests
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
locator.set('abort-dispatcher', {
|
|
164
|
+
dispatch: (request, session) => {
|
|
165
|
+
// Abort the request with a custom error
|
|
166
|
+
const error = new Error('Request Aborted');
|
|
167
|
+
error.code = 'E_REQUEST_ABORTED';
|
|
168
|
+
session.abortion.abort(error);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Update the routes in the settings
|
|
173
|
+
const settings = {
|
|
174
|
+
router: {
|
|
175
|
+
routes: {
|
|
176
|
+
abort: {
|
|
177
|
+
criteria: '/abort',
|
|
178
|
+
dispatcher: 'abort-dispatcher',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Bootstrap and start the server
|
|
185
|
+
await server.bootstrap(settings);
|
|
186
|
+
await server.listen(3000);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> [!NOTE]
|
|
190
|
+
> Will result in a `status 500` response `{ "error": "Request Aborted", "code": "E_REQUEST_ABORTED" }`
|
|
191
|
+
|
|
192
|
+
### Streaming Server-Sent Events (SSE)
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
locator.set('sse-dispatcher', {
|
|
196
|
+
dispatch: (request, session) => {
|
|
197
|
+
// Write events to the stream
|
|
198
|
+
session.view.stream.write({ data: 'First message' });
|
|
199
|
+
session.view.stream.write({ data: 'Second message' });
|
|
200
|
+
|
|
201
|
+
// End the stream
|
|
202
|
+
session.view.stream.end();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Update the routes in the settings
|
|
207
|
+
const settings = {
|
|
208
|
+
router: {
|
|
209
|
+
routes: {
|
|
210
|
+
sse: {
|
|
211
|
+
criteria: '/sse',
|
|
212
|
+
dispatcher: 'sse-dispatcher',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Bootstrap and start the server
|
|
219
|
+
await server.bootstrap(settings);
|
|
220
|
+
await server.listen(3000);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> [!NOTE]
|
|
224
|
+
> By default responds with a `text/event-stream` content type:
|
|
225
|
+
> ```
|
|
226
|
+
> data: { "data": "First message" }
|
|
227
|
+
>
|
|
228
|
+
> data: { "data": "Second message" }
|
|
229
|
+
> ```
|
|
230
|
+
|
|
231
|
+
### Custom Logging
|
|
232
|
+
|
|
233
|
+
You can override the default logging methods to integrate with your logging system.
|
|
234
|
+
|
|
235
|
+
#### Turn Off Info Logs
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
server.log.info = () => null;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Custom Error Logging
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
server.log.error = (error) => {
|
|
245
|
+
// TODO: custom error logging logic...
|
|
246
|
+
};
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Turn Off Log Colors
|
|
250
|
+
|
|
251
|
+
By default, the logger renders a colored output.
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
server.log.format = server.log.simple;
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## API
|
|
258
|
+
|
|
259
|
+
### `HttpServer`
|
|
260
|
+
|
|
261
|
+
The main class responsible for handling HTTP requests.
|
|
262
|
+
|
|
263
|
+
- **Constructor**: The server can be instantiated or located via the `Locator`.
|
|
264
|
+
- Use `locator.locate('@superhero/http-server')` to get an instance.
|
|
265
|
+
|
|
266
|
+
- **Methods**:
|
|
267
|
+
- `async bootstrap(settings)`: Bootstraps the server with the provided settings.
|
|
268
|
+
- `settings`: An object containing server and router configurations.
|
|
269
|
+
- `async listen(port)`: Starts the server on the specified port.
|
|
270
|
+
- `port`: The port number to listen on.
|
|
271
|
+
- `async close()`: Closes the server and all active sessions.
|
|
272
|
+
|
|
273
|
+
### `request`
|
|
274
|
+
|
|
275
|
+
An object used to read
|
|
276
|
+
|
|
277
|
+
- **Properties**:
|
|
278
|
+
- `body`: The request body (Promise).
|
|
279
|
+
- `method`: The request HTTP method.
|
|
280
|
+
- `headers`: The request HTTP headers.
|
|
281
|
+
- `url`: The requested URL.
|
|
282
|
+
|
|
283
|
+
### `session.view`
|
|
284
|
+
|
|
285
|
+
An object used within dispatchers to manipulate the response.
|
|
286
|
+
|
|
287
|
+
- **Properties**:
|
|
288
|
+
- `body`: The response body to be sent to the client.
|
|
289
|
+
- `headers`: An object containing response headers.
|
|
290
|
+
- `status`: HTTP status code of the response.
|
|
291
|
+
- `stream`: A writable stream for sending SSE data, or to be configured to stream some other type of response to the client.
|
|
292
|
+
|
|
293
|
+
### `session.abortion`
|
|
294
|
+
|
|
295
|
+
An `AbortController` used to trigger and manage dispatch abortion.
|
|
296
|
+
|
|
297
|
+
- **Methods**:
|
|
298
|
+
- `abort(error)`: Aborts the request with the provided error.
|
|
299
|
+
|
|
300
|
+
## Testing
|
|
301
|
+
|
|
302
|
+
The test suite uses Node.js's built-in testing module.
|
|
303
|
+
|
|
304
|
+
### Running Tests
|
|
305
|
+
|
|
306
|
+
To run the tests, execute:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npm test
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Test Coverage
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
▶ @superhero/http-server
|
|
316
|
+
▶ Lifecycle
|
|
317
|
+
✔ Can instantiate HttpServer (8.419758ms)
|
|
318
|
+
✔ Can bootstrap server with non-secure settings (2.635146ms)
|
|
319
|
+
✔ Listens and closes the server as expected (3.349393ms)
|
|
320
|
+
✔ Rejects if server is not available to listen error (2.108227ms)
|
|
321
|
+
✔ Rejects if server is not available to close error (1.792574ms)
|
|
322
|
+
✔ Lifecycle (19.74534ms)
|
|
323
|
+
|
|
324
|
+
▶ Routing and Requests
|
|
325
|
+
▶ HTTP/1
|
|
326
|
+
✔ Can dispatch a request aligned to the route map (39.125646ms)
|
|
327
|
+
✔ Can alter the output body (5.52864ms)
|
|
328
|
+
✔ Can stream HTML5 standard Server-Sent Events (SSE) (7.515151ms)
|
|
329
|
+
✔ Can alter the output headers (6.829782ms)
|
|
330
|
+
✔ Can alter the output status (5.509801ms)
|
|
331
|
+
✔ Can abort the dispatcher (6.059604ms)
|
|
332
|
+
✔ Can describe an abortion in detail (6.212658ms)
|
|
333
|
+
✔ Can manage thrown errors in the dispatcher (6.902106ms)
|
|
334
|
+
✔ Can not mistakenly access the wrong view property (4.354173ms)
|
|
335
|
+
✔ Can not mistakenly assign a value to the wrong view property (7.322497ms)
|
|
336
|
+
✔ Support connection keep-alive header (6.339005ms)
|
|
337
|
+
✔ HTTP/1 (103.550623ms)
|
|
338
|
+
|
|
339
|
+
▶ HTTP/2
|
|
340
|
+
✔ Can dispatch a request aligned to the route map (67.303149ms)
|
|
341
|
+
✔ Can alter the output body (6.051175ms)
|
|
342
|
+
✔ Can stream HTML5 standard Server-Sent Events (SSE) (5.646976ms)
|
|
343
|
+
✔ Can alter the output headers (4.816813ms)
|
|
344
|
+
✔ Can alter the output status (8.330617ms)
|
|
345
|
+
✔ Can abort the dispatcher (6.803324ms)
|
|
346
|
+
✔ Can describe an abortion in detail (4.771987ms)
|
|
347
|
+
✔ Can manage thrown errors in the dispatcher (6.57489ms)
|
|
348
|
+
✔ Can not mistakenly access the wrong view property (4.451118ms)
|
|
349
|
+
✔ Can not mistakenly assign a value to the wrong view property (4.587527ms)
|
|
350
|
+
✔ HTTP/2 (120.216844ms)
|
|
351
|
+
✔ Routing and Requests (224.037442ms)
|
|
352
|
+
|
|
353
|
+
▶ HTTPS server with self-signed certificate
|
|
354
|
+
▶ TLSv1.2
|
|
355
|
+
▶ RSA:2048
|
|
356
|
+
✔ HTTP1 (9.766994ms)
|
|
357
|
+
✔ HTTP2 (10.784857ms)
|
|
358
|
+
✔ RSA:2048 (157.12133ms)
|
|
359
|
+
|
|
360
|
+
▶ RSA:4096
|
|
361
|
+
✔ HTTP1 (11.566225ms)
|
|
362
|
+
✔ HTTP2 (18.136774ms)
|
|
363
|
+
✔ RSA:4096 (581.128109ms)
|
|
364
|
+
|
|
365
|
+
▶ ECDSA:P-256
|
|
366
|
+
✔ HTTP1 (5.324231ms)
|
|
367
|
+
✔ HTTP2 (8.312658ms)
|
|
368
|
+
✔ ECDSA:P-256 (50.979123ms)
|
|
369
|
+
|
|
370
|
+
▶ ECDSA:P-384
|
|
371
|
+
✔ HTTP1 (6.277003ms)
|
|
372
|
+
✔ HTTP2 (9.918662ms)
|
|
373
|
+
✔ ECDSA:P-384 (52.076847ms)
|
|
374
|
+
|
|
375
|
+
▶ ECDSA:P-521
|
|
376
|
+
✔ HTTP1 (10.988173ms)
|
|
377
|
+
✔ HTTP2 (13.745049ms)
|
|
378
|
+
✔ ECDSA:P-521 (63.762337ms)
|
|
379
|
+
|
|
380
|
+
▶ EdDSA:Ed25519
|
|
381
|
+
✔ HTTP1 (4.940083ms)
|
|
382
|
+
✔ HTTP2 (8.791915ms)
|
|
383
|
+
✔ EdDSA:Ed25519 (48.717009ms)
|
|
384
|
+
|
|
385
|
+
▶ EdDSA:Ed448
|
|
386
|
+
✔ HTTP1 (6.589414ms)
|
|
387
|
+
✔ HTTP2 (8.132894ms)
|
|
388
|
+
✔ EdDSA:Ed448 (50.727502ms)
|
|
389
|
+
✔ TLSv1.2 (1005.148618ms)
|
|
390
|
+
|
|
391
|
+
▶ TLSv1.3
|
|
392
|
+
▶ RSA:2048
|
|
393
|
+
✔ HTTP1 (6.038652ms)
|
|
394
|
+
✔ HTTP2 (8.748363ms)
|
|
395
|
+
✔ RSA:2048 (119.601474ms)
|
|
396
|
+
|
|
397
|
+
▶ RSA:4096
|
|
398
|
+
✔ HTTP1 (12.785668ms)
|
|
399
|
+
✔ HTTP2 (14.531181ms)
|
|
400
|
+
✔ RSA:4096 (622.520543ms)
|
|
401
|
+
|
|
402
|
+
▶ ECDSA:P-256
|
|
403
|
+
✔ HTTP1 (6.356325ms)
|
|
404
|
+
✔ HTTP2 (10.260146ms)
|
|
405
|
+
✔ ECDSA:P-256 (59.91212ms)
|
|
406
|
+
|
|
407
|
+
▶ ECDSA:P-384
|
|
408
|
+
✔ HTTP1 (8.192784ms)
|
|
409
|
+
✔ HTTP2 (16.138147ms)
|
|
410
|
+
✔ ECDSA:P-384 (66.214344ms)
|
|
411
|
+
|
|
412
|
+
▶ ECDSA:P-521
|
|
413
|
+
✔ HTTP1 (9.829523ms)
|
|
414
|
+
✔ HTTP2 (14.905145ms)
|
|
415
|
+
✔ ECDSA:P-521 (71.622241ms)
|
|
416
|
+
|
|
417
|
+
▶ EdDSA:Ed25519
|
|
418
|
+
✔ HTTP1 (6.453652ms)
|
|
419
|
+
✔ HTTP2 (6.992268ms)
|
|
420
|
+
✔ EdDSA:Ed25519 (50.780468ms)
|
|
421
|
+
|
|
422
|
+
▶ EdDSA:Ed448
|
|
423
|
+
✔ HTTP1 (5.421677ms)
|
|
424
|
+
✔ HTTP2 (7.588945ms)
|
|
425
|
+
✔ EdDSA:Ed448 (49.520972ms)
|
|
426
|
+
✔ TLSv1.3 (1040.826701ms)
|
|
427
|
+
✔ HTTPS server with self-signed certificate (2046.10023ms)
|
|
428
|
+
✔ @superhero/http-server (2290.563163ms)
|
|
429
|
+
|
|
430
|
+
tests 68
|
|
431
|
+
suites 8
|
|
432
|
+
pass 68
|
|
433
|
+
|
|
434
|
+
--------------------------------------------------------------------------------------------------------------
|
|
435
|
+
file | line % | branch % | funcs % | uncovered lines
|
|
436
|
+
--------------------------------------------------------------------------------------------------------------
|
|
437
|
+
index.js | 91.75 | 91.18 | 74.07 | 92-94 128-129 135-137 266-269 369-373 389-394 397-402 405-410
|
|
438
|
+
index.test.js | 100.00 | 100.00 | 100.00 |
|
|
439
|
+
view.js | 92.98 | 88.89 | 84.21 | 133-138 196-200 238-239 247-253
|
|
440
|
+
--------------------------------------------------------------------------------------------------------------
|
|
441
|
+
all files | 95.31 | 93.68 | 86.61 |
|
|
442
|
+
--------------------------------------------------------------------------------------------------------------
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## License
|
|
446
|
+
|
|
447
|
+
This project is licensed under the MIT License.
|
|
448
|
+
|
|
449
|
+
## Contributing
|
|
450
|
+
|
|
451
|
+
Feel free to submit issues or pull requests for improvements or additional features.
|