sleepy-serv 0.18.0 → 0.20.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.
Files changed (52) hide show
  1. package/README.md +113 -17
  2. package/dist/cli/build.d.ts +2 -0
  3. package/dist/cli/build.d.ts.map +1 -0
  4. package/dist/cli/dev.d.ts +2 -0
  5. package/dist/cli/dev.d.ts.map +1 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/init.d.ts +2 -0
  9. package/dist/cli/init.d.ts.map +1 -0
  10. package/dist/core/errors.d.ts.map +1 -0
  11. package/dist/core/index.d.ts +17 -0
  12. package/dist/core/index.d.ts.map +1 -0
  13. package/dist/core/messages.d.ts.map +1 -0
  14. package/dist/core/middleware.d.ts.map +1 -0
  15. package/dist/core/socket.d.ts.map +1 -0
  16. package/dist/{utils.d.ts → core/utils.d.ts} +15 -2
  17. package/dist/core/utils.d.ts.map +1 -0
  18. package/dist/index.d.ts +3 -16
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/plugin/codegen.d.ts +3 -0
  21. package/dist/plugin/codegen.d.ts.map +1 -0
  22. package/dist/plugin/config.d.ts +14 -0
  23. package/dist/plugin/config.d.ts.map +1 -0
  24. package/dist/plugin/index.d.ts +2 -0
  25. package/dist/plugin/index.d.ts.map +1 -0
  26. package/dist/plugin/scanner.d.ts +15 -0
  27. package/dist/plugin/scanner.d.ts.map +1 -0
  28. package/package.json +8 -1
  29. package/src/cli/build.ts +105 -0
  30. package/src/cli/dev.ts +27 -0
  31. package/src/cli/index.ts +53 -0
  32. package/src/cli/init.ts +108 -0
  33. package/src/core/index.ts +406 -0
  34. package/src/{socket.ts → core/socket.ts} +18 -18
  35. package/src/{utils.ts → core/utils.ts} +18 -2
  36. package/src/index.ts +5 -571
  37. package/src/plugin/codegen.ts +101 -0
  38. package/src/plugin/config.ts +34 -0
  39. package/src/plugin/index.ts +30 -0
  40. package/src/plugin/scanner.ts +148 -0
  41. package/dist/errors.d.ts.map +0 -1
  42. package/dist/messages.d.ts.map +0 -1
  43. package/dist/middleware.d.ts.map +0 -1
  44. package/dist/socket.d.ts.map +0 -1
  45. package/dist/utils.d.ts.map +0 -1
  46. /package/dist/{errors.d.ts → core/errors.d.ts} +0 -0
  47. /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
  48. /package/dist/{middleware.d.ts → core/middleware.d.ts} +0 -0
  49. /package/dist/{socket.d.ts → core/socket.d.ts} +0 -0
  50. /package/src/{errors.ts → core/errors.ts} +0 -0
  51. /package/src/{messages.ts → core/messages.ts} +0 -0
  52. /package/src/{middleware.ts → core/middleware.ts} +0 -0
package/src/index.ts CHANGED
@@ -1,574 +1,8 @@
1
- import fs from 'fs'
2
- import path from 'path'
3
- import querystring from 'querystring'
4
- import readline from 'node:readline'
1
+ export * from './core'
5
2
 
6
- import { stdin, stdout } from 'node:process'
3
+ import type { App, AppOptions } from './core'
7
4
 
