galbe 0.8.0 → 0.9.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.
@@ -31,17 +31,17 @@ const fmtRes = (r, p = false) => {
31
31
  if (typeof r === 'object') {
32
32
  try {
33
33
  let resp = fmtObject(r, p, 2)
34
- return process.stdout.write(`${resp}\n`)
34
+ return Bun.write(Bun.stdout, `${resp}\n`)
35
35
  } catch (err) {
36
36
  console.error(err)
37
- return process.stdout.write(`${r}\n`)
37
+ return Bun.write(Bun.stdout, `${r}\n`)
38
38
  }
39
39
  }
40
- if (!p) return process.stdout.write(`${r}\n`)
41
- if (typeof r === 'boolean') return process.stdout.write(`${ansi(p, '38;2;255;128;0', r)}\n`)
42
- if (typeof r === 'number') return process.stdout.write(`${ansi(p, '38;2;10;180;220', r)}\n`)
43
- if (typeof r === 'string') return process.stdout.write(`${ansi(p, '38;2;125;170;0', r)}\n`)
44
- return process.stdout.write(`${r}\n`)
40
+ if (!p) return Bun.write(Bun.stdout, `${r}\n`)
41
+ if (typeof r === 'boolean') return Bun.write(Bun.stdout, `${ansi(p, '38;2;255;128;0', r)}\n`)
42
+ if (typeof r === 'number') return Bun.write(Bun.stdout, `${ansi(p, '38;2;10;180;220', r)}\n`)
43
+ if (typeof r === 'string') return Bun.write(Bun.stdout, `${ansi(p, '38;2;125;170;0', r)}\n`)
44
+ return Bun.write(Bun.stdout, `${r}\n`)
45
45
  }
46
46
 
