@momsfriendlydevco/cowboy 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/LICENSE +20 -0
- package/README.md +375 -0
- package/lib/cowboy.js +178 -0
- package/lib/debug.js +8 -0
- package/lib/request.js +39 -0
- package/lib/response.js +101 -0
- package/lib/testkit.js +139 -0
- package/middleware/cors.js +18 -0
- package/middleware/index.js +13 -0
- package/middleware/validate.js +13 -0
- package/middleware/validateBody.js +7 -0
- package/middleware/validateParams.js +7 -0
- package/middleware/validateQuery.js +7 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 1337 IP Pty Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
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, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
@MomsfriendlyDevCo/Cowboy
|
|
2
|
+
=========================
|
|
3
|
+
A friendler wrapper around the Cloudflare [Wrangler SDK](https://github.com/cloudflare/workers-sdk)
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
|
|
7
|
+
* Automatic CORS handling
|
|
8
|
+
* Basic router support
|
|
9
|
+
* Express-like `req` + `res` object for routes
|
|
10
|
+
* Built in middleware + request validation via [Joi](https://joi.dev)
|
|
11
|
+
* Built-in debug support for testkits + Wrangler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
========
|
|
16
|
+
|
|
17
|
+
Simple request output
|
|
18
|
+
---------------------
|
|
19
|
+
Example `src/worker.js` file providing a GET server which generates random company profiles:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import {faker} from '@faker-js/faker';
|
|
23
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
24
|
+
|
|
25
|
+
export default cowboy()
|
|
26
|
+
.use('cors') // Inject CORS functionality in every request
|
|
27
|
+
.get('/', ()=> ({
|
|
28
|
+
name: faker.company.name(),
|
|
29
|
+
motto: faker.company.catchPhrase(),
|
|
30
|
+
}))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ReST server example
|
|
36
|
+
-------------------
|
|
37
|
+
Example `src/worker.js` file providing a GET / POST ReST-like server:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
41
|
+
|
|
42
|
+
export default cowboy()
|
|
43
|
+
.use('cors')
|
|
44
|
+
.get('/widgets', ()=> // Fetch a list of widgets
|
|
45
|
+
widgetStore.fetchAll()
|
|
46
|
+
)
|
|
47
|
+
.post('/widgets', async (req, res) => { // Create a new widget
|
|
48
|
+
let newWidget = await widgetStore.create(req.body);
|
|
49
|
+
res.send({id: newWidget.id}); // Explicitly send response
|
|
50
|
+
})
|
|
51
|
+
.get('/widgets/:id', // Validate params + fetch an existing widget
|
|
52
|
+
['validateParams', joi => ({ // Use the 'validateParams' middleware with options
|
|
53
|
+
id: joi.number().required().above(10000).below(99999),
|
|
54
|
+
})],
|
|
55
|
+
req => widgetStore.fetch(req.params.id),
|
|
56
|
+
)
|
|
57
|
+
.delete('/widgets/:id', // Try to delete a widget
|
|
58
|
+
(req, res) => { // Apply custom middleware
|
|
59
|
+
let isAllowed = await widgetStore.userIsValid(req.headers.auth);
|
|
60
|
+
if (!isAllowed) return res.sendStatus(403); // Stop bad actors
|
|
61
|
+
},
|
|
62
|
+
req => widgetStore.delete(req.params.id)
|
|
63
|
+
)
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Debugging
|
|
68
|
+
---------
|
|
69
|
+
This module uses the [Debug NPM](https://github.com/visionmedia/debug#readme). To enable simply set the `DEBUG` environment variable to include `cowboy`.
|
|
70
|
+
|
|
71
|
+
Debugging workers in Testkits will automatically detect this token and enable debugging there. Use the `debug` export within Testkits to see output.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
API
|
|
76
|
+
===
|
|
77
|
+
|
|
78
|
+
cowboy()
|
|
79
|
+
--------
|
|
80
|
+
```javascript
|
|
81
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
82
|
+
```
|
|
83
|
+
Instanciate a `Cowboy` class instance and provide a simple router skeleton.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Cowboy
|
|
87
|
+
------
|
|
88
|
+
```javascript
|
|
89
|
+
import {Cowboy} from '@momsfriendlydevco/cowboy';
|
|
90
|
+
```
|
|
91
|
+
The instance created by `cowboy()`.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
Cowboy.delete(path) / .get() / .head() / .post() / .put() / .options()
|
|
95
|
+
----------------------------------------------------------------------
|
|
96
|
+
Queue up a route with a given path.
|
|
97
|
+
|
|
98
|
+
Each component is made up of a path + any number of middleware handlers.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
let router = new Cowboy()
|
|
102
|
+
.get('/my/path', middleware1, middleware2...)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Notes:
|
|
106
|
+
* All middleware items are called in sequence - and are async waited-on)
|
|
107
|
+
* If any middleware functions fail the entire chain aborts with an error
|
|
108
|
+
* All middleware functions are called as `(CowboyRequest, CowboyResponse)`
|
|
109
|
+
* If any middleware functions call `res.end()` (or any of its automatic methods like `res.send()` / `res.sendStatus()`) the chain also aborts successfully
|
|
110
|
+
* If the last middleware function returns a non response object - i.e. the function didn't call `res.send()` its assumed to be a valid output and is automatically wrapped
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
Cowboy.use(middleware)
|
|
114
|
+
----------------------
|
|
115
|
+
Queue up a universal middleware handler which will be used on *all* endpoints.
|
|
116
|
+
Middleware is called as per `Cowboy.get()` and its equivelents.
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
Cowboy.resolve(CowboyRequest)
|
|
120
|
+
-----------------------------
|
|
121
|
+
Find the matching route that would be used if given a prototype request.
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
Cowboy.fetch(CloudflareRequest, CloudflareEnv)
|
|
125
|
+
----------------------------------------------
|
|
126
|
+
Execute the router when given various Cloudflare inputs.
|
|
127
|
+
|
|
128
|
+
This function will, in order:
|
|
129
|
+
|
|
130
|
+
1. Enable debugging if required
|
|
131
|
+
2. Create `(req:CowboyRequest, res:CowboyResponse)`
|
|
132
|
+
3. Execute all middleware setup via `Cowboy.use()`
|
|
133
|
+
4. Find a matching route - if no route is found, raise a 404 and quit
|
|
134
|
+
5. Execute the matching route middleware, in sequence
|
|
135
|
+
6. Return the final response - if it the function did not already explicitly do so
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
CowboyRequest
|
|
139
|
+
-------------
|
|
140
|
+
```javascript
|
|
141
|
+
import CowboyRequest from '@momsfriendlydevco/cowboy/request';
|
|
142
|
+
```
|
|
143
|
+
A wrapped version of the incoming `CloudflareRequest` object.
|
|
144
|
+
|
|
145
|
+
This object is identical to the original [CloudflareRequest](https://developers.cloudflare.com/workers/runtime-apis/request/#properties) object with the following additions:
|
|
146
|
+
|
|
147
|
+
| Property | Type | Description |
|
|
148
|
+
|------------|----------|----------------------------------------------------------|
|
|
149
|
+
| `path` | `String` | Extracted `url.pathname` portion of the incoming request |
|
|
150
|
+
| `hostname` | `String` | Extracted `url.hostname` portion of the incoming request |
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
CowboyResponse
|
|
154
|
+
--------------
|
|
155
|
+
```javascript
|
|
156
|
+
import CowboyResponse from '@momsfriendlydevco/cowboy/request';
|
|
157
|
+
```
|
|
158
|
+
An Express-like response object.
|
|
159
|
+
Calling any method which ends the session will cause the middleware chain to terminate and the response to be served back.
|
|
160
|
+
|
|
161
|
+
This object contains various Express-like utility functions:
|
|
162
|
+
|
|
163
|
+
| Method | Description |
|
|
164
|
+
|-------------------------------------|----------------------------------------------------------|
|
|
165
|
+
| `set(options)` | Set response output headers (using an object) |
|
|
166
|
+
| `set(header, value)` | Alternate method to set headers individually |
|
|
167
|
+
| `send(data, end=true)` | Set the output response and optionally end the session |
|
|
168
|
+
| `end(data?, end=true)` | Set the output response and optionally end the session |
|
|
169
|
+
| `sendStatus(code, data?, end=true)` | Send a HTTP response code and optionally end the session |
|
|
170
|
+
| `status(code)` | Set the HTTP response code |
|
|
171
|
+
| `toCloudflareResponse()` | Return the equivelent CloudflareResponse object |
|
|
172
|
+
|
|
173
|
+
All functions (except `toCloudflareResponse()`) are chainable and return the original `CowboyResponse` instance.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
CowboyTestkit
|
|
177
|
+
-------------
|
|
178
|
+
```javascript
|
|
179
|
+
import CowboyTestkit from '@momsfriendlydevco/cowboy/testkit';
|
|
180
|
+
```
|
|
181
|
+
A series of utilities to help write testkits with Wrangler + Cowboy.
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
CowboyTestkit.cowboyMocha()
|
|
185
|
+
---------------------------
|
|
186
|
+
Inject various Mocha before/after tooling.
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
import axios from 'axios';
|
|
190
|
+
import {cowboyMocha} from '@momsfriendlydevco/cowboy/testkit';
|
|
191
|
+
import {expect} from 'chai';
|
|
192
|
+
|
|
193
|
+
describe('My Wrangler Endpoint', ()=> {
|
|
194
|
+
|
|
195
|
+
// Inject Cowboy/mocha testkit handling
|
|
196
|
+
cowboyMocha({
|
|
197
|
+
axios,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let checkCors = headers => {
|
|
201
|
+
expect(headers).to.be.an.instanceOf(axios.AxiosHeaders);
|
|
202
|
+
expect(headers).to.have.property('access-control-allow-origin', '*');
|
|
203
|
+
expect(headers).to.have.property('access-control-allow-methods', 'GET, POST, OPTIONS');
|
|
204
|
+
expect(headers).to.have.property('access-control-allow-headers', '*');
|
|
205
|
+
expect(headers).to.have.property('content-type', 'application/json;charset=UTF-8');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
it('should expose CORS headers', ()=>
|
|
209
|
+
axios('/', {
|
|
210
|
+
method: 'OPTIONS',
|
|
211
|
+
}).then(({data, headers}) => {
|
|
212
|
+
expect(data).to.be.equal('ok');
|
|
213
|
+
checkCors(headers);
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
it('should do something useful', ()=>
|
|
218
|
+
axios('/', {
|
|
219
|
+
method: 'get',
|
|
220
|
+
}).then(({data, headers}) => {
|
|
221
|
+
checkCors(headers);
|
|
222
|
+
|
|
223
|
+
// ... Your functionality checks ... //
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
CowboyTestkit.start(options)
|
|
232
|
+
----------------------------
|
|
233
|
+
Boot a wranger instance in the background and prepare for testing.
|
|
234
|
+
Returns a promise.
|
|
235
|
+
|
|
236
|
+
| Option | Type | Default | Description |
|
|
237
|
+
|----------------|------------|---------------|-----------------------------------------------------------|
|
|
238
|
+
| `axios` | `Axios` | | Axios instance to mutate with the base URL, if specified |
|
|
239
|
+
| `logOutput` | `Function` | | Function to wrap STDOUT output. Called as `(line:String)` |
|
|
240
|
+
| `logOutputErr` | `Function` | | Function to wrap STDERR output. Called as `(line:String)` |
|
|
241
|
+
| `host` | `String` | `'127.0.0.1'` | Host to run Wrangler on |
|
|
242
|
+
| `port` | `String` | `8787` | Host to run Wrangler on |
|
|
243
|
+
| `logLevel` | `String` | `'log'` | Log level to instruct Wrangler to run as |
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
CowboyTestkit.stop()
|
|
247
|
+
--------------------
|
|
248
|
+
Terminate any running Wrangler background processes.
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
Middleware
|
|
252
|
+
==========
|
|
253
|
+
Cowboy ships with out-of-the-box middleware.
|
|
254
|
+
Middleware are simple functions which accept the paramters `(req:CowboyRequest, res:CowboyResponse)` and can modify the request, halt output with a call to `res` or perform other Async actions before continuing to the next middleware item.
|
|
255
|
+
|
|
256
|
+
To use middleware in your routes you can either declare it using `.use(middleware)` - which installs it globally or `.ROUTE(middleware...)` which installs it only for that route.
|
|
257
|
+
|
|
258
|
+
Middleware can be declared in the following ways:
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
262
|
+
|
|
263
|
+
// Shorthand with defaults - just specify the name
|
|
264
|
+
cowboy()
|
|
265
|
+
.get('/path',
|
|
266
|
+
'cors',
|
|
267
|
+
(req, res) => /* ... */
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
// Name + options - specify an array with an optional options object
|
|
271
|
+
cowboy()
|
|
272
|
+
.get('/path',
|
|
273
|
+
['cors', {
|
|
274
|
+
option1: value1,
|
|
275
|
+
/* ... */
|
|
276
|
+
}],
|
|
277
|
+
(req, res) => /* ... */
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
// Middleware function - include the import
|
|
282
|
+
import cors from '@momsfriendlydevco/cowboy/middleware/cors';
|
|
283
|
+
cowboy()
|
|
284
|
+
.get('/path',
|
|
285
|
+
cors({
|
|
286
|
+
option1: value1,
|
|
287
|
+
/* ... */
|
|
288
|
+
}),
|
|
289
|
+
(req, res) => /* ... */
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
cors
|
|
295
|
+
----
|
|
296
|
+
Inject simple CORS headers to allow websites to use the endpoint from the browser frontend.
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
validate
|
|
300
|
+
--------
|
|
301
|
+
Validate the incoming `req` object using [Joyful](https://github.com/MomsFriendlyDevCo/Joyful).
|
|
302
|
+
The only argument is the Joyful validator which is run against `req`.
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
306
|
+
|
|
307
|
+
// Shorthand with defaults - just specify the name
|
|
308
|
+
cowboy()
|
|
309
|
+
.get('/path',
|
|
310
|
+
['validate', joi => {
|
|
311
|
+
body: {
|
|
312
|
+
widget: joi.string().required().valid('froody', 'doodad'),
|
|
313
|
+
size: joi.number().optional(),
|
|
314
|
+
},
|
|
315
|
+
})],
|
|
316
|
+
(req, res) => /* ... */
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
validateBody
|
|
321
|
+
------------
|
|
322
|
+
Shorthand validator which runs validation on the `req.body` parameter only.
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
327
|
+
|
|
328
|
+
// Shorthand with defaults - just specify the name
|
|
329
|
+
cowboy()
|
|
330
|
+
.get('/path',
|
|
331
|
+
['validateBody', joi => {
|
|
332
|
+
widget: joi.string().required().valid('froody', 'doodad'),
|
|
333
|
+
size: joi.number().optional(),
|
|
334
|
+
})],
|
|
335
|
+
(req, res) => /* ... */
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
validateParams
|
|
341
|
+
--------------
|
|
342
|
+
Shorthand validator which runs validation on the `req.params` parameter only.
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
347
|
+
|
|
348
|
+
// Shorthand with defaults - just specify the name
|
|
349
|
+
cowboy()
|
|
350
|
+
.get('/widgets/:id',
|
|
351
|
+
['validateParams', joi => {
|
|
352
|
+
id: joi.string().requried(),
|
|
353
|
+
})],
|
|
354
|
+
(req, res) => /* ... */
|
|
355
|
+
)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
validateQuery
|
|
360
|
+
-------------
|
|
361
|
+
Shorthand validator which runs validation on the `req.query` parameter only.
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
import cowboy from '@momsfriendlydevco/cowboy';
|
|
366
|
+
|
|
367
|
+
// Shorthand with defaults - just specify the name
|
|
368
|
+
cowboy()
|
|
369
|
+
.get('/widgets/search',
|
|
370
|
+
['validateQuery', joi => {
|
|
371
|
+
q: joi.string().requried(),
|
|
372
|
+
})],
|
|
373
|
+
(req, res) => /* ... */
|
|
374
|
+
)
|
|
375
|
+
```
|
package/lib/cowboy.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import debug from '#lib/debug';
|
|
2
|
+
import CowboyMiddleware from '#middleware';
|
|
3
|
+
import CowboyRequest from '#lib/request';
|
|
4
|
+
import CowboyResponse from '#lib/response';
|
|
5
|
+
|
|
6
|
+
export class Cowboy {
|
|
7
|
+
/**
|
|
8
|
+
* @name CowboyMiddleware
|
|
9
|
+
* @description An instance of a middleware item
|
|
10
|
+
* @type {Function|String|Array<String,Object>} Either a compiled function factory, pointer to a known CowboyMiddleware entity or Record(name, options) pair
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @name CoyboyRoute
|
|
15
|
+
* @description A Cowboy Route entity
|
|
16
|
+
* @type {Object}
|
|
17
|
+
* @property {Array<String>} methods Methods to accept (each is an upper case HTTP method)
|
|
18
|
+
* @property {Array<String|RegExp>} paths Path matchers to accept
|
|
19
|
+
* @property {Array<CowboyMiddleware>} [middleware] Middleware / resolvers to use for the route, should it match
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* List of middleware which will be called on all matching routes
|
|
24
|
+
* @type Array<CowboyMiddleware>
|
|
25
|
+
*/
|
|
26
|
+
earlyMiddleware = [];
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List of routes which will be examined in order until a match occurs
|
|
31
|
+
* @type {Array<CowboyRoute>}
|
|
32
|
+
*/
|
|
33
|
+
routes = [];
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Queue up a middleware path
|
|
38
|
+
* All given middleware is called in sequence, if middleware
|
|
39
|
+
*
|
|
40
|
+
* @param {String|Array<String>} methods A method matcher or array of available methods
|
|
41
|
+
* @param {String|RegExp|Array<String|RegExp>} paths A prefix path to match
|
|
42
|
+
* @param {CowboyMiddleware} middleware... Middleware to call in sequence
|
|
43
|
+
*
|
|
44
|
+
* @returns {Cowboy} This chainable Cowboy router instance
|
|
45
|
+
*/
|
|
46
|
+
route(methods, paths, ...middleware) {
|
|
47
|
+
this.routes.push({
|
|
48
|
+
methods: Array.isArray(methods) ? methods : [methods],
|
|
49
|
+
paths: Array.isArray(paths) ? paths : [paths],
|
|
50
|
+
middleware,
|
|
51
|
+
})
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prepend middleware which will be used for all routes
|
|
58
|
+
* @param {CowboyMiddleware} middleware Middleware to use
|
|
59
|
+
*/
|
|
60
|
+
use(...middleware) {
|
|
61
|
+
this.earlyMiddleware.push(middleware);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the route to use when passed a prototype request
|
|
68
|
+
* @param {CowboyRequest} req The incoming request to match
|
|
69
|
+
* @returns {CowboyRoute} The matching route to use, if any
|
|
70
|
+
*/
|
|
71
|
+
resolve(req) {
|
|
72
|
+
return this.routes.find(route =>
|
|
73
|
+
route.methods.includes(req.method) // Method matches
|
|
74
|
+
&& route.paths.some(path => // Path matches
|
|
75
|
+
typeof path == 'string' ? req.path == path
|
|
76
|
+
: path instanceof RegExp ? path.test(req.path)
|
|
77
|
+
: (()=> { throw new Error('Path is not a String or RegExp') })()
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Action an incoming route by resolving + walking down its middleware chain
|
|
85
|
+
* @param {CloudflareRequest} req The incoming request
|
|
86
|
+
* @param {Object} [env] Optional environment passed from Cloudflare
|
|
87
|
+
* @returns {Promise<CowboyResponse>} A promise which will eventually resolve when all middleware completes
|
|
88
|
+
*/
|
|
89
|
+
async fetch(cfReq, env) {
|
|
90
|
+
if (env.COWBOY_DEBUG) {
|
|
91
|
+
debug.enabled = true;
|
|
92
|
+
debug('Cowboy Worker debugging is enabled');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create basic [req]uest / [res]ponse objects
|
|
96
|
+
let req = new CowboyRequest(cfReq, {router: this});
|
|
97
|
+
let res = new CowboyResponse();
|
|
98
|
+
|
|
99
|
+
// Exec all earlyMiddleware - every time
|
|
100
|
+
await this.execMiddleware({req, res, middleware: this.earlyMiddleware});
|
|
101
|
+
|
|
102
|
+
// Find matching route
|
|
103
|
+
let route = this.resolve(req);
|
|
104
|
+
if (!route) {
|
|
105
|
+
if (debug.enabled) {
|
|
106
|
+
debug(`No matching route for "${req.method} ${req.path}"`);
|
|
107
|
+
this.routes.forEach((r, i) =>
|
|
108
|
+
debug(
|
|
109
|
+
`Route #${i}`,
|
|
110
|
+
r.methods.length == 1 ? r.methods[0] : r.methods.join('|'),
|
|
111
|
+
r.paths.length == 1 ? r.paths[0] : r.paths.join('|'),
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return res.sendStatus(404).toCloudflareResponse(); // No matching route
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Exec route middleware
|
|
119
|
+
let response = await this.execMiddleware({req, res, middleware: route.middleware});
|
|
120
|
+
|
|
121
|
+
if (!response) throw new Error('Middleware chain ended without returning a response!');
|
|
122
|
+
if (!response.toCloudflareResponse) throw new Error('Eventual middleware chain output should have a .toCloudflareResponse() method');
|
|
123
|
+
return response.toCloudflareResponse();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async execMiddleware({middleware, req, res}) {
|
|
128
|
+
let middlewareStack = [...middleware] // Shallow copy middleware stack to execute
|
|
129
|
+
.map(m => {
|
|
130
|
+
let mFunc =
|
|
131
|
+
typeof m == 'function' ? m // Already a function
|
|
132
|
+
: typeof m == 'string' ? CowboyMiddleware[m]() // Lookup from middleware with defaults
|
|
133
|
+
: Array.isArray(m) ? CowboyMiddleware[m[0]](m[1]) // Lookup from middleware with options
|
|
134
|
+
: (()=> { throw new Error(`Unknown middleware type "${typeof m}"`) })()
|
|
135
|
+
|
|
136
|
+
if (!mFunc) throw new Error('Cowboy Middleware must be a function, string or Record(name, options)');
|
|
137
|
+
return mFunc;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let response; // Response to eventually send
|
|
141
|
+
while (middlewareStack.length > 0) {
|
|
142
|
+
let middleware = middlewareStack.shift();
|
|
143
|
+
response = await middleware(req, res);
|
|
144
|
+
if (response?.hasSent) { // Stop middleware chain as some intermediate has signalled the chain should end
|
|
145
|
+
response = res;
|
|
146
|
+
break;
|
|
147
|
+
} else if (response && !(response instanceof CowboyResponse) && middlewareStack.length == 0) { // Last item in middleware chain returned something but it doesn't look like a regular response - wrap it
|
|
148
|
+
response = res.end(response);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return response;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
// Alias functions to route
|
|
156
|
+
delete(path, ...middleware) { return this.route('DELETE', path, ...middleware) }
|
|
157
|
+
get(path, ...middleware) { return this.route('GET', path, ...middleware) }
|
|
158
|
+
head(path, ...middleware) { return this.route('HEAD', path, ...middleware) }
|
|
159
|
+
post(path, ...middleware) { return this.route('POST', path, ...middleware) }
|
|
160
|
+
put(path, ...middleware) { return this.route('PUT', path, ...middleware) }
|
|
161
|
+
options(path, ...middleware) { return this.route('OPTIONS', path, ...middleware) }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Wrap an incoming Wrangler request
|
|
167
|
+
* @returns {Object} A Wrangler compatible object
|
|
168
|
+
*/
|
|
169
|
+
export default function cowboy(options) {
|
|
170
|
+
let cowboyInstance = new Cowboy(options);
|
|
171
|
+
|
|
172
|
+
// Utterly ridiculous fix to subclass 'fetch' as a POJO function as Wrangler seems to only check hasOwnProperty for the fetch method
|
|
173
|
+
Object.assign(cowboyInstance, {
|
|
174
|
+
fetch: cowboyInstance.fetch.bind(cowboyInstance),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return cowboyInstance;
|
|
178
|
+
}
|
package/lib/debug.js
ADDED
package/lib/request.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny wrapper around Wrangler to wrap its default Request object in an Express-like structure
|
|
3
|
+
* @extends CloudflareRequest
|
|
4
|
+
*/
|
|
5
|
+
export default class CowboyRequest {
|
|
6
|
+
/**
|
|
7
|
+
* Extracted request path with leading slash
|
|
8
|
+
* @type {String}
|
|
9
|
+
*/
|
|
10
|
+
path;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extracted hostname being addressed
|
|
15
|
+
*/
|
|
16
|
+
hostname;
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
constructor(cfReq, props) {
|
|
20
|
+
// Copy all cfReq keys locally as a shallow copy
|
|
21
|
+
Object.assign(
|
|
22
|
+
this,
|
|
23
|
+
Object.fromEntries(
|
|
24
|
+
Object.keys(Request.prototype).map(key =>
|
|
25
|
+
[
|
|
26
|
+
key,
|
|
27
|
+
cfReq[key],
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
),
|
|
31
|
+
props,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Break appart the incoming URL
|
|
35
|
+
let url = new URL(cfReq.url);
|
|
36
|
+
this.path = url.pathname;
|
|
37
|
+
this.hostname = url.hostname;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/response.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic all-in-one response wrapper to mangle responses without having to memorize all the weird syntax that Wrangler / Cloudflare workers need
|
|
3
|
+
*/
|
|
4
|
+
export default class CowboyResponse {
|
|
5
|
+
body = '';
|
|
6
|
+
code = null;
|
|
7
|
+
headers = {};
|
|
8
|
+
hasSent = false;
|
|
9
|
+
CloudflareResponse = Response;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Assign various output headers
|
|
13
|
+
* @param {Object|String} options Either an header object to be merged or the header to set
|
|
14
|
+
* @param {*} [value] If `options` is a string, the value of the header
|
|
15
|
+
* @returns {CowboyResponse} This chainable instance
|
|
16
|
+
*/
|
|
17
|
+
set(options, value) {
|
|
18
|
+
if (typeof options == 'string') {
|
|
19
|
+
this.headers[options] = value;
|
|
20
|
+
} else {
|
|
21
|
+
Object.assign(this.headers, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send data and (optionally) mark the response as complete
|
|
30
|
+
* @param {*} data The data to transmit
|
|
31
|
+
* @param {Boolean} [end=true] Whether to also end the transmision
|
|
32
|
+
* @returns {CowboyResponse} This chainable instance
|
|
33
|
+
*/
|
|
34
|
+
send(data, end = true) {
|
|
35
|
+
if (this.code === null) this.code = 200; // Assume OK if not told otherwise
|
|
36
|
+
|
|
37
|
+
if (typeof data == 'string') {
|
|
38
|
+
this.body = data;
|
|
39
|
+
} else {
|
|
40
|
+
this.body = JSON.stringify(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Mark transmition as ended
|
|
44
|
+
if (end) this.hasSent = true;
|
|
45
|
+
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mark the transmission as complete
|
|
52
|
+
* @param {*} [data] Optional data to send before ending
|
|
53
|
+
* @returns {CowboyResponse} This chainable instance
|
|
54
|
+
*/
|
|
55
|
+
end(data) {
|
|
56
|
+
if (data) this.send(data);
|
|
57
|
+
this.hasSent = true;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set the status code we are responding with
|
|
64
|
+
* @param {Number} code The HTTP response code to respond with
|
|
65
|
+
* @returns {CowboyResponse} This chainable instance
|
|
66
|
+
*/
|
|
67
|
+
status(code) {
|
|
68
|
+
this.code = code;
|
|
69
|
+
if (!this.body) this.body = 'ok'; // Set body payload if we don't already have one
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the response status code and (optionally) end the transmission
|
|
76
|
+
* @param {Number} code The HTTP response code to respond with
|
|
77
|
+
* @param {*} [data] Optional data to send before ending
|
|
78
|
+
* @param {Boolean} [end=true] Whether to also end the transmision
|
|
79
|
+
* @returns {CowboyResponse} This chainable instance
|
|
80
|
+
*/
|
|
81
|
+
sendStatus(code, data, end = true) {
|
|
82
|
+
if (data) throw new Error('Data is not allowed with CowboyResponse.sendStatus(code) - use CowBoyresponse.status(CODE).send(DATA) instead');
|
|
83
|
+
this.status(code);
|
|
84
|
+
if (end) this.end();
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert the current CoyboyResponse into a CloudflareResponse object
|
|
91
|
+
* @returns {CloudflareResponse} The cloudflare output object
|
|
92
|
+
*/
|
|
93
|
+
toCloudflareResponse() {
|
|
94
|
+
let cfOptions = {
|
|
95
|
+
status: this.code,
|
|
96
|
+
headers: this.headers,
|
|
97
|
+
};
|
|
98
|
+
console.log('Build response', JSON.stringify(cfOptions));
|
|
99
|
+
return new this.CloudflareResponse(this.body, cfOptions);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/lib/testkit.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import Debug from 'debug';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import toml from 'toml';
|
|
5
|
+
|
|
6
|
+
const debug = Debug('cowboy');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The currently active worker (if any)
|
|
10
|
+
* @type {ChildProcess}
|
|
11
|
+
*/
|
|
12
|
+
export let worker;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Boot a wranger instance in the background
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} [options] Additional options to mutate behaviour
|
|
19
|
+
* @param {Axios} [options.axios] Axios instance to mutate with the base URL, if specified
|
|
20
|
+
* @param {Function} [options.logOutput] Function to wrap STDOUT output. Called as `(line:String)`
|
|
21
|
+
* @param {Function} [options.logOutputErr] Function to wrap STDERR output. Called as `(line:String)`
|
|
22
|
+
* @param {String} [options.host='127.0.0.1'] Host to run Wrangler on
|
|
23
|
+
* @param {String} [options.port=8787] Host to run Wrangler on
|
|
24
|
+
* @param {String} [options.logLevel='log'] Log level to instruct Wrangler to run as
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
27
|
+
*/
|
|
28
|
+
export function start(options) {
|
|
29
|
+
let settings = {
|
|
30
|
+
axios: null,
|
|
31
|
+
logOutput: output => console.log('WRANGLER>', output),
|
|
32
|
+
logOutputErr: output => console.log('WRANGLER!', output),
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
port: 8787,
|
|
35
|
+
logLevel: 'log',
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
debug('Start cowboy testkit');
|
|
40
|
+
let wranglerConfig; // Eventual wrangler config
|
|
41
|
+
|
|
42
|
+
return Promise.resolve()
|
|
43
|
+
// Read in project `wrangler.toml` {{{
|
|
44
|
+
.then(()=> fs.readFile('wrangler.toml', 'utf8'))
|
|
45
|
+
.then(contents => toml.parse(contents))
|
|
46
|
+
.then(config => {
|
|
47
|
+
debug('Read config', config);
|
|
48
|
+
if (!Object.hasOwn(config, 'send_metrics')) throw new Error('Please append `send_metrics = false` to wrangler.toml to Warngler asking questions during boot');
|
|
49
|
+
wranglerConfig = config;
|
|
50
|
+
})
|
|
51
|
+
// }}}
|
|
52
|
+
// Launch worker {{{
|
|
53
|
+
.then(()=> {
|
|
54
|
+
debug('Running Wrangler against script', wranglerConfig.main);
|
|
55
|
+
|
|
56
|
+
let isRunning = false;
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
worker = spawn('node', [
|
|
59
|
+
'./node_modules/.bin/wrangler',
|
|
60
|
+
'dev',
|
|
61
|
+
`--host=${settings.host}`,
|
|
62
|
+
`--port=${settings.port}`,
|
|
63
|
+
`--log-level=${settings.logLevel}`,
|
|
64
|
+
...(debug.enabled ? [
|
|
65
|
+
'--var=COWBOY_DEBUG:1'
|
|
66
|
+
]: []),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
worker.stdout.on('data', data => {
|
|
70
|
+
let output = data.toString().replace(/\r?\n$/, '');
|
|
71
|
+
|
|
72
|
+
if (!isRunning && /Ready on https?:\/\//.test(output)) {
|
|
73
|
+
isRunning = true;
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
settings.logOutput(output);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
worker.stderr.on('data', data => {
|
|
81
|
+
settings.logOutputErr(data.toString().replace(/\r?\n$/, ''))
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
worker.on('error', reject);
|
|
85
|
+
|
|
86
|
+
worker.on('close', code => {
|
|
87
|
+
debug('Wrangler exited with code', code);
|
|
88
|
+
worker = null;
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
// }}}
|
|
93
|
+
// .then(()=> new Promise(resolve => setTimeout(resolve, 10 * 1000)))
|
|
94
|
+
// Mutate axios if provided {{{
|
|
95
|
+
.then(()=> {
|
|
96
|
+
if (settings.axios) {
|
|
97
|
+
let baseURL = `http://${settings.host}:${settings.port}`;
|
|
98
|
+
debug('Setting axios BaseURL', baseURL);
|
|
99
|
+
settings.axios.defaults.baseURL = baseURL;
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
// }}}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stop background wrangler instances
|
|
107
|
+
* @returns {Promise} A promise which resolves when the operation has completed
|
|
108
|
+
*/
|
|
109
|
+
export function stop() {
|
|
110
|
+
if (!worker) return; // Worker not active anyway
|
|
111
|
+
debug('Stop cowboy testkit');
|
|
112
|
+
|
|
113
|
+
debug(`Stopping active Wrangler worker PID #${worker.pid}`);
|
|
114
|
+
worker.kill('SIGTERM');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Inject various Mocha before/after tooling
|
|
119
|
+
* @param {Object} [options] Additional options to pass to `start()`
|
|
120
|
+
*/
|
|
121
|
+
export function cowboyMocha(options) {
|
|
122
|
+
|
|
123
|
+
before('start cowboy/testkit', function() {
|
|
124
|
+
this.timeout(30 * 1000);
|
|
125
|
+
return start(options);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
after('stop cowboy/testkit', function() {
|
|
129
|
+
this.timeout(5 * 1000);
|
|
130
|
+
return stop();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default {
|
|
136
|
+
cowboyMocha,
|
|
137
|
+
stop,
|
|
138
|
+
start,
|
|
139
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function CowboyMiddlewareCORS(headers) {
|
|
2
|
+
let injectHeaders = headers || {
|
|
3
|
+
'Access-Control-Allow-Origin': '*',
|
|
4
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
5
|
+
'Access-Control-Allow-Headers': '*',
|
|
6
|
+
'Content-Type': 'application/json;charset=UTF-8',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
return (req, res) => {
|
|
10
|
+
// Always inject CORS headers
|
|
11
|
+
res.set(injectHeaders);
|
|
12
|
+
|
|
13
|
+
// Handle hits to OPTIONS '/' endpoint
|
|
14
|
+
req.router.options('/', (req, res) => {
|
|
15
|
+
return res.sendStatus(200);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import cors from '#middleware/cors';
|
|
2
|
+
import validate from '#middleware/validate';
|
|
3
|
+
import validateBody from '#middleware/validateBody';
|
|
4
|
+
import validateParams from '#middleware/validateParams';
|
|
5
|
+
import validateQuery from '#middleware/validateQuery';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
cors,
|
|
9
|
+
validate,
|
|
10
|
+
validateBody,
|
|
11
|
+
validateParams,
|
|
12
|
+
validateQuery,
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import joyful from '@momsfriendlydevco/joyful';
|
|
2
|
+
|
|
3
|
+
export default function CowboyMiddlewareValidate(validator) {
|
|
4
|
+
return (req, res) => {
|
|
5
|
+
let joyfulResult = joyful(req, validator, {throw: false});
|
|
6
|
+
|
|
7
|
+
if (joyfulResult !== true) { // Failed body validation?
|
|
8
|
+
return res
|
|
9
|
+
.status(400)
|
|
10
|
+
.send(joyfulResult)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@momsfriendlydevco/cowboy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Wrapper around Cloudflare Wrangler to provide a more Express-like experience",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"lint": "eslint ."
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"wrangler"
|
|
10
|
+
],
|
|
11
|
+
"type": "module",
|
|
12
|
+
"imports": {
|
|
13
|
+
"#lib/*": "./lib/*.js",
|
|
14
|
+
"#middleware": "./middleware/index.js",
|
|
15
|
+
"#middleware/*": "./middleware/*.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./lib/cowboy.js",
|
|
19
|
+
"./*": "./lib/*.js",
|
|
20
|
+
"middleware": "./middleware/index.js",
|
|
21
|
+
"middleware/*": "./middleware/*.js"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/MomsFriendlyDevCo/Cowboy.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Matt Carter <matt@mfdc.biz>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/MomsFriendlyDevCo/Cowboy/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/MomsFriendlyDevCo/Cowboy#readme",
|
|
33
|
+
"engineStrict": false,
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20.x"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@momsfriendlydevco/joyful": "^1.0.0",
|
|
39
|
+
"toml": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@momsfriendlydevco/eslint-config": "^1.0.7",
|
|
43
|
+
"eslint": "^8.50.0"
|
|
44
|
+
},
|
|
45
|
+
"eslintConfig": {
|
|
46
|
+
"extends": "@momsfriendlydevco",
|
|
47
|
+
"env": {
|
|
48
|
+
"es6": true,
|
|
49
|
+
"node": true
|
|
50
|
+
},
|
|
51
|
+
"parserOptions": {
|
|
52
|
+
"ecmaVersion": 13,
|
|
53
|
+
"sourceType": "module"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|