galbe 0.1.6 → 0.1.7

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/bin/cli.ts CHANGED
@@ -5,7 +5,7 @@ import type { RouteMeta } from '../src/routes'
5
5
  import { program } from 'commander'
6
6
  import { relative, resolve } from 'path'
7
7
  import { mkdir, readdir, lstat, rm } from 'fs/promises'
8
- import { metaAnalysis } from '../src/routes'
8
+ import { DEFAULT_ROUTE_PATTERN, metaAnalysis } from '../src/routes'
9
9
  import { randomUUID } from 'crypto'
10
10
  import { Galbe } from '../src'
11
11
 
@@ -14,7 +14,8 @@ const BUILD_ID = randomUUID()
14
14
 
15
15
  Bun.env.FORCE_COLOR = '1'
16
16
 
17
- const parseRoutes = async (routes?: string | string[]): Promise<{ path: string; meta: RouteMeta }[]> => {
17
+ const parseRoutes = async (routes?: boolean | string | string[]): Promise<{ path: string; meta: RouteMeta }[]> => {
18
+ routes = routes === true ? DEFAULT_ROUTE_PATTERN : routes
18
19
  if (!routes) return []
19
20
  let files: { path: string; meta: RouteMeta }[] = []
20
21
  if (typeof routes === 'string') {
@@ -61,11 +62,12 @@ program.name('galbe').description('CLI to execute galbe utilities').version('0.1
61
62
 
62
63
  program
63
64
  .command('dev')
64
- .description('Run a dev server running your galbe API')
65
+ .description('Start a dev server running your Galbe application')
65
66
  .argument('<string>', 'filename')
66
67
  .option('-p, --port <number>', 'port number', '')
68
+ .option('-w, --watch', 'watch file changes', 'true')
67
69
  .action(async (fileName, props) => {
68
- const { port } = props
70
+ const { port, watch } = props
69
71
  const devRoot = resolve(ROOT, '.galbe', 'dev')
70
72
  await mkdir(devRoot, { recursive: true })
71
73
  await Bun.write(
@@ -76,15 +78,15 @@ program
76
78
  await rm(resolve(ROOT, '.galbe', 'dev'), { recursive: true })
77
79
  })
78
80
 
79
- await $`BUN_ENV=development bun run --watch ${resolve(devRoot, 'index.ts')}`.cwd(ROOT)
81
+ await $`BUN_ENV=development bun run ${watch ? '--watch' : ''} ${resolve(devRoot, 'index.ts')}`.cwd(ROOT)
80
82
  })
81
83
 
82
84
  program
83
85
  .command('build')
84
- .description('Build your galbe API')
86
+ .description('undle your Galbe application')
85
87
  .argument('<string>', 'filename')
86
- .option('-o, --out <string>', 'output file', '')
87
- .option('-c, --compile', 'standalone executable', false)
88
+ .option('-o, --out <string>', 'output file/directory', '')
89
+ .option('-c, --compile', 'create a standalone executable', false)
88
90
  .action(async (fileName, props) => {
89
91
  const { out, compile } = props
90
92
  const g: Galbe = (await import(resolve(ROOT, fileName))).default
@@ -97,7 +99,7 @@ program
97
99
  buildIndex,
98
100
  '--target',
99
101
  'bun',
100
- ...(compile ? ['--compile', '--outfile', out ? out : 'api'] : ['--outdir', out ? out : 'dist'])
102
+ ...(compile ? ['--compile', '--outfile', out ? out : 'app'] : ['--outdir', out ? out : 'dist'])
101
103
  ].filter(c => c)
102
104
  Bun.spawn(cmds, {
103
105
  cwd: ROOT,
package/bun.lockb CHANGED
Binary file
@@ -1,5 +1,223 @@
1
- ### Create a project
1
+ # Getting started
2
2
 
3
- ```shell
4
- bun create pierre-cm/create-galbe
3
+ Galbe is a Javascript web framework to build fast and versatile backend servers with Bun.
4
+
5
+ It was designed with simplicity in mind, allowing you to quickly create and setup a project. In addition, Galbe also offers usefull features, allowing you to focus on your application logic rather than the rest.
6
+
7
+ ## Requirements
8
+
9
+ To start developing your Galbe project, you first need to install [Bun](https://bun.sh).
10
+
11
+ ## Automatic Installation
12
+
13
+ This is the recommended way of setting up a Galbe project.
14
+
15
+ ```bash
16
+ bun create galbe app
17
+ cd app
18
+ bun install
5
19
  ```
20
+
21
+ This will create a new project under `app` directory and install it.
22
+
23
+ Now you can start the dev server by running:
24
+
25
+ ```bash
26
+ bun dev
27
+ ```
28
+
29
+ This will start a web server on `loclahost:3000`.
30
+
31
+ To verify that the project was setup correctly and is running, try to reach `loclahost:3000/hello` endpoint, this should return following greeting message:
32
+
33
+ ```bash
34
+ $ curl localhost:3000/hello
35
+ Hello from Galbe!
36
+ ```
37
+
38
+ > [!TIP]
39
+ > By default, the dev server automatically reloads on every file change.
40
+
41
+ ## Manual Installation
42
+
43
+ Init a new Bun project and add Galbe as dependency:
44
+
45
+ ```bash
46
+ bun init
47
+ bun add galbe
48
+ ```
49
+
50
+ Open `package.json` file and add the following scripts:
51
+
52
+ ```json
53
+ {
54
+ "scripts": {
55
+ "dev": "galbe dev index.ts",
56
+ "build": "galbe build index.ts",
57
+ "test": "bun test"
58
+ }
59
+ }
60
+ ```
61
+
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).
63
+
64
+ This require your `index.ts` to export a default Galbe instance in order to work. As in the following example:
65
+
66
+ ```ts
67
+ import { Galbe } from 'galbe'
68
+
69
+ const g = new Galbe({ port: 3000 })
70
+ g.get('/hello', () => 'Hello Mom!')
71
+
72
+ export default galbe
73
+ ```
74
+
75
+ This is the recommended way to proceed but it is not mandatory. Galbe instances also provide a `listen` method that will allow you to manually start your server instance from the code.
76
+
77
+ > [!WARNING]
78
+ > 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
+
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
+ ## Configuration
115
+
116
+ To configure your Galbe server, you should pass your configuration to the Galbe constructor when you instanciate it.
117
+
118
+ ```ts
119
+ const galbe = new Galbe(configuration)
120
+ ```
121
+
122
+ ### Properties
123
+
124
+ #### port
125
+
126
+ The port number that the server will be listening on. Default is `3000`.
127
+
128
+ #### basePath
129
+
130
+ The base path is added as a prefix to all the routes created.
131
+
132
+ #### routes
133
+
134
+ A Glob Pattern or a list of Glob patterns defining the route files to be analyzed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer). Default is `src/**/*.route.{js,ts}`.
135
+
136
+ #### plugin
137
+
138
+ 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
+
140
+ ### Examples
141
+
142
+ An common way to handle server configuration is to create new file a `galbe.config.(js|ts|json)` at the root of your project directory and import it in your code. Here is an example:
143
+
144
+ galbe.config.js
145
+
146
+ ```js
147
+ export default {
148
+ port: Bun.env.GALBE_PORT
149
+ routes: 'src/**/*.route.ts',
150
+ }
151
+ ```
152
+
153
+ index.js
154
+
155
+ ```js
156
+ import { Galbe } from 'galbe'
157
+ import config from './galbe.config'
158
+
159
+ export default new Galbe(config)
160
+ ```
161
+
162
+ > [!TIP]
163
+ > If you are using Typescript, you can import `GalbeConfig` type from galbe package to ensure type consistency for your configuration. Here is an example:
164
+ >
165
+ > ```ts
166
+ > import type { GalbeConfig } from 'galbe'
167
+ > const config: GalbeConfig = {
168
+ > port: Number(Bun.env.GALBE_PORT),
169
+ > routes: 'routes/*.route.ts'
170
+ > }
171
+ > export default config
172
+ > ```
173
+
174
+ ## Project Structure
175
+
176
+ One key aspect of Galbe, is its versatility in terms of project structure. This is partly allowed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer) and the `routes` config property which defaults to `src/**/*.route.{js,ts}`.
177
+
178
+ Here are two examples of valid project structures by default:
179
+
180
+ **Example 1**
181
+
182
+ ```txt
183
+ ┌── src
184
+ │ ├── hooks
185
+ │ │   └── log.hook.ts
186
+ │ ├── routes
187
+ │ │   ├── foo.route.ts
188
+ │ │   └── foo.route.ts
189
+ │ └── schemas
190
+ │ ├── bar.schema.ts
191
+ │ └── bar.schema.ts
192
+ ├── galbe.config.ts
193
+ ├── index.ts
194
+ ├── package.json
195
+ ├── README.md
196
+ └── tsconfig.json
197
+ ```
198
+
199
+ **Example 2**
200
+
201
+ ```txt
202
+ ┌── src
203
+ │ ├── hooks
204
+ │ │   └── log.hook.ts
205
+ │ ├── foo
206
+ │ │   ├── foo.route.ts
207
+ │ │   └── foo.schema.ts
208
+ │ └── bar
209
+ │ ├── bar.route.ts
210
+ │ └── bar.schema.ts
211
+ ├── galbe.config.ts
212
+ ├── index.ts
213
+ ├── package.json
214
+ ├── README.md
215
+ └── tsconfig.json
216
+ ```
217
+
218
+ In both cases, the [Automatic Route Analyzer](routes.md#automatic-route-analyzer) will analyze `foo.route.ts` and `bar.route.ts` Route Files to find route definitions.
219
+
220
+ You can find more info about Route Files definition under the [Routes](routes.md) section.
221
+
222
+ > [!NOTE]
223
+ > Those are just examples that will work with the default configuration. You can of course redefine `routes` property with your own pattern(s) to fit your own project structure.
package/docs/routes.md ADDED
@@ -0,0 +1,124 @@
1
+ # Routes
2
+
3
+ ## Route Definition
4
+
5
+ Here is how to define routes in Galbe.
6
+
7
+ ```ts
8
+ galbe.[method](path: string, schema?: Schema, hooks?: Hooks[], handler: Handler)
9
+ ```
10
+
11
+ **method** ( get | post | put | delete | patch | options )
12
+
13
+ The [HTTP Request Method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) for the defined route.
14
+
15
+ **path** (string)
16
+
17
+ The path of the route. It should be composed of a sequence of segments separated by `/`. Each segment can be composed alphanumeric characters and dashes, but should not start or end with a dash.
18
+
19
+ There are two special segments:
20
+
21
+ - `:param` Any segment starting with `:` indicates a parameter segment.
22
+ - `*` To indicate a wildcard segment. This will match any segment or sequence of segments.
23
+
24
+ **schema** (Schema) _Optional_
25
+
26
+ See [Schemas](schemas) section.
27
+
28
+ **hooks** (Hook[]) _Optional_
29
+
30
+ See [Hooks](hooks) section.
31
+
32
+ **handler** (Handler)
33
+
34
+ See [Handler](handler) section.
35
+
36
+ ### Examples
37
+
38
+ **Basic route**
39
+
40
+ ```js
41
+ galbe.get('/foo', ctx => 'Hello World!')
42
+ ```
43
+
44
+ **Route with Schema**
45
+
46
+ <!-- prettier-ignore -->
47
+ ```js
48
+ galbe.get(
49
+ '/foo/:bar',
50
+ { params: { bar: $T.string() } },
51
+ ctx => `Hello ${ctx.params.bar} !`
52
+ )
53
+ ```
54
+
55
+ **Route with Hooks**
56
+
57
+ <!-- prettier-ignore -->
58
+ ```js
59
+ galbe.get(
60
+ '/foo/:bar',
61
+ [() => console.log('Hook1'), () => console.log('Hook2')],
62
+ ctx => `Hello ${ctx.params.bar} !`
63
+ )
64
+ ```
65
+
66
+ **Route with Schemas and Hooks**
67
+
68
+ <!-- prettier-ignore -->
69
+ ```js
70
+ galbe.get(
71
+ '/foo/:bar',
72
+ { params: { bar: $T.string() } },
73
+ [() => console.log('Hook')],
74
+ ctx => `Hello ${ctx.params.bar} !`
75
+ )
76
+ ```
77
+
78
+ ## Automatic Route Analyzer
79
+
80
+ > [!NOTE]
81
+ > This feature is only available if you run/build the app via the [Galbe CLI](), which is the case by default if you created your app following the [Automatic Installation]() step or properly configured your package.json to do so.
82
+
83
+ The Automatic Route Analyzer is in charge of analyzing all the Route Files of your project and set up the routes defintions to your Glabe server automatically.
84
+
85
+ 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.
86
+
87
+ ### Route Files
88
+
89
+ In order to be properly analyzed, Route Files must export a default function that takes a Galbe instance as unique argument. Your routes should be defined using that Galbe instance. Here a basic js example:
90
+
91
+ ```ts
92
+ export default g => {
93
+ g.get('/foo/:bar', ctx => ctx.params.bar)
94
+ }
95
+ ```
96
+
97
+ The same example using 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
+ The Automatic Route Analyzer is also capable of collecting metadata about your Routefile and your routes by analyzing multiline comments. This can be used by some plugins to perform specific tasks. Here is an example of Routefile with multiline comments metadata.
107
+
108
+ ```js
109
+ /**
110
+ * This is the header's head comment
111
+ * @annotation example of header's annotation
112
+ */
113
+ export default g => {
114
+ /**
115
+ * This is a route head comment
116
+ * @deprecated
117
+ * @tag tag1
118
+ * @tag tag2
119
+ */
120
+ g.get('/foo/:bar', ctx => ctx.params.bar)
121
+ }
122
+ ```
123
+
124
+ You will find more information about comment's metadata and how to use them along with examples in a plugin in the [Plugin](plugins) section.
@@ -0,0 +1,244 @@
1
+ # Shemas
2
+
3
+ Galbe offers a custom Schema Type processor that provides type safety along with data parsing and validation.
4
+
5
+ The prime intention of that features is to offer an easy way to manage automatically request inputs validation and error handling. Moreover, it also greatly improve developper's experience by infering static Typescript types from schema definitions.
6
+
7
+ ## Schema Types
8
+
9
+ To get started with Schema defintion, just import `$T` from `galbe` library:
10
+
11
+ ```js
12
+ import { $T } from 'galbe'
13
+ ```
14
+
15
+ Here the list of available Schema types in Galbe:
16
+
17
+ #### Boolean
18
+
19
+ Schema Type matching `boolean` values.
20
+
21
+ ```ts
22
+ const boolSchema = $T.boolean()
23
+ ```
24
+
25
+ #### String
26
+
27
+ Schema Type matching `string` vlues.
28
+
29
+ ```ts
30
+ const strSchema = $T.string(options)
31
+ ```
32
+
33
+ #### Number
34
+
35
+ Schema Type matching `number` values.
36
+
37
+ ```ts
38
+ const numSchema = $T.number(options)
39
+ ```
40
+
41
+ #### Integer
42
+
43
+ Schema Type matching integer `number` values.
44
+
45
+ ```ts
46
+ const intSchema = $T.integer(options)
47
+ ```
48
+
49
+ #### Any
50
+
51
+ Schema Type matching `any` of the previous Schema Types.
52
+
53
+ ```ts
54
+ const anySchema = $T.any()
55
+ ```
56
+
57
+ #### Array
58
+
59
+ Schema Type matching `array` values.
60
+
61
+ ```ts
62
+ const arraySchema = $T.array($T.any(), options)
63
+ ```
64
+
65
+ #### Union
66
+
67
+ Creates an union of Schema Types. .
68
+
69
+ ```ts
70
+ const unionSchema = $T.union([$T.string(), $T.number()])
71
+ ```
72
+
73
+ #### TOptional
74
+
75
+ Makes any type optional. In practice, this allows for `undefined` values.
76
+
77
+ ```ts
78
+ const optionalSchema = $T.optional($T.string())
79
+ ```
80
+
81
+ ## Request Schema definition
82
+
83
+ The Request Schema definition allows you to define a schema for your request on your [Route Definition](). It must be defined right after the [path]() of your route.
84
+
85
+ ```js
86
+ const schema = {}
87
+ galbe.get('/foo/:bar', schema, ctx => {})
88
+ ```
89
+
90
+ The Request Schema has 4 optional properties
91
+
92
+ ### headers
93
+
94
+ ```ts
95
+ headers: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
96
+ ```
97
+
98
+ This is a key-value object where each key represents a request `header` name and the value the Schema associated.
99
+
100
+ **Example**:
101
+
102
+ ```ts
103
+ const schema = {
104
+ headers: {
105
+ 'User-Agent': $T.optional($T.string({ pattern: '^Bun' }))
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### params
111
+
112
+ ```ts
113
+ params: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
114
+ ```
115
+
116
+ This is a key-value object where each key represents a request `path parameter` name and the value the Schema associated.
117
+
118
+ **Example**:
119
+
120
+ ```ts
121
+ const schema = {
122
+ params: {
123
+ name: $T.string(),
124
+ age: $T.integer({ min: 0 })
125
+ }
126
+ }
127
+ ```
128
+
129
+ > [!WARNING]
130
+ > Every key should match an existing [route path]() parameter. Otherwise Typescript will show you an error.
131
+ >
132
+ > By default, if no schema is defined for a given parameter. Galbe will assume it is of type `string`.
133
+
134
+ ### query
135
+
136
+ ```ts
137
+ query: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
138
+ ```
139
+
140
+ This is a key-value object where each key represents a request `query parameter` name and the value the Schema associated.
141
+
142
+ **Example**:
143
+
144
+ ```ts
145
+ const schema = {
146
+ query: {
147
+ name: $T.literal('Galbe'),
148
+ list: $T.array($T.number())
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### body
154
+
155
+ ```ts
156
+ body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STMulripartForm | STUrlForm
157
+ ```
158
+
159
+ #### Json
160
+
161
+ To define an `application/json` request body. You must use `STObject` Schema Type. Example:
162
+
163
+ ```ts
164
+ const jsonBody = $T.object({
165
+ name: $T.string(),
166
+ age: $T.integer({ min: 0 })
167
+ })
168
+ ```
169
+
170
+ #### Multipart
171
+
172
+ To define a `multipart/form-data` request body. You must use `TMultipartForm` Schema Type. Example:
173
+
174
+ ```ts
175
+ const multipartBody = $T.multipartForm({
176
+ name: $T.string(),
177
+ age: $T.integer({ minimum: 0 })
178
+ })
179
+ ```
180
+
181
+ #### Url Form
182
+
183
+ To define an `application/x-www-form-urlencoded` request body. You must use `TUrlForm` Schema Type. Example:
184
+
185
+ ```ts
186
+ const urlBody = $T.urlForm({
187
+ name: $T.string(),
188
+ age: $T.integer({ minimum: 0 })
189
+ })
190
+ ```
191
+
192
+ #### Stream
193
+
194
+ Some body request types can be streamed by using `STStream` Schema Type wrapper. Streamable Schema Types are `STByteArray`, `STString`, `STUrlForm` and `STMultipartForm`.
195
+
196
+ This can be usefull to imporve performances in case you have heavy body payloads and you want to perform early validations on the body.
197
+
198
+ Let's see a concrete example where that could be usefull. Imagine you want a `multipart/form-data` body request that has two properties `username` and `heavyImageFile`. In the normal case you would define something like that:
199
+
200
+ ```ts
201
+ galbe.post(
202
+ 'user/create',
203
+ {
204
+ body: $T.multipartForm({
205
+ username: $T.string(),
206
+ heavyImageFile: $T.byteArray()
207
+ })
208
+ },
209
+ ctx => {
210
+ // At that point, the full body request has been processed
211
+ if(!isValid(ctx.body.username))
212
+ throw new RequestError({ status: 400 })
213
+ else ctx.set.status = 201
214
+ }
215
+ })
216
+ ```
217
+
218
+ This means that in the case where the username wouldn't pass the validation, the full request body including the `heavyImageFile` would have been processed for nothing as it is not used. Inducing unnecessary time and resource consumption.
219
+
220
+ The `STStream` Schema Type wrapper was created to remediate to that issue. In practice it allows you to perform validations on the fly.
221
+
222
+ Now in your handler, instead of receiving an object as ctx.body, you'll receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
223
+
224
+ ```ts
225
+ galbe.post(
226
+ 'user/create',
227
+ {
228
+ body: $T.stream($T.multipartForm({
229
+ username: $T.string(),
230
+ heavyImageFile: $T.byteArray()
231
+ }))
232
+ },
233
+ ctx => {
234
+ // At that point, the body has not been processed yet.
235
+ for await (const [key, value] of ctx.body) {
236
+ if (key === "username" && !isValid(value)) {
237
+ // Returns an early response before heavyImageFile is processed
238
+ throw new RequestError({ status: 400 })
239
+ }
240
+ }
241
+ ctx.set.status = 201
242
+ }
243
+ })
244
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
@@ -41,7 +41,6 @@
41
41
  "typescript": "^5.0.0"
42
42
  },
43
43
  "dependencies": {
44
- "@sinclair/typebox": "^0.31.28",
45
44
  "@swc/core": "^1.3.107",
46
45
  "@swc/wasm": "^1.4.0",
47
46
  "acorn": "^8.11.2",