8
- import {
9
- toSegments,
10
- executeMiddlewareChain,
11
- } from './utils'
12
-
13
- import {
14
- buildSocketState,
15
- buildSocketServer,
16
- buildSocketHandlers,
17
- buildSocketCommands,
18
- } from './socket'
19
-
20
- import {
21
- RequestError,
22
- NotFoundError,
23
- MethodNotAllowedError,
24
- InternalServerError,
25
- } from './errors'
26
-
27
- import type { BunRequest } from 'bun'
28
-
29
- import type {
30
- AsyncHandlerResult,
31
- HttpMethod,
32
- Middleware,
33
- MiddlewareChain,
34
- EndpointRequest,
35
- SocketCommands,
36
- AppOptions,
37
- Server,
38
- } from './utils'
39
-
40
- import type {
41
- SocketRoute,
42
- SocketState,
43
- } from './socket'
44
-
45
- export * from './errors'
46
- export { StatusCode, CloseCode, CloseReason, HttpMethod } from './utils'
47
-
48
- export {
49
- parseJsonBody,
50
- setValidationFormats,
51
- validateSchemas,
52
- } from './middleware'
53
-
54
- export type { FilterFn, SessionEntry, SocketCommands } from './utils'
55
-
56
- export type {
57
- AppOptions,
58
- AsyncHandlerResult,
59
- BaseRequest,
60
- EndpointRequest,
61
- FormattedError,
62
- Handler,
63
- Middleware,
64
- MiddlewareChain,
65
- NextFn,
66
- HandlerResult,
67
- Request,
68
- Server,
69
- SocketConnection,
70
- SocketOptions,
71
- WebSocketRequest,
72
- } from './utils'
73
-
74
- export type {
75
- FormatterField,
76
- FormatterSchema,
77
- ValidationSchemas,
78
- } from './middleware'
79
-
80
- type OutputRoutes = Record<string, string[]>
81
- type ServerRoutes = Record<string, Record<string, EndpointHandler>>
82
-
83
- type EndpointHandler = (
84
- bunReq: BunRequest,
85
- server: Server,
86
- ) => AsyncHandlerResult
87
-
88
- type DirEntry = {
89
- path: string
90
- stat: fs.Stats
91
- }
92
-
93
- type RoutePath = {
94
- method: HttpMethod
95
- path: string
96
- metaMiddlewarePath: string[]
97
- modulePath: string
98
- }
99
-
100
- type ChainRoute = {
101
- method: HttpMethod
102
- path: string
103
- chain: MiddlewareChain
104
- }
105
-
106
- type ModuleRoute = {
107
- method: HttpMethod
108
- path: string
109
- handler: EndpointHandler
110
- }
111
-
112
- type RoutingOptions = {
113
- basePath: string
114
- mountPath: string
115
- metadata: string[]
116
- }
117
-
118
- type AppRoutes = {
119
- server: ServerRoutes
120
- output: OutputRoutes
121
- socket: SocketRoute[]
122
- }
123
-
124
- type CloseFn = (force?: boolean) => Promise<void>
125
-
126
- export type App = {
127
- server: Server
128
- routes: OutputRoutes
129
- ws: SocketCommands
130
- close: CloseFn
131
- }
132
-
133
- const ALLOWED_FILES_META = ['meta.js', 'meta.ts']
134
-
135
- const ALLOWED_FILES_METHODS = [
136
- 'head.js',
137
- 'head.ts',
138
- 'get.js',
139
- 'get.ts',
140
- 'put.js',
141
- 'put.ts',
142
- 'post.js',
143
- 'post.ts',
144
- 'patch.js',
145
- 'patch.ts',
146
- 'delete.js',
147
- 'delete.ts',
148
- ]
149
-
150
- function methodNotAllowedHandler (_req: unknown): never {
151
- throw new MethodNotAllowedError()
152
- }
153
-
154
- function defaultMethodMap (): Record<string, EndpointHandler> {
155
- return {
156
- HEAD: methodNotAllowedHandler,
157
- GET: methodNotAllowedHandler,
158
- PUT: methodNotAllowedHandler,
159
- POST: methodNotAllowedHandler,
160
- PATCH: methodNotAllowedHandler,
161
- DELETE: methodNotAllowedHandler,
162
- }
163
- }
164
-
165
- function buildEndpointRequest (
166
- bunReq: BunRequest,
167
- server: Server,
168
- ws: SocketCommands,
169
- ): EndpointRequest {
170
- const url = new URL(bunReq.url)
171
- const qs = url.search.replace('?', '')
172
-
173
- let bodyPromise: Promise<unknown> | null = null
174
-
175
- const json = (): Promise<unknown> => {
176
- if (!bodyPromise) {
177
- bodyPromise = bunReq.json()
178
- }
179
-
180
- return bodyPromise
181
- }
182
-
183
- return {
184
- method: bunReq.method as HttpMethod,
185
- route: url.pathname,
186
- headers: bunReq.headers,
187
- params: bunReq.params ?? {},
188
- query: querystring.parse(qs),
189
- raw: bunReq,
190
- server,
191
- json,
192
- ws,
193
- }
194
- }
195
-
196
- function validateLeafDirectory (
197
- targetPath: string,
198
- filenames: string[],
199
- entries: DirEntry[],
200
- ): void {
201
- const hasDirectories = entries.some(entry => entry.stat.isDirectory())
202
-
203
- if (!hasDirectories) {
204
- const hasMethodEntry = filenames.some(filename =>
205
- ALLOWED_FILES_METHODS.includes(filename),
206
- )
207
-
208
- if (!hasMethodEntry) {
209
- throw new TypeError(`
210
- Directory is a leaf, but doesn't contain a method file:
211
- ${targetPath}
212
- `.trim())
213
- }
214
- }
215
- }
216
-
217
- function validateDirectory (targetPath: string, entries: DirEntry[]): void {
218
- const filenames = entries
219
- .filter(entry => entry.stat.isFile())
220
- .map(entry => path.basename(entry.path))
221
-
222
- validateLeafDirectory(targetPath, filenames, entries)
223
- }
224
-
225
- function getAllFilePathsRec (targetPath: string, paths: string[]): string[] {
226
- const entries = fs.readdirSync(targetPath)
227
-
228
- const children = entries.map(item => {
229
- const fullPath = path.join(targetPath, item)
230
-
231
- return {
232
- path: fullPath,
233
- stat: fs.statSync(fullPath),
234
- }
235
- })
236
-
237
- validateDirectory(targetPath, children)
238
-
239
- return children.reduce<string[]>((accum, curr) => {
240
- const result = curr.stat.isDirectory()
241
- ? getAllFilePathsRec(curr.path, paths)
242
- : [curr.path]
243
-
244
- return [...accum, ...result]
245
- }, [])
246
- }
247
-
248
- function getFilteredFilePaths (
249
- targetPath: string,
250
- allowedFiles: string[],
251
- ): string[] {
252
- const allPaths = getAllFilePathsRec(targetPath, [])
253
-
254
- return allPaths.filter(item =>
255
- allowedFiles.includes(path.basename(item)),
256
- )
257
- }
258
-
259
- function getMethodFilePaths (targetPath: string): string[] {
260
- return getFilteredFilePaths(targetPath, ALLOWED_FILES_METHODS)
261
- }
262
-
263
- function getMetaFilePaths (targetPath: string): string[] {
264
- return getFilteredFilePaths(targetPath, ALLOWED_FILES_META)
265
- }
266
-
267
- function selectMetaPaths (metadata: string[], modulePath: string): string[] {
268
- return metadata
269
- .filter(metaPath => modulePath.startsWith(path.dirname(metaPath)))
270
- .sort((a, b) => a.length - b.length)
271
- }
272
-
273
- async function resolveMetaMiddleware (
274
- metaPaths: string[],
275
- ): Promise<Middleware[]> {
276
- const modules = await Promise.all(metaPaths.map(item => import(item)))
277
-
278
- return modules
279
- .map(item => item.middleware)
280
- .reduce((accum, curr) => [
281
- ...accum,
282
- ...(curr || []),
283
- ], [])
284
- }
285
-
286
- function buildRoutePaths (
287
- rootPath: string,
288
- mountPath: string,
289
- metadata: string[],
290
- ): RoutePath[] {
291
- const paths = getMethodFilePaths(rootPath)
292
-
293
- return paths.map(modulePath => {
294
- const trimmedPath = modulePath.replace(rootPath, '')
295
- const segments = trimmedPath.split('.')[0].split('/')
296
- const lastIndex = segments.length - 1
297
- const basePath = segments.slice(0, segments.length - 1).join('/')
298
-
299
- const joinedPath = [mountPath, basePath]
300
- .filter(item => item)
301
- .join('') || '/'
302
-
303
- const metaMiddlewarePath = selectMetaPaths(metadata, modulePath)
304
- const rawMethod = segments[lastIndex].toUpperCase()
305
-
306
- return {
307
- method: rawMethod as HttpMethod,
308
- path: joinedPath,
309
- metaMiddlewarePath,
310
- modulePath,
311
- }
312
- })
313
- }
314
-
315
- async function buildChain (
316
- route: RoutePath,
317
- rootMiddleware: Middleware[],
318
- ): Promise<ChainRoute> {
319
- const module = await import(route.modulePath)
320
- const metaMiddleware = await resolveMetaMiddleware(route.metaMiddlewarePath)
321
-
322
- if (!module.default) {
323
- throw new ReferenceError(`
324
- No default export defined in:
325
- ${route.modulePath}
326
- `.trim())
327
- }
328
-
329
- const baseChain = Array.isArray(module.default)
330
- ? module.default
331
- : [module.default]
332
-
333
- return {
334
- method: route.method,
335
- path: route.path,
336
- chain: [
337
- ...rootMiddleware,
338
- ...metaMiddleware,
339
- ...baseChain,
340
- ],
341
- }
342
- }
343
-
344
- function buildNormalRoutes (
345
- routePaths: RoutePath[],
346
- rootMiddleware: Middleware[],
347
- ): Promise<ChainRoute[]> {
348
- return Promise.all(
349
- routePaths.map(route => buildChain(route, rootMiddleware)),
350
- )
351
- }
352
-
353
- async function buildMergedRoutes (
354
- routePaths: ChainRoute[],
355
- middleware: Middleware[],
356
- state: SocketState,
357
- opts: RoutingOptions,
358
- ): Promise<ChainRoute[]> {
359
- const { basePath, mountPath, metadata } = opts
360
- const socketRoutes = buildSocketHandlers(state)
361
-
362
- for (const socketRoute of socketRoutes) {
363
- const mountedPath = `${mountPath}${socketRoute.path}`
364
-
365
- const targetItem = routePaths.find(item => (
366
- item.method === socketRoute.method &&
367
- item.path === mountedPath
368
- ))
369
-
370
- if (targetItem) {
371
- targetItem.chain.push(socketRoute.handler)
372
-
373
- continue
374
- }
375
-
376
- const method = socketRoute.method.toLowerCase()
377
- const modulePath = path.join(basePath, socketRoute.path, `${method}.js`)
378
- const metaPaths = selectMetaPaths(metadata, modulePath)
379
- const metaMiddleware = await resolveMetaMiddleware(metaPaths)
380
-
381
- routePaths.push({
382
- method: socketRoute.method,
383
- path: mountedPath,
384
- chain: [
385
- ...middleware,
386
- ...metaMiddleware,
387
- socketRoute.handler,
388
- ],
389
- })
390
- }
391
-
392
- return routePaths
393
- }
394
-
395
- function buildSocketRoutes (mergedRoutes: ChainRoute[]): SocketRoute[] {
396
- return mergedRoutes.map(route => ({
397
- ...route,
398
- segments: toSegments(route.path),
399
- }))
400
- }
401
-
402
- function buildModuleRoutes (
403
- socketRoutes: SocketRoute[],
404
- ws: SocketCommands,
405
- ): ModuleRoute[] {
406
- return socketRoutes.map(route => {
407
- const handler: EndpointHandler = async (bunReq, server) => {
408
- const req = buildEndpointRequest(bunReq, server, ws)
409
-
410
- return executeMiddlewareChain(req, route.chain)
411
- }
412
-
413
- return {
414
- method: route.method,
415
- path: route.path,
416
- handler,
417
- }
418
- })
419
- }
420
-
421
- function buildServerRoutes (moduleRoutes: ModuleRoute[]): ServerRoutes {
422
- return moduleRoutes.reduce<ServerRoutes>(
423
- (accum, curr) => {
424
- if (!accum[curr.path]) {
425
- accum[curr.path] = defaultMethodMap()
426
- }
427
-
428
- accum[curr.path][curr.method] = curr.handler
429
-
430
- return accum
431
- }, {})
432
- }
433
-
434
- function buildOutputRoutes (moduleRoutes: ModuleRoute[]): OutputRoutes {
435
- return moduleRoutes.reduce<OutputRoutes>((accum, curr) => {
436
- accum[curr.path] = accum[curr.path] || []
437
- accum[curr.path].push(curr.method)
438
-
439
- return accum
440
- }, {})
441
- }
442
-
443
- async function buildRoutes (
444
- rootPath: string,
445
- state: SocketState,
446
- ws: SocketCommands,
447
- opts: AppOptions,
448
- ): Promise<AppRoutes> {
449
- const basePath = `${rootPath}/api`
450
- const mountPath = opts.mountPath || ''
451
- const middleware = opts.middleware || []
452
- const metadata = getMetaFilePaths(basePath)
453
- const routePaths = buildRoutePaths(basePath, mountPath, metadata)
454
- const normalRoutes = await buildNormalRoutes(routePaths, middleware)
455
-
456
- const routingOpts = {
457
- basePath,
458
- mountPath,
459
- metadata,
460
- }
461
-
462
- const mergedRoutes = await buildMergedRoutes(
463
- normalRoutes,
464
- middleware,
465
- state,
466
- routingOpts,
467
- )
468
-
469
- const socketRoutes = buildSocketRoutes(mergedRoutes)
470
- const moduleRoutes = buildModuleRoutes(socketRoutes, ws)
471
- const serverRoutes = buildServerRoutes(moduleRoutes)
472
- const outputRoutes = buildOutputRoutes(moduleRoutes)
473
-
474
- return {
475
- server: serverRoutes,
476
- socket: socketRoutes,
477
- output: outputRoutes,
478
- }
479
- }
480
-
481
- function buildServer (
482
- port: number,
483
- routes: AppRoutes,
484
- state: SocketState,
485
- ws: SocketCommands,
486
- opts: AppOptions,
487
- ): Server {
488
- const hostname = opts.hostname || '0.0.0.0'
489
-
490
- const websocketServer = buildSocketServer(
491
- routes.socket,
492
- state,
493
- ws,
494
- )
495
-
496
- return Bun.serve({
497
- port,
498
- hostname,
499
- routes: routes.server,
500
- websocket: websocketServer,
501
- async fetch (_req, _server) {
502
- throw new NotFoundError()
503
- },
504
- error (err) {
505
- console.error(err)
506
-
507
- const isRequestError = !(err instanceof RequestError)
508
- const httpError = isRequestError ? new InternalServerError() : err
509
- const { status } = httpError.constructor as typeof RequestError
510
-
511
- return Response.json(httpError.output, { status })
512
- },
513
- })
514
- }
515
-
516
- function processIO (
517
- port: number,
518
- server: Server,
519
- opts: AppOptions,
520
- ): CloseFn {
521
- const onClose = opts.onClose || (() => {})
522
-
523
- console.info(`Running on port: ${port}`)
524
- console.info('')
525
- console.info('Press Ctrl+D to gracefully shutdown')
526
- console.info('')
527
-
528
- const shutdown = async (force = false): Promise<void> => {
529
- await server.stop(force)
530
- await onClose()
531
- }
532
-
533
- if (!stdin.isTTY) {
534
- return shutdown
535
- }
536
-
537
- const rl = readline.createInterface({
538
- input: stdin,
539
- output: stdout,
540
- })
541
-
542
- const handleClose = async (): Promise<void> => {
543
- await shutdown()
544
- process.exit(0)
545
- }
546
-
547
- rl.on('close', handleClose)
548
-
549
- return async (force = false) => {
550
- rl.off('close', handleClose)
551
- rl.close()
552
-
553
- await shutdown(force)
554
- }
555
- }
556
-
557
- export async function createApp (
5
+ export declare function createApp (
558
6
  port: number,
559
- rootPath: string,
560
- opts: AppOptions = {},
561
- ): Promise<App> {
562
- const state = buildSocketState(opts)
563
- const ws = buildSocketCommands(state)
564
- const routes = await buildRoutes(rootPath, state, ws, opts)
565
- const server = buildServer(port, routes, state, ws, opts)
566
- const close = processIO(port, server, opts)
567
-
568
- return {
569
- routes: routes.output,
570
- server,
571
- ws,
572
- close,
573
- }
574
- }
7
+ opts?: AppOptions,
8
+ ): App
@@ -0,0 +1,101 @@
1
+ import type { ScanResult } from './scanner'
2
+
3
+ export function generateBarrelModule (
4
+ scanResult: ScanResult,
5
+ ): string {
6
+ const lines: string[] = [
7
+ 'export * from \'./core\'',
8
+ '',
9
+ 'import { createApp as _createApp } from \'./core\'',
10
+ '',
11
+ ]
12
+
13
+ for (let i = 0; i < scanResult.methods.length; i++) {
14
+ const entry = scanResult.methods[i]
15
+
16
+ lines.push(
17
+ `import handler_${i} from '${entry.modulePath}'`,
18
+ )
19
+ }
20
+
21
+ for (let i = 0; i < scanResult.meta.length; i++) {
22
+ const entry = scanResult.meta[i]
23
+
24
+ lines.push(
25
+ `import * as metaModule_${i}`
26
+ + ` from '${entry.modulePath}'`,
27
+ )
28
+ }
29
+
30
+ lines.push('')
31
+
32
+ for (let i = 0; i < scanResult.methods.length; i++) {
33
+ const entry = scanResult.methods[i]
34
+
35
+ lines.push(
36
+ `if (typeof handler_${i} === 'undefined') {`,
37
+ ' throw new ReferenceError(',
38
+ ` 'No default export defined in:\\n${entry.modulePath}',`,
39
+ ' )',
40
+ '}',
41
+ '',
42
+ )
43
+ }
44
+
45
+ for (let i = 0; i < scanResult.methods.length; i++) {
46
+ lines.push(
47
+ `const chain_${i} = Array.isArray(handler_${i})`
48
+ + ` ? handler_${i}`
49
+ + ` : [handler_${i}]`,
50
+ )
51
+ }
52
+
53
+ lines.push('')
54
+
55
+ const routeEntries = scanResult.methods.map((entry, i) => {
56
+ const applicableMeta = scanResult.meta
57
+ .filter(m => entry.path.startsWith(m.path))
58
+ .sort((a, b) => a.path.length - b.path.length)
59
+
60
+ const metaSpreads = applicableMeta
61
+ .map(m => {
62
+ const metaIndex = scanResult.meta.indexOf(m)
63
+
64
+ return `...(metaModule_${metaIndex}.middleware ?? [])`
65
+ })
66
+
67
+ const chainParts = [...metaSpreads, `...chain_${i}`]
68
+
69
+ return [
70
+ ' {',
71
+ ` method: '${entry.method}',`,
72
+ ` path: '${entry.path}',`,
73
+ ` chain: [${chainParts.join(', ')}],`,
74
+ ' }',
75
+ ].join('\n')
76
+ })
77
+
78
+ const metaEntries = scanResult.meta.map((entry, i) => {
79
+ return [
80
+ ' {',
81
+ ` path: '${entry.path}',`,
82
+ ` middleware: metaModule_${i}.middleware ?? [],`,
83
+ ' }',
84
+ ].join('\n')
85
+ })
86
+
87
+ lines.push('const config = {')
88
+ lines.push(' routes: [')
89
+ lines.push(routeEntries.join(',\n'))
90
+ lines.push(' ],')
91
+ lines.push(' meta: [')
92
+ lines.push(metaEntries.join(',\n'))
93
+ lines.push(' ],')
94
+ lines.push('}')
95
+ lines.push('')
96
+ lines.push('export function createApp (port, opts = {}) {')
97
+ lines.push(' return _createApp(port, config, opts)')
98
+ lines.push('}')
99
+
100
+ return lines.join('\n')
101
+ }
@@ -0,0 +1,34 @@
1
+ import path from 'path'
2
+
3
+ export type SleepyConfig = {
4
+ app?: {
5
+ root?: string
6
+ entrypoint?: string
7
+ }
8
+ build?: {
9
+ compile?: boolean | string
10
+ bytecode?: boolean
11
+ outdir?: string
12
+ plugins?: unknown[]
13
+ }
14
+ }
15
+
16
+ export async function loadConfig (
17
+ startDir?: string,
18
+ ): Promise<SleepyConfig> {
19
+ const dir = startDir ?? process.cwd()
20
+
21
+ for (const ext of ['.ts', '.js']) {
22
+ const configPath = path.join(dir, `sleepy.config${ext}`)
23
+
24
+ try {
25
+ const mod = await import(configPath)
26
+
27
+ return mod.default ?? {}
28
+ } catch {
29
+ continue
30
+ }
31
+ }
32
+
33
+ return {}
34
+ }