sleepy-serv 0.4.0 → 0.5.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/package.json +1 -1
- package/src/index.js +11 -2
- package/README.md +0 -483
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import querystring from 'querystring'
|
|
|
4
4
|
import readline from 'node:readline'
|
|
5
5
|
import { stdin, stdout } from 'node:process'
|
|
6
6
|
|
|
7
|
-
import * as _middleware from './middleware
|
|
7
|
+
import * as _middleware from './middleware'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
NotFoundError,
|
|
@@ -13,15 +13,24 @@ import {
|
|
|
13
13
|
|
|
14
14
|
export * from './errors'
|
|
15
15
|
|
|
16
|
-
const ALLOWED_FILES_META = [
|
|
16
|
+
const ALLOWED_FILES_META = [
|
|
17
|
+
'meta.js',
|
|
18
|
+
'meta.ts',
|
|
19
|
+
]
|
|
17
20
|
|
|
18
21
|
const ALLOWED_FILES_METHODS = [
|
|
19
22
|
'head.js',
|
|
23
|
+
'head.ts',
|
|
20
24
|
'get.js',
|
|
25
|
+
'get.ts',
|
|
21
26
|
'put.js',
|
|
27
|
+
'put.ts',
|
|
22
28
|
'post.js',
|
|
29
|
+
'post.ts',
|
|
23
30
|
'patch.js',
|
|
31
|
+
'patch.ts',
|
|
24
32
|
'delete.js',
|
|
33
|
+
'delete.ts',
|
|
25
34
|
]
|
|
26
35
|
|
|
27
36
|
const ALLOWED_FILES_ALL = [
|
package/README.md
DELETED
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
# Sleepy Server
|
|
2
|
-
|
|
3
|
-
A directory-driven web server designed for REST-ful applications
|
|
4
|
-
|
|
5
|
-
## Important Notes
|
|
6
|
-
|
|
7
|
-
- This package requires [`bun.sh`](https://bun.sh) instead of NodeJS to run.
|
|
8
|
-
- This project requires bun v1.2.3 or higher
|
|
9
|
-
|
|
10
|
-
## Getting Started
|
|
11
|
-
|
|
12
|
-
Here's a minimalist example on how to create a sleepy-serv app:
|
|
13
|
-
|
|
14
|
-
```js
|
|
15
|
-
import {
|
|
16
|
-
middleware,
|
|
17
|
-
createApp,
|
|
18
|
-
} from 'sleepy-serv'
|
|
19
|
-
|
|
20
|
-
const PORT = 3000
|
|
21
|
-
|
|
22
|
-
const app = await createApp(PORT, import.meta.dirname)
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The parameter for `import.meta.dirname` can be any directory you prefer, but it's common to point to the same directory as your root `index.js` file. The next step is to create an `/api` directory in the directory that you point to, and begin adding routes.
|
|
26
|
-
|
|
27
|
-
### Return Value
|
|
28
|
-
|
|
29
|
-
`sleepy-serv` was originally built for NodeJS, but it was ported to `bun` recently (before the initial release). The `createApp()` function merely calls `Bun.serve()` under-the-hood, and returns the `app` object that contains two properties:
|
|
30
|
-
- `routes`: Contains a list of all of the routes defined by the file structure. This is useful for debugging.
|
|
31
|
-
- `server`: this is the object that's returned from `Bun.serve()`. The `server` object has an `async` `.stop()` method on it, which can also be used for graceful shutdowns.
|
|
32
|
-
|
|
33
|
-
### Adding Routes
|
|
34
|
-
|
|
35
|
-
Routes are made up as _resources_ and _methods_. Resources are described by a directory path, and methods are described by files created inside of those directories. Resource segments can also represent dynamic routing params by starting the directory name with a colon (`:`).
|
|
36
|
-
|
|
37
|
-
Here's file structure example:
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
/src
|
|
41
|
-
index.js # this is where we called `createApp()`
|
|
42
|
-
/api
|
|
43
|
-
/users
|
|
44
|
-
get.js
|
|
45
|
-
post.js
|
|
46
|
-
/:userId
|
|
47
|
-
get.js
|
|
48
|
-
delete.js
|
|
49
|
-
put.js
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The file structure above create the following routes:
|
|
53
|
-
|
|
54
|
-
- `GET /users`
|
|
55
|
-
- `POST /users`
|
|
56
|
-
- `GET /users/:userId`
|
|
57
|
-
- `DELETE /users/:userId`
|
|
58
|
-
- `PUT /users/:userId`
|
|
59
|
-
|
|
60
|
-
These methods are supported:
|
|
61
|
-
|
|
62
|
-
- GET
|
|
63
|
-
- HEAD
|
|
64
|
-
- PATCH
|
|
65
|
-
- POST
|
|
66
|
-
- PUT
|
|
67
|
-
- DELETE
|
|
68
|
-
|
|
69
|
-
### Method Definition Files
|
|
70
|
-
|
|
71
|
-
The route's logic is implemented in the route definition files. These must `export default` either a function, or an array of functions for middleware purposes. The function signature takes a `BunRequest` object, and must return a `BunResponse` object.
|
|
72
|
-
|
|
73
|
-
Here's an example of a simple handler function:
|
|
74
|
-
|
|
75
|
-
```js
|
|
76
|
-
export default function (req, res, next) {
|
|
77
|
-
console.log('res:', res)
|
|
78
|
-
console.log('next:', next) // null for route handlers
|
|
79
|
-
|
|
80
|
-
return new Response('Hello world')
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
These functions take three parameters:
|
|
85
|
-
- `req`: a `BunRequest` object
|
|
86
|
-
- `res`: a results object
|
|
87
|
-
- `next`: a function to call the next middleware in the chain (or `null` for the final handler)
|
|
88
|
-
|
|
89
|
-
The `res` parameter might seem familiar if coming from ExpressJS, but this parameter has a different purpose. The `res` parameter is meant to act as a dedicated, persistent object that middleware can write their results to. This way, the `req` object stays as a lean `BunRequest`.
|
|
90
|
-
|
|
91
|
-
The `next` parameter is a function that allows middleware to pass control to the next function in the middleware chain. For route handlers (the final function in the chain), `next` will be `null`.
|
|
92
|
-
|
|
93
|
-
These handlers can also be `async` functions and `sleepy-serv` will wait for them to finish before moving on.
|
|
94
|
-
|
|
95
|
-
### Middleware
|
|
96
|
-
|
|
97
|
-
As mentioned earlier, method definition files can also export an array of functions:
|
|
98
|
-
|
|
99
|
-
```js
|
|
100
|
-
export default [
|
|
101
|
-
async (req, res, next) => {
|
|
102
|
-
res.body = await req.json()
|
|
103
|
-
return next() /* call the next middleware */
|
|
104
|
-
},
|
|
105
|
-
(req, res) => { /* notice that the last function doesn't take `next` */
|
|
106
|
-
console.log('JSON body:', res.body)
|
|
107
|
-
|
|
108
|
-
return new Response('Hello world')
|
|
109
|
-
},
|
|
110
|
-
]
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
This is useful if you want to break common logic up into reusable functions. The functions in the array are called in-order, and each receives three parameters: `req`, `res`, and `next`.
|
|
114
|
-
|
|
115
|
-
#### Middleware Chain Behavior
|
|
116
|
-
|
|
117
|
-
Each middleware function must do one of the following:
|
|
118
|
-
- **Call `next()`**: Continue to the next middleware in the chain by returning `next()`
|
|
119
|
-
- **Return a Response**: End the chain (early) by returning a `BunResponse` object
|
|
120
|
-
- **Throw an error**: Stop the chain and trigger error handling
|
|
121
|
-
|
|
122
|
-
The `next` parameter will be `null` for the final function in the chain (the route handler), indicating there are no more middleware functions to call.
|
|
123
|
-
|
|
124
|
-
#### Example with Explicit `next()` Calls
|
|
125
|
-
|
|
126
|
-
```js
|
|
127
|
-
export default [
|
|
128
|
-
(req, res, next) => {
|
|
129
|
-
console.log('1st middleware')
|
|
130
|
-
|
|
131
|
-
if (req.headers.get('authorization')) {
|
|
132
|
-
return next() // continue to next middleware
|
|
133
|
-
} else {
|
|
134
|
-
return new Response('Unauthorized', { status: 401 }) // end chain early
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
(req, res, next) => {
|
|
138
|
-
console.log('2nd middleware')
|
|
139
|
-
|
|
140
|
-
res.user = {
|
|
141
|
-
id: 123,
|
|
142
|
-
name: 'John',
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return next() // continue to route handler
|
|
146
|
-
},
|
|
147
|
-
(req, res) => {
|
|
148
|
-
console.log('Final, "route" handler')
|
|
149
|
-
|
|
150
|
-
return new Response(`Hello ${res.user.name}`)
|
|
151
|
-
},
|
|
152
|
-
]
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
#### Middleware Execution Order
|
|
156
|
-
|
|
157
|
-
Middleware functions are executed in the following order:
|
|
158
|
-
1. **App-level middleware** (defined in `createApp()` options)
|
|
159
|
-
2. **Directory-level middleware** (from `meta.js` files, from root to leaf)
|
|
160
|
-
3. **Route-level middleware** (defined in the route file array)
|
|
161
|
-
|
|
162
|
-
Each middleware function receives the same `req` and `res` objects, allowing data to be passed between middleware functions through the `res` object.
|
|
163
|
-
|
|
164
|
-
#### Breaking the Middleware Chain - Responses
|
|
165
|
-
|
|
166
|
-
You might want to break the middleware chain with a response early.
|
|
167
|
-
|
|
168
|
-
Here's an example:
|
|
169
|
-
|
|
170
|
-
```js
|
|
171
|
-
export default [
|
|
172
|
-
(req, res, next) => {
|
|
173
|
-
if (req.params.userId === '123') {
|
|
174
|
-
return new Response('Returned early')
|
|
175
|
-
} else {
|
|
176
|
-
return next() // continue to next middleware
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
(req, res) => {
|
|
180
|
-
return new Response('End of chain')
|
|
181
|
-
},
|
|
182
|
-
]
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
In the case above, the 1st middleware function will return a `Response` object if the route param's `userId` is equal to a specific value. In those cases, the last function in the middleware chain will not execute for that request. If the condition is not met, `next()` is called to continue to the next middleware.
|
|
186
|
-
|
|
187
|
-
#### Breaking the Middleware Chain - Errors
|
|
188
|
-
|
|
189
|
-
It's also common to throw errors for things like request validation or when a desired resource is not found. `sleepy-serv` has defined custom `Error` types for every type of 4xx and 5xx error:
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
import { NotFoundError } from 'sleepy-serv'
|
|
193
|
-
|
|
194
|
-
export default async function (req, res) {
|
|
195
|
-
const users = await sql`
|
|
196
|
-
SELECT * FROM Users
|
|
197
|
-
WHERE userId=${req.params.userId}
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
const foundUser = users[0]
|
|
201
|
-
|
|
202
|
-
if (foundUser) {
|
|
203
|
-
return new Response.json(foundUser)
|
|
204
|
-
} else {
|
|
205
|
-
throw new NotFoundError()
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
If the user is found in the database, then the request will return with a successful response containing the user's data. If the user is not found, then the `NotFoundError()` is thrown which will automatically respond with a _404 NotFound_ error.
|
|
211
|
-
|
|
212
|
-
Throwing generic errors also works too:
|
|
213
|
-
|
|
214
|
-
```js
|
|
215
|
-
export default async function (req, res) {
|
|
216
|
-
throw new Error('A problem occurred')
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
`sleepy-serv` will automatically respond with a _500 InternalServerError_ for any error types that aren't part of the `sleepy-serv` package.
|
|
221
|
-
|
|
222
|
-
## Metadata Modules
|
|
223
|
-
|
|
224
|
-
It's also possible for resource directories to contain a `meta.js` file. These files can export various things that have some sort semantic relationship to the part of the route that they're defined in.
|
|
225
|
-
|
|
226
|
-
#### Directory-Level Middleware
|
|
227
|
-
|
|
228
|
-
`meta.js` files can also export an array of middleware functions:
|
|
229
|
-
|
|
230
|
-
```js
|
|
231
|
-
// meta.js
|
|
232
|
-
|
|
233
|
-
export const middleware = [
|
|
234
|
-
(req, res, next) => {
|
|
235
|
-
/* do middleware things */
|
|
236
|
-
return next() // Continue to next middleware
|
|
237
|
-
},
|
|
238
|
-
]
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
This middleware will be applied to all sibling and descendent method definition files within that directory.
|
|
242
|
-
|
|
243
|
-
For example:
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
/src
|
|
247
|
-
index.js
|
|
248
|
-
/api
|
|
249
|
-
meta.js # metadata
|
|
250
|
-
get.js
|
|
251
|
-
/users
|
|
252
|
-
meta.js # metadata
|
|
253
|
-
get.js
|
|
254
|
-
post.js
|
|
255
|
-
/:userId
|
|
256
|
-
meta.js # metadata
|
|
257
|
-
get.js
|
|
258
|
-
delete.js
|
|
259
|
-
put.js
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
The middleware defined in `/api/meta.js` will be applied by the following routes:
|
|
263
|
-
|
|
264
|
-
- `GET /`
|
|
265
|
-
- `GET /users`
|
|
266
|
-
- `POST /users`
|
|
267
|
-
- `GET /users/:userId`
|
|
268
|
-
- `DELETE /users/:userId`
|
|
269
|
-
- `PUT /users/:userId`
|
|
270
|
-
|
|
271
|
-
The middleware defined in `/api/users/meta.js` will be applied the following routes:
|
|
272
|
-
|
|
273
|
-
- `GET /users`
|
|
274
|
-
- `POST /users`
|
|
275
|
-
- `GET /users/:userId`
|
|
276
|
-
- `DELETE /users/:userId`
|
|
277
|
-
- `PUT /users/:userId`
|
|
278
|
-
|
|
279
|
-
The middleware defined in `/api/users/:userId/meta.js` will be applied the following routes:
|
|
280
|
-
|
|
281
|
-
- `GET /users/:userId`
|
|
282
|
-
- `DELETE /users/:userId`
|
|
283
|
-
- `PUT /users/:userId`
|
|
284
|
-
|
|
285
|
-
### Meta Files
|
|
286
|
-
|
|
287
|
-
At the time of this writing, `meta.js` only exports middleware functions, but it will eventually be able to provide additional configuration for the endpoint definitions within its scope.
|
|
288
|
-
|
|
289
|
-
## Built-In Middleware
|
|
290
|
-
|
|
291
|
-
`sleepy-serv` also comes built-in with a few useful middleware functions that are commonly used.
|
|
292
|
-
|
|
293
|
-
### parseJson(req, res, next)
|
|
294
|
-
|
|
295
|
-
This middleware parses the request's `body` property as a JSON string, and then stores the results in `res.body`. It calls `next()` if parsing succeeds, and throws a `BadRequestError` if parsing fails. Additional body parsers can be written in the future to accommodate other body encoding schemes (such as XML or protobuf).
|
|
296
|
-
|
|
297
|
-
Example usage:
|
|
298
|
-
|
|
299
|
-
```js
|
|
300
|
-
import { middleware } from 'sleepy-serv'
|
|
301
|
-
|
|
302
|
-
export default [
|
|
303
|
-
middleware.parseJson,
|
|
304
|
-
(req, res) => {
|
|
305
|
-
console.log('Parsed JSON:', res.body)
|
|
306
|
-
|
|
307
|
-
return new Response('JSON received')
|
|
308
|
-
},
|
|
309
|
-
]
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### validateSchema(schemas)
|
|
313
|
-
|
|
314
|
-
This function returns a middleware function that validates request data against provided schemas. The returned middleware function has the signature `(req, res, next)` and automatically calls `next()` after successful validation.
|
|
315
|
-
|
|
316
|
-
The `schemas` object can contain these optional properties:
|
|
317
|
-
- `headers`: takes a string formatter schema to evaluate `req.headers`
|
|
318
|
-
- `params`: takes a string formatter schema to evaluate `req.params`
|
|
319
|
-
- `query`: takes a string formatter schema to evaluate `req.query`
|
|
320
|
-
- `body`: takes a JSON validation schema to evaluate `res.body`
|
|
321
|
-
|
|
322
|
-
Example usage:
|
|
323
|
-
|
|
324
|
-
```js
|
|
325
|
-
import { middleware } from 'sleepy-serv'
|
|
326
|
-
|
|
327
|
-
export default [
|
|
328
|
-
middleware.parseJson,
|
|
329
|
-
middleware.validateSchema({
|
|
330
|
-
params: {
|
|
331
|
-
userId: { type: 'format', value: 'uuid' }
|
|
332
|
-
},
|
|
333
|
-
body: {
|
|
334
|
-
type: 'object',
|
|
335
|
-
properties: {
|
|
336
|
-
name: { type: 'string' },
|
|
337
|
-
email: { type: 'string', format: 'email' }
|
|
338
|
-
},
|
|
339
|
-
required: ['name', 'email']
|
|
340
|
-
}
|
|
341
|
-
}),
|
|
342
|
-
(req, res, next) => {
|
|
343
|
-
// Validation passed, process the request
|
|
344
|
-
return new Response('Data is valid')
|
|
345
|
-
},
|
|
346
|
-
]
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Here's an example of the schema in action:
|
|
350
|
-
|
|
351
|
-
```
|
|
352
|
-
PUT /contacts/123/addresses
|
|
353
|
-
|
|
354
|
-
{
|
|
355
|
-
street1: '123 Main St.',
|
|
356
|
-
street2: '#100',
|
|
357
|
-
city: 'Las Vegas',
|
|
358
|
-
state: 'NV',
|
|
359
|
-
postalCode: '12345',
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
***
|
|
364
|
-
TODO: add JSON schema example.
|
|
365
|
-
|
|
366
|
-
Please take a look at the `lib/src/middleware.test.js` test file for a far a comprehensive set of examples on how to use this middleware.
|
|
367
|
-
***
|
|
368
|
-
|
|
369
|
-
The `headers`, `params`, and `query` (querystrings) can only be strings, so they're evaulated using a simplified _string format schema_ instead of a full JSON schema.
|
|
370
|
-
|
|
371
|
-
The string formatter schema looks like this:
|
|
372
|
-
|
|
373
|
-
```javascript
|
|
374
|
-
{
|
|
375
|
-
type: 'format',
|
|
376
|
-
value: 'email',
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
**`type`**
|
|
381
|
-
|
|
382
|
-
Can either set to `'format'` or `'pattern'`. The value of `type` determines how the `value` property is used.
|
|
383
|
-
|
|
384
|
-
**`value`**
|
|
385
|
-
|
|
386
|
-
This determines how to evaulate the string based on the `type` parameter. If `type` is set to `'format'`, then `value` can be set to one of the pre-defined formats that's provided by the JSON schema specification. If `type` is set to `'pattern'`, then `value` can be set to a regex pattern instead.
|
|
387
|
-
|
|
388
|
-
Custom formats can also be provided by using the `setValidationFormats()` during app initialization. Here's an example:
|
|
389
|
-
|
|
390
|
-
```javascript
|
|
391
|
-
middleware.setValidationFormats({
|
|
392
|
-
phone: /^\d{10}$/, /* 10-digit, numeric string */
|
|
393
|
-
postalCode: /^\d{5}$/, /* 5-digit, numeric string */
|
|
394
|
-
})
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Calling `setValidationFormats()` extends the possible values that can be passed to the `value` property when evaulating strings in either the string formatter schema, or the `res.body`'s JSON schema.
|
|
398
|
-
|
|
399
|
-
## `createApp()` Options
|
|
400
|
-
|
|
401
|
-
### `middleware`
|
|
402
|
-
|
|
403
|
-
You can define app-level middleware that will be applied to all routes:
|
|
404
|
-
|
|
405
|
-
```js
|
|
406
|
-
import { middleware } from 'sleepy-serv'
|
|
407
|
-
|
|
408
|
-
const app = await createApp(PORT, import.meta.dirname, {
|
|
409
|
-
middleware: [
|
|
410
|
-
middleware.parseJson,
|
|
411
|
-
(req, res, next) => {
|
|
412
|
-
console.log(`${req.method} ${req.url}`)
|
|
413
|
-
return next()
|
|
414
|
-
},
|
|
415
|
-
],
|
|
416
|
-
})
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
App-level middleware is executed before any directory-level or route-level middleware.
|
|
420
|
-
|
|
421
|
-
### `hostname`
|
|
422
|
-
|
|
423
|
-
The hostname can be customized like so:
|
|
424
|
-
|
|
425
|
-
```js
|
|
426
|
-
createApp(import.meta.dirname, {
|
|
427
|
-
hostname: 'test.sleepy-serv.com',
|
|
428
|
-
})
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
### `mountPath`
|
|
432
|
-
|
|
433
|
-
This adds a prefix to all routes. For example:
|
|
434
|
-
|
|
435
|
-
```js
|
|
436
|
-
createApp(import.meta.dirname, {
|
|
437
|
-
mountPath: 'api/public',
|
|
438
|
-
})
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
With the directory structure
|
|
442
|
-
|
|
443
|
-
```
|
|
444
|
-
/src
|
|
445
|
-
index.js # createApp()
|
|
446
|
-
/api
|
|
447
|
-
get.js
|
|
448
|
-
/users
|
|
449
|
-
get.js
|
|
450
|
-
post.js
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
Yields these routes:
|
|
454
|
-
|
|
455
|
-
- `GET /api/public`
|
|
456
|
-
- `GET /api/public/users`
|
|
457
|
-
- `POST /api/public/users`
|
|
458
|
-
|
|
459
|
-
### `onClose`
|
|
460
|
-
|
|
461
|
-
When the app is started, the app can be shutdown gracefully by pressing Ctrl+D in the terminal. The `onClose` hook will be called during that shutdown if it's defined. `onClose` can also be `async` as well.
|
|
462
|
-
|
|
463
|
-
```js
|
|
464
|
-
const app = await createApp(PORT, import.meta.dirname, {
|
|
465
|
-
onClose: () => console.info('closing down...'),
|
|
466
|
-
})
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
## Running the Local Example App
|
|
470
|
-
|
|
471
|
-
1. Install [`bun.sh`](https://bun.sh)
|
|
472
|
-
1. Link the library package:
|
|
473
|
-
- `$ cd lib`
|
|
474
|
-
- `$ bun link`
|
|
475
|
-
1. Link the library to the project
|
|
476
|
-
- `$ cd ../example`
|
|
477
|
-
- `$ npm link sleepy-serv`
|
|
478
|
-
1. Finally, run the app: `$ bun --watch run start`
|
|
479
|
-
|
|
480
|
-
## Running Tests
|
|
481
|
-
|
|
482
|
-
- Use `bun`'s built-in test runner
|
|
483
|
-
- Run tests from the `./lib` directory
|