galbe 0.14.0 → 0.15.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.
@@ -2,7 +2,8 @@ import { $ } from 'bun'
2
2
  import { devNull } from 'os'
3
3
  import { Command, Option } from 'commander'
4
4
  import { resolve, relative, extname } from 'path'
5
- import { exists, readFile } from 'fs/promises'
5
+ import { readFile } from 'fs/promises'
6
+ import { existsSync } from 'fs'
6
7
 
7
8
  import { CWD, fmtList, fmtVal } from '../../util'
8
9
  import { applyPlan, planFromOapi, type GenerationPlan } from './code/openapi.parser'
@@ -114,10 +115,17 @@ export default (cmd: Command) => {
114
115
  const outDir = resolve(CWD, out)
115
116
 
116
117
  // Compute per-scope merge result.
117
- const mergeResults: { scopeKey: string; content: string; added: RouteId[]; updated: RouteId[]; removed: RouteId[]; stale: RouteId[] }[] = []
118
+ const mergeResults: {
119
+ scopeKey: string
120
+ content: string
121
+ added: RouteId[]
122
+ updated: RouteId[]
123
+ removed: RouteId[]
124
+ stale: RouteId[]
125
+ }[] = []
118
126
  for (const scope of plan.scopes) {
119
127
  const routePath = resolve(outDir, `${scope.routeFile}.${target}`)
120
- const existing = (await exists(routePath)) ? await readFile(routePath, 'utf-8') : null
128
+ const existing = existsSync(routePath) ? await readFile(routePath, 'utf-8') : null
121
129
  const r = mergeRouteFile(existing, scope, mergeOpts)
122
130
  mergeResults.push({ scopeKey: scope.scopeKey, ...r })
123
131
  }
@@ -125,7 +125,7 @@ export default (cmd: Command) => {
125
125
  FROM information_schema.tables
126
126
  WHERE table_schema = '${schema}'
127
127
  AND table_type = 'BASE TABLE'`)
128
- tables = r.map(r => r.table_name)
128
+ tables = r.map((r: Record<string, string>) => r.table_name)
129
129
  }
130
130
 
131
131
  for (const tableName of tables) {
@@ -133,7 +133,7 @@ export default (cmd: Command) => {
133
133
  FROM information_schema.columns
134
134
  WHERE table_schema = '${schema}' AND table_name = '${tableName}'`)
135
135
  types[tableName] = `type ${toPascalCase(tableName)} = {\n${t
136
- .map(r => ` ${r.column_name}: ${TYPE_MAP?.[r.data_type] ?? 'any'}${r.is_nullable ? ' | null' : ''}`)
136
+ .map((r: Record<string, string>) => ` ${r.column_name}: ${TYPE_MAP?.[r.data_type] ?? 'any'}${r.is_nullable ? ' | null' : ''}`)
137
137
  .join(';\n')}\n}`
138
138
  }
139
139
 
