sleepy-serv 0.18.0 → 0.19.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 -15
  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} +13 -0
  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 +82 -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/{utils.ts → core/utils.ts} +16 -0
  35. package/src/index.ts +5 -571
  36. package/src/plugin/codegen.ts +101 -0
  37. package/src/plugin/config.ts +34 -0
  38. package/src/plugin/index.ts +30 -0
  39. package/src/plugin/scanner.ts +148 -0
  40. package/dist/errors.d.ts.map +0 -1
  41. package/dist/messages.d.ts.map +0 -1
  42. package/dist/middleware.d.ts.map +0 -1
  43. package/dist/socket.d.ts.map +0 -1
  44. package/dist/utils.d.ts.map +0 -1
  45. /package/dist/{errors.d.ts → core/errors.d.ts} +0 -0
  46. /package/dist/{messages.d.ts → core/messages.d.ts} +0 -0
  47. /package/dist/{middleware.d.ts → core/middleware.d.ts} +0 -0
  48. /package/dist/{socket.d.ts → core/socket.d.ts} +0 -0
  49. /package/src/{errors.ts → core/errors.ts} +0 -0
  50. /package/src/{messages.ts → core/messages.ts} +0 -0
  51. /package/src/{middleware.ts → core/middleware.ts} +0 -0
  52. /package/src/{socket.ts → core/socket.ts} +0 -0
