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 +4 -1
- package/bin/commands/build.ts +3 -0
- package/bin/commands/dev.ts +11 -3
- package/bin/commands/generate/client.ts +16 -8
- package/bin/res/cli.template.js +1 -1
- package/bin/res/client.template.ts +6 -3
- package/bin/util.ts +20 -1
- package/docs/cli.md +336 -0
- package/docs/getting-started.md +61 -46
- package/docs/plugins.md +40 -45
- package/docs/routes.md +1 -12
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +4 -1
- package/src/index.ts +18 -7
- package/src/routes.ts +3 -3
- package/src/server.ts +14 -12
- package/src/types.ts +5 -3
- package/src/util.ts +27 -7
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
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
|
[](https://github.com/pierre-cm/galbe/actions/workflows/build_test.yml)
|
|
4
7
|
[](https://github.com/pierre-cm/galbe/blob/main/LICENSE)
|
package/bin/commands/build.ts
CHANGED
|
@@ -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 {
|
package/bin/commands/dev.ts
CHANGED
|
@@ -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]]
|
|
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((
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 })
|
package/bin/res/cli.template.js
CHANGED
|
@@ -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.
|
|
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[
|
|
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
|
+
```
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
37
|
+
done
|
|
30
38
|
|
|
31
|
-
|
|
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
|
|
45
|
+
$ curl localhost:3000/hello/John?age=32
|
|
46
|
+
Hello John! You're 32 y.o.
|
|
36
47
|
```
|
|
37
48
|
|
|
38
49
|
> [!TIP]
|
|
39
|
-
>
|
|
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
|
|
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
|
|
70
|
-
|
|
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.
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
106
|
-
// index.ts
|
|
103
|
+
index.ts
|
|
107
104
|
|
|
105
|
+
```ts
|
|
108
106
|
import { Galbe } from 'galbe'
|
|
109
|
-
import
|
|
110
|
-
import myPlugin from './myPlugin'
|
|
107
|
+
import deprecatedPlugin from './deprecated.plugin'
|
|
111
108
|
|
|
112
|
-
const galbe = new Galbe(
|
|
113
|
-
galbe.use(
|
|
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
|
@@ -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 = [
|
|
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
|
-
|
|
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
|
|
159
|
-
console.log(`\x1b[
|
|
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.
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 = {
|