@@ -0,0 +1,184 @@
1
+ // Inlined into the generated client — no exports
2
+ import type { BodyInit } from 'bun'
3
+
4
+ type GalbeClientConfig = {
5
+ server?: { url?: string }
6
+ headers?: Record<string, string>
7
+ fetch?: (req: Request) => Promise<Response>
8
+ }
9
+
10
+ class GalbeClientError extends Error {
11
+ readonly status: number
12
+ readonly headers: Headers
13
+ readonly body: string
14
+ constructor(status: number, headers: Headers, body: string) {
15
+ super(`HTTP ${status}`)
16
+ this.name = 'GalbeClientError'
17
+ this.status = status
18
+ this.headers = headers
19
+ this.body = body
20
+ }
21
+ }
22
+
23
+ const _parseResponse = async (res: Response): Promise<any> => {
24
+ const ct = res.headers.get('content-type') ?? ''
25
+ if (ct.includes('application/json')) return res.json()
26
+ if (ct.includes('application/octet-stream')) return new Uint8Array(await res.arrayBuffer())
27
+ return res.text()
28
+ }
29
+
30
+ const _buildUrl = (base: string | undefined, path: string, query?: Record<string, any>): string => {
31
+ let url = `${base ?? ''}${path}`
32
+ if (query) {
33
+ const params = new URLSearchParams()
34
+ for (const [k, v] of Object.entries(query)) {
35
+ if (v === undefined || v === null) continue
36
+ if (Array.isArray(v)) for (const item of v) params.append(k, String(item))
37
+ else params.set(k, String(v))
38
+ }
39
+ const qs = params.toString()
40
+ if (qs) url += `?${qs}`
41
+ }
42
+ return url
43
+ }
44
+
45
+ const _formdata = (data: Record<string, string | string[] | Blob>): FormData => {
46
+ const form = new FormData()
47
+ for (const [k, v] of Object.entries(data)) {
48
+ if (Array.isArray(v)) for (const item of v) form.append(k, String(item))
49
+ else form.append(k, v instanceof Blob ? v : String(v))
50
+ }
51
+ return form
52
+ }
53
+
54
+ type _RequestOptions = {
55
+ query?: Record<string, any>
56
+ headers?: Record<string, string>
57
+ contentType?: string
58
+ }
59
+
60
+ const _doFetch = (
61
+ config: GalbeClientConfig,
62
+ method: string,
63
+ path: string,
64
+ body?: any,
65
+ options?: _RequestOptions
66
+ ): Promise<Response> => {
67
+ const url = _buildUrl(config.server?.url, path, options?.query)
68
+ let bodyInit: BodyInit | undefined
69
+ const bodyHeaders: Record<string, string> = {}
70
+
71
+ if (body !== undefined && body !== null) {
72
+ const ct = options?.contentType
73
+ if (ct === 'urlForm') {
74
+ bodyInit = new URLSearchParams(body).toString()
75
+ bodyHeaders['content-type'] = 'application/x-www-form-urlencoded'
76
+ } else if (ct === 'multipart') {
77
+ bodyInit = _formdata(body)
78
+ } else if (ct === 'byteArray' || body instanceof Uint8Array) {
79
+ bodyInit = body
80
+ bodyHeaders['content-type'] = 'application/octet-stream'
81
+ } else if (ct === 'text') {
82
+ bodyInit = String(body)
83
+ bodyHeaders['content-type'] = 'text/plain'
84
+ } else {
85
+ bodyInit = JSON.stringify(body)
86
+ bodyHeaders['content-type'] = 'application/json'
87
+ }
88
+ }
89
+
90
+ const req = new Request(url, {
91
+ method,
92
+ headers: { ...config.headers, ...bodyHeaders, ...options?.headers },
93
+ ...(bodyInit !== undefined ? { body: bodyInit } : {}),
94
+ })
95
+
96
+ return (config.fetch ?? fetch)(req)
97
+ }
98
+
99
+ class GalbeRequest<T, E = any> {
100
+ #promise: Promise<[Response, Response]>
101
+ #main?: Promise<T>
102
+
103
+ constructor(fetchPromise: Promise<Response>) {
104
+ this.#promise = fetchPromise.then(res => [res, res.clone()] as [Response, Response])
105
+ }
106
+
107
+ #getMain(): Promise<T> {
108
+ if (!this.#main) {
109
+ this.#main = this.#promise.then(async ([res]) => {
110
+ if (!res.ok) throw new GalbeClientError(res.status, res.headers, await res.text())
111
+ return _parseResponse(res) as T
112
+ })
113
+ }
114
+ return this.#main
115
+ }
116
+
117
+ then<R1 = T, R2 = never>(
118
+ onfulfilled?: ((value: T) => R1 | PromiseLike<R1>) | null,
119
+ onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
120
+ ): Promise<R1 | R2> {
121
+ return this.#getMain().then(onfulfilled, onrejected)
122
+ }
123
+
124
+ catch<R = never>(onrejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T | R> {
125
+ return this.#getMain().then(undefined, onrejected)
126
+ }
127
+
128
+ finally(onfinally?: (() => void) | null): Promise<T> {
129
+ return this.#getMain().finally(onfinally)
130
+ }
131
+
132
+ async safe(): Promise<{ ok: true; data: T } | { ok: false; error: E }> {
133
+ const [mainRes, cloneRes] = await this.#promise
134
+ if (mainRes.ok) {
135
+ return { ok: true, data: (await _parseResponse(cloneRes)) as T }
136
+ } else {
137
+ const body = await _parseResponse(cloneRes)
138
+ return { ok: false, error: { status: mainRes.status, headers: mainRes.headers, body } as E }
139
+ }
140
+ }
141
+ }
142
+
143
+ const _createRequest = <T, E = any>(
144
+ config: GalbeClientConfig,
145
+ method: string,
146
+ path: string,
147
+ body?: any,
148
+ options?: _RequestOptions
149
+ ): GalbeRequest<T, E> => new GalbeRequest<T, E>(_doFetch(config, method, path, body, options))
150
+
151
+ const _createRawRequest = async (
152
+ config: GalbeClientConfig,
153
+ method: string,
154
+ path: string,
155
+ body?: any,
156
+ options?: _RequestOptions
157
+ ): Promise<any> => {
158
+ const res = await _doFetch(config, method, path, body, options)
159
+ return {
160
+ status: res.status,
161
+ ok: res.ok,
162
+ redirected: res.redirected,
163
+ statusText: res.statusText,
164
+ type: res.type,
165
+ url: res.url,
166
+ headers: res.headers,
167
+ body: {
168
+ json: () => res.json(),
169
+ text: () => res.text(),
170
+ byteArray: () => res.arrayBuffer().then((b: ArrayBuffer) => new Uint8Array(b)),
171
+ stream: (): AsyncGenerator<Uint8Array, void, unknown> => {
172
+ const reader = res.body?.getReader()
173
+ return (async function* () {
174
+ if (!reader) return
175
+ while (true) {
176
+ const { value, done } = await reader.read()
177
+ if (done) break
178
+ yield value!
179
+ }
180
+ })()
181
+ },
182
+ },
183
+ }
184
+ }
@@ -110,7 +110,7 @@ export default class GalbeClient {
110
110
  text: 'text/plain',
111
111
  json: 'application/json',
112
112
  urlForm: 'application/x-www-form-urlencoded',
113
- }[options.contentType],
113
+ }[options.contentType as 'byteArray' | 'text' | 'json' | 'urlForm'],
114
114
  }
