galbe 0.9.1 → 0.10.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.
@@ -9,9 +9,16 @@ import { CWD, fmtVal, silentExec } from '../util'
9
9
  import { Galbe } from '../../src'
10
10
  import { defineRoutes, GalbeProxy } from '../../src/routes'
11
11
  import { BuildConfig } from 'bun'
12
+ import { existsSync } from 'fs'
12
13
 
13
14
  const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string) => {
14
15
  const buildPath = resolve(tmpdir(), buildId)
16
+ const indexDir = dirname(indexPath)
17
+
18
+ let configPath = ''
19
+ if (existsSync(`${indexDir}/galbe.config.ts`)) configPath = `${indexDir}/galbe.config.ts`
20
+ else if (existsSync(`${indexDir}/galbe.config.js`)) configPath = `${indexDir}/galbe.config.js`
21
+
15
22
  const routes = new Map<string, { filepath: string, static?: { path: string, root: string } }>()
16
23
  let errors: any[] = []
17
24
  // Create GalbeProxy here
@@ -30,6 +37,9 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string) =>
30
37
 
31
38
  let buildIndex =
32
39
  `import galbe from '${relative(buildPath, indexPath)}';\n` +
40
+ (configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
41
+ (configPath ? `import {softMerge} from '${relative(buildPath, `${indexDir}/node_modules/galbe/src/util`)}';\n` : '') +
42
+ (configPath ? `let conf = galbe.config;\ngalbe.config = softMerge(config, conf)\n` : '') +
33
43
  `${[...routes.values()].map((r, idx) => `import _${idx} from '${relative(buildPath, r.filepath)}'`).join(';\n')}\n` +
34
44
  `Bun.env.BUN_ENV = 'production';\n` +
35
45
  `Bun.env.GALBE_BUILD = '${buildId}';\n` +
@@ -92,10 +102,10 @@ export default (cmd: Command) => {
92
102
 
93
103
  const buildConfig: BuildConfig = {
94
104
  publicPath: `${outPath}/`,
95
- ...Object.fromEntries(Object.entries(bunfig).filter(([k, v]) => v)),
105
+ sourcemap: 'external',
106
+ ...Object.fromEntries(Object.entries(bunfig).filter(([_, v]) => v)),
96
107
  entrypoints: [buildIndex],
97
108
  outdir: outPath,
98
- sourcemap: 'external',
99
109
  target: 'bun',
100
110
  }
101
111
 
@@ -1,11 +1,13 @@
1
1
  import { $ } from 'bun'
2
2
  import { Command, Option } from 'commander'
3
- import { resolve } from 'path'
3
+ import { resolve, dirname } from 'path'
4
4
 
5
- import { CWD, fmtInterval, fmtVal, instanciateRoutes, killPort, watchDir } from '../util'
5
+ import { CWD, fmtInterval, fmtVal, instanciateRoutes, watchDir } from '../util'
6
6
  import { Galbe } from '../../src'
7
+ import { softMerge } from '../../src/util'
8
+ import { existsSync } from 'fs'
7
9
 
8
- const defaultPort = 3000
10
+ const DEFAULT_PORT = 3000
9
11
 
10
12
  export default (cmd: Command) => {
11
13
  cmd
@@ -18,29 +20,29 @@ export default (cmd: Command) => {
18
20
  console.log(`error: port range must be between ${fmtInterval(1, 65535)}`)
19
21
  process.exit(1)
20
22
  })
21
- .default(null, fmtVal(defaultPort))
23
+ .default(null, fmtVal(DEFAULT_PORT))
22
24
  )
23
- .addOption(new Option('-w, --watch <dir>', 'watch file changes').default(false, fmtVal(false)))
25
+ .addOption(new Option('-w, --watch [dir]', 'watch file changes').default(false, fmtVal(false)))
24
26
  .addOption(new Option('-wi, --watchignore <regexp>', 'ignore file changes').default(false, fmtVal(false)))
25
27
  .addOption(new Option('-nc, --noclear', "don't clear on file changes").default(false, fmtVal(false)))
26
- .addOption(
27
- new Option('-f, --force', 'kills any process running on defined port before strating the server').default(
28
- false,
29
- fmtVal(false)
30
- )
31
- )
32
28
  .action(async (index, props) => {
33
- const { port, watch, watchignore, noclear, force } = props
34
- let watch_dir = typeof watch === 'string' ? watch : CWD
35
- const clear = !noclear
29
+ const { port, watch, watchignore, noclear } = props
36
30
  const indexPath = resolve(CWD, index)
31
+ const indexDir = dirname(indexPath)
32
+ let watch_dir = typeof watch === 'string' ? watch : watch ? indexDir : ''
33
+ const clear = !noclear
34
+ let galbeConfig = {}
37
35
  let g: Galbe
38
36
 
39
- if (!Bun.env.BUN_ENV) Bun.env.BUN_ENV = 'development'
37
+ if (existsSync(`${indexDir}/galbe.config.ts`)) {
38
+ galbeConfig = (await import(`${indexDir}/galbe.config.ts`)).default
39
+ } else if (existsSync(`${indexDir}/galbe.config.js`)) {
40
+ galbeConfig = (await import(`${indexDir}/galbe.config.js`)).default
41
+ }
40
42
 
41
- if (force) await killPort(port || 3000)
43
+ if (!Bun.env.BUN_ENV) Bun.env.BUN_ENV = 'development'
42
44
 
43
- if (!!watch) {
45
+ if (!!watch_dir) {
44
46
  await watchDir(
45
47
  watch_dir,
46
48
  async () => {
@@ -55,8 +57,10 @@ export default (cmd: Command) => {
55
57
  )
56
58
  }
57
59
 
58
- if (!!watch && clear) await $`clear`
60
+ if (!!watch_dir && clear) await $`clear`
59
61
  g = (await import(indexPath)).default
62
+ let conf = g.config
63
+ g.config = softMerge(galbeConfig, conf)
60
64
  await instanciateRoutes(g)
61
65
  await g.listen(port)
62
66
  })
@@ -282,7 +282,7 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
282
282
  )
283
283
 
284
284
  let body = ''
285
- if(!['get', 'options', 'head'].includes(method)){
285
+ if(!['get', 'delete', 'options', 'head'].includes(method)){
286
286
  let _rb = def?.requestBody as OpenAPIV3.ReferenceObject
287
287
  if (_rb?.$ref) {
288
288
  body = unref(` body: %ref:${_rb.$ref}%`, m => {
@@ -1,9 +1,11 @@
1
1
  import { Command, Option } from 'commander'
2
2
  import { resolve, relative, extname } from 'path'
3
3
  import { dump as ymlDump, load as ymlLoad } from 'js-yaml'
4
- import { CWD, fmtList, instanciateRoutes, silentExec, softMerge } from '../../util'
4
+ import { CWD, fmtList, instanciateRoutes, silentExec } from '../../util'
5
5
  import { Galbe } from '../../../src'
6
6
  import { OpenAPISerializer } from '../../../src/extras'
7
+ import { softMerge } from '../../../src/util'
8
+ import { OpenAPIV3 } from 'openapi-types'
7
9
 
8
10
  const specTargets = ['openapi:3.0:json', 'openapi:3.0:yaml']
9
11
  const parsePckgAuthoRgx = /^\s*([^<(]*)(?:<([^>]+)>)?\s*(?:\(([^)]*)\))?\s*$/
@@ -85,7 +87,7 @@ export default (cmd: Command) => {
85
87
  version: pckg?.version || '0.1.0'
86
88
  }
87
89
  }
88
- openapiSpec = softMerge(openapiSpec, baseSpec)
90
+ openapiSpec = softMerge(openapiSpec, baseSpec) as OpenAPIV3.Document
89
91
  Bun.write(resolve(CWD, out), tFormat === 'json' ? JSON.stringify(openapiSpec, null, 2) : ymlDump(openapiSpec))
90
92
  }
91
93
 
package/bin/util.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { relative } from 'path'
2
2
  import { watch } from 'chokidar'
3
- import { Galbe, Route } from '../src'
3
+ import { Galbe, type Route } from '../src'
4
4
  import { logRoute, walkRoutes } from '../src/util'
5
- import { GalbeProxy, RouteMeta, defineRoutes } from '../src/routes'
5
+ import { GalbeProxy, type RouteMeta, defineRoutes } from '../src/routes'
6
6
 
7
7
  export { default as pckg } from '../package.json'
8
8
 
@@ -15,7 +15,7 @@ export const fmtVal = (v: any) => {
15
15
  if (typeof v === 'number') return `\x1b[36m${v}\x1b[0m`
16
16
  return v
17
17
  }
18
- export const fmtList = (l: any) => `[${l.map(v => fmtVal(v)).join(', ')}]`
18
+ export const fmtList = (l: any) => `[${l.map((v:any) => fmtVal(v)).join(', ')}]`
19
19
  export const fmtInterval = (a: any, b: any) => `[${fmtVal(a)}-${fmtVal(b)}]`
20
20
 
21
21
  export const silentExec = async (fn: () => any) => {
@@ -95,16 +95,6 @@ export const instanciateRoutes = async (g: Galbe) => {
95
95
  console.log('\x1b[1;30m\x1b[32mdone\x1b[0m\n')
96
96
  }
97
97
 
98
- export const softMerge = (base, override) => {
99
- for (const key in override) {
100
- if (override[key] instanceof Object && !(override[key] instanceof Array)) {
101
- if (!base[key]) Object.assign(base, { [key]: {} })
102
- softMerge(base[key], override[key])
103
- } else Object.assign(base, { [key]: override[key] })
104
- }
105
- return base
106
- }
107
-
108
98
  export const killPort = async (port: number) => {
109
99
  let getProcCmd: string[], killCmd: (port: string) => string[]
110
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.9.1",
3
+ "version": "0.10.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",
@@ -240,12 +240,12 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
240
240
  responses = Object.fromEntries(
241
241
  Object.entries(r.schema.response).map(([status, v]) => {
242
242
  if(!v) return []
243
- let s = Number(status) as keyof typeof HttpStatus
243
+ let s = status as keyof typeof HttpStatus | 'default'
244
244
  let { schema, isJson } = schemaToOpenapi(v)
245
245
  let { type, format } = resolveRef(schema)
246
246
  let media = schemaToMedia({ type, format, isJson } as SchemaType)
247
247
  let response: OpenAPIV3.ResponseObject = {
248
- description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
248
+ description: v.description || HttpStatus[s as keyof typeof HttpStatus] || 'Response',
249
249
  content: { [media]: { schema: schema } }
250
250
  }
251
251
  if (components.responses && r.schema.response?.[s]?.id) {
@@ -261,9 +261,12 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
261
261
  default: { description: HttpStatus[200] }
262
262
  }
263
263
  }
264
+ let summary = meta?.head.match(/^([^\n]+)/)?.[1]
265
+ console.log('#', r.method, r.path)
266
+ console.log(r.schema.body)
264
267
  paths[path][r.method] = {
265
268
  tags: tags.length ? tags : undefined,
266
- summary: meta?.head,
269
+ summary: summary,
267
270
  operationId: meta?.operationId,
268
271
  parameters: parameters.length ? parameters : undefined,
269
272
  requestBody,
package/src/index.ts CHANGED
@@ -111,6 +111,8 @@ export const $T = new SchemaType()
111
111
 
112
112
  export { RequestError } from './types'
113
113
 
114
+ export const config = (config: GalbeConfig) => config
115
+
114
116
  /**
115
117
  * #### Galbe Server
116
118
  * Instanciate a Galbe web server
@@ -136,13 +138,10 @@ export class Galbe {
136
138
  plugins: GalbePlugin[] = []
137
139
  constructor(config?: GalbeConfig) {
138
140
  this.config = config ?? {}
139
- this.config.routes = this.config.routes ?? true
140
141
  this.router = new GalbeRouter({
141
142
  prefix: this.config?.basePath || '',
142
143
  cacheEnabled: this.config?.router?.cacheEnabled
143
144
  })
144
- this.config.requestValidator = config?.requestValidator ?? { enabled: true }
145
- this.config.responseValidator = config?.responseValidator ?? { enabled: true }
146
145
  }
147
146
  private add(route: any) {
148
147
  this.router.add(route)
package/src/routes.ts CHANGED
@@ -234,7 +234,7 @@ export const defineRoutes = async (
234
234
  options: Pick<GalbeConfig, 'routes'>,
235
235
  proxy: GalbeProxy,
236
236
  ) => {
237
- const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
237
+ const routes = options?.routes === undefined || options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
238
238
  if (!routes) return
239
239
  const root = process.cwd()
240
240
  if (typeof routes === 'string') {
package/src/server.ts CHANGED
@@ -30,6 +30,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
30
30
 
31
31
  const server = Bun.serve({
32
32
  port: port || galbe.config?.port || 3000,
33
+ reusePort: galbe?.config?.reusePort,
33
34
  hostname: hostname || galbe.config?.hostname || 'localhost',
34
35
  tls: galbe.config?.tls,
35
36
 
@@ -81,7 +82,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
81
82
  context.params = inParams
82
83
 
83
84
  // request validation
84
- if (galbe.config?.requestValidator?.enabled) {
85
+ if (galbe.config?.requestValidator?.enabled !== false) {
85
86
  let errors: RequestError[] = []
86
87
  try {
87
88
  if (schema?.headers)
@@ -150,7 +151,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
150
151
 
151
152
  const parsedResponse = responseParser(response, context as Context, schema.response)
152
153
 
153
- if (galbe.config?.responseValidator?.enabled && schema.response)
154
+ if (galbe.config?.responseValidator?.enabled !== false && schema.response)
154
155
  validateResponse(response, schema.response, parsedResponse.status || 200)
155
156
 
156
157
  for (const p of pluginsCb.afterHandle) {
@@ -172,14 +173,20 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
172
173
  headers: { 'Content-Type': 'application/json' }
173
174
  })
174
175
  } else if (error instanceof RequestError) {
175
- let payload = ''
176
- if (typeof error.payload === 'string') payload = error.payload
177
- try {
178
- payload = JSON.stringify(error.payload)
179
- } catch (err) { }
176
+ let payload = error.payload
177
+ let headers = new Headers(error?.headers || {})
178
+ if (!headers.has('content-type')) {
179
+ if (typeof error.payload === 'string') headers.set('content-type', 'text/plain')
180
+ else {
181
+ headers.set('content-type', 'application/json')
182
+ try {
183
+ payload = JSON.stringify(error.payload)
184
+ } catch (err) { }
185
+ }
186
+ }
180
187
  return new Response(payload, {
181
188
  status: error.status,
182
- headers: { 'Content-Type': 'application/json' }
189
+ headers
183
190
  })
184
191
  } else console.log(error)
185
192
  return new Response('"Internal Server Error"', {
package/src/types.ts CHANGED
@@ -99,15 +99,25 @@ export type STQuery = Record<string, STQueryValue>
99
99
  * ```
100
100
  */
101
101
  export type GalbeConfig = {
102
+ /** The port number that the server will be listening on. */
102
103
  port?: number
104
+ /** Allow to share the same port across processes (Linux only). */
105
+ reusePort?: boolean
106
+ /** The hostname of the server. */
103
107
  hostname?: string
108
+ /** The base path is added as a prefix to all the routes created. */
104
109
  basePath?: string
110
+ /** Enable or disable TLS support. */
105
111
  tls?: TLSOptions
106
112
  server?: Exclude<ServeOptions, 'port'> | TLSServeOptions
113
+ /** A Glob Pattern or a list of Glob patterns defining the route files to be analyzed by the Automatic Route Analyzer. */
107
114
  routes?: boolean | string | string[]
108
115
  router?: { cacheEnabled: boolean }
116
+ /** A property that can be used by plugins to add plugin's specific configuration. */
109
117
  plugin?: Record<string, any>
118
+ /** Enable or disable the request schema validation.*/
110
119
  requestValidator?: { enabled: boolean }
120
+ /** Enable or disable the response schema validation.*/
111
121
  responseValidator?: { enabled: boolean }
112
122
  }
113
123
  /**
@@ -245,10 +255,12 @@ export type StaticEndpoint<P extends string = string, T extends string = string>
245
255
 
246
256
  export class RequestError {
247
257
  status: number
248
- payload: any
249
- constructor(options: { status?: number; payload?: any }) {
258
+ payload?: any
259
+ headers?: Record<string, string>
260
+ constructor(options: { status?: number; payload?: any, headers?: Record<string, string> }) {
250
261
  this.status = options.status ?? 500
251
262
  this.payload = options.payload
263
+ this.headers = options.headers
252
264
  }
253
265
  }
254
266
 
package/src/util.ts CHANGED
@@ -12,6 +12,15 @@ const METHOD_COLOR: Record<string, string> = {
12
12
  }
13
13
  const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
14
14
 
15
+ export const softMerge = <T> (base: T, override: T): T => {
16
+ for (const key in override) {
17
+ if (override[key] instanceof Object && !(override[key] instanceof Array)) {
18
+ if (!base[key]) Object.assign(base as any, { [key]: {} })
19
+ softMerge(base[key], override[key])
20
+ } else Object.assign(base as any, { [key]: override[key] })
21
+ }
22
+ return base
23
+ }
15
24
  export const logRoute = (
16
25
  r: { method: string; path: string, static?: { path: string, root: string } },
17
26
  meta?: RouteMeta,