hanni 1.0.3 → 1.0.5
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/hanni.js +29 -24
- package/src/router.js +10 -7
- package/src/swagger.js +7 -40
- package/src/scalar.js +0 -0
package/package.json
CHANGED
package/src/hanni.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { Router } from './router.js'
|
|
1
|
+
import { Router as _Router } from './router.js'
|
|
2
2
|
import { createContext } from './context.js'
|
|
3
3
|
import { buildSpec, swaggerHTML } from './swagger.js'
|
|
4
4
|
import { compose } from './middleware.js'
|
|
5
5
|
import { parseBody, corsHeaders } from './utils.js'
|
|
6
6
|
|
|
7
|
+
export { _Router as Router }
|
|
8
|
+
|
|
7
9
|
export function Hanni(config = {}) {
|
|
8
|
-
const router = new
|
|
10
|
+
const router = new _Router()
|
|
9
11
|
const middlewares = []
|
|
12
|
+
const mounts = []
|
|
10
13
|
const swaggerCfg = config.swagger
|
|
11
14
|
|
|
12
15
|
const app = {}
|
|
13
16
|
|
|
14
17
|
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'ALL']
|
|
15
|
-
|
|
16
18
|
for (const m of methods) {
|
|
17
19
|
app[m.toLowerCase()] = (path, handler, meta) => {
|
|
18
20
|
router.add(m, path, handler, meta)
|
|
@@ -20,8 +22,12 @@ export function Hanni(config = {}) {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
app.use =
|
|
24
|
-
|
|
25
|
+
app.use = (arg1, arg2) => {
|
|
26
|
+
if (typeof arg1 === 'string' && arg2 && arg2.list) {
|
|
27
|
+
mounts.push({ path: arg1, router: arg2 })
|
|
28
|
+
} else if (typeof arg1 === 'function') {
|
|
29
|
+
middlewares.push(arg1)
|
|
30
|
+
}
|
|
25
31
|
return app
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -33,26 +39,31 @@ export function Hanni(config = {}) {
|
|
|
33
39
|
|
|
34
40
|
if (swaggerCfg) {
|
|
35
41
|
const base = swaggerCfg.path
|
|
36
|
-
|
|
37
42
|
if (url.pathname === base || url.pathname === base + '/') {
|
|
38
|
-
return new Response(
|
|
39
|
-
swaggerHTML(base + '/json', swaggerCfg),
|
|
40
|
-
{ headers: { 'Content-Type': 'text/html' } }
|
|
41
|
-
)
|
|
43
|
+
return new Response(swaggerHTML(base + '/json', swaggerCfg), { headers: { 'Content-Type': 'text/html' } })
|
|
42
44
|
}
|
|
43
|
-
|
|
44
45
|
if (url.pathname === base + '/json') {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const allRoutes = [
|
|
47
|
+
...router.list(),
|
|
48
|
+
...mounts.flatMap(mount => mount.router.list().map(r => ({ ...r, path: mount.path + r.path })))
|
|
49
|
+
]
|
|
50
|
+
return Response.json(buildSpec(allRoutes, swaggerCfg))
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
if (config.cors && req.method === 'OPTIONS') {
|
|
52
|
-
|
|
54
|
+
if (config.cors && req.method === 'OPTIONS') return new Response(null, { headers: corsHeaders() })
|
|
55
|
+
|
|
56
|
+
let match = router.match(req.method, url.pathname)
|
|
57
|
+
if (!match) {
|
|
58
|
+
for (const mount of mounts) {
|
|
59
|
+
if (url.pathname.startsWith(mount.path)) {
|
|
60
|
+
const subPath = url.pathname.slice(mount.path.length) || '/'
|
|
61
|
+
match = mount.router.match(req.method, subPath)
|
|
62
|
+
if (match) break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
const match = router.match(req.method, url.pathname)
|
|
56
67
|
if (!match) return new Response('Not Found', { status: 404 })
|
|
57
68
|
|
|
58
69
|
const ctx = createContext(req, match.params)
|
|
@@ -61,17 +72,11 @@ export function Hanni(config = {}) {
|
|
|
61
72
|
const handler = compose(middlewares, match.handler)
|
|
62
73
|
const res = await handler(ctx)
|
|
63
74
|
|
|
64
|
-
if (config.cors)
|
|
65
|
-
Object.entries(corsHeaders()).forEach(([k, v]) =>
|
|
66
|
-
res.headers.set(k, v)
|
|
67
|
-
)
|
|
68
|
-
}
|
|
75
|
+
if (config.cors) Object.entries(corsHeaders()).forEach(([k, v]) => res.headers.set(k, v))
|
|
69
76
|
|
|
70
77
|
return res
|
|
71
78
|
}
|
|
72
79
|
})
|
|
73
|
-
|
|
74
|
-
//console.log(`hanni.js running on ${port}`)
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
return app
|
package/src/router.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
export class Router {
|
|
2
2
|
constructor() {
|
|
3
3
|
this.routes = []
|
|
4
|
+
const methods = ['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS','ALL']
|
|
5
|
+
for (const m of methods) {
|
|
6
|
+
this[m.toLowerCase()] = (path, handler, meta = {}) => this.add(m, path, handler, meta)
|
|
7
|
+
}
|
|
4
8
|
}
|
|
5
9
|
|
|
6
10
|
add(method, path, handler, meta = {}) {
|
|
7
11
|
const keys = []
|
|
8
12
|
const regex = new RegExp(
|
|
9
13
|
'^' +
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
path.replace(/\/:([^/]+)/g, (_, k) => {
|
|
15
|
+
keys.push(k)
|
|
16
|
+
return '/([^/]+)'
|
|
17
|
+
}) +
|
|
18
|
+
'$'
|
|
15
19
|
)
|
|
16
|
-
|
|
17
20
|
this.routes.push({ method, path, regex, keys, handler, meta })
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -24,7 +27,7 @@ export class Router {
|
|
|
24
27
|
if (!m) continue
|
|
25
28
|
|
|
26
29
|
const params = {}
|
|
27
|
-
r.keys.forEach((k,
|
|
30
|
+
r.keys.forEach((k,i) => params[k] = m[i+1])
|
|
28
31
|
return { handler: r.handler, params, meta: r.meta }
|
|
29
32
|
}
|
|
30
33
|
}
|
package/src/swagger.js
CHANGED
|
@@ -30,7 +30,6 @@ function extractQueryParams(meta) {
|
|
|
30
30
|
function buildRequestBody(meta, method) {
|
|
31
31
|
if (!meta?.body) return undefined
|
|
32
32
|
if (method === 'get' || method === 'head') return undefined
|
|
33
|
-
|
|
34
33
|
return {
|
|
35
34
|
required: true,
|
|
36
35
|
content: {
|
|
@@ -46,13 +45,12 @@ export function buildSpec(routes, cfg = {}) {
|
|
|
46
45
|
const tagsMap = new Map()
|
|
47
46
|
|
|
48
47
|
for (const r of routes) {
|
|
48
|
+
if (r.meta?.swagger === false) continue
|
|
49
49
|
const openPath = normalizePath(r.path)
|
|
50
50
|
if (!paths[openPath]) paths[openPath] = {}
|
|
51
51
|
|
|
52
52
|
const methods =
|
|
53
|
-
r.method === 'ALL'
|
|
54
|
-
? HTTP_METHODS
|
|
55
|
-
: [r.method.toLowerCase()]
|
|
53
|
+
r.method === 'ALL' ? HTTP_METHODS : [r.method.toLowerCase()]
|
|
56
54
|
|
|
57
55
|
const routeTags =
|
|
58
56
|
Array.isArray(r.meta?.tags) && r.meta.tags.length
|
|
@@ -105,7 +103,7 @@ export function swaggerHTML(jsonPath, cfg = {}) {
|
|
|
105
103
|
const title = cfg.title || 'Hanni Docs'
|
|
106
104
|
const favicon =
|
|
107
105
|
cfg.favicon ||
|
|
108
|
-
`
|
|
106
|
+
`https://cdn.2mathewww.my.id/f/614b9f1c86c5fa177f22513bc320eaaa6e5f51c111bf93f7ce2dcfa4d0dcd7f6.jpg`
|
|
109
107
|
|
|
110
108
|
return `
|
|
111
109
|
<!doctype html>
|
|
@@ -113,44 +111,22 @@ export function swaggerHTML(jsonPath, cfg = {}) {
|
|
|
113
111
|
<head>
|
|
114
112
|
<meta charset="utf-8"/>
|
|
115
113
|
<title>${title}</title>
|
|
116
|
-
|
|
117
114
|
<link rel="icon" href="${favicon}">
|
|
118
|
-
|
|
119
|
-
<!-- Tailwind CDN -->
|
|
120
115
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
121
|
-
|
|
122
|
-
<!-- Font Awesome -->
|
|
123
|
-
<link
|
|
124
|
-
rel="stylesheet"
|
|
125
|
-
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
|
|
126
|
-
/>
|
|
127
|
-
|
|
116
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"/>
|
|
128
117
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
|
|
129
|
-
|
|
130
118
|
<style>
|
|
131
119
|
body.dark .swagger-ui {
|
|
132
120
|
filter: invert(1) hue-rotate(180deg);
|
|
133
121
|
}
|
|
134
122
|
</style>
|
|
135
123
|
</head>
|
|
136
|
-
|
|
137
124
|
<body class="bg-white text-slate-900 transition-all">
|
|
138
|
-
|
|
139
|
-
<!-- Toggle Button -->
|
|
140
|
-
<button
|
|
141
|
-
id="theme-toggle"
|
|
142
|
-
class="fixed top-4 right-4 z-50 w-11 h-11 rounded-xl
|
|
143
|
-
bg-blue-600 text-white flex items-center justify-center
|
|
144
|
-
shadow-lg hover:bg-blue-700 transition"
|
|
145
|
-
aria-label="Toggle theme"
|
|
146
|
-
>
|
|
125
|
+
<button id="theme-toggle" class="fixed top-4 right-4 z-50 w-11 h-11 rounded-xl bg-blue-600 text-white flex items-center justify-center shadow-lg hover:bg-blue-700 transition" aria-label="Toggle theme">
|
|
147
126
|
<i class="fa-solid fa-moon"></i>
|
|
148
127
|
</button>
|
|
149
|
-
|
|
150
128
|
<div id="swagger"></div>
|
|
151
|
-
|
|
152
129
|
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
|
|
153
|
-
|
|
154
130
|
<script>
|
|
155
131
|
const btn = document.getElementById('theme-toggle')
|
|
156
132
|
const icon = btn.querySelector('i')
|
|
@@ -160,23 +136,14 @@ function setTheme(theme) {
|
|
|
160
136
|
document.body.classList.toggle('bg-slate-950', theme === 'dark')
|
|
161
137
|
document.body.classList.toggle('text-slate-100', theme === 'dark')
|
|
162
138
|
document.body.classList.toggle('bg-white', theme !== 'dark')
|
|
163
|
-
|
|
164
|
-
icon.className =
|
|
165
|
-
theme === 'dark'
|
|
166
|
-
? 'fa-solid fa-sun'
|
|
167
|
-
: 'fa-solid fa-moon'
|
|
168
|
-
|
|
139
|
+
icon.className = theme === 'dark' ? 'fa-solid fa-sun' : 'fa-solid fa-moon'
|
|
169
140
|
localStorage.setItem('swagger-theme', theme)
|
|
170
141
|
}
|
|
171
142
|
|
|
172
143
|
setTheme(localStorage.getItem('swagger-theme') || 'light')
|
|
173
144
|
|
|
174
145
|
btn.onclick = () => {
|
|
175
|
-
setTheme(
|
|
176
|
-
document.body.classList.contains('dark')
|
|
177
|
-
? 'light'
|
|
178
|
-
: 'dark'
|
|
179
|
-
)
|
|
146
|
+
setTheme(document.body.classList.contains('dark') ? 'light' : 'dark')
|
|
180
147
|
}
|
|
181
148
|
|
|
182
149
|
SwaggerUIBundle({
|
package/src/scalar.js
DELETED
|
File without changes
|