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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanni",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Minimalist Bun web framework with built-in Swagger UI",
5
5
  "type": "module",
6
6
  "main": "./src/hanni.js",
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 Router()
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 = fn => {
24
- middlewares.push(fn)
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
- return Response.json(
46
- buildSpec(router.list(), swaggerCfg)
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
- return new Response(null, { headers: corsHeaders() })
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
- path.replace(/\/:([^/]+)/g, (_, k) => {
11
- keys.push(k)
12
- return '/([^/]+)'
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, i) => (params[k] = m[i + 1]))
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
- `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'><path d='M12 2L2 7l10 5 10-5-10-5z'/></svg>`
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