115
115
  : {}),
116
116
  ...(options?.headers || {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
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",
@@ -35,11 +35,10 @@
35
35
  "scripts": {
36
36
  "test": "bun test",
37
37
  "typecheck": "tsc --noEmit --emitDeclarationOnly false",
38
- "postinstall": "bun run ./scripts/postinstall.ts",
39
38
  "release": "release-it"
40
39
  },
41
40
  "devDependencies": {
42
- "@types/bun": "^1.2.22",
41
+ "@types/bun": "latest",
43
42
  "openapi-types": "^12.1.3",
44
43
  "release-it": "^17.1.1"
45
44
  },
package/src/extras.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { OpenAPISerializer } from './extras/spec/openapi.serializer'
2
- export type { GalbeCLICommand, GalbeCLIOptions } from './types'
2
+ export type { GalbeCLICommand, GalbeCLIOptions, GalbeClientRoute, GalbeClientOptions } from './types'
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ const overloadDiscriminer = <
35
35
  P extends Partial<STParams<Path>>,
36
36
  Q extends STQuery,
37
37
  B extends STBody,
38
- R extends STResponse
38
+ R extends STResponse,
39
39
  >(
40
40
  galbe: Galbe,
41
41
  method: M,
@@ -69,7 +69,7 @@ const galbeMethod = <
69
69
  P extends Partial<STParams<Path>>,
70
70
  Q extends STQuery,
71
71
  B extends STBody,
72
- R extends STResponse
72
+ R extends STResponse,
73
73
  >(
74
74
  _galbe: Galbe,
75
75
  method: M,
@@ -130,7 +130,7 @@ export class Galbe {
130
130
  stopCb: (() => void)[] = []
131
131
  errorCb: ErrorHandler[] = []
132
132
  listening: boolean = false
133
- server?: Server
133
+ server?: Server<any>
134
134
  plugins: GalbePlugin[] = []
135
135
  constructor(config?: GalbeConfig) {
136
136
  this.config = config ?? {}
@@ -186,7 +186,7 @@ export class Galbe {
186
186
  H extends STHeaders,
187
187
  Q extends STQuery,
188
188
  B extends STBody,
189
- R extends STResponse
189
+ R extends STResponse,
190
190
  >(
191
191
  path: Path,
192
192
  arg2:
@@ -205,7 +205,7 @@ export class Galbe {
205
205
  H extends STHeaders,
206
206
  Q extends STQuery,
207
207
  B extends STBody,
208
- R extends STResponse
208
+ R extends STResponse,
209
209
  >(
210
210
  path: Path,
211
211
  arg2:
@@ -223,7 +223,7 @@ export class Galbe {
223
223
  H extends STHeaders,
224
224
  Q extends STQuery,
225
225
  B extends STBody,
226
- R extends STResponse
226
+ R extends STResponse,
227
227
  >(
228
228
  path: Path,
229
229
  arg2:
@@ -241,7 +241,7 @@ export class Galbe {
241
241
  H extends STHeaders,
242
242
  Q extends STQuery,
243
243
  B extends STBody,
244
- R extends STResponse
244
+ R extends STResponse,
245
245
  >(
246
246
  path: Path,
247
247
  arg2:
@@ -259,7 +259,7 @@ export class Galbe {
259
259
  H extends STHeaders,
260
260
  Q extends STQuery,
261
261
  B extends STBody,
262
- R extends STResponse
262
+ R extends STResponse,
263
263
  >(
264
264
  path: Path,
265
265
  arg2:
@@ -277,7 +277,7 @@ export class Galbe {
277
277
  H extends STHeaders,
278
278
  Q extends STQuery,
279
279
  B extends STBody,
280
- R extends STResponse
280
+ R extends STResponse,
281
281
  >(
282
282
  path: Path,
283
283
  arg2:
@@ -295,7 +295,7 @@ export class Galbe {
295
295
  H extends STHeaders,
296
296
  Q extends STQuery,
297
297
  B extends STBody,
298
- R extends STResponse
298
+ R extends STResponse,
299
299
  >(
300
300
  path: Path,
301
301
  arg2:
package/src/server.ts CHANGED
@@ -171,7 +171,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
171
171
 
172
172
  const parsedResponse = responseParser(response, context as Context, cookies, schema.response)
173
173
 
174
- if (galbe.config?.responseValidator?.enabled !== false && schema.response)
174
+ if (galbe.config?.responseValidator?.enabled !== false && schema.response && !(response instanceof Response))
175
175
  validateResponse(response, schema.response, parsedResponse.status || 200)
176
176
 
177
177
  for (const p of pluginsCb.afterHandle) {
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ServeOptions, SocketAddress, TLSOptions, TLSServeOptions } from 'bun'
1
+ import type { Serve, SocketAddress, TLSOptions } from 'bun'
2
2
  import type {
3
3
  STAny,
4
4
  STArray,
@@ -140,7 +140,7 @@ export type GalbeConfig = {
140
140
  basePath?: string
141
141
  /** Enable or disable TLS support. */
142
142
  tls?: TLSOptions
143
- server?: Exclude<ServeOptions, 'port'> | TLSServeOptions
143
+ server?: Exclude<Serve.Options<any>, 'port'> | TLSOptions
144
144
  /** A Glob Pattern or a list of Glob patterns defining the route files to be analyzed by the Automatic Route Analyzer. */
145
145
  routes?: boolean | string | string[]
146
146
  router?: { cacheEnabled: boolean }
@@ -343,7 +343,7 @@ export class RequestError extends Error {
343
343
  const message =
344
344
  typeof options.payload === 'string'
345
345
  ? options.payload
346
- : HttpStatus[status as keyof typeof HttpStatus] ?? 'Request Error'
346
+ : (HttpStatus[status as keyof typeof HttpStatus] ?? 'Request Error')
347
347
  super(message)
348
348
  this.name = new.target?.name ?? 'RequestError'
349
349
  this.status = status
@@ -484,5 +484,29 @@ export type GalbeCLIOptions = {
484
484
  args: Record<string, string>,
485
485
  options: Record<string, any>
486
486
  ) => MaybePromise<Request>
487
- responseFormatter?: (res: Response) => MaybePromise<string>
487
+ responseFormatter?: (
488
+ res: Response,
489
+ command: GalbeCLICommand,
490
+ args: Record<string, string>,
491
+ options: Record<string, any>
492
+ ) => MaybePromise<string>
493
+ }
494
+
495
+ export type GalbeClientRoute = {
496
+ method: string
497
+ path: string
498
+ operationId: string
499
+ autoDerived: boolean
500
+ params: Record<string, { type: string; description?: string }>
501
+ query: Record<string, { type: string; optional: boolean; description?: string }>
502
+ headers: Record<string, { type: string; optional: boolean; description?: string }>
503
+ body: Record<string, STSchema> | null
504
+ response: STResponse | null
505
+ summary?: string
506
+ description?: string
507
+ tags: string[]
508
+ }
509
+
510
+ export type GalbeClientOptions = {
511
+ className?: string
488
512
  }
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- const COMMIT_MSG = await Bun.file(Bun.argv[2]).text()
4
- const COMMIT_EMOJI = { feat: '✨', fix: '🔧', doc: '📚', chore: '🧹' }
5
-
6
- const newCommitMsg = COMMIT_MSG.replace(/^(feat|fix|doc|chore):(.*)/, (_, type, msg) => {
7
- return `${COMMIT_EMOJI[type]} ${type}:${msg}`
8
- })
9
-
10
- await Bun.write(Bun.argv[2], newCommitMsg)
@@ -1,9 +0,0 @@
1
- import { $ } from 'bun'
2
- import { existsSync } from 'fs'
3
-
4
- if (existsSync('.git')) {
5
- console.log('Setting up dev environment')
6
- Bun.write('.git/hooks/prepare-commit-msg', Bun.file('scripts/hooks/prepare-commit-msg'))
7
- await $`chmod +x .git/hooks/prepare-commit-msg`
8
- console.log('done')
9
- }