@@ -0,0 +1,108 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ const TEMPLATES: Record<string, string> = {
5
+ 'sleepy.config.ts': 'export default {}',
6
+
7
+ 'tsconfig.json': JSON.stringify({
8
+ compilerOptions: {
9
+ strict: true,
10
+ skipLibCheck: true,
11
+ target: 'ESNext',
12
+ module: 'ESNext',
13
+ moduleResolution: 'bundler',
14
+ types: ['bun-types'],
15
+ },
16
+ }, null, 2) + '\n',
17
+
18
+ 'src/index.ts': `
19
+ import { createApp } from \'sleepy-serv\'
20
+
21
+ createApp(3000)
22
+ `.trim().concat('\n'),
23
+
24
+ 'src/api/get.ts': `
25
+ export default function handler (): Response {
26
+ return Response.json({ ok: true })
27
+ }
28
+ `.trim().concat('\n'),
29
+ }
30
+
31
+ function writeIfMissing (filePath: string, content: string): boolean {
32
+ if (fs.existsSync(filePath)) {
33
+ console.warn(` skip: ${filePath} (already exists)`)
34
+
35
+ return false
36
+ }
37
+
38
+ const dir = path.dirname(filePath)
39
+
40
+ fs.mkdirSync(dir, { recursive: true })
41
+ fs.writeFileSync(filePath, content)
42
+ console.log(` create: ${filePath}`)
43
+
44
+ return true
45
+ }
46
+
47
+ function writePackageJson (): void {
48
+ const filePath = 'package.json'
49
+ const name = path.basename(process.cwd())
50
+
51
+ const scripts = {
52
+ dev: 'sleepy dev',
53
+ build: 'sleepy build',
54
+ }
55
+
56
+ if (fs.existsSync(filePath)) {
57
+ const raw = fs.readFileSync(filePath, 'utf-8')
58
+ const existing = JSON.parse(raw)
59
+
60
+ existing.scripts = {
61
+ ...existing.scripts,
62
+ ...scripts,
63
+ }
64
+
65
+ fs.writeFileSync(
66
+ filePath,
67
+ JSON.stringify(existing, null, 2) + '\n',
68
+ )
69
+
70
+ console.log(` update: ${filePath}`)
71
+
72
+ return
73
+ }
74
+
75
+ const pkg = {
76
+ name,
77
+ version: '0.0.0',
78
+ type: 'module',
79
+ scripts,
80
+ dependencies: {
81
+ 'sleepy-serv': 'latest',
82
+ },
83
+ }
84
+
85
+ fs.writeFileSync(
86
+ filePath,
87
+ JSON.stringify(pkg, null, 2) + '\n',
88
+ )
89
+
90
+ console.log(` create: ${filePath}`)
91
+ }
92
+
93
+ export async function init (): Promise<void> {
94
+ console.log('')
95
+
96
+ writePackageJson()
97
+
98
+ for (const [relPath, content] of Object.entries(TEMPLATES)) {
99
+ writeIfMissing(relPath, content)
100
+ }
101
+
102
+ console.log('')
103
+ console.log('Created sleepy-serv project in ./')
104
+ console.log('')
105
+ console.log(' bun install')
106
+ console.log(' sleepy dev')
107
+ console.log('')
108
+ }
@@ -0,0 +1,406 @@
1
+ import querystring from 'querystring'
2
+ import readline from 'node:readline'
3
+
4
+ import { stdin, stdout } from 'node:process'
5
+
6
+ import {
7
+ toSegments,
8
+ executeMiddlewareChain,
9
+ } from './utils'
10
+
11
+ import {
12
+ buildSocketState,
13
+ buildSocketServer,
14
+ buildSocketHandlers,
15
+ buildSocketCommands,
16
+ } from './socket'
17
+
18
+ import {
19
+ RequestError,
20
+ NotFoundError,
21
+ MethodNotAllowedError,
22
+ InternalServerError,
23
+ } from './errors'
24
+
25
+ import type { BunRequest } from 'bun'
26
+
27
+ import type {
28
+ AsyncHandlerResult,
29
+ HttpMethod,
30
+ Middleware,
31
+ MiddlewareChain,
32
+ EndpointRequest,
33
+ MetaEntry,
34
+ RouteConfig,
35
+ RouteDefinition,
36
+ SocketCommands,
37
+ AppOptions,
38
+ Server,
39
+ } from './utils'
40
+
41
+ import type {
42
+ SocketRoute,
43
+ SocketState,
44
+ } from './socket'
45
+
46
+ export * from './errors'
47
+ export { StatusCode, CloseCode, CloseReason, HttpMethod } from './utils'
48
+
49
+ export {
50
+ parseJsonBody,
51
+ setValidationFormats,
52
+ validateSchemas,
53
+ } from './middleware'
54
+
55
+ export type { FilterFn, SessionEntry, SocketCommands } from './utils'
56
+
57
+ export type {
58
+ AppOptions,
59
+ AsyncHandlerResult,
60
+ BaseRequest,
61
+ EndpointRequest,
62
+ FormattedError,
63
+ Handler,
64
+ MetaEntry,
65
+ Middleware,
66
+ MiddlewareChain,
67
+ NextFn,
68
+ HandlerResult,
69
+ Request,
70
+ RouteConfig,
71
+ RouteDefinition,
72
+ Server,
73
+ SocketConnection,
74
+ SocketOptions,
75
+ WebSocketRequest,
76
+ } from './utils'
77
+
78
+ export type {
79
+ FormatterField,
80
+ FormatterSchema,
81
+ ValidationSchemas,
82
+ } from './middleware'
83
+
84
+ type OutputRoutes = Record<string, string[]>
85
+ type ServerRoutes = Record<string, Record<string, EndpointHandler>>
86
+
87
+ type EndpointHandler = (
88
+ bunReq: BunRequest,
89
+ server: Server,
90
+ ) => AsyncHandlerResult
91
+
92
+ type ChainRoute = {
93
+ method: HttpMethod
94
+ path: string
95
+ chain: MiddlewareChain
96
+ }
97
+
98
+ type ModuleRoute = {
99
+ method: HttpMethod
100
+ path: string
101
+ handler: EndpointHandler
102
+ }
103
+
104
+ type AppRoutes = {
105
+ server: ServerRoutes
106
+ output: OutputRoutes
107
+ socket: SocketRoute[]
108
+ }
109
+
110
+ type CloseFn = (force?: boolean) => Promise<void>
111
+
112
+ export type App = {
113
+ server: Server
114
+ routes: OutputRoutes
115
+ ws: SocketCommands
116
+ close: CloseFn
117
+ }
118
+
119
+ function methodNotAllowedHandler (_req: unknown): never {
120
+ throw new MethodNotAllowedError()
121
+ }
122
+
123
+ function defaultMethodMap (): Record<string, EndpointHandler> {
124
+ return {
125
+ HEAD: methodNotAllowedHandler,
126
+ GET: methodNotAllowedHandler,
127
+ PUT: methodNotAllowedHandler,
128
+ POST: methodNotAllowedHandler,
129
+ PATCH: methodNotAllowedHandler,
130
+ DELETE: methodNotAllowedHandler,
131
+ }
132
+ }
133
+
134
+ function buildEndpointRequest (
135
+ bunReq: BunRequest,
136
+ server: Server,
137
+ ws: SocketCommands,
138
+ ): EndpointRequest {
139
+ const url = new URL(bunReq.url)
140
+ const qs = url.search.replace('?', '')
141
+
142
+ let bodyPromise: Promise<unknown> | null = null
143
+
144
+ const json = (): Promise<unknown> => {
145
+ if (!bodyPromise) {
146
+ bodyPromise = bunReq.json()
147
+ }
148
+
149
+ return bodyPromise
150
+ }
151
+
152
+ return {
153
+ method: bunReq.method as HttpMethod,
154
+ route: url.pathname,
155
+ headers: bunReq.headers,
156
+ params: bunReq.params ?? {},
157
+ query: querystring.parse(qs),
158
+ raw: bunReq,
159
+ server,
160
+ json,
161
+ ws,
162
+ }
163
+ }
164
+
165
+ function normalizeChain (
166
+ route: RouteDefinition,
167
+ ): ChainRoute {
168
+ const chain = Array.isArray(route.chain)
169
+ ? route.chain
170
+ : [route.chain]
171
+
172
+ return {
173
+ method: route.method,
174
+ path: route.path,
175
+ chain,
176
+ }
177
+ }
178
+
179
+ function buildMergedRoutes (
180
+ routePaths: ChainRoute[],
181
+ middleware: Middleware[],
182
+ meta: MetaEntry[],
183
+ state: SocketState,
184
+ mountPath: string,
185
+ ): ChainRoute[] {
186
+ const socketRoutes = buildSocketHandlers(state)
187
+
188
+ for (const socketRoute of socketRoutes) {
189
+ const mountedPath = `${mountPath}${socketRoute.path}`
190
+
191
+ const targetItem = routePaths.find(item => (
192
+ item.method === socketRoute.method &&
193
+ item.path === mountedPath
194
+ ))
195
+
196
+ if (targetItem) {
197
+ targetItem.chain.push(socketRoute.handler)
198
+
199
+ continue
200
+ }
201
+
202
+ const metaMiddleware = meta
203
+ .filter(entry => socketRoute.path.startsWith(entry.path))
204
+ .sort((a, b) => a.path.length - b.path.length)
205
+ .flatMap(entry => entry.middleware)
206
+
207
+ routePaths.push({
208
+ method: socketRoute.method,
209
+ path: mountedPath,
210
+ chain: [
211
+ ...middleware,
212
+ ...metaMiddleware,
213
+ socketRoute.handler,
214
+ ],
215
+ })
216
+ }
217
+
218
+ return routePaths
219
+ }
220
+
221
+ function buildSocketRoutes (mergedRoutes: ChainRoute[]): SocketRoute[] {
222
+ return mergedRoutes.map(route => ({
223
+ ...route,
224
+ segments: toSegments(route.path),
225
+ }))
226
+ }
227
+
228
+ function buildModuleRoutes (
229
+ socketRoutes: SocketRoute[],
230
+ ws: SocketCommands,
231
+ ): ModuleRoute[] {
232
+ return socketRoutes.map(route => {
233
+ const handler: EndpointHandler = async (bunReq, server) => {
234
+ const req = buildEndpointRequest(bunReq, server, ws)
235
+
236
+ return executeMiddlewareChain(req, route.chain)
237
+ }
238
+
239
+ return {
240
+ method: route.method,
241
+ path: route.path,
242
+ handler,
243
+ }
244
+ })
245
+ }
246
+
247
+ function buildServerRoutes (moduleRoutes: ModuleRoute[]): ServerRoutes {
248
+ return moduleRoutes.reduce<ServerRoutes>(
249
+ (accum, curr) => {
250
+ if (!accum[curr.path]) {
251
+ accum[curr.path] = defaultMethodMap()
252
+ }
253
+
254
+ accum[curr.path][curr.method] = curr.handler
255
+
256
+ return accum
257
+ }, {})
258
+ }
259
+
260
+ function buildOutputRoutes (moduleRoutes: ModuleRoute[]): OutputRoutes {
261
+ return moduleRoutes.reduce<OutputRoutes>((accum, curr) => {
262
+ accum[curr.path] = accum[curr.path] || []
263
+ accum[curr.path].push(curr.method)
264
+
265
+ return accum
266
+ }, {})
267
+ }
268
+
269
+ function buildRoutes (
270
+ config: RouteConfig,
271
+ state: SocketState,
272
+ ws: SocketCommands,
273
+ opts: AppOptions,
274
+ ): AppRoutes {
275
+ const mountPath = opts.mountPath || ''
276
+ const middleware = opts.middleware || []
277
+ const meta = config.meta || []
278
+
279
+ const normalRoutes = config.routes.map(route => {
280
+ const normalized = normalizeChain(route)
281
+ const routePath = normalized.path === '/' ? '' : normalized.path
282
+
283
+ const joinedPath = [mountPath, routePath]
284
+ .filter(item => item)
285
+ .join('') || '/'
286
+
287
+ return {
288
+ method: normalized.method,
289
+ path: joinedPath,
290
+ chain: [
291
+ ...middleware,
292
+ ...normalized.chain,
293
+ ],
294
+ }
295
+ })
296
+
297
+ const mergedRoutes = buildMergedRoutes(
298
+ normalRoutes,
299
+ middleware,
300
+ meta,
301
+ state,
302
+ mountPath,
303
+ )
304
+
305
+ const socketRoutes = buildSocketRoutes(mergedRoutes)
306
+ const moduleRoutes = buildModuleRoutes(socketRoutes, ws)
307
+ const serverRoutes = buildServerRoutes(moduleRoutes)
308
+ const outputRoutes = buildOutputRoutes(moduleRoutes)
309
+
310
+ return {
311
+ server: serverRoutes,
312
+ socket: socketRoutes,
313
+ output: outputRoutes,
314
+ }
315
+ }
316
+
317
+ function buildServer (
318
+ port: number,
319
+ routes: AppRoutes,
320
+ state: SocketState,
321
+ ws: SocketCommands,
322
+ opts: AppOptions,
323
+ ): Server {
324
+ const hostname = opts.hostname || '0.0.0.0'
325
+
326
+ const websocketServer = buildSocketServer(
327
+ routes.socket,
328
+ state,
329
+ ws,
330
+ )
331
+
332
+ return Bun.serve({
333
+ port,
334
+ hostname,
335
+ routes: routes.server,
336
+ websocket: websocketServer,
337
+ async fetch (_req, _server) {
338
+ throw new NotFoundError()
339
+ },
340
+ error (err) {
341
+ console.error(err)
342
+
343
+ const isRequestError = !(err instanceof RequestError)
344
+ const httpError = isRequestError ? new InternalServerError() : err
345
+ const { status } = httpError.constructor as typeof RequestError
346
+
347
+ return Response.json(httpError.output, { status })
348
+ },
349
+ })
350
+ }
351
+
352
+ function processIO (server: Server, opts: AppOptions): CloseFn {
353
+ const onClose = opts.onClose || (() => {})
354
+
355
+ console.info(`Running on port: ${server.port}`)
356
+ console.info('')
357
+ console.info('Press Ctrl+D to gracefully shutdown')
358
+ console.info('')
359
+
360
+ const shutdown = async (force = false): Promise<void> => {
361
+ await server.stop(force)
362
+ await onClose()
363
+ }
364
+
365
+ if (!stdin.isTTY) {
366
+ return shutdown
367
+ }
368
+
369
+ const rl = readline.createInterface({
370
+ input: stdin,
371
+ output: stdout,
372
+ })
373
+
374
+ const handleClose = async (): Promise<void> => {
375
+ await shutdown()
376
+ process.exit(0)
377
+ }
378
+
379
+ rl.on('close', handleClose)
380
+
381
+ return async (force = false) => {
382
+ rl.off('close', handleClose)
383
+ rl.close()
384
+
385
+ await shutdown(force)
386
+ }
387
+ }
388
+
389
+ export function createApp (
390
+ port: number,
391
+ config: RouteConfig,
392
+ opts: AppOptions = {},
393
+ ): App {
394
+ const state = buildSocketState(opts)
395
+ const ws = buildSocketCommands(state)
396
+ const routes = buildRoutes(config, state, ws, opts)
397
+ const server = buildServer(port, routes, state, ws, opts)
398
+ const close = processIO(server, opts)
399
+
400
+ return {
401
+ routes: routes.output,
402
+ server,
403
+ ws,
404
+ close,
405
+ }
406
+ }
@@ -204,6 +204,22 @@ export type SocketConnection = {
204
204
 
205
205
  export type Server = BunServer<SocketData>
206
206
 
207
+ export type RouteDefinition = {
208
+ method: HttpMethod
209
+ path: string
210
+ chain: Handler | MiddlewareChain
211
+ }
212
+
213
+ export type MetaEntry = {
214
+ path: string
215
+ middleware: Middleware[]
216
+ }
217
+
218
+ export type RouteConfig = {
219
+ routes: RouteDefinition[]
220
+ meta?: MetaEntry[]
221
+ }
222
+
207
223
  export type AppOptions = {
208
224
  hostname?: string
209
225
  mountPath?: string