47
47
  const fetchApi = async (method, path, props) => {
@@ -84,9 +84,9 @@ const fetchApi = async (method, path, props) => {
84
84
  let isJson = res.headers.get('content-type') === 'application/json'
85
85
  if (isJson) fmtRes(await res.json(), format.has('p'))
86
86
  else if (res.headers.get('content-type')?.match(/^text\//)) fmtRes(await res.text(), format.has('p'))
87
- else process.stdout.write(await res.arrayBuffer())
87
+ else Bun.write(Bun.stdout, await res.arrayBuffer())
88
88
  }
89
- if (format.has('t')) process.stdout.write(`${(endTime / 1_000_000).toFixed(2)}ms\n`)
89
+ if (format.has('t')) Bun.write(Bun.stdout, `${(endTime / 1_000_000).toFixed(2)}ms\n`)
90
90
  process.exit(res.ok ? 0 : 1)
91
91
  }
92
92
 
@@ -100,7 +100,7 @@ const formatDefault = def =>
100
100
  : def ?? 'undefined';
101
101
 
102
102
  result = commands.map(c=>{
103
- let args = c.arguments.map(a=>`.argument("${a.name}", "${a.description || a.name+' argument' || ''}")`)
103
+ let args = c.arguments.map(a=>`.argument("${a.name}", "${JSON.stringify(a.description).slice(1,-1) || a.name+' argument' || ''}")`)
104
104
  let optionsBase = [
105
105
  {name: '%format', short:'%f', type: '[string]', description: 'response format [\'s\',\'h\',\'b\',\'t\',\'p\']', default:["s","b","p"]},
106
106
  {name: '%header', short:'%h', type: '<string...>', description: 'request header formated as headerName=headerValue', default:[]},
@@ -108,12 +108,12 @@ result = commands.map(c=>{
108
108
  {name: '%body', short:'%b', type: '<string>', description: 'request body', default:''},
109
109
  {name: '%bodyFile', short:'%bf', type: '<path>', description: 'request body file', default:''}
110
110
  ]
111
- let options = [...optionsBase,...(c.options||[])].map(o=>`.addOption(new Option("-${o.short}, --${o.name} ${o.type}", "${o.description}").default(${formatDefault(o.default)}))`)
112
- let action = `.action(async (${c.arguments.map(a=>`${a.name},`)} props) => {
111
+ let options = [...optionsBase,...(c.options||[])].map(o=>`.addOption(new Option("-${o.short}, --${o.name} ${o.type}", "${JSON.stringify(o.description).slice(1,-1)}").default(${formatDefault(o.default)}))`)
112
+ let action = `.action(async (${c.arguments.map(a=>`${a.name},`).join('')} props) => {
113
113
  ${c.action ? ';('+c.action.toString()+')(props)' : ''}
114
114
  return await fetchApi("${c.route.method.toUpperCase()}",\`${c.route.pathT}\`, props)
115
115
  })`
116
- return `program.command("${c.name}").description("${c.description}")${args.join('')}${options.join('')}${action}`
116
+ return `program.command("${c.name}").description("${JSON.stringify(c.description).slice(1,-1)}")${args.join('')}${options.join('')}${action}`
117
117
  })
118
118
  %*/
119
119
 
@@ -6,8 +6,8 @@ export type GalbeClientConfig = {
6
6
  export const Kind = Symbol.for('json.string')
7
7
  type Json<T> = { T: T }
8
8
 
9
- interface GR<S extends number = number, B = any, OKS extends number = OKStatusCode> {
10
- status: S
9
+ interface GR<S extends number | 'default' = 'default', B = any, OKS extends number = OKStatusCode> {
10
+ status: Exclude<S, "default">
11
11
  ok: S extends OKS ? true : false
12
12
  redirected: boolean
13
13
  statusText: string
@@ -18,16 +18,16 @@ interface GR<S extends number = number, B = any, OKS extends number = OKStatusCo
18
18
  stream?: ST
19
19
  ) => B extends Uint8Array
20
20
  ? ST extends true
21
- ? Promise<AsyncGenerator<Uint8Array, void, unknown>>
22
- : B extends Json<infer T>
23
- ? Promise<T>
24
- : Promise<B>
21
+ ? Promise<AsyncGenerator<Uint8Array, void, unknown>>
22
+ : B extends Json<infer T>
23
+ ? Promise<T>
24
+ : Promise<B>
25
25
  : B extends string
26
26
  ? ST extends true
27
- ? Promise<AsyncGenerator<string, void, unknown>>
28
- : B extends Json<infer T>
29
- ? Promise<T>
30
- : Promise<B>
27
+ ? Promise<AsyncGenerator<string, void, unknown>>
28
+ : B extends Json<infer T>
29
+ ? Promise<T>
30
+ : Promise<B>
31
31
  : B extends Json<infer T>
32
32
  ? Promise<T>
33
33
  : Promise<B>
@@ -35,9 +35,9 @@ interface GR<S extends number = number, B = any, OKS extends number = OKStatusCo
35
35
 
36
36
  export type OKStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
37
37
  // prettier-ignore
38
- export type HttpStatusCode = 100|101|102|103|OKStatusCode|300|301|302|303|304|305|307|308|400|401|402|403|404|405|406|407|408|409|410|411|412|413|414|415|416|417|418|421|422|423|424|426|428|429|431|451|500|501|502|503|504|505|506|507|508|510|511
38
+ export type HttpStatusCode = 100 | 101 | 102 | 103 | OKStatusCode | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511
39
39
  type PGR<
40
- S extends number = number,
40
+ S extends number | 'default' = 'default',
41
41
  B = any,
42
42
  O extends number = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
43
43
  > = Promise<GR<S, B, O>>
@@ -54,9 +54,15 @@ const DEFAULT_HEADERS = {
54
54
  'user-agent': 'Galbe//*%(()=>version)()%*/'
55
55
  }
56
56
 
57
+ // Typescript types
58
+ /*%
59
+ Object.entries(types).map(([tk, t])=>{
60
+ return `export type ${tk} = ${t}`
61
+ })
62
+ %*/
63
+
57
64
  export default class GalbeClient {
58
65
  config?: GalbeClientConfig
59
-
60
66
  /*%
61
67
  Object.entries(routes).map(([method, list])=>{
62
68
  return`${method} = {\n${list.map( r => {
@@ -66,7 +72,7 @@ export default class GalbeClient {
66
72
  ''
67
73
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
68
74
  let responses = Object.keys(r.schemas?.response||{}).length ?
69
- `${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}>,any${oks?.length?`,${oks.join('|')}`:''}>`:
75
+ `${Object.entries(r.schemas.response).filter(([s,_])=>s!=='"default"').map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).filter(s=>s!=='"default"').join('|')}>,${'"default"' in r.schemas.response ? r.schemas.response['"default"'] : 'any'}${oks?.length?`,${oks.join('|')}`:''}>`:
70
76
  `PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
71
77
  return ` '${r.path}':(${p.length?p.map(([k,v])=>`${k}:${v.type}`).join(',')+', ':''}options:RequestOptions${schemas}={})=>this.fetch(\`${r.pathT}\`,{...options,method:'${r.method.toUpperCase()}'}) as ${responses}`
72
78
  }).join(',\n')}\n}`
@@ -155,9 +161,13 @@ export default class GalbeClient {
155
161
  ''
156
162
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
157
163
  let responses = Object.keys(r.schemas?.response||{}).length ?
158
- `${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}>,any${oks?.length?`,${oks.join('|')}`:''}>`:
164
+ `${Object.entries(r.schemas.response).filter(([s,_])=>s!=='"default"').map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).filter(s=>s!=='"default"').join('|')}>,${'"default"' in r.schemas.response ? r.schemas.response['"default"'] : 'any'}${oks?.length?`,${oks.join('|')}`:''}>`:
159
165
  `PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
160
- return `${r.alias}(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
166
+ let summary = r.summary ? ` * ${r.summary || ''}\n *\n` : ''
167
+ let description = r.description ? ` * ${r.description.replace(/\n/g,'\n * ')}` : ''
168
+ let params = Object.entries(r.schema.params || {}).map( ([k,v])=>`\n * @param ${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
169
+ let query = Object.entries(r.schema.query || {}).map( ([k,v])=>`\n * @param options.query.${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
170
+ return `/**\n${summary}${description}\n *${params}${query}\n *\/\n ${r.alias}(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
161
171
  }).join(' ')
162
172
  })
163
173
  %*/
package/bin/util.ts CHANGED
@@ -2,7 +2,7 @@ import { relative } from 'path'
2
2
  import { watch } from 'chokidar'
3
3
  import { Galbe, Route } from '../src'
4
4
  import { logRoute, walkRoutes } from '../src/util'
5
- import { RouteMeta, defineRoutes } from '../src/routes'
5
+ import { GalbeProxy, RouteMeta, defineRoutes } from '../src/routes'
6
6
 
7
7
  export { default as pckg } from '../package.json'
8
8
 
@@ -22,7 +22,7 @@ export const silentExec = async (fn: () => any) => {
22
22
  let consoleMock = Object.fromEntries(
23
23
  Object.entries(console)
24
24
  .filter(([_, v]) => typeof v === 'function')
25
- .map(([k, _]) => [k, () => {}])
25
+ .map(([k, _]) => [k, () => { }])
26
26
  )
27
27
  const _console = console
28
28
  const _processStdoutWrite = process.stdout.write
@@ -30,9 +30,9 @@ export const silentExec = async (fn: () => any) => {
30
30
  //@ts-ignore
31
31
  global.console = consoleMock
32
32
  //@ts-ignore
33
- process.stdout.write = function () {}
33
+ process.stdout.write = function () { }
34
34
  //@ts-ignore
35
- process.stderr.write = function () {}
35
+ process.stderr.write = function () { }
36
36
  const r = await fn()
37
37
  global.console = _console
38
38
  process.stdout.write = _processStdoutWrite
@@ -65,29 +65,32 @@ export const instanciateRoutes = async (g: Galbe) => {
65
65
  hasMainRoutes = true
66
66
  logRoute(r)
67
67
  })
68
- if (hasMainRoutes) process.stdout.write('\n')
68
+ if (hasMainRoutes) Bun.write(Bun.stdout, '\n')
69
69
  // Route Files Analysis
70
70
  let routes: Record<string, { route?: Route; meta?: RouteMeta; error?: any }[]> = {}
71
71
  let errors: Record<string, any> = {}
72
- await defineRoutes({ routes: g?.config?.routes }, g, ({ type, route, error, filepath, meta }) => {
72
+
73
+ const proxy = new GalbeProxy(g, ({ type, route, error, filepath, meta }) => {
74
+ if (meta?.ignore || meta?.hide) return
73
75
  if (!filepath) return
74
76
  if (!(filepath in routes)) routes[filepath] = []
75
77
  if (type === 'add' && route && filepath) routes[filepath].push({ route, meta })
76
78
  if (type === 'error') errors[filepath] = error
77
79
  })
80
+ await defineRoutes({ routes: g?.config?.routes }, proxy)
78
81
  for (let [fp, e] of Object.entries(routes)) {
79
82
  console.log(`\x1b\[0;36m ${relative(CWD, fp)}\x1b[0m`)
80
83
  let maxPathLength = e.reduce((p, c) => {
81
84
  return Math.max(p, c.route?.path.length || 0)
82
85
  }, 0)
83
86
  for (let r of e) {
84
- if (r.route) logRoute(r.route, r.meta, { maxPathLength })
87
+ if (r.route && !r.meta?.ignore && !r.meta?.hide) logRoute(r.route, r.meta, { maxPathLength })
85
88
  }
86
89
  if (errors?.[fp]) {
87
90
  console.log(`\x1b\[0;31m Error:\x1b[0m`)
88
91
  console.log(errors?.[fp])
89
92
  }
90
- process.stdout.write('\n')
93
+ console.log("")
91
94
  }
92
95
  console.log('\x1b[1;30m\x1b[32mdone\x1b[0m\n')
93
96
  }
@@ -1,38 +1,49 @@
1
1
  # Error handler
2
2
 
3
- Any error happening during a request lifecycle will be intercepted by the error handler.
3
+ Any error happening during a request lifecycle will be intercepted by the error
4
+ handler.
4
5
 
5
- You can customize the default error handling behavior by defining a custom error handler using Galbe's intance `onError` method.
6
+ You can customize the default error handling behavior by defining a custom error
7
+ handler using Galbe's intance `onError` method.
6
8
 
7
9
  ```js
8
- const galbe = new Galbe()
9
- galbe.onError(customErrorHandler)
10
+ const galbe = new Galbe();
11
+ galbe.onError(customErrorHandler);
10
12
  ```
11
13
 
12
14
  ## Definition
13
15
 
14
- The error handler should be a function that takes two aguments: an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) and a [Context](context.md). This function may potentially return a [Response type](handler.md#response-types).
16
+ The error handler should be a function that takes two aguments: an
17
+ [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
18
+ and a [Context](context.md). This function may potentially return a
19
+ [Response type](handler.md#response-types).
15
20
 
16
21
  ```js
17
- galbe.onErrorHandler((error, ctx) => {
22
+ galbe.onError((error, ctx) => {
18
23
  if (error.status === 500) {
19
- return new Response(`Server error ❌`, { status: 500 })
24
+ return new Response(`Server error ❌`, { status: 500 });
20
25
  }
21
26
  if (error.status === 404) {
22
- return new Response(`Not found 🔎`, { status: 404 })
27
+ return new Response(`Not found 🔎`, { status: 404 });
23
28
  }
24
- })
29
+ });
25
30
  ```
26
31
 
27
- The `error` argument could be any type of error thrown by your application. If the error originates from Galbe framework, it will be an instance of [RequestError](#request-error).
32
+ The `error` argument could be any type of error thrown by your application. If
33
+ the error originates from Galbe framework, it will be an instance of
34
+ [RequestError](#request-error).
28
35
 
29
- For instance, the [Router](router.md) will throw a `RequestError` with a `404` status if no route matches the incoming request path. Similarly, the Parser will throw a `RequestError` with a `400` status.
36
+ For instance, the [Router](router.md) will throw a `RequestError` with a `404`
37
+ status if no route matches the incoming request path. Similarly, the Parser will
38
+ throw a `RequestError` with a `400` status.
30
39
 
31
40
  ## Request Error
32
41
 
33
- The `RequestError` class is utilized to instanciate a runtime request error in Galbe. It has two optional attributes: a `status` and a `payload`.
42
+ The `RequestError` class is utilized to instanciate a runtime request error in
43
+ Galbe. It has two optional attributes: a `status` and a `payload`.
34
44
 
35
- If your application throws a `RequestError` instance, Galbe will, by default, construct a Response from your `RequestError` and send it back to the client.
45
+ If your application throws a `RequestError` instance, Galbe will, by default,
46
+ construct a Response from your `RequestError` and send it back to the client.
36
47
 
37
48
  ```js
38
49
  import { Galbe, RequestError } from 'galbe'
package/docs/routes.md CHANGED
@@ -111,3 +111,6 @@ export default g => {
111
111
  g.get('/foo/:bar', ctx => ctx.params.bar)
112
112
  }
113
113
  ```
114
+
115
+ > [!TIP]
116
+ > You can ignore a specific route from being analyzed by adding a `//@galbe-ignore` comment before the route definition. This is useful if you want to exclude certain routes from automatic analysis or documentation generation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
@@ -12,10 +12,10 @@ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
12
12
  isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
13
13
  ? 'application/json'
14
14
  : format === 'byte'
15
- ? 'application/octet-stream'
16
- : type === 'string'
17
- ? 'text/plain'
18
- : '*/*'
15
+ ? 'application/octet-stream'
16
+ : type === 'string'
17
+ ? 'text/plain'
18
+ : '*/*'
19
19
 
20
20
  export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
21
21
  let paths: any = {}
@@ -115,9 +115,9 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
115
115
  type: type,
116
116
  ...(type === 'object'
117
117
  ? {
118
- properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
119
- ...(required.length ? { required } : {})
120
- }
118
+ properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
119
+ ...(required.length ? { required } : {})
120
+ }
121
121
  : {})
122
122
  }
123
123
  } else if (kind === 'union') {
@@ -178,8 +178,13 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
178
178
  (routes, c) => ({ ...routes, ...c.routes }),
179
179
  {} as Record<string, Record<string, Record<string, any>>>
180
180
  )
181
+ let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter((([_, d]) => d?.static)))
182
+
181
183
  walkRoutes(g.router.routes, r => {
182
184
  let meta = metaRoutes?.[r.path]?.[r.method]
185
+ if (r.static?.root)
186
+ meta = metaStatic[r.static?.root]?.static
187
+ if (meta?.hide) return
183
188
  let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
184
189
  if (!(path in paths)) paths[path] = {}
185
190
  let tags = [
@@ -196,19 +201,19 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
196
201
  : []
197
202
  let headerParam = r.schema?.headers
198
203
  ? Object.entries(r.schema?.headers as Record<string, STSchema>)
199
- .map(([k, v]) => {
200
- let p = parseParam(k, v, 'header')
201
- if (k.match(/authorization/i)) {
202
- // TODO: handle other auth methods
203
- if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
204
- security.push({ bearerAuth: [] })
205
- components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
206
- return null
207
- }
204
+ .map(([k, v]) => {
205
+ let p = parseParam(k, v, 'header')
206
+ if (k.match(/authorization/i)) {
207
+ // TODO: handle other auth methods
208
+ if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
209
+ security.push({ bearerAuth: [] })
210
+ components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
211
+ return null
208
212
  }
209
- return p
210
- })
211
- .filter(p => p)
213
+ }
214
+ return p
215
+ })
216
+ .filter(p => p)
212
217
  : []
213
218
  // TODO cookieParam
214
219
  let parameters = [...pathParam, ...queryParam, ...headerParam]
@@ -242,7 +247,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
242
247
  description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
243
248
  content: { [media]: { schema: schema } }
244
249
  }
245
- if (components.responses && r.schema.response?.[s].id) {
250
+ if (components.responses && r.schema.response?.[s]?.id) {
246
251
  components.responses[r.schema.response?.[s].id as string] = response
247
252
  //@ts-ignore
248
253
  response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
package/src/index.ts CHANGED
@@ -14,9 +14,14 @@ import type {
14
14
  STResponse,
15
15
  STParams,
16
16
  STHeaders,
17
- STQuery
17
+ STQuery,
18
+ StaticEndpoint,
19
+ Route,
20
+ StaticEndpointOptions
18
21
  } from './types'
19
22
 
23
+ import { readdirSync, statSync } from 'fs'
24
+ import { resolve as resolvePath } from 'path'
20
25
  import server from './server'
21
26
  import { GalbeRouter } from './router'
22
27
  import { SchemaType, type STObject, type Static } from './schema'
@@ -41,7 +46,7 @@ const overloadDiscriminer = <
41
46
  | Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
42
47
  | Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
43
48
  arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
44
- ) => {
49
+ ): Route<M, Path, P, H, Q, B, R> => {
45
50
  const defaultSchema = {}
46
51
  if (typeof arg2 === 'function') {
47
52
  return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
@@ -70,7 +75,7 @@ const galbeMethod = <
70
75
  schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
71
76
  hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
72
77
  handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
73
- ) => {
78
+ ): Route<M, Path, P, H, Q, B, R> => {
74
79
  schema = schema ?? {}
75
80
  hooks = hooks || []
76
81
  const context: Context<M, Path, typeof schema> = {
@@ -85,7 +90,8 @@ const galbeMethod = <
85
90
  state: {},
86
91
  set: {} as {
87
92
  headers: {
88
- [header: string]: string
93
+ 'set-cookie': string[]
94
+ [header: string]: string | string[]
89
95
  }
90
96
  status?: number
91
97
  }
@@ -306,6 +312,40 @@ export class Galbe {
306
312
  | Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
307
313
  arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
308
314
  ) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
315
+ static: StaticEndpoint = (path: string, target: string, options?: StaticEndpointOptions) => {
316
+ let { resolve } = options ?? {}
317
+ const rootPath = path
318
+
319
+ const walkStatic = (path: string, target: string) => {
320
+ path = path?.[0] === '/' ? path : `/${path}`
321
+ path = path.endsWith('/') ? path.slice(0, -1) : path;
322
+
323
+ let t = target
324
+ if (Bun.env.BUN_ENV === 'production') {
325
+ t = resolvePath(import.meta.dir, `static-${Bun.env.GALBE_BUILD}/${target}`)
326
+ }
327
+
328
+ if (!statSync(t).isDirectory()) {
329
+ let ut: string | null | undefined | void = t
330
+ if (path.endsWith('.html')) path = path.slice(0, -5)
331
+ if (resolve) ut = resolve(path, ut)
332
+ if (ut) {
333
+ let handler = () => new Response(Bun.file(ut))
334
+ this.add({ ...galbeMethod(this, 'get', path, {}, undefined, handler), static: { path: ut, root: rootPath } })
335
+ }
336
+ } else {
337
+ let root = readdirSync(t)
338
+ for (let f of root) {
339
+ let p = f === 'index.html' ? path : `${path}/${f}`
340
+ walkStatic(p, `${target}/${f}`)
341
+ }
342
+ }
343
+
344
+ return { ...galbeMethod(this, 'get', path, {}, undefined, () => { }), static: { path: t, root: rootPath } }
345
+ }
346
+
347
+ return walkStatic(path, target)
348
+ }
309
349
  }
310
350
 
311
351
  export * from './types'
package/src/parser.ts CHANGED
@@ -89,8 +89,8 @@ export const requestBodyParser = async (
89
89
  let received = contentType?.match(FORM_HEADER_RX)
90
90
  ? 'urlForm'
91
91
  : contentType?.match(MP_HEADER_RX)
92
- ? 'multipartForm'
93
- : contentType
92
+ ? 'multipartForm'
93
+ : contentType
94
94
  throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received ${received}` } })
95
95
  } else if (!contentType || contentType === BA_HEADER) {
96
96
  if (!schema) {
@@ -377,7 +377,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
377
377
  payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
378
378
  })
379
379
  }
380
- const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
380
+ const parseMultipartHeader = (header: string): { name: string;[key: string]: string } | null => {
381
381
  if (!header) return null
382
382
  let disposition = 'form-data'
383
383
  const multipartHeader = [
@@ -537,24 +537,24 @@ const paramParser = (
537
537
  if (typeof value === 'boolean') return value
538
538
  if (value === 'true') return true
539
539
  if (value === 'false') return false
540
- else throw `${value} is not a valid boolean. Should be 'true' or 'false'`
540
+ else throw `Not a valid boolean. Should be 'true' or 'false'`
541
541
  } else if (type[Kind] === 'integer') {
542
- if (value === null || value === undefined) throw `${value} is not a valid integer`
542
+ if (value === null || value === undefined) throw `Not a valid integer`
543
543
  const parsedValue = parseInt(value, 10)
544
- if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid integer`
544
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid integer`
545
545
  validate(parsedValue, type)
546
546
  return parsedValue
547
547
  } else if (type[Kind] === 'number') {
548
- if (value === null || value === undefined) throw `${value} is not a valid number`
548
+ if (value === null || value === undefined) throw `Not a valid number`
549
549
  const parsedValue = Number(value)
550
- if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid number`
550
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid number`
551
551
  validate(parsedValue, type)
552
552
  return parsedValue
553
553
  } else if (type[Kind] === 'string') {
554
554
  validate(value, type)
555
555
  return value
556
556
  } else if (type[Kind] === 'literal') {
557
- if (value !== type.value) throw `${value} is not a valid value`
557
+ if (value !== type.value) throw `Not a valid value`
558
558
  return value
559
559
  } else if (type[Kind] === 'array') {
560
560
  return [paramParser(value, type.items as STMultipartFormValues) as Static<STUrlFormValues>]
@@ -569,9 +569,9 @@ const paramParser = (
569
569
  continue
570
570
  }
571
571
  }
572
- throw `${value} could not be parsed to any of ${union
572
+ throw `Could not be parsed to any of [${union
573
573
  .map(u => (u as STLiteral)?.value ?? (u as STSchema)[Kind])
574
- .join(', ')}`
574
+ .join(', ')}]`
575
575
  } else if (type[Kind] === 'any') {
576
576
  return value
577
577
  }
@@ -645,7 +645,12 @@ export const parseEntry = <T extends STProps>(
645
645
  export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
646
646
  const details = {
647
647
  status: ctx.set.status || 200,
648
- headers: new Headers(ctx.set.headers)
648
+ headers: new Headers()
649
+ }
650
+ for (const [key, value] of Object.entries(ctx.set.headers)) {
651
+ if (Array.isArray(value)) {
652
+ value.forEach(v => details.headers.append(key, v))
653
+ } else details.headers.set(key, value)
649
654
  }
650
655
  if (response instanceof Response) return response
651
656
  else if (typeof response === 'string') {