rajt 0.0.0 → 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present, ZUNQ.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ <p align="left">
2
+ <a href="https://npmjs.com/package/rajt"><img src="https://img.shields.io/npm/v/rajt.svg" alt="npm package"></a>
3
+ <a href="https://pr.new/attla/rajt"><img src="https://developer.stackblitz.com/img/start_pr_dark_small.svg" alt="Start new PR in StackBlitz Codeflow"></a>
4
+ </p>
5
+ <br/>
6
+
7
+ # Rajt λ
8
+
9
+ > This framework is fully geared towards the serverless world, specifically AWS Lambda (Node.js and LLRT) / Cloudflare Workers.
10
+
11
+ - 💡 Instant Server Start
12
+ - ⚡️ Fast Cold Start
13
+ - 📦 Optimized Build
14
+
15
+ [Read the Docs to Learn More](DOCS.md).
16
+
17
+ ## Packages
18
+
19
+ | Package | Version (click for changelogs) |
20
+ | ----------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
21
+ | [rajt](https://github.com/attla/rajt) | [![rajt version](https://img.shields.io/npm/v/rajt.svg?label=%20)](https://github.com/attla/rajt/CHANGELOG.md) |
22
+ | [create-rajt](https://github.com/attla/create-rajt) | [![create-rajt version](https://img.shields.io/npm/v/create-rajt.svg?label=%20)](https://github.com/attla/create-rajt/CHANGELOG.md) |
23
+
24
+ ## License
25
+
26
+ This package is licensed under the [MIT license](LICENSE) © [Zunq](https://zunq.com).
package/package.json CHANGED
@@ -1 +1,45 @@
1
- {"name":"rajt","version":"0.0.0"}
1
+ {
2
+ "name": "rajt",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./http": "./src/http.ts",
9
+ "./types": "./src/types.ts"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "sam": "sam local start-api --warm-containers LAZY --debug",
16
+ "dev": "tsx watch src/dev.ts",
17
+ "prod": "bun run build && bun run sam",
18
+ "build": "bun run cache:routes && bun run export && bun run clean:temp",
19
+ "build:watch": "chokidar \"{actions,src,models,utils,configs}/**/*.ts\" -c \"bun run build\" --initial",
20
+ "export": "esbuild --bundle --minify --outfile=dist/index.js --platform=node --target=node20 --format=esm --tree-shaking=true --legal-comments=none src/prod.ts",
21
+ "cache:routes": "tsx src/scripts/cache-routes.ts",
22
+ "ensure-dirs": "rm -rf dist tmp && mkdir -p ./tmp && chmod 755 ./tmp && mkdir -p ./dist && chmod 755 ./dist",
23
+ "clean": "rm -rf dist tmp",
24
+ "clean:build": "rm -rf dist",
25
+ "clean:temp": "rm -rf tmp",
26
+ "zip": "zip -j lambda.zip dist/index.js",
27
+ "update": "aws lambda update-function-code --zip-file fileb://lambda.zip --function-name hello",
28
+ "deploy": "run-s build zip update",
29
+ "start": "node dist/index.js"
30
+ },
31
+ "dependencies": {
32
+ "@hono/zod-validator": "^0.4.3",
33
+ "dotenv": "^16.5.0",
34
+ "hono": "^4.7.6"
35
+ },
36
+ "devDependencies": {
37
+ "@hono/node-server": "^1.14.1",
38
+ "@types/node": "^20.11.0",
39
+ "chokidar-cli": "^3.0.0",
40
+ "esbuild": "^0.25.2",
41
+ "tsconfig-paths": "^4.2.0",
42
+ "tsx": "^4.19.3",
43
+ "typescript": "^5.8.3"
44
+ }
45
+ }
package/src/action.ts ADDED
@@ -0,0 +1,151 @@
1
+ import { Context, Handler, HonoRequest, MiddlewareHandler, Next, ValidationTargets } from 'hono'
2
+ import { z, ZodObject, ZodRawShape } from 'zod'
3
+ import { zValidator } from '@hono/zod-validator'
4
+ // import { JSONValue } from 'hono/utils/types'
5
+ import json from './response'
6
+ import JsonResponse from './response'
7
+ import { bufferToFormData } from 'hono/utils/buffer'
8
+ import { HTTPException } from 'hono/http-exception'
9
+ import { BodyData } from 'hono/utils/body'
10
+
11
+ export type ActionType = Function | Handler | Action | (new () => Action)
12
+
13
+ type RuleDefinition = {
14
+ schema: z.ZodObject<any>
15
+ target: keyof ValidationTargets
16
+ eTarget?: 'fieldErrors' | 'formErrors'
17
+ }
18
+
19
+ export default abstract class Action {
20
+ protected context!: Context
21
+ protected errorTarget!: string
22
+ protected rules(): RuleDefinition[] | RuleDefinition | null {
23
+ return null
24
+ }
25
+
26
+ rule<T extends keyof ValidationTargets>(target: T): { schema: (schema: ZodObject<any>) => RuleDefinition }
27
+ rule<T extends keyof ValidationTargets>(target: T, schema: ZodObject<any>): RuleDefinition
28
+
29
+ public rule<T extends keyof ValidationTargets>(target: T, schema?: ZodObject<any>):
30
+ | { schema: (schema: ZodObject<any>) => RuleDefinition }
31
+ | RuleDefinition
32
+ {
33
+ if (schema !== undefined) {
34
+ return {
35
+ target,
36
+ schema,
37
+ eTarget: 'fieldErrors' // | 'formErrors'
38
+ } satisfies RuleDefinition
39
+ }
40
+
41
+ return {
42
+ schema: (schema: ZodObject<any>) => ({
43
+ target,
44
+ schema,
45
+ eTarget: 'fieldErrors' // | 'formErrors'
46
+ })
47
+ }
48
+ }
49
+
50
+ public param(key: string) {
51
+ return this.context.req.param(key)
52
+ }
53
+
54
+ public query() {
55
+ return this.context.req.query()
56
+ }
57
+
58
+ public async form(cType?: string) {
59
+ cType ??= this.context.req.header('Content-Type')
60
+ if (!cType) return {}
61
+
62
+ let formData: FormData
63
+
64
+ if (this.context.req.bodyCache.formData) {
65
+ formData = await this.context.req.bodyCache.formData
66
+ } else {
67
+ try {
68
+ const arrayBuffer = await this.context.req.arrayBuffer()
69
+ formData = await bufferToFormData(arrayBuffer, cType)
70
+ this.context.req.bodyCache.formData = formData
71
+ } catch (e) {
72
+ throw new HTTPException(400, {
73
+ message: 'Malformed FormData request.'
74
+ + (e instanceof Error ? ` ${e.message}` : ` ${String(e)}`)
75
+ })
76
+ }
77
+ }
78
+
79
+ const form: BodyData<{ all: true }> = {}
80
+ formData.forEach((value, key) => {
81
+ if (key.endsWith('[]')) {
82
+ ;((form[key] ??= []) as unknown[]).push(value)
83
+ } else if (Array.isArray(form[key])) {
84
+ ;(form[key] as unknown[]).push(value)
85
+ } else if (key in form) {
86
+ form[key] = [form[key] as string | File, value]
87
+ } else {
88
+ form[key] = value
89
+ }
90
+ })
91
+
92
+ return form
93
+ }
94
+
95
+ public async json<E>() {
96
+ try {
97
+ return await this.context.req.json<E>()
98
+ } catch {
99
+ throw new HTTPException(400, { message: 'Malformed JSON in request body' })
100
+ }
101
+ }
102
+
103
+ public async body<E>() {
104
+ const cType = this.context.req.header('Content-Type')
105
+ if (!cType) return {}
106
+
107
+ if (/^application\/([a-z-\.]+\+)?json(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)) {
108
+ return await this.json<E>()
109
+ }
110
+
111
+ if (
112
+ /^multipart\/form-data(;\s?boundary=[a-zA-Z0-9'"()+_,\-./:=?]+)?$/.test(cType)
113
+ && ! /^application\/x-www-form-urlencoded(;\s*[a-zA-Z0-9\-]+\=([^;]+))*$/.test(cType)
114
+ ) {
115
+ return await this.form() as E
116
+ }
117
+
118
+ return {}
119
+ }
120
+
121
+ public get response() {
122
+ return this.context ? JsonResponse.setContext(this.context) : JsonResponse
123
+ }
124
+
125
+ public validate() {
126
+ const rules = this.rules()
127
+ const h = async (c: Context) => {
128
+ this.context = c
129
+ return await this.handle(c)
130
+ }
131
+ if (!rules) return [h]
132
+
133
+ const rulesArray = (Array.isArray(rules) ? rules : [rules])
134
+ .map(rule => zValidator(rule.target, rule.schema, (result, c) => {
135
+ if (!result.success) {
136
+ // @ts-ignore
137
+ return json.badRequest({ ...result.error.flatten()[rule.eTarget] })
138
+ }
139
+ }))
140
+
141
+ rulesArray.push(h)
142
+
143
+ return rulesArray
144
+ }
145
+
146
+ public run() {
147
+ return this.validate()
148
+ }
149
+
150
+ abstract handle(c: Context): Promise<Response>
151
+ }
@@ -0,0 +1,124 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Hono } from 'hono'
3
+ import type { Env, Context, ErrorHandler, NotFoundHandler, Next } from 'hono'
4
+ // import type { Env, Context, ErrorHandler, MiddlewareHandler, NotFoundHandler, Next } from 'hono'
5
+ // import { createMiddleware } from 'hono/factory'
6
+ // import type { H, Handler, HandlerResponse } from 'hono/types'
7
+ import { HTTPResponseError } from 'hono/types'
8
+ import { Routes } from './types'
9
+ import { BadRequest, Unauthorized } from './exceptions'
10
+ import response from './response'
11
+ import resolve from './utils/resolve'
12
+ import { getHandler } from './register'
13
+ import env from './utils/environment'
14
+
15
+ type InitFunction<E extends Env = Env> = (app: Hono<E>) => void
16
+
17
+ export type ServerOptions<E extends Env = Env> = Partial<{
18
+ routes: Routes,
19
+ notFound: NotFoundHandler<E>,
20
+ onError: ErrorHandler<E>,
21
+ root: string,
22
+ app?: Hono<E>,
23
+ init?: InitFunction<E>,
24
+ }>
25
+
26
+ const isDev = env() === 'dev'
27
+ const NFHandler = () => response.notFound()
28
+ const EHandler = async (e: Error | HTTPResponseError) => {
29
+ console.error(e)
30
+
31
+ switch (true) {
32
+ case e instanceof Unauthorized:
33
+ case 'status' in e && e.status === 401:
34
+ return response.unauthorized()
35
+
36
+ case e instanceof BadRequest:
37
+ case 'status' in e && e.status === 400:
38
+ // @ts-ignore
39
+ return response.badRequest(undefined, e?.message)
40
+
41
+ // case e.message.includes('Not Found'):
42
+ // // @ts-ignore
43
+ // case e?.status === 404:
44
+ // return json.notFound();
45
+
46
+ default:
47
+ return response.internalError(
48
+ // @ts-ignore
49
+ isDev
50
+ ? e.stack?.split('\n').map(line =>
51
+ line.replace(
52
+ /at (.+ )?\(?([^)]+)\)?/g,
53
+ (match, method, path) => {
54
+ if (!path) return match
55
+
56
+ const nodeModulesIndex = path.indexOf('node_modules')
57
+ if (nodeModulesIndex > -1) {
58
+ return `${method || ''}(node_modules${path.slice(nodeModulesIndex + 'node_modules'.length)})`
59
+ }
60
+
61
+ const projectRoot = process.cwd()
62
+ const relativePath = path.startsWith(projectRoot)
63
+ ? path.slice(projectRoot.length + 1)
64
+ : path
65
+ return `${method || ''}(${relativePath})`
66
+ }
67
+ ).trim()
68
+ )
69
+ : undefined,
70
+ e.message || 'Internal Error'
71
+ )
72
+ }
73
+
74
+ // return json.internalError(
75
+ // // @ts-ignore
76
+ // isDev ? e.stack?.split('\n at ').map() : undefined,
77
+ // e.message || 'Internal Error'
78
+ // )
79
+ // error: e.message,
80
+ // cause: e.cause || 'Desconhecido',
81
+ // stack: isDev ? e.stack : undefined
82
+ }
83
+
84
+ export const createApp = <E extends Env>(options?: ServerOptions<E>) => {
85
+ // const root = options?.root ?? '/'
86
+ const app = options?.app ?? new Hono<E>()
87
+
88
+ const middlewares = [
89
+ async (c: Context, next: Next) => {
90
+ response.setContext(c)
91
+ await next()
92
+ },
93
+ ]
94
+ middlewares.forEach(middleware => app.use(middleware))
95
+
96
+ app.onError(options?.onError || EHandler)
97
+ app.notFound(options?.notFound || NFHandler)
98
+
99
+ if (options?.init)
100
+ options.init(app)
101
+
102
+ const routes = options?.routes || []
103
+ for (const route of routes) {
104
+ if (Array.isArray(route)) {
105
+ const handle = getHandler(route[3])
106
+ // @ts-ignore
107
+ app[route[0]](route[1], ...mw(route[2], route[3]), ...resolve(handle))
108
+ } else {
109
+ // @ts-ignore
110
+ app[route.method](route.path, ...mw(route.middlewares, route.name), ...resolve(route.handle))
111
+ }
112
+ }
113
+
114
+ return app
115
+ }
116
+
117
+ function mw(...objs: string[]): Function[] {
118
+ return objs.flatMap(obj => {
119
+ // @ts-ignore
120
+ return getHandler(obj)?.mw || null
121
+ }).flat().filter(Boolean)
122
+ }
123
+
124
+ export default createApp
package/src/dev.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { config } from 'dotenv'
2
+ import { serve } from '@hono/node-server'
3
+ import createApp from './create-app'
4
+ import getRoutes from './routes'
5
+ import { getAvailablePort } from './utils/port'
6
+
7
+ config({ path: '.env.dev' })
8
+
9
+ const routes = await getRoutes()
10
+ const fetch = createApp({ routes }).fetch
11
+
12
+ const desiredPort = process.env?.PORT ? Number(process.env.PORT) : 3000
13
+ getAvailablePort(desiredPort)
14
+ .then(port => {
15
+ if (port != desiredPort)
16
+ console.log(`🟨 Port ${desiredPort} was in use, using ${port} as a fallback`)
17
+
18
+ console.log(`🚀 API running on http://localhost:${port}`)
19
+ serve({ fetch, port })
20
+ }).catch(err => {
21
+ console.error('Error finding available port:', err)
22
+ })
@@ -0,0 +1,15 @@
1
+ export class Unauthorized extends Error {
2
+ status = 401
3
+ constructor(message = 'Unauthorized') {
4
+ super(message)
5
+ this.name = 'UnauthorizedError'
6
+ }
7
+ }
8
+
9
+ export class BadRequest extends Error {
10
+ status = 400
11
+ constructor(message = 'Bad Request') {
12
+ super(message)
13
+ this.name = 'BadRequestError'
14
+ }
15
+ }
package/src/http.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { MiddlewareHandler } from 'hono'
2
+ import BaseMiddleware, { MiddlewareType } from './middleware'
3
+
4
+ function method(method: string, path = '/') {
5
+ return function (target: any) {
6
+ target.m = method
7
+ target.p = path
8
+ target.mw = []
9
+ }
10
+ }
11
+
12
+ export function Get(path = '/') {
13
+ return method('get', path)
14
+ }
15
+
16
+ export function Post(path = '/') {
17
+ return method('post', path)
18
+ }
19
+
20
+ export function Put(path = '/') {
21
+ return method('put', path)
22
+ }
23
+
24
+ export function Patch(path = '/') {
25
+ return method('patch', path)
26
+ }
27
+
28
+ export function Delete(path = '/') {
29
+ return method('delete', path)
30
+ }
31
+
32
+ export function Middleware(...handlers: MiddlewareType[]) {
33
+ return function (target: any) {
34
+ const middlewareHandlers = handlers.map(handler => {
35
+ if (typeof handler === 'function' && handler.length === 2)
36
+ return handler
37
+
38
+ if (handler instanceof BaseMiddleware)
39
+ return handler.handle
40
+
41
+ if (BaseMiddleware.isPrototypeOf(handler)) {
42
+ const instance = new (handler as new () => BaseMiddleware)()
43
+ return instance.handle
44
+ }
45
+
46
+ throw new Error('Invalid middleware provided. Must be a Hono middleware function or MiddlewareClass instance/constructor')
47
+ })
48
+
49
+ const existingMiddlewares: MiddlewareHandler[] = target?.mw || []
50
+ const allMiddlewares = [...existingMiddlewares, ...middlewareHandlers]
51
+
52
+ target.mw = allMiddlewares
53
+ }
54
+ }
55
+ export function Middlewares(...handlers: MiddlewareType[]) {
56
+ return Middleware(...handlers)
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { default as Action } from './action'
2
+ export { default as Middleware } from './middleware'
3
+ export { default as JsonResponse } from './response'
@@ -0,0 +1,12 @@
1
+ import type { Context, MiddlewareHandler, Next } from 'hono'
2
+
3
+ export type IMiddleware = {
4
+ handle(c: Context, next: Next): Promise<void> | void
5
+ }
6
+
7
+ export default abstract class Middleware implements IMiddleware {
8
+ public abstract handle(c: Context, next: Next): Promise<void> | void
9
+ }
10
+
11
+ // export type MiddlewareHandler = (c: Context, next: Next) => Promise<void> | void
12
+ export type MiddlewareType = MiddlewareHandler | Middleware | (new () => Middleware)
package/src/prod.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { config } from 'dotenv'
2
+ import { handle } from 'hono/aws-lambda'
3
+ import createApp from './create-app'
4
+
5
+ config({ path: '.env.prod' })
6
+
7
+ // @ts-ignore
8
+ await import('../tmp/import-routes.mjs')
9
+
10
+ // @ts-ignore
11
+ const routes = (await import('../tmp/routes.json')).default
12
+ // @ts-ignore
13
+ const app = createApp({ routes })
14
+
15
+ // export default app // AWS Lambda & Cloudflare Workers
16
+ export const handler = handle(app) // AWS Lambda (LLRT)
@@ -0,0 +1,25 @@
1
+ export const handlers = {}
2
+
3
+ export function registerHandler(id: string, handler: any) {
4
+ if (typeof handler === 'function') {
5
+ // @ts-ignore
6
+ handlers[id] = handler
7
+ } else if (handler.prototype?.handle) {
8
+ const instance = new handler()
9
+ // @ts-ignore
10
+ handlers[id] = instance.handle.bind(instance)
11
+ } else if (handler.run) {
12
+ const instance = new handler()
13
+ // @ts-ignore
14
+ handlers[id] = instance.run.bind(instance)
15
+ } else {
16
+ console.warn(`Handler ${id} could not be registered - unsupported type`)
17
+ }
18
+ }
19
+
20
+ export function getHandler(id: string): Function {
21
+ // @ts-ignore
22
+ const handler = handlers[id] || null
23
+ if (!handler) throw new Error(`Handler ${id} not registered`)
24
+ return handler
25
+ }
@@ -0,0 +1,137 @@
1
+ import { Context } from 'hono'
2
+ import { ContentfulStatusCode, StatusCode } from 'hono/utils/http-status'
3
+ import { ErrorResponse, Errors, LambdaResponse, ResponseOrInit } from './types'
4
+ import { InvalidJSONValue, JSONValue } from 'hono/utils/types'
5
+
6
+ class NullContext {
7
+ resp = new Response('Context not found. Use JsonResponse.setContext() first.', { status: 500 })
8
+
9
+ newResponse(body?: any, init?: ResponseOrInit<StatusCode>): Response {
10
+ return this.resp
11
+ }
12
+ json(json: JSONValue | {} | InvalidJSONValue, status: ContentfulStatusCode = 500): Response {
13
+ return this.resp
14
+ }
15
+ // overide do hono context
16
+ // newResponse(body?: any, init?: ResponseOrInit<StatusCode>): Response {
17
+ // return new Response(body ?? null, typeof init === 'number' ? {status: init} : init ?? {status: 500})
18
+ // }
19
+ // json(json: JSONValue | {} | InvalidJSONValue, status: ContentfulStatusCode = 500): Response {
20
+ // return new Response(JSON.stringify(json), {
21
+ // status,
22
+ // headers: { 'Content-Type': 'application/json' }
23
+ // })
24
+ // }
25
+ }
26
+
27
+ export default class JsonResponse {
28
+ private static _c?: Context
29
+
30
+ static setContext(c: Context) {
31
+ this._c = c
32
+ return this
33
+ }
34
+
35
+ private static context(): Context | NullContext {
36
+ return this._c ?? new NullContext()
37
+ }
38
+
39
+ static raw(status?: StatusCode, body?: string) {
40
+ return this.context().newResponse(body ? body : null, { status })
41
+ }
42
+
43
+ static ok(): Response
44
+ static ok<T>(data: T): Response
45
+ static ok<T>(data?: T) {
46
+ if (data === undefined)
47
+ return this.raw(200)
48
+
49
+ return this.context().json(data, 200)
50
+ }
51
+
52
+ static created(): Response
53
+ static created<T>(data: T): Response
54
+ static created<T>(data?: T) {
55
+ if (data === undefined)
56
+ return this.raw(201)
57
+
58
+ return this.context().json(data, 201)
59
+ }
60
+
61
+ static accepted(): Response
62
+ static accepted<T>(data: T): Response
63
+ static accepted<T>(data?: T) {
64
+ if (data === undefined)
65
+ return this.raw(202)
66
+
67
+ return this.context().json(data, 202)
68
+ }
69
+
70
+ static deleted(): Response
71
+ static deleted<T>(data: T): Response
72
+ static deleted<T>(data?: T) {
73
+ return this.noContent()
74
+ }
75
+
76
+ static noContent() {
77
+ return this.raw(204)
78
+ }
79
+
80
+ static badRequest(): Response
81
+ static badRequest(errors?: Errors, msg?: string) {
82
+ return this.error(errors, msg, 400)
83
+ }
84
+
85
+ static unauthorized(): Response
86
+ static unauthorized<T>(data: T): Response
87
+ static unauthorized<T>(data?: T) {
88
+ if (data === undefined)
89
+ return this.raw(401)
90
+
91
+ return this.context().json(data, 401)
92
+ }
93
+
94
+ static forbidden(): Response
95
+ static forbidden<T>(data: T): Response
96
+ static forbidden<T>(data?: T) {
97
+ if (data === undefined)
98
+ return this.raw(403)
99
+
100
+ return this.context().json(data, 403)
101
+ }
102
+
103
+ static notFound(): Response
104
+ static notFound(msg: string): Response
105
+ static notFound(msg?: string) {
106
+ return this.raw(404, msg)
107
+ }
108
+
109
+ static conflict(): Response
110
+ static conflict(errors?: Errors, msg?: string) {
111
+ return this.error(errors, msg, 409)
112
+ }
113
+
114
+ static unsupportedMediaType(): Response
115
+ static unsupportedMediaType(errors?: Errors, msg?: string) {
116
+ return this.error(errors, msg, 415)
117
+ }
118
+
119
+ static internalError(): Response
120
+ static internalError(errors?: Errors, msg?: string) {
121
+ return this.error(errors, msg, 500)
122
+ }
123
+
124
+ static error(errors?: Errors, msg?: string, status?: ContentfulStatusCode) {
125
+ const context = this.context()
126
+ status ??= 500
127
+
128
+ if (!errors && !msg)
129
+ return this.raw(status, msg)
130
+
131
+ const resp: ErrorResponse = {}
132
+ if (msg) resp.m = msg
133
+ if (errors) resp.e = errors
134
+
135
+ return context.json(resp, status)
136
+ }
137
+ }
package/src/routes.ts ADDED
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs'
2
+ import path, { relative } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { Route } from './types'
5
+ import { registerHandler } from './register'
6
+ import { isAnonFn } from './utils/func'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ export default async function getRoutes(all: boolean = false, baseDir: string = 'actions'): Promise<Route[]> {
12
+ const routes: Route[] = []
13
+
14
+ const walk = async (dir: string, middlewares: Function[] = []): Promise<void> => {
15
+ const files = fs.readdirSync(dir)
16
+
17
+ for (const file of files) {
18
+ const fullPath = path.join(dir, file)
19
+ const stat = fs.statSync(fullPath)
20
+
21
+ if (stat.isDirectory()) {
22
+ const indexFile = path.join(fullPath, 'index.ts')
23
+
24
+ if (fs.existsSync(indexFile)) {
25
+ const mod = await import(indexFile)
26
+ const group = mod.default
27
+ registerHandler(group.name, group)
28
+ !isAnonFn(group) && middlewares.push(group.name)
29
+
30
+ all && routes.push({
31
+ method: '',
32
+ path: '',
33
+ name: group.name.replace(/\.ts$/, ''),
34
+ file: relative(process.cwd(), indexFile),
35
+ middlewares,
36
+ handle: group,
37
+ })
38
+ }
39
+
40
+ await walk(fullPath, middlewares)
41
+ } else if (file.endsWith('.ts')) {
42
+ const mod = await import(fullPath)
43
+ const handle = mod.default
44
+
45
+ if (handle?.m) {
46
+ registerHandler(handle.name, handle)
47
+
48
+ routes.push({
49
+ method: handle.m.toLowerCase(),
50
+ path: handle.p,
51
+ name: handle.name.replace(/\.ts$/, ''),
52
+ file: relative(process.cwd(), fullPath),
53
+ middlewares,
54
+ handle,
55
+ })
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ await walk(path.resolve(__dirname, '..', baseDir))
62
+ return routes
63
+ }
@@ -0,0 +1,49 @@
1
+ import { writeFileSync } from 'node:fs'
2
+ import getRoutes from '../routes'
3
+ import ensureDir from '../utils/ensuredir'
4
+
5
+ async function cacheRoutes() {
6
+ const routes = await getRoutes(true)
7
+
8
+ const iPath = './tmp/import-routes.mjs'
9
+ ensureDir(iPath)
10
+ writeFileSync(iPath, `// AUTO-GENERATED FILE - DO NOT EDIT
11
+ import { registerHandler } from '../src/register'
12
+
13
+ ${routes.map(r => `import ${r.name} from '../${normalizePath(r.file)}'`).join('\n')}
14
+
15
+ try {
16
+ const handlers = {${routes.map(r => r.name).join()}}
17
+
18
+ for (const [name, handler] of Object.entries(handlers)) {
19
+ if (typeof handler === 'function' || handler.prototype?.handle) {
20
+ registerHandler(name, handler)
21
+ }
22
+ }
23
+ } catch (error) {
24
+ console.error('Failed to register handlers:', error)
25
+ }
26
+ `)
27
+
28
+ const rPath = './tmp/routes.json'
29
+ ensureDir(rPath)
30
+ writeFileSync(rPath, JSON.stringify(routes.filter(r => r.method && r.path).map(route => [
31
+ route.method,
32
+ route.path,
33
+ route.middlewares,
34
+ route.name,
35
+ ])))
36
+ }
37
+
38
+ function normalizePath(file: string) {
39
+ return file.replace(/\.tsx?$/i, '').replace(/(\/index)+$/i, '').replace(/\/+$/g, '')
40
+ }
41
+
42
+ cacheRoutes()
43
+ .then(() => {
44
+ console.log('✅ Routes cached!')
45
+ process.exit(0)
46
+ }).catch(e => {
47
+ console.error('❌ Error: ', e)
48
+ process.exit(1)
49
+ })
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { Handler } from 'hono'
2
+ import { ResponseHeader } from 'hono/utils/headers'
3
+ import { StatusCode } from 'hono/utils/http-status'
4
+ import { BaseMime } from 'hono/utils/mime'
5
+ import Action from './action'
6
+
7
+ export type Route = {
8
+ method: string,
9
+ path: string,
10
+ name: string,
11
+ file: string,
12
+ middlewares: Function[],
13
+ handle: Handlers,
14
+ }
15
+
16
+ export type Handlers = (Function | Handler | (new () => Action))[]
17
+
18
+ export type Routes = Route[]
19
+
20
+ export type LambdaResponse = {
21
+ statusCode: StatusCode,
22
+ body: string,
23
+ }
24
+
25
+ export type Errors = Record<string, string | string[]>
26
+ export type ErrorResponse = {
27
+ m?: string, // message
28
+ // c?: number, // http code
29
+ e?: Errors, // error bag
30
+ // e?: Record<string, string | string[]>, // error bag
31
+ }
32
+
33
+ // export type Response<E> = E | ErrorResponse
34
+
35
+ export type ResponseHeadersInit = [
36
+ string,
37
+ string
38
+ ][] | Record<"Content-Type", BaseMime> | Record<ResponseHeader, string> | Record<string, string> | Headers
39
+ export type ResponseInit<T extends StatusCode = StatusCode> = {
40
+ headers?: ResponseHeadersInit,
41
+ status?: T,
42
+ statusText?: string,
43
+ }
44
+ export type ResponseOrInit<T extends StatusCode = StatusCode> = ResponseInit<T> | Response
45
+ // export type JSONValue =
46
+ // | string
47
+ // | number
48
+ // | boolean
49
+ // | null
50
+ // | { [key: string]: JSONValue }
51
+ // | JSONValue[]
52
+
53
+
54
+ export type { Context, Next } from 'hono'
@@ -0,0 +1,7 @@
1
+ import { mkdirSync, existsSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+
4
+ export default function ensureDirectoryExists(filePath: string) {
5
+ const dir = dirname(filePath)
6
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
7
+ }
@@ -0,0 +1,22 @@
1
+ export default function getEnvironment() {
2
+ if (process.env?.npm_lifecycle_event === 'dev')
3
+ return 'dev'
4
+
5
+ if (
6
+ process.argv?.includes('--dev')
7
+ || process.execArgv?.includes('--watch')
8
+ || import.meta.url?.includes('localhost')
9
+ )
10
+ return 'dev'
11
+
12
+ // @ts-ignore
13
+ if (typeof Bun === 'undefined') return 'prod'
14
+
15
+ // @ts-ignore
16
+ if (Bun.argv.includes('--prod')) return 'prod'
17
+ // @ts-ignore
18
+ if (Bun.argv.includes('--dev') || Bun.main.endsWith('.ts')) return 'dev'
19
+
20
+
21
+ return 'prod'
22
+ }
@@ -0,0 +1,8 @@
1
+ export const isAsyncFn = (fn: any) => {
2
+ return fn?.constructor?.name === 'AsyncFunction'
3
+ || fn.toString().toLowerCase().trim().startsWith('async')
4
+ }
5
+
6
+ export const isAnonFn = (fn: any) => {
7
+ return fn?.name === '' || fn?.name === 'anonymous'
8
+ }
@@ -0,0 +1,30 @@
1
+ import net from 'node:net'
2
+
3
+ export async function isPortInUse(port: number) {
4
+ return new Promise(resolve => {
5
+ const server = net.createServer()
6
+ .once('error', () => resolve(true))
7
+ .once('listening', () => {
8
+ server.close(() => resolve(false))
9
+ })
10
+ .listen(port)
11
+ })
12
+ }
13
+
14
+ export async function getAvailablePort(startPort: number, maxAttempts = 100) {
15
+ let port = startPort
16
+ let attempts = 0
17
+
18
+ while (attempts < maxAttempts) {
19
+ const inUse = await isPortInUse(port)
20
+
21
+ if (!inUse) {
22
+ return port
23
+ }
24
+
25
+ port++
26
+ attempts++
27
+ }
28
+
29
+ throw new Error(`No available ports found after ${maxAttempts} attempts`)
30
+ }
@@ -0,0 +1,56 @@
1
+ import Action, { ActionType } from '../action'
2
+ // import BaseMiddleware, { MiddlewareType } from '../middleware'
3
+
4
+ export default function resolve(obj: ActionType) {
5
+ if (typeof obj === 'function' && obj?.length === 2)
6
+ return [obj]
7
+
8
+ if (obj instanceof Action)
9
+ return obj.run()
10
+
11
+ const instance = new (obj as new () => Action)()
12
+ if (Action.isPrototypeOf(obj))
13
+ return instance.run()
14
+
15
+ if (obj?.prototype?.handle)
16
+ return [instance.handle]
17
+
18
+ throw new Error('Invalid action')
19
+ }
20
+
21
+ // export function resolveMiddleware(obj: MiddlewareType) {
22
+ // if (typeof obj === 'function' && obj.length === 2)
23
+ // return obj
24
+
25
+ // if (obj instanceof BaseMiddleware)
26
+ // return obj.handle
27
+
28
+ // if (BaseMiddleware.isPrototypeOf(obj)) {
29
+ // const instance = new (obj as new () => BaseMiddleware)()
30
+ // return instance.handle
31
+ // }
32
+
33
+ // throw new Error('Invalid middleware provided. Must be a Hono middleware function or MiddlewareClass instance/constructor')
34
+ // }
35
+
36
+ // // import Action, { ActionType } from '../action'
37
+
38
+ // export default function resolve(obj: any) {
39
+ // if (typeof obj === 'function' && obj?.length === 2)
40
+ // return [obj]
41
+
42
+ // // if (obj instanceof Action)
43
+ // // return obj.run()
44
+
45
+ // // const instance = new (obj as new () => Action)()
46
+ // // @ts-ignore
47
+ // const instance = new obj()
48
+ // // if (Action.isPrototypeOf(obj))
49
+ // if (obj?.prototype?.run)
50
+ // return instance.run()
51
+
52
+ // if (obj?.prototype?.handle)
53
+ // return [instance.handle]
54
+
55
+ // throw new Error('Invalid action')
56
+ // }