galbe 0.4.1 → 0.6.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.
package/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # Galbe
1
+ <p align="center">
2
+ <a href="https://galbe.dev"><img src="https://galbe.dev/galbe.svg" alt="Logo" height=150></a>
3
+ </p>
4
+ <h1 align="center">Galbe</h1>
2
5
 
3
6
  [![Build & Test](https://github.com/pierre-cm/galbe/actions/workflows/build_test.yml/badge.svg?branch=main)](https://github.com/pierre-cm/galbe/actions/workflows/build_test.yml)
4
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/pierre-cm/galbe/blob/main/LICENSE)
@@ -79,9 +79,12 @@ export default (cmd: Command) => {
79
79
  ...Object.fromEntries(Object.entries(bunfig).filter(([k, v]) => v)),
80
80
  entrypoints: [buildIndex],
81
81
  outdir: resolve(CWD, out),
82
+ sourcemap: 'external',
82
83
  target: 'bun'
83
84
  }
84
85
 
86
+ await rm(resolve(CWD, out), { recursive: true })
87
+
85
88
  let bo = await Bun.build(buildConfig)
86
89
  if (bo.success) process.stdout.write(' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
87
90
  else {
@@ -2,7 +2,7 @@ import { $ } from 'bun'
2
2
  import { Command, Option } from 'commander'
3
3
  import { resolve } from 'path'
4
4
 
5
- import { CWD, fmtInterval, fmtVal, instanciateRoutes, watchDir } from '../util'
5
+ import { CWD, fmtInterval, fmtVal, instanciateRoutes, killPort, watchDir } from '../util'
6
6
  import { Galbe } from '../../src'
7
7
 
8
8
  const defaultPort = 3000
@@ -22,13 +22,21 @@ export default (cmd: Command) => {
22
22
  )
23
23
  .addOption(new Option('-w, --watch', 'watch file changes').default(false, fmtVal(false)))
24
24
  .addOption(new Option('-nc, --noclear', "don't clear on file changes").default(false, fmtVal(false)))
25
+ .addOption(
26
+ new Option('-f, --force', 'kills any process running on defined port before strating the server').default(
27
+ false,
28
+ fmtVal(false)
29
+ )
30
+ )
25
31
  .action(async (index, props) => {
26
- const { port, watch, noclear } = props
32
+ const { port, watch, noclear, force } = props
27
33
  const clear = !noclear
28
34
  const indexPath = resolve(CWD, index)
29
35
  let g: Galbe
30
36
 
31
- Bun.env.BUN_ENV = 'development'
37
+ if (!Bun.env.BUN_ENV) Bun.env.BUN_ENV = 'development'
38
+
39
+ if (force) await killPort(port || 3000)
32
40
 
33
41
  if (watch) {
34
42
  await watchDir(
@@ -82,7 +82,14 @@ export default (cmd: Command) => {
82
82
  [...r.path.matchAll(/:([^\/]+)/g)]?.map(m => [
83
83
  m?.[1],
84
84
  {
85
- ...(r.schema?.params?.[m?.[1]] ? { type: schemaToTypeStr(r.schema.params[m[1]]) } : {})
85
+ ...(r.schema?.params?.[m?.[1]]
86
+ ? {
87
+ type: schemaToTypeStr(r.schema.params[m[1]]),
88
+ ...(r.schema.params[m[1]]?.description
89
+ ? { description: r.schema.params[m[1]].description as string }
90
+ : {})
91
+ }
92
+ : { type: 'string' })
86
93
  }
87
94
  ])
88
95
  ) || {},
@@ -106,14 +113,15 @@ export default (cmd: Command) => {
106
113
  description: meta.head,
107
114
  route,
108
115
  arguments:
109
- Object.entries((r.schema?.params || {}) as Record<string, STSchema>)?.map(([k, p]) => {
110
- let type = schemaToTypeStr({ ...p, [Optional]: false })
111
- return {
112
- name: k,
113
- type: type === 'boolean' ? '' : `<${type}>`,
114
- description: p?.description || ''
116
+ Object.entries((route?.params || {}) as Record<string, { type: string; description?: string }>)?.map(
117
+ ([k, p]) => {
118
+ return {
119
+ name: k,
120
+ type: p.type === 'boolean' ? '' : `<${p.type}>`,
121
+ description: p?.description || ''
122
+ }
115
123
  }
116
- }) || [],
124
+ ) || [],
117
125
  options:
118
126
  Object.entries((r.schema?.query || {}) as Record<string, STSchema>)?.map(([k, o]) => {
119
127
  let type = schemaToTypeStr({ ...o, [Optional]: false })
@@ -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.type}", "${a.description}")`)
103
+ let args = c.arguments.map(a=>`.argument("${a.name}", "${a.description || 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:[]},
@@ -42,8 +42,9 @@ type PGR<
42
42
  O extends number = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
43
43
  > = Promise<GR<S, B, O>>
44
44
 
45
- type RequestOptions<H = any, B = any> = {
45
+ type RequestOptions<H = any, Q = any, B = any> = {
46
46
  headers?: H
47
+ query?: Q
47
48
  body?: B
48
49
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'
49
50
  }
@@ -61,7 +62,7 @@ export default class GalbeClient {
61
62
  return`${method} = {\n${list.map( r => {
62
63
  let p = Object.entries(r.params)
63
64
  let schemas = Object.keys(r.schemas).length ?
64
- `<${r.schemas.headers??'any'},${r.schemas.body??'any'}>`:
65
+ `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'}>`:
65
66
  ''
66
67
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
67
68
  let responses = Object.keys(r.schemas?.response||{}).length ?
@@ -79,6 +80,8 @@ export default class GalbeClient {
79
80
 
80
81
  async fetch(path: string, options: RequestOptions) {
81
82
  let url = `${this?.config?.server?.url ?? ''}${path}`
83
+ const params = new URLSearchParams(options?.query || {})
84
+ url = `${url}?${params.toString()}`
82
85
  let res = await fetch(url, {
83
86
  method: options?.method || 'GET',
84
87
  headers: { ...DEFAULT_HEADERS, ...(options?.headers || {}) },
@@ -148,7 +151,7 @@ export default class GalbeClient {
148
151
  return list.filter(r=>r.alias).map(r => {
149
152
  let p = Object.entries(r.params)
150
153
  let schemas = Object.keys(r.schemas).length ?
151
- `<${r.schemas.headers??'any'},${r.schemas.body??'any'}>`:
154
+ `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'}>`:
152
155
  ''
153
156
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
154
157
  let responses = Object.keys(r.schemas?.response||{}).length ?
package/bin/util.ts CHANGED
@@ -58,7 +58,7 @@ export const watchDir = async (
58
58
  })
59
59
  }
60
60
  export const instanciateRoutes = async (g: Galbe) => {
61
- console.log('🏗️ \x1b[1;30mConstructing routes\x1b[0m\n')
61
+ console.log('🏗️ \x1b[1mConstructing routes\x1b[0m\n')
62
62
  // Main thread routes definitions
63
63
  let hasMainRoutes = false
64
64
  walkRoutes(g.router.routes, r => {
@@ -102,6 +102,25 @@ export const softMerge = (base, override) => {
102
102
  return base
103
103
  }
104
104
 
105
+ export const killPort = async (port: number) => {
106
+ let getProcCmd: string[], killCmd: (port: string) => string[]
107
+
108
+ if (process.platform === 'win32') {
109
+ getProcCmd = ['cmd', '-c', `netstat -aon | findstr ${port}`]
110
+ killCmd = (pid: string) => ['taskkill', '/pid', pid, '/f']
111
+ } else {
112
+ getProcCmd = ['lsof', '-t', `-i:${port}`, '-sTCP:LISTEN']
113
+ killCmd = (pid: string) => ['kill', '-9', pid]
114
+ }
115
+
116
+ let proc = Bun.spawn(getProcCmd, { stdout: 'pipe' })
117
+ const pid = (await new Response(proc.stdout).text()).trim()
118
+ if (pid) {
119
+ proc = Bun.spawn(killCmd(pid), { stdout: 'pipe' })
120
+ await proc.exited
121
+ }
122
+ }
123
+
105
124
  export const HttpStatus = {
106
125
  100: 'Continue',
107
126
  101: 'Switching Protocols',
package/docs/cli.md ADDED
@@ -0,0 +1,336 @@
1
+ # CLI
2
+
3
+ A Command Line Interface is shipped with Galbe package. You can use it to perform useful tasks around your application.
4
+
5
+ After [Installing Galbe](getting-started.md#automatic-installation), the CLI will be available locally to your project.
6
+
7
+ However, if you want to use it directly from your terminal, you must either:
8
+
9
+ Install it globally using the following command:
10
+
11
+ ```bash
12
+ $ bun install -g galbe
13
+ ```
14
+
15
+ Run it with `bunx`:
16
+
17
+ ```bash
18
+ $ bunx galbe
19
+ ```
20
+
21
+ ## dev
22
+
23
+ Start a dev server running your Galbe application.
24
+
25
+ #### Arguments
26
+
27
+ | Name | Description |
28
+ | ----- | -------------------------------------------------------- |
29
+ | index | The js or ts file that export you Galbe server instance. |
30
+
31
+ #### Options
32
+
33
+ | Short | Long | Descritpion | Default |
34
+ | ----- | --------- | --------------------------- | ------- |
35
+ | -p | --port | port number [1-65535] | 3000 |
36
+ | -w | --watch | watch file changes | false |
37
+ | -nc | --noclear | don't clear on file changes | false |
38
+
39
+ #### Example
40
+
41
+ ```js
42
+ // index.js
43
+
44
+ import { Galbe } from 'galbe'
45
+
46
+ export default new Galbe()
47
+ ```
48
+
49
+ ```bash
50
+ $ galbe dev index.js -p 7357 --watch
51
+ ```
52
+
53
+ ## build
54
+
55
+ Bundle your Galbe application.
56
+
57
+ #### Arguments
58
+
59
+ | Name | Description |
60
+ | ----- | -------------------------------------------------------- |
61
+ | index | The js or ts file that export you Galbe server instance. |
62
+
63
+ #### Options
64
+
65
+ | Short | Long | Descritpion | Default |
66
+ | ----- | --------- | ------------------------------ | -------- |
67
+ | -o | --out | output directory | dist/app |
68
+ | -C | --compile | create a standalone executable | false |
69
+ | -c | --config | bun config (js or ts) | - |
70
+
71
+ #### Example
72
+
73
+ ```js
74
+ // index.js
75
+
76
+ import { Galbe } from 'galbe'
77
+
78
+ export default new Galbe()
79
+ ```
80
+
81
+ ```bash
82
+ $ galbe build index.js
83
+ ```
84
+
85
+ ## generate
86
+
87
+ Generate resources arround your Galbe application.
88
+
89
+ ### client
90
+
91
+ Generate a client for your Galbe application.
92
+
93
+ #### Arguments
94
+
95
+ | Name | Description |
96
+ | ----- | -------------------------------------------------------- |
97
+ | index | The js or ts file that export you Galbe server instance. |
98
+
99
+ #### Options
100
+
101
+ | Short | Long | Descritpion | Default |
102
+ | ----- | -------- | -------------------------- | ------------------------------------ |
103
+ | -o | --out | output file | dist/(client.ts \| client.js \| cli) |
104
+ | -t | --target | build target [ts, js, cli] | ts |
105
+
106
+ #### Examples
107
+
108
+ Let's setup a new Galbe project:
109
+
110
+ ```bash
111
+ $ bun create galbe galbe-example -t hello -l ts
112
+ $ cd galbe-example
113
+ $ bun install
114
+ ```
115
+
116
+ > [!NOTE]
117
+ > In order for the following examples to work, you must ensure that an instance of you galbe app is running on port 3000.
118
+ > You can do that by running `bun run dev`.
119
+
120
+ ##### JS or TS client
121
+
122
+ To generate a JS or TS client of that application, you can run the following command:
123
+
124
+ ```bash
125
+ $ galbe generate client index.ts
126
+ ```
127
+
128
+ This will generate a `dist/client.ts` client lib by default.
129
+ You can import it and use it like in the following example:
130
+
131
+ ```ts
132
+ // client_example.ts
133
+
134
+ import HelloClient from './dist/client'
135
+
136
+ const client = new HelloClient({ server: { url: 'http://localhost:3000' } })
137
+
138
+ const response = await client.hello('Bob', { query: { age: 42 } })
139
+ // This is equivalent as calling
140
+ // const response = await client.get["/hello/:name"]("Bob", { query: { age: 42 } })
141
+
142
+ if (response.ok) console.log(await response.body())
143
+ // Hello Bob! You're 42 y.o.
144
+ ```
145
+
146
+ ##### CLI client
147
+
148
+ To generate a CLI of that application, you can run the following command:
149
+
150
+ ```bash
151
+ $ galbe generate client index.ts -t cli
152
+ ```
153
+
154
+ This will generate a `cli` binary file under `dist` directory by default.
155
+
156
+ ```bash
157
+ $ ./dist/cli --help
158
+ Usage: galbe-example [options] [command]
159
+
160
+ Options:
161
+ -V, --version output the version number
162
+ -h, --help display help for command
163
+
164
+ Commands:
165
+ hello [options] Greeting endpoint
166
+ help [command] display help for command
167
+ ```
168
+
169
+ ```bash
170
+ $ ./dist/cli hello --help
171
+ Usage: galbe-example hello [options] <name>
172
+
173
+ Greeting endpoint
174
+
175
+ Arguments:
176
+ name name argument
177
+
178
+ Options:
179
+ -%f, --%format [string] response format ['s','h','b','t','p'] (default: ["s","b","p"])
180
+ -%h, --%header <string...> request header formated as headerName=headerValue (default: [])
181
+ -%q, --%query <string...> query param formated as paramName=paramValue (default: [])
182
+ -%b, --%body <string> request body (default: "")
183
+ -%bf, --%bodyFile <path> request body file (default: "")
184
+ -a, --age <number>
185
+ -h, --help display help for command
186
+ ```
187
+
188
+ ```bash
189
+ $ ./dist/cli hello Pierre -a 29
190
+ 200
191
+ Hello Pierre! You're 29 y.o.
192
+ ```
193
+
194
+ > [!NOTE]
195
+ > In order to be able to fetch the correct server url, the cli requires to set an env variable `GCLI_SERVER_URL` with the url of your Galbe app.
196
+
197
+ ### spec
198
+
199
+ Generate the spec of your Galbe application.
200
+
201
+ #### Arguments
202
+
203
+ | Name | Description |
204
+ | ----- | -------------------------------------------------------- |
205
+ | index | The js or ts file that export you Galbe server instance. |
206
+
207
+ #### Options
208
+
209
+ | Short | Long | Descritpion | Default |
210
+ | ----- | -------- | ------------------------------------------------ | ----------------------- |
211
+ | -t | --target | spec target [openapi:3.0:json, openapi:3.0:yaml] | openapi:3.0:yaml |
212
+ | -b | --base | base spec file | - |
213
+ | -o | --out | output file | spec/api.(yaml \| json) |
214
+
215
+ #### Example
216
+
217
+ Let's try to generate the specof the project defined in the previous client section. You can then run:
218
+
219
+ ```bash
220
+ $ galbe generate spec index.ts
221
+ ```
222
+
223
+ This should generate the following `spec/api.yaml` file:
224
+
225
+ ```yaml
226
+ openapi: 3.0.3
227
+ info:
228
+ title: galbe-app
229
+ version: 0.1.0
230
+ paths:
231
+ /hello/{name}:
232
+ get:
233
+ summary: Greeting endpoint
234
+ operationId: hello
235
+ parameters:
236
+ - name: age
237
+ in: query
238
+ required: true
239
+ schema:
240
+ type: integer
241
+ responses:
242
+ '200':
243
+ description: OK
244
+ content:
245
+ text/plain:
246
+ schema:
247
+ type: string
248
+ ```
249
+
250
+ ### code
251
+
252
+ Generate the code and project structure from spec.
253
+
254
+ #### Arguments
255
+
256
+ | Name | Description |
257
+ | ----- | ---------------------------------------------------------- |
258
+ | input | The input spec file from which the code will be generated. |
259
+
260
+ #### Options
261
+
262
+ | Short | Long | Descritpion | Default |
263
+ | ----- | -------- | ------------------------------------------------- | -------------------------- |
264
+ | -f | --format | input format [openapi:3.0:yaml, openapi:3.0:json] | openapi:3.0:(yaml \| json) |
265
+ | -t | --target | source target [ts, js] | ts |
266
+ | -o | --out | output dir | src |
267
+ | -F | --force | force overriding output | false |
268
+
269
+ #### Example
270
+
271
+ For that example, we will generate the Galbe source code from the [Swagger Petstore Openapi spec](https://petstore3.swagger.io/).
272
+
273
+ First, initiate a new bun project and install the galbe dependency.
274
+
275
+ ```bash
276
+ $ mkdir petstore && cd petstore
277
+ $ bun init && bun add galbe
278
+ ```
279
+
280
+ Now modify the `index.ts` file with the following content:
281
+
282
+ ```ts
283
+ import { Galbe } from 'galbe'
284
+
285
+ export default new Galbe()
286
+ ```
287
+
288
+ Then download the petstore json spec from Swagger website into `petstore.spec.json`:
289
+
290
+ ```bash
291
+ $ curl -o petstore.spec.json https://petstore3.swagger.io/api/v3/openapi.json
292
+ ```
293
+
294
+ You can now generate the sources from the petstore spec:
295
+
296
+ ```bash
297
+ $ galbe generate code petstore.spec.json
298
+ ```
299
+
300
+ This should generate the code of our application in the `src` directory by default.
301
+
302
+ To test that the code was successfully generated, you can run:
303
+
304
+ ```bash
305
+ $ bunx galbe dev index.ts
306
+ 🏗️ Constructing routes
307
+
308
+ src/routes/pet.route.ts
309
+ [PUT] /pet Update an existing pet
310
+ [POST] /pet Add a new pet to the store
311
+ [GET] /pet/findByStatus Finds Pets by status
312
+ [GET] /pet/findByTags Finds Pets by tags
313
+ [GET] /pet/:petId Find pet by ID
314
+ [POST] /pet/:petId Updates a pet in the store with form data
315
+ [DELETE] /pet/:petId Deletes a pet
316
+ [POST] /pet/:petId/uploadImage uploads an image
317
+
318
+ src/routes/store.route.ts
319
+ [GET] /store/inventory Returns pet inventories by status
320
+ [POST] /store/order Place an order for a pet
321
+ [GET] /store/order/:orderId Find purchase order by ID
322
+ [DELETE] /store/order/:orderId Delete purchase order by ID
323
+
324
+ src/routes/user.route.ts
325
+ [POST] /user Create user
326
+ [POST] /user/createWithList Creates list of users with given input array
327
+ [GET] /user/login Logs user into the system
328
+ [GET] /user/logout Logs out current logged in user session
329
+ [GET] /user/:username Get user by user name
330
+ [PUT] /user/:username Update user
331
+ [DELETE] /user/:username Delete user
332
+
333
+ done
334
+
335
+ 🚀 Server running at http://localhost:3000
336
+ ```
@@ -14,29 +14,40 @@ This is the recommended way of setting up a Galbe project.
14
14
 
15
15
  ```bash
16
16
  $ bun create galbe app
17
+ ```
18
+
19
+ The Galbe starter CLI will request you to chose a template and a target language for your project. Let's select `hello` as template and `ts` as language. This will create a new project under `app` directory.
20
+
21
+ Now you can navigate to your newly created project and install it:
22
+
23
+ ```bash
17
24
  $ cd app
18
25
  $ bun install
19
26
  ```
20
27
 
21
- This will create a new project under `app` directory and install it.
22
-
23
- Now you can start the dev server by running:
28
+ And start the dev server by running:
24
29
 
25
30
  ```bash
26
31
  $ bun dev
27
- ```
32
+ 🏗️ Constructing routes
33
+
34
+ hello.route.ts
35
+ [GET] /hello/:name Greeting endpoint
28
36
 
29
- This will start a web server on `localhost:3000`.
37
+ done
30
38
 
31
- To verify that the project was setup correctly and is running, try to reach `localhost:3000/hello` endpoint, this should return following greeting message:
39
+ 🚀 Server running at http://localhost:3000
40
+ ```
41
+
42
+ Let's try to reach the hello endpoint:
32
43
 
33
44
  ```bash
34
- $ curl localhost:3000/hello
35
- Hello from Galbe!
45
+ $ curl localhost:3000/hello/John?age=32
46
+ Hello John! You're 32 y.o.
36
47
  ```
37
48
 
38
49
  > [!TIP]
39
- > By default, the dev server automatically reloads on every file change.
50
+ > If you want to have a more complete view of Galbe capabilities, feel free to take a look at the `demo` template from the Galbe starter CLI.
40
51
 
41
52
  ## Manual installation
42
53
 
@@ -59,15 +70,15 @@ Open `package.json` file and add the following scripts:
59
70
  }
60
71
  ```
61
72
 
62
- As you can see, those scripts rely on Galbe CLI to run and build the application. You will find more info about Galbe CLI available options in the next section [Galbe CLI](#galbe-cli).
73
+ As you can see, those scripts rely on Galbe CLI to run and build the application. You will find more info about it on the [CLI](cli.md) page.
63
74
 
64
75
  This require your `index.ts` to export a default Galbe instance in order to work. As in the following example:
65
76
 
66
77
  ```ts
67
78
  import { Galbe } from 'galbe'
68
79
 
69
- const g = new Galbe({ port: 3000 })
70
- g.get('/hello', () => 'Hello Mom!')
80
+ const galbe = new Galbe({ port: 3000 })
81
+ galbe.get('/hello', () => 'Hello Mom!')
71
82
 
72
83
  export default galbe
73
84
  ```
@@ -77,40 +88,6 @@ This is the recommended way to proceed but it is not mandatory. Galbe instances
77
88
  > [!WARNING]
78
89
  > In the case you decide to not rely on Galbe CLI to run/build your app, you will not have access to [Automatic Route Analyzer](routes.md#automatic-route-analyzer) feature.
79
90
 
80
- ### Galbe CLI
81
-
82
- ```bash
83
- $ galbe <command> <argument> [options]
84
- ```
85
-
86
- Here are the available commands:
87
-
88
- **dev**
89
-
90
- Start a dev server running your Galbe application.
91
-
92
- _argument_
93
-
94
- The path of the file exporting your Galbe instance
95
-
96
- _options_
97
-
98
- - `--port` or `-p`: port number (default: 3000)
99
- - `--watch` or `-w`: watch file changes (default: true)
100
-
101
- **build**
102
-
103
- Bundle your Galbe application.
104
-
105
- _argument_
106
-
107
- The path of the file exporting your Galbe instance
108
-
109
- _options_
110
-
111
- - `--out` or `-o`: output file | directory (default: app | dist )
112
- - `--compile` or `-c`: create a standalone executable (default: false)
113
-
114
91
  ## Configuration
115
92
 
116
93
  To configure your Galbe server, you should pass your configuration to the Galbe constructor when you instanciate it.
@@ -121,6 +98,10 @@ const galbe = new Galbe(configuration)
121
98
 
122
99
  ### Properties
123
100
 
101
+ **hostname**
102
+
103
+ The hostname of the server. Default is `localhost`.
104
+
124
105
  **port**
125
106
 
126
107
  The port number that the server will be listening on. Default is `3000`.
@@ -137,6 +118,16 @@ A Glob Pattern or a list of Glob patterns defining the route files to be analyze
137
118
 
138
119
  A property that can be used by plugins to add plugin's specific configuration. Every key should correspond to a [Unique Plugin Identifier](plugins.md).
139
120
 
121
+ **tls**
122
+
123
+ Enable or disable TLS support. Default value is `false`.
124
+
125
+ - **tls.key**: The path to the private key file
126
+
127
+ - **tls.cert**: The path to the certificate file
128
+
129
+ - **tls.ca**: The path to the certificate authority file
130
+
140
131
  **requestValidator.enabled**
141
132
 
142
133
  Enable or disable the _request_ schema validation (See [Request Schema definition](schemas.md#request-schema-definition)). Default value is `true`.
@@ -229,3 +220,27 @@ You can find more info about Route Files definition in the [Routes Files](routes
229
220
 
230
221
  > [!NOTE]
231
222
  > The examples provided above will work with the default configuration, but you can easily customize the routes property to fit your own project structure. Simply redefine the `routes` property with your own pattern(s) to to fit your own project structure.
223
+
224
+ ## How to debug
225
+
226
+ The easiest way to debug your app is by installing the [VSCode Bun extension](https://marketplace.visualstudio.com/items?itemName=oven.bun-vscode).
227
+
228
+ You can then create a `.vscode/launch.json` config file in your project root directory. Here is an example of configuration:
229
+
230
+ ```json
231
+ {
232
+ "version": "0.2.0",
233
+ "configurations": [
234
+ {
235
+ "type": "bun",
236
+ "request": "launch",
237
+ "name": "Debug Galbe",
238
+ "program": "node_modules/galbe/bin/cli.ts",
239
+ "env": { "TERM": "xterm" },
240
+ "cwd": "${workspaceFolder}",
241
+ "runtime": "bun",
242
+ "runtimeArgs": ["dev", "index.ts", "--watch", "--force"]
243
+ }
244
+ ]
245
+ }
246
+ ```
package/docs/plugins.md CHANGED
@@ -17,7 +17,7 @@ type GalbePlugin = {
17
17
 
18
18
  **name**
19
19
 
20
- The name should be a Unique Plugin Identifier. It should be chosen to be unique to avoid conflicts with other potential plugins. For example, Galbe's official plugins names will always start with `dev.galbe.*`.
20
+ The name should be a Unique Plugin Identifier. It should be chosen to be unique to avoid conflicts with other potential plugins. Ideally, it will have the form of `com.example.myplugin`.
21
21
 
22
22
  **init**
23
23
 
@@ -60,59 +60,54 @@ galbe.use(plugin)
60
60
 
61
61
  Here is an example of a plugin implementation that handles routes tagged with `@deprecated` metadata (See [Route files](routes.md#route-files) section about metadata).
62
62
 
63
+ deprecated.plugin.ts
64
+
63
65
  ```ts
64
- // myPlugin.ts
65
-
66
- import { Galbe, type Context, type Route } from 'galbe'
67
-
68
- class MyPlugin {
69
- name = 'dev.galbe.example'
70
- deprecated: Record<string, string[]> = {}
71
- // Retrieve and store all route with a @deprecated flag metadata
72
- init(config: any, galbe: Galbe) {
73
- if (config?.enabled && galbe.meta) {
74
- for (const f of galbe.meta) {
75
- for (const [path, methods] of Object.entries(f.routes)) {
76
- for (const [method, meta] of Object.entries(methods)) {
77
- if (meta.deprecated) {
78
- if (!this.deprecated?.[method]) this.deprecated[method] = []
79
- this.deprecated[method].push(path)
80
- }
81
- }
82
- }
66
+ import type { GalbePlugin } from 'galbe'
67
+ import { walkMetaRoutes } from 'galbe/utils'
68
+
69
+ const PLUGIN_ID = 'dev.galbe.deprecated'
70
+
71
+ export default () => {
72
+ let deprecateds = new Set<string>()
73
+ return {
74
+ name: PLUGIN_ID,
75
+ // Init the plugin, check for deprecated metadata tags
76
+ init(_config, galbe) {
77
+ if (galbe.meta) {
78
+ walkMetaRoutes(galbe.meta, (method, path, meta) => {
79
+ if (meta.deprecated) deprecateds.add(JSON.stringify({ method, path }))
80
+ })
81
+ }
82
+ },
83
+ // Check if the current route is deprecated; if so, flag it as such and log it
84
+ onRoute(context) {
85
+ let r = context.route
86
+ if (!r) return
87
+ if (deprecateds.has(JSON.stringify({ method: r.method, path: r.path }))) {
88
+ console.warn(`Call to deprecated route "${r.method} ${r.path}"`)
89
+ }
90
+ },
91
+ // Add a header to the response if the route has been flagged as deprecated
92
+ afterHandle(response, context) {
93
+ let r = context.route
94
+ if (!r) return
95
+ if (deprecateds.has(JSON.stringify({ method: r.method, path: r.path }))) {
96
+ response.headers.set('x-deprecated', 'true')
83
97
  }
84
98
  }
85
- }
86
- // Check if the current route is deprecated; if so, flag it as such and log it
87
- onRoute(context: Context) {
88
- let route = context.route
89
- if (this.deprecated?.[route.method]?.includes(route.path)) {
90
- context.state[this.name] = { deprecated: true }
91
- console.warn(`Call to deprecated route [${route.method}]${route.path}`)
92
- }
93
- }
94
- // Add a header if the request has previously been flagged as deprecated
95
- afterHandle(response: Response, context: Context) {
96
- if (context.state?.[this.name]?.deprecated) {
97
- response.headers.set('x-deprecated', 'true')
98
- }
99
- }
99
+ } as GalbePlugin
100
100
  }
101
-
102
- export default new MyPlugin()
103
101
  ```
104
102
 
105
- ```ts
106
- // index.ts
103
+ index.ts
107
104
 
105
+ ```ts
108
106
  import { Galbe } from 'galbe'
109
- import config from './galbe.config'
110
- import myPlugin from './myPlugin'
107
+ import deprecatedPlugin from './deprecated.plugin'
111
108
 
112
- const galbe = new Galbe(config)
113
- galbe.use(myPlugin)
109
+ const galbe = new Galbe()
110
+ galbe.use(deprecatedPlugin())
114
111
 
115
112
  export default galbe
116
113
  ```
117
-
118
- As you can see in this example, the [Context](context.md#definition) `state` property is used to persist information between plugins interceptor methods. It is a good practice to scope any information stored in the state with the plugin name, as it can also be used by other plugins and hooks to store data in the context.
package/docs/routes.md CHANGED
@@ -82,7 +82,7 @@ galbe.get(
82
82
  > [!NOTE]
83
83
  > This feature is only available if you run/build the app via the [Galbe CLI](getting-started.md#galbe-cli), which is the case by default if you created your app following the [Automatic Installation](getting-started.md#automatic-installation) step or if you configured your package.json to do so.
84
84
 
85
- The Automatic Route Analyzer is responsible for analyzing all the Route Files of your project and setting up the route definitions for your Galbe server automatically. By default, the analyzer will search for Route Files matching paths like `src/**/*.route.{js,ts}`. This can be configured by modifying the value of `routes` property of your Galbe configuration. A value of `false` will disable the analyzer.
85
+ The Automatic Route Analyzer is responsible for analyzing all the Route Files of your project and setting up the route definitions for your Galbe server automatically. By default, the analyzer will search for Route Files matching paths like `src/**/*.route.{js,ts}`. This can be configured by modifying the value of `routes` property of your Galbe configuration. A value of `false` will disable the route analyzer.
86
86
 
87
87
  ### Route Files
88
88
 
@@ -94,15 +94,6 @@ export default g => {
94
94
  }
95
95
  ```
96
96
 
97
- The same example in Typescript:
98
-
99
- ```ts
100
- import type { Galbe } from 'galbe'
101
- export default (g: Galbe) => {
102
- g.get('/foo/:bar', ctx => ctx.params.bar)
103
- }
104
- ```
105
-
106
97
  The Automatic Route Analyzer can also collect metadata about your Route File and your routes by analyzing multiline comments. This can be used by some plugins to perform specific tasks. Here's an example of a Route File with multiline comment metadata:
107
98
 
108
99
  ```js
@@ -120,5 +111,3 @@ export default g => {
120
111
  g.get('/foo/:bar', ctx => ctx.params.bar)
121
112
  }
122
113
  ```
123
-
124
- You will find more information about comment metadata and how to use them along with examples in the [Plugin](plugins.md) section.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.4.1",
3
+ "version": "0.6.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",
@@ -165,7 +165,10 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
165
165
  let meta = metaRoutes?.[r.path]?.[r.method]
166
166
  let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
167
167
  if (!(path in paths)) paths[path] = {}
168
- let tags = [...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []), ...(meta?.tag || [])]
168
+ let tags = [
169
+ ...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []),
170
+ ...(typeof meta?.tag === 'string' ? [meta?.tag] : meta?.tag || [])
171
+ ]
169
172
  let security: Record<string, any> = []
170
173
 
171
174
  let pathParam = r.schema?.params
package/src/index.ts CHANGED
@@ -122,7 +122,9 @@ export class Galbe {
122
122
  config: GalbeConfig
123
123
  meta?: Array<RouteFileMeta> = []
124
124
  router: GalbeRouter
125
- errorHandler?: ErrorHandler
125
+ startCb: (() => void)[] = []
126
+ stopCb: (() => void)[] = []
127
+ errorCb: ErrorHandler[] = []
126
128
  listening: boolean = false
127
129
  server?: Server
128
130
  plugins: GalbePlugin[] = []
@@ -148,24 +150,34 @@ export class Galbe {
148
150
  if (p.init) await p.init(this.config?.plugin?.[p.name] || {}, this)
149
151
  }
150
152
  }
151
- async listen(port?: number) {
153
+ async listen(port?: number, hostname?: string) {
152
154
  port = port || this.config?.port || 3000
155
+ hostname = hostname || this.config?.hostname || 'localhost'
153
156
  this.config.port = port
157
+ this.config.hostname = hostname
154
158
  if (this.listening) this.stop()
155
159
  await this.init()
156
- this.server = await server(this, port)
160
+ this.server = await server(this, port, hostname)
157
161
  if (Bun.env.BUN_ENV === 'development') {
158
- const url = `http://localhost:${port}${this.config?.basePath || ''}`
159
- console.log(`\x1b[1;30m🚀 Server running at\x1b[0m \x1b[4;34m${url}\x1b[0m\n`)
162
+ const url = `http${this.config.tls ? 's' : ''}://${hostname}:${port}${this.config?.basePath || ''}`
163
+ console.log(`\x1b[1m🚀 Server running at\x1b[0m \x1b[4;34m${url}\x1b[0m\n`)
160
164
  }
161
165
  this.listening = true
166
+ for (let sh of this.startCb) sh()
162
167
  return this.server
163
168
  }
164
169
  stop() {
165
170
  this.server?.stop(true)
171
+ for (let sh of this.stopCb) sh()
172
+ }
173
+ onStart(callback: () => void) {
174
+ this.startCb.push(callback)
175
+ }
176
+ onStop(callback: () => void) {
177
+ this.stopCb.push(callback)
166
178
  }
167
179
  onError(handler: ErrorHandler) {
168
- this.errorHandler = handler
180
+ this.errorCb.push(handler)
169
181
  }
170
182
  get: Endpoint<'get'> = <
171
183
  Path extends string,
@@ -203,7 +215,6 @@ export class Galbe {
203
215
  | Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
204
216
  | Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
205
217
  arg4?: Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>
206
- //@ts-ignore
207
218
  ) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
208
219
  put: Endpoint<'put'> = <
209
220
  Path extends string,
package/src/routes.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GalbeConfig, Route } from './types'
1
+ import type { GalbeConfig, Method, Route } from './types'
2
2
 
3
3
  import { readdir, lstat } from 'fs/promises'
4
4
  import { extname } from 'path'
@@ -13,7 +13,7 @@ export const DEFAULT_ROUTE_PATTERN = 'src/**/*.route.{js,ts}'
13
13
  export type RouteMeta = { head?: string } & Record<string, boolean | string | string[]>
14
14
  export type RoutesMeta = {
15
15
  header: Record<string, boolean | string | string[]>
16
- routes: Record<string, Record<string, RouteMeta>>
16
+ routes: Record<string, Partial<Record<Method, RouteMeta>>>
17
17
  }
18
18
  export type RouteInstanciationCallback = <T extends 'add' | 'error'>(event: {
19
19
  type: T
@@ -204,7 +204,7 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
204
204
  // @ts-ignore
205
205
  const path = node.arguments[0].value
206
206
  // @ts-ignore
207
- const method = node.callee.property.name
207
+ const method = node.callee.property.name as Method
208
208
  const line = node.loc?.start.line || -1
209
209
  const col = node.loc?.start.column || -1
210
210
  const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
package/src/server.ts CHANGED
@@ -4,6 +4,9 @@ import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
6
  import { validateResponse } from './validator'
7
+ import { logger } from 'girok'
8
+
9
+ type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
7
10
 
8
11
  const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
9
12
  const EMPTY_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD']
@@ -20,7 +23,7 @@ const setupPluginCallbacks = (galbe: Galbe) => ({
20
23
  afterHandle: galbe.plugins.filter(p => p.afterHandle)
21
24
  })
22
25
 
23
- export default async (galbe: Galbe, port?: number) => {
26
+ export default async (galbe: Galbe, port?: number, hostname?: string) => {
24
27
  const router = galbe.router
25
28
  if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
26
29
  galbe.config.basePath = `/${galbe?.config?.basePath}`
@@ -28,17 +31,16 @@ export default async (galbe: Galbe, port?: number) => {
28
31
 
29
32
  return Bun.serve({
30
33
  port: port || galbe.config?.port || 3000,
34
+ hostname: hostname || galbe.config?.hostname || 'localhost',
35
+ tls: galbe.config?.tls,
36
+
31
37
  async fetch(req) {
32
38
  if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
33
- const context: Context = {
39
+ const context = {
34
40
  request: req,
35
41
  set: { headers: {} },
36
- headers: {},
37
- params: {},
38
- query: {},
39
- body: {},
40
42
  state: {}
41
- }
43
+ } as MakeOptional<Context, 'headers' | 'params' | 'query' | 'body'>
42
44
  for (const p of pluginsCb.onFetch) {
43
45
  //@ts-ignore
44
46
  const r = await p.onFetch(context)
@@ -130,23 +132,23 @@ export default async (galbe: Galbe, port?: number) => {
130
132
  await callChain[idx + 1].call()
131
133
  }
132
134
  }
133
- let r = await hook(context, next)
135
+ let r = await hook(context as Context, next)
134
136
  if (r) return r
135
137
  if (!nextCalled && !handlerCalled) await next()
136
138
  }
137
139
  }))
138
140
  callChain.push({
139
141
  call: async () => {
140
- response = await handlerWrapper(context)
142
+ response = await handlerWrapper(context as Context)
141
143
  context.set.status = response instanceof Response ? response.status : 200
142
144
  }
143
145
  })
144
146
  if (callChain.length > 1) {
145
147
  let r = await callChain[0].call()
146
148
  if (r) response = r
147
- } else response = await handlerWrapper(context)
149
+ } else response = await handlerWrapper(context as Context)
148
150
 
149
- const parsedResponse = responseParser(response, context, schema.response)
151
+ const parsedResponse = responseParser(response, context as Context, schema.response)
150
152
 
151
153
  if (galbe.config?.responseValidator?.enabled && schema.response)
152
154
  validateResponse(response, schema.response, parsedResponse.status || 200)
@@ -161,7 +163,7 @@ export default async (galbe: Galbe, port?: number) => {
161
163
  } catch (error) {
162
164
  context.set.status = error instanceof RequestError ? error.status : 500
163
165
  let customError
164
- if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
166
+ for (let eh of galbe.errorCb) customError = responseParser(eh(error, context as Context), context as Context)
165
167
  if (customError) return customError
166
168
  if (error instanceof InternalError) {
167
169
  console.log(`Internal Error`, error?.payload || '')
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ServeOptions, TLSServeOptions } from 'bun'
1
+ import type { ServeOptions, TLSOptions, TLSServeOptions } from 'bun'
2
2
  import type {
3
3
  STAny,
4
4
  STArray,
@@ -96,7 +96,9 @@ export type STQuery = Record<string, STQueryValue>
96
96
  */
97
97
  export type GalbeConfig = {
98
98
  port?: number
99
+ hostname?: string
99
100
  basePath?: string
101
+ tls?: TLSOptions
100
102
  server?: Exclude<ServeOptions, 'port'> | TLSServeOptions
101
103
  routes?: boolean | string | string[]
102
104
  router?: { cacheEnabled: boolean }
@@ -306,8 +308,8 @@ export class InternalError extends RequestError {
306
308
  export type GalbePlugin = {
307
309
  name: string
308
310
  init?: (config: any, galbe: Galbe) => MaybePromise<void>
309
- onFetch?: (context: Context) => MaybePromise<Response | void>
310
- onRoute?: (context: Context) => MaybePromise<Response | void>
311
+ onFetch?: (context: Pick<Context, 'request' | 'set' | 'state'>) => MaybePromise<Response | void>
312
+ onRoute?: (context: Pick<Context, 'request' | 'set' | 'state' | 'route'>) => MaybePromise<Response | void>
311
313
  beforeHandle?: (context: Context) => MaybePromise<Response | void>
312
314
  afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
313
315
  cli?: (commands: GalbeCLICommand[]) => MaybePromise<GalbeCLICommand[] | void>
package/src/util.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Route, RouteNode } from '.'
2
- import type { RouteMeta } from './routes'
1
+ import type { Method, Route, RouteNode } from '.'
2
+ import type { RouteFileMeta, RouteMeta } from './routes'
3
3
 
4
4
  const METHOD_COLOR: Record<string, string> = {
5
5
  get: '\x1b[32m',
@@ -10,6 +10,7 @@ const METHOD_COLOR: Record<string, string> = {
10
10
  options: '',
11
11
  head: ''
12
12
  }
13
+ const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
13
14
 
14
15
  export const logRoute = (
15
16
  r: { method: string; path: string },
@@ -17,19 +18,38 @@ export const logRoute = (
17
18
  format?: { maxPathLength?: number }
18
19
  ) => {
19
20
  let color = METHOD_COLOR?.[r.method] || ''
20
- console.log(
21
- ` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path
22
- .padEnd(format?.maxPathLength ?? r.path.length, ' ')
23
- .replaceAll(/:([^\/]+)/g, '\x1b[0;33m:$1\x1b[0m')}${meta?.head ? ` ${meta.head}` : ''}`
24
- )
21
+ let routeLog = `[${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path
22
+ .padEnd(format?.maxPathLength ?? r.path.length, ' ')
23
+ .replaceAll(/:([^\/]+)/g, '\x1b[0;33m:$1\x1b[0m')}${meta?.head ? ` ${meta.head}` : ''}\x1b[0m`
24
+ if (meta?.deprecated) routeLog = `\x1b[0;9m\x1b[38;5;244m${routeLog.replaceAll(ansiRegex, '')}\x1b[0m`
25
+ console.log(` ${routeLog}`)
25
26
  }
26
27
 
28
+ /**
29
+ * Walk over the routes tree. A callback is called for each route with Route infos.
30
+ */
27
31
  export const walkRoutes = (node: RouteNode, cb: (route: Route) => void) => {
28
32
  if (node?.routes) Object.values(node.routes).forEach(r => cb(r))
29
33
  for (let c of Object.values(node?.children || {})) walkRoutes(c, cb)
30
34
  if (node?.param) walkRoutes(node.param, cb)
31
35
  }
32
36
 
37
+ /**
38
+ * Walk over the routes metadata tree. A callback is called for each route metadata with RouteMeta infos.
39
+ */
40
+ export const walkMetaRoutes = (
41
+ meta: RouteFileMeta[],
42
+ cb: (method: Method, path: string, routeMeta: RouteMeta) => void
43
+ ) => {
44
+ for (const f of meta) {
45
+ for (const [path, methods] of Object.entries(f.routes)) {
46
+ for (const [method, meta] of Object.entries(methods)) {
47
+ cb(method as Method, path, meta)
48
+ }
49
+ }
50
+ }
51
+ }
52
+
33
53
  export const isIterator = (obj: any) => typeof obj?.next === 'function'
34
54
 
35
55
  export const HttpStatus = {