galbe 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build_test.yml +2 -4
- package/.github/workflows/release.yml +2 -2
- package/bin/cli.ts +8 -104
- package/bin/commands/build.ts +95 -0
- package/bin/commands/dev.ts +53 -0
- package/bin/commands/generate/client.ts +192 -0
- package/bin/commands/generate/code/openapi.parser.ts +479 -0
- package/bin/commands/generate/code.ts +75 -0
- package/bin/commands/generate/index.ts +12 -0
- package/bin/commands/generate/spec.ts +94 -0
- package/bin/res/cli.template.js +120 -0
- package/bin/res/client.template.ts +161 -0
- package/bin/util.ts +164 -0
- package/bun.lockb +0 -0
- package/docs/getting-started.md +8 -0
- package/docs/hooks.md +4 -4
- package/docs/plugins.md +3 -9
- package/docs/routes.md +3 -3
- package/docs/schemas.md +19 -2
- package/package.json +10 -8
- package/scripts/postinstall.ts +1 -3
- package/src/extras/spec/openapi.serializer.ts +264 -0
- package/src/extras.ts +1 -0
- package/src/index.ts +122 -80
- package/src/parser.ts +29 -8
- package/src/router.ts +27 -28
- package/src/routes.ts +142 -40
- package/src/schema.ts +79 -8
- package/src/server.ts +55 -35
- package/src/types.ts +109 -53
- package/src/util.ts +85 -3
- package/src/validator.ts +27 -5
- package/test/parser.test.ts +34 -0
- package/test/requests.test.ts +5 -12
- package/test/resources/test.route.comment.ts +20 -0
- package/test/responses.test.ts +386 -13
- package/test/routeFiles.test.ts +44 -27
- package/test/router.test.ts +67 -42
- package/scripts/build.ts +0 -14
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaybeArray, STBody, Context } from './index'
|
|
1
|
+
import type { MaybeArray, STBody, Context, STResponse } from './index'
|
|
2
2
|
import type {
|
|
3
3
|
STStream,
|
|
4
4
|
STUrlForm,
|
|
@@ -17,6 +17,7 @@ import { readableStreamToArrayBuffer } from 'bun'
|
|
|
17
17
|
import { Kind, Optional, Stream } from './schema'
|
|
18
18
|
import { validate } from './validator'
|
|
19
19
|
import { InternalError, RequestError } from './index'
|
|
20
|
+
import { isIterator } from './util'
|
|
20
21
|
|
|
21
22
|
const textDecoder = new TextDecoder()
|
|
22
23
|
const textEncoder = new TextEncoder()
|
|
@@ -282,6 +283,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
|
|
|
282
283
|
}
|
|
283
284
|
async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: STMultipartForm) {
|
|
284
285
|
const bound = textEncoder.encode(boundary)
|
|
286
|
+
const delimiter = textEncoder.encode('\r\n\r\n')
|
|
285
287
|
let rest = new Uint8Array()
|
|
286
288
|
let bK: Uint8Array = new Uint8Array()
|
|
287
289
|
let bV: Uint8Array = new Uint8Array()
|
|
@@ -293,6 +295,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
293
295
|
start = 0
|
|
294
296
|
for (let i = 0; i < chunk.length; i++) {
|
|
295
297
|
let matchBound = true
|
|
298
|
+
let matchDelimiter = true
|
|
296
299
|
for (let b = 0; b < bound.length; b++) {
|
|
297
300
|
if (chunk[i + b] === bound[b]) continue
|
|
298
301
|
else {
|
|
@@ -300,6 +303,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
300
303
|
break
|
|
301
304
|
}
|
|
302
305
|
}
|
|
306
|
+
if (!matchBound) {
|
|
307
|
+
for (let b = 0; b < delimiter.length; b++) {
|
|
308
|
+
if (chunk[i + b] === delimiter[b]) continue
|
|
309
|
+
else {
|
|
310
|
+
matchDelimiter = false
|
|
311
|
+
break
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
303
315
|
if (matchBound) {
|
|
304
316
|
bV = new Uint8Array(rest.length + i - start)
|
|
305
317
|
bV.set(rest)
|
|
@@ -323,7 +335,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
323
335
|
start = i + bound.length
|
|
324
336
|
rest = new Uint8Array()
|
|
325
337
|
i = start
|
|
326
|
-
} else if (
|
|
338
|
+
} else if (matchDelimiter) {
|
|
327
339
|
bK = new Uint8Array(rest.length + i - start)
|
|
328
340
|
bK.set(rest)
|
|
329
341
|
bK.set(chunk.slice(start, i), rest.length)
|
|
@@ -594,7 +606,7 @@ export const requestPathParser = (input: string, path: string) => {
|
|
|
594
606
|
}
|
|
595
607
|
|
|
596
608
|
export const parseEntry = <T extends STProps>(
|
|
597
|
-
params: { [key: string]:
|
|
609
|
+
params: { [key: string]: any },
|
|
598
610
|
schema: T,
|
|
599
611
|
options?: { name?: string; i?: boolean }
|
|
600
612
|
): Static<STObject<T>> => {
|
|
@@ -629,16 +641,24 @@ export const parseEntry = <T extends STProps>(
|
|
|
629
641
|
return parsedParams as Static<STObject<T>>
|
|
630
642
|
}
|
|
631
643
|
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
export const responseParser = (response: any, ctx: Context) => {
|
|
644
|
+
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
635
645
|
const details = {
|
|
636
646
|
status: ctx.set.status || 200,
|
|
637
647
|
headers: new Headers(ctx.set.headers)
|
|
638
648
|
}
|
|
639
649
|
if (response instanceof Response) return response
|
|
640
650
|
else if (typeof response === 'string') {
|
|
641
|
-
if (!details?.headers?.has('content-type'))
|
|
651
|
+
if (!details?.headers?.has('content-type')) {
|
|
652
|
+
if (schema?.[details.status][Kind] === 'json') {
|
|
653
|
+
details?.headers?.set('content-type', 'application/json')
|
|
654
|
+
response = `"${response}"`
|
|
655
|
+
} else details?.headers?.set('content-type', 'text/plain')
|
|
656
|
+
}
|
|
657
|
+
return new Response(response, details)
|
|
658
|
+
} else if (response instanceof Uint8Array) {
|
|
659
|
+
if (!details?.headers?.has('content-type')) {
|
|
660
|
+
details?.headers?.set('content-type', 'application/octet-stream')
|
|
661
|
+
}
|
|
642
662
|
return new Response(response, details)
|
|
643
663
|
}
|
|
644
664
|
if (response instanceof ReadableStream) {
|
|
@@ -667,7 +687,8 @@ export const responseParser = (response: any, ctx: Context) => {
|
|
|
667
687
|
} else {
|
|
668
688
|
try {
|
|
669
689
|
if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
|
|
670
|
-
|
|
690
|
+
if (details.headers.get('content-type') === 'application/json') response = JSON.stringify(response)
|
|
691
|
+
return new Response(response, details)
|
|
671
692
|
} catch (error) {
|
|
672
693
|
console.error(error)
|
|
673
694
|
throw new InternalError()
|
package/src/router.ts
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
|
-
import type { Route, RouteNode
|
|
2
|
-
import { NotFoundError } from './types'
|
|
1
|
+
import type { Method, Route, RouteNode } from './types'
|
|
2
|
+
import { MethodNotAllowedError, NotFoundError } from './types'
|
|
3
3
|
|
|
4
4
|
const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const walk = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
|
|
7
7
|
if (path.length < 1) throw new NotFoundError()
|
|
8
|
-
if (path.length === 1 && node.
|
|
8
|
+
if (path.length === 1 && node.routes && !!Object.keys(node.routes).length) return node
|
|
9
9
|
|
|
10
10
|
if (node.children?.['*']) alts.push(node.children['*'])
|
|
11
11
|
if (node.param) alts.push(node.param)
|
|
12
12
|
|
|
13
13
|
if (node.children && path[1] in node.children) {
|
|
14
14
|
path.shift()
|
|
15
|
-
return
|
|
15
|
+
return walk(path, node.children[path[0]], alts)
|
|
16
16
|
}
|
|
17
17
|
if (node.param) {
|
|
18
18
|
path.shift()
|
|
19
19
|
alts.pop()
|
|
20
|
-
|
|
21
|
-
return walkRoutes(path, node.param, alts)
|
|
22
|
-
} catch (error) {
|
|
23
|
-
if (error instanceof NotFoundError) console.log(error)
|
|
24
|
-
else throw error
|
|
25
|
-
}
|
|
20
|
+
return walk(path, node.param, alts)
|
|
26
21
|
}
|
|
27
22
|
if (alts.length > 1) {
|
|
28
|
-
return
|
|
23
|
+
return walk(path, alts.pop() as RouteNode, alts)
|
|
29
24
|
}
|
|
30
25
|
if (alts.length === 1) {
|
|
31
26
|
let lastAlt = alts.pop() as RouteNode
|
|
32
27
|
try {
|
|
33
|
-
return
|
|
28
|
+
return walk(path, lastAlt, alts)
|
|
34
29
|
} catch (error) {
|
|
35
30
|
if (error instanceof NotFoundError) {
|
|
36
|
-
if (lastAlt?.
|
|
31
|
+
if (lastAlt?.routes) return lastAlt
|
|
37
32
|
} else throw error
|
|
38
33
|
}
|
|
39
34
|
}
|
|
@@ -42,12 +37,12 @@ const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): Ro
|
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
export class GalbeRouter {
|
|
45
|
-
routes:
|
|
40
|
+
routes: RouteNode
|
|
46
41
|
prefix: string
|
|
47
42
|
cacheEnabled: boolean
|
|
48
43
|
cachedRoutes: Map<string, Route | null>
|
|
49
44
|
constructor(options?: { prefix?: string; cacheEnabled?: boolean }) {
|
|
50
|
-
this.routes = {
|
|
45
|
+
this.routes = { routes: {} }
|
|
51
46
|
let prefix = options?.prefix || ''
|
|
52
47
|
if (prefix && !prefix.match(/^\//)) prefix = `/${prefix}`
|
|
53
48
|
this.prefix = prefix
|
|
@@ -62,42 +57,46 @@ export class GalbeRouter {
|
|
|
62
57
|
route.path = `${this.prefix || ''}${route.path}`
|
|
63
58
|
let path = route.path.replace(/^\/$(.*)\/?$/, '$1').split('/')
|
|
64
59
|
path.shift()
|
|
65
|
-
let r = this.routes
|
|
60
|
+
let r = this.routes
|
|
66
61
|
if (!path.length) {
|
|
67
|
-
r.route = route
|
|
62
|
+
r.routes[route.method] = route
|
|
68
63
|
} else {
|
|
69
64
|
while (path.length) {
|
|
70
65
|
let p = path.shift()
|
|
71
66
|
if (p === undefined) break
|
|
72
67
|
if (!path.length) {
|
|
73
|
-
if (p.match(/^:/))
|
|
74
|
-
|
|
68
|
+
if (p.match(/^:/)) {
|
|
69
|
+
if (!r.param) r.param = { routes: {} }
|
|
70
|
+
r.param.routes[route.method] = route
|
|
71
|
+
} else {
|
|
75
72
|
if (!r.children) r.children = {}
|
|
76
|
-
r.children[p] = {
|
|
73
|
+
if (!(p in r.children)) r.children[p] = { routes: {} }
|
|
74
|
+
r.children[p].routes[route.method] = route
|
|
77
75
|
}
|
|
78
76
|
} else {
|
|
79
77
|
if (p.match(/^:/)) {
|
|
80
|
-
if (!r.param) r.param = {}
|
|
78
|
+
if (!r.param) r.param = { routes: {} }
|
|
81
79
|
r = r.param
|
|
82
80
|
} else {
|
|
83
81
|
if (!r.children) r.children = {}
|
|
84
|
-
if (!(p in r.children)) r.children[p] = {}
|
|
82
|
+
if (!(p in r.children)) r.children[p] = { routes: {} }
|
|
85
83
|
r = r.children[p]
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
|
-
find(method:
|
|
89
|
+
find(method: Method, path: string): Route {
|
|
92
90
|
const staticRoute = this.cachedRoutes.get(`[${method}]${path}`)
|
|
93
91
|
if (staticRoute === null) throw new NotFoundError()
|
|
94
92
|
if (staticRoute !== undefined) return staticRoute
|
|
95
|
-
let parts = path.split('/')
|
|
96
|
-
|
|
97
|
-
if (!
|
|
93
|
+
let parts = path === '/' ? [''] : path.split('/')
|
|
94
|
+
let r = walk(parts, this.routes)
|
|
95
|
+
if (!r || !Object.keys(r.routes).length) {
|
|
98
96
|
if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, null)
|
|
99
97
|
throw new NotFoundError()
|
|
100
|
-
}
|
|
98
|
+
} else if (!(method in r.routes)) throw new MethodNotAllowedError()
|
|
99
|
+
const route = r.routes[method] as Route
|
|
101
100
|
if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, route)
|
|
102
101
|
return route
|
|
103
102
|
}
|
package/src/routes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GalbeConfig } from './types'
|
|
1
|
+
import type { GalbeConfig, Route } from './types'
|
|
2
2
|
|
|
3
3
|
import { readdir, lstat } from 'fs/promises'
|
|
4
4
|
import { extname } from 'path'
|
|
@@ -10,14 +10,116 @@ import { Glob } from 'bun'
|
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_ROUTE_PATTERN = 'src/**/*.route.{js,ts}'
|
|
12
12
|
|
|
13
|
-
export type RouteMeta = {
|
|
13
|
+
export type RouteMeta = { head?: string } & Record<string, boolean | string | string[]>
|
|
14
|
+
export type RoutesMeta = {
|
|
14
15
|
header: Record<string, boolean | string | string[]>
|
|
15
|
-
routes: Record<string, Record<string,
|
|
16
|
+
routes: Record<string, Record<string, RouteMeta>>
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
export type RouteInstanciationCallback = <T extends 'add' | 'error'>(event: {
|
|
19
|
+
type: T
|
|
20
|
+
error?: T extends 'error' ? any : undefined
|
|
21
|
+
route: T extends 'add' ? Route : undefined
|
|
22
|
+
filepath?: string
|
|
23
|
+
meta: T extends 'add' ? RouteMeta : undefined
|
|
24
|
+
}) => any | Promise<any>
|
|
18
25
|
export type RouteFileMeta = {
|
|
19
26
|
file: string
|
|
20
|
-
} &
|
|
27
|
+
} & RoutesMeta
|
|
28
|
+
|
|
29
|
+
class GalbeProxy {
|
|
30
|
+
#g: Galbe
|
|
31
|
+
#cb?: RouteInstanciationCallback
|
|
32
|
+
filepath?: string
|
|
33
|
+
meta?: RoutesMeta
|
|
34
|
+
constructor(g: Galbe, cb?: RouteInstanciationCallback) {
|
|
35
|
+
this.#g = g
|
|
36
|
+
this.#cb = cb
|
|
37
|
+
}
|
|
38
|
+
async get(...args: any[]) {
|
|
39
|
+
//@ts-ignore
|
|
40
|
+
const route = this.#g.get(...args) as Route
|
|
41
|
+
if (this.#cb)
|
|
42
|
+
await this.#cb({
|
|
43
|
+
type: 'add',
|
|
44
|
+
route,
|
|
45
|
+
filepath: this.filepath || '',
|
|
46
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
47
|
+
})
|
|
48
|
+
return route
|
|
49
|
+
}
|
|
50
|
+
async post(...args: any[]) {
|
|
51
|
+
//@ts-ignore
|
|
52
|
+
const route = this.#g.post(...args) as Route
|
|
53
|
+
if (this.#cb)
|
|
54
|
+
await this.#cb({
|
|
55
|
+
type: 'add',
|
|
56
|
+
route,
|
|
57
|
+
filepath: this.filepath,
|
|
58
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
59
|
+
})
|
|
60
|
+
return route
|
|
61
|
+
}
|
|
62
|
+
async put(...args: any[]) {
|
|
63
|
+
//@ts-ignore
|
|
64
|
+
const route = this.#g.put(...args) as Route
|
|
65
|
+
if (this.#cb)
|
|
66
|
+
await this.#cb({
|
|
67
|
+
type: 'add',
|
|
68
|
+
route,
|
|
69
|
+
filepath: this.filepath,
|
|
70
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
71
|
+
})
|
|
72
|
+
return route
|
|
73
|
+
}
|
|
74
|
+
async patch(...args: any[]) {
|
|
75
|
+
//@ts-ignore
|
|
76
|
+
const route = this.#g.patch(...args) as Route
|
|
77
|
+
if (this.#cb)
|
|
78
|
+
await this.#cb({
|
|
79
|
+
type: 'add',
|
|
80
|
+
route,
|
|
81
|
+
filepath: this.filepath,
|
|
82
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
83
|
+
})
|
|
84
|
+
return route
|
|
85
|
+
}
|
|
86
|
+
async delete(...args: any[]) {
|
|
87
|
+
//@ts-ignore
|
|
88
|
+
const route = this.#g.delete(...args) as Route
|
|
89
|
+
if (this.#cb)
|
|
90
|
+
await this.#cb({
|
|
91
|
+
type: 'add',
|
|
92
|
+
route,
|
|
93
|
+
filepath: this.filepath,
|
|
94
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
95
|
+
})
|
|
96
|
+
return route
|
|
97
|
+
}
|
|
98
|
+
async options(...args: any[]) {
|
|
99
|
+
//@ts-ignore
|
|
100
|
+
const route = this.#g.options(...args) as Route
|
|
101
|
+
if (this.#cb)
|
|
102
|
+
await this.#cb({
|
|
103
|
+
type: 'add',
|
|
104
|
+
route,
|
|
105
|
+
filepath: this.filepath,
|
|
106
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
107
|
+
})
|
|
108
|
+
return route
|
|
109
|
+
}
|
|
110
|
+
async head(...args: any[]) {
|
|
111
|
+
//@ts-ignore
|
|
112
|
+
const route = this.#g.head(...args) as Route
|
|
113
|
+
if (this.#cb)
|
|
114
|
+
await this.#cb({
|
|
115
|
+
type: 'add',
|
|
116
|
+
route,
|
|
117
|
+
filepath: this.filepath,
|
|
118
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
119
|
+
})
|
|
120
|
+
return route
|
|
121
|
+
}
|
|
122
|
+
}
|
|
21
123
|
|
|
22
124
|
const parseComment = (comment: string): Record<string, string | string[]> => {
|
|
23
125
|
const head =
|
|
@@ -40,11 +142,11 @@ const parseComment = (comment: string): Record<string, string | string[]> => {
|
|
|
40
142
|
}
|
|
41
143
|
return refs
|
|
42
144
|
}
|
|
43
|
-
export const metaAnalysis = async (filePath: string): Promise<
|
|
145
|
+
export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
44
146
|
const file = Bun.file(filePath)
|
|
45
147
|
const fileExt = extname(filePath)
|
|
46
148
|
let content = await file.text()
|
|
47
|
-
let meta:
|
|
149
|
+
let meta: RoutesMeta = { header: {}, routes: {} }
|
|
48
150
|
|
|
49
151
|
if (fileExt === '.ts') {
|
|
50
152
|
//// Much faster but doesn't include comments. See https://github.com/oven-sh/bun/pull/7055
|
|
@@ -69,25 +171,31 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
69
171
|
}).code
|
|
70
172
|
}
|
|
71
173
|
|
|
72
|
-
const comments: Record<number, string
|
|
174
|
+
const comments: Record<number, Record<number, string>> = {}
|
|
73
175
|
|
|
74
176
|
const ast = parse(content, {
|
|
75
177
|
ecmaVersion: 'latest',
|
|
76
178
|
sourceType: 'module',
|
|
77
179
|
locations: true,
|
|
78
180
|
onComment: (isBlock, text, _start, _end, _locStart, locEnd) => {
|
|
79
|
-
if (isBlock && locEnd?.line
|
|
181
|
+
if (isBlock && locEnd?.line !== undefined && locEnd?.column !== undefined) {
|
|
182
|
+
if (!comments?.[locEnd.line]) comments[locEnd.line] = []
|
|
183
|
+
comments[locEnd.line][locEnd.column] = text
|
|
184
|
+
}
|
|
80
185
|
}
|
|
81
186
|
})
|
|
82
187
|
simple(ast, {
|
|
83
188
|
ExportDefaultDeclaration(node) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const headerLine = node.loc?.start.line
|
|
88
|
-
const headerCom = headerLine !== undefined && comments?.[headerLine] ? comments[headerLine] : ''
|
|
189
|
+
const headerLine = node.loc?.start.line || -1
|
|
190
|
+
const headerCol = node.loc?.start.column || -1
|
|
191
|
+
const headerCom = comments?.[headerLine]?.[headerCol - 1] ? comments[headerLine][headerCol - 1] : ''
|
|
89
192
|
const headerRef = parseComment(headerCom)
|
|
90
193
|
meta.header = headerRef
|
|
194
|
+
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
let galbeIdentifier = node.declaration?.params?.[0]?.name
|
|
197
|
+
if (!galbeIdentifier) return meta
|
|
198
|
+
|
|
91
199
|
// @ts-ignore
|
|
92
200
|
simple(node.declaration.body, {
|
|
93
201
|
CallExpression(node) {
|
|
@@ -97,8 +205,9 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
97
205
|
const path = node.arguments[0].value
|
|
98
206
|
// @ts-ignore
|
|
99
207
|
const method = node.callee.property.name
|
|
100
|
-
const line = node.loc?.start.line
|
|
101
|
-
const
|
|
208
|
+
const line = node.loc?.start.line || -1
|
|
209
|
+
const col = node.loc?.start.column || -1
|
|
210
|
+
const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
|
|
102
211
|
|
|
103
212
|
const routeRefs = parseComment(com)
|
|
104
213
|
|
|
@@ -112,48 +221,41 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
112
221
|
return meta
|
|
113
222
|
}
|
|
114
223
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
export const defineRoutes = async (options: GalbeConfig, galbe: Galbe) => {
|
|
224
|
+
export const defineRoutes = async (
|
|
225
|
+
options: Pick<GalbeConfig, 'routes'>,
|
|
226
|
+
galbe: Galbe,
|
|
227
|
+
cb?: RouteInstanciationCallback
|
|
228
|
+
) => {
|
|
121
229
|
const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
125
|
-
}
|
|
230
|
+
const proxy = new GalbeProxy(galbe, cb)
|
|
231
|
+
if (!routes) return
|
|
126
232
|
const root = process.cwd()
|
|
127
233
|
if (typeof routes === 'string') {
|
|
128
|
-
let noRouteFound = true
|
|
129
234
|
for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
|
|
130
|
-
noRouteFound = false
|
|
131
235
|
const isDir = (await lstat(path)).isDirectory()
|
|
132
236
|
|
|
133
237
|
let files: string[] = []
|
|
134
238
|
if (!isDir) files.push(path)
|
|
135
239
|
else files = files.concat((await readdir(path)).map(f => `${path}/${f}`))
|
|
136
|
-
if (files.length === 0) console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
|
|
137
240
|
for (const f of files) {
|
|
138
241
|
try {
|
|
139
242
|
const metadata = await metaAnalysis(f)
|
|
243
|
+
proxy.filepath = f
|
|
244
|
+
proxy.meta = metadata
|
|
140
245
|
galbe.meta?.push({ file: path, ...metadata })
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
246
|
+
const imported = await import(f)
|
|
247
|
+
if (!imported?.default) throw new Error('No default export function')
|
|
248
|
+
if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
|
|
249
|
+
const routes = imported.default
|
|
250
|
+
routes(proxy)
|
|
251
|
+
} catch (err: any) {
|
|
252
|
+
if (cb) await cb({ type: 'error', error: err, filepath: f, route: undefined, meta: undefined })
|
|
146
253
|
}
|
|
147
254
|
}
|
|
148
255
|
}
|
|
149
|
-
if (noRouteFound) {
|
|
150
|
-
process.stdout.write('\r\x1b[K')
|
|
151
|
-
console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
256
|
} else if (Array.isArray(routes)) {
|
|
155
257
|
for (const r of routes) {
|
|
156
|
-
await defineRoutes({ routes: r }, galbe)
|
|
258
|
+
await defineRoutes({ routes: r }, galbe, cb)
|
|
157
259
|
}
|
|
158
260
|
}
|
|
159
261
|
}
|
package/src/schema.ts
CHANGED
|
@@ -3,6 +3,7 @@ export const Optional = Symbol.for('Galbe.SchemaType.Optional')
|
|
|
3
3
|
export const Stream = Symbol.for('Galbe.SchemaType.Stream')
|
|
4
4
|
|
|
5
5
|
export interface Options {
|
|
6
|
+
id?: string
|
|
6
7
|
title?: string
|
|
7
8
|
description?: string
|
|
8
9
|
default?: any
|
|
@@ -38,11 +39,11 @@ export interface STSchema extends Options {
|
|
|
38
39
|
| 'literal'
|
|
39
40
|
| 'array'
|
|
40
41
|
| 'object'
|
|
42
|
+
| 'json'
|
|
41
43
|
| 'urlForm'
|
|
42
44
|
| 'multipartForm'
|
|
43
45
|
| 'any'
|
|
44
46
|
| 'union'
|
|
45
|
-
| 'stream'
|
|
46
47
|
[Optional]?: boolean
|
|
47
48
|
[Stream]?: boolean
|
|
48
49
|
params: unknown[]
|
|
@@ -63,6 +64,16 @@ export type STPropsValue =
|
|
|
63
64
|
export type STProps = Record<string | number, STPropsValue>
|
|
64
65
|
|
|
65
66
|
type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Infer the static TypeScript type from a {@link https://galbe.dev/documentation/schemas#schema-types Schema Type}
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const schema = $T.object({ foo: $T.string() })
|
|
73
|
+
* type T = Static<typeof schema>
|
|
74
|
+
* // ^? type T = { foo: string }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
66
77
|
export type Static<T extends STSchema, P extends unknown[] = unknown[]> = (T & { params: P })['static']
|
|
67
78
|
|
|
68
79
|
// Utils
|
|
@@ -162,6 +173,11 @@ export interface STObject<T extends STProps = STProps> extends STSchema {
|
|
|
162
173
|
static: ObjectStatic<T, this['params']>
|
|
163
174
|
props: T
|
|
164
175
|
}
|
|
176
|
+
export interface STJson<T extends STBoolean | STNumber | STString | STObject = any> extends STSchema {
|
|
177
|
+
[Kind]: 'json'
|
|
178
|
+
type: 'boolean' | 'number' | 'string' | 'object' | 'unknown'
|
|
179
|
+
static: Static<T>
|
|
180
|
+
}
|
|
165
181
|
type ObjectStatic<T extends STProps, P extends unknown[]> = ObjectStaticProps<T, { [K in keyof T]: Static<T[K], P> }>
|
|
166
182
|
type OptionalPropertyKeys<T extends STProps> = {
|
|
167
183
|
[K in keyof T]: T[K] extends STOptional<STSchema> ? K : never
|
|
@@ -180,6 +196,13 @@ function _Object<T extends STProps>(properties?: T, options: Options = {}): STOb
|
|
|
180
196
|
? { ...options, [Kind]: 'object', props: clonedProperties, required: requiredKeys }
|
|
181
197
|
: { ...options, [Kind]: 'object', props: clonedProperties }) as unknown as STObject<T>
|
|
182
198
|
}
|
|
199
|
+
function _Json<T extends STBoolean | STNumber | STString | STObject>(value?: T, options: Options = {}): STJson<T> {
|
|
200
|
+
if (value?.[Kind] === 'boolean') return { ..._Bool(options), [Kind]: 'json', type: 'boolean' }
|
|
201
|
+
if (value?.[Kind] === 'number') return { ..._Number(options), [Kind]: 'json', type: 'number' }
|
|
202
|
+
if (value?.[Kind] === 'string') return { ..._String(options), [Kind]: 'json', type: 'string' }
|
|
203
|
+
if (value?.[Kind] === 'object') return { ..._Object(value.props, options), [Kind]: 'json', type: 'object' }
|
|
204
|
+
throw Error('Invalid Json type definition')
|
|
205
|
+
}
|
|
183
206
|
|
|
184
207
|
// UrlForm
|
|
185
208
|
export type STUrlFormValues =
|
|
@@ -221,7 +244,19 @@ export interface MultipartFormData<K extends string = string, V extends Static<S
|
|
|
221
244
|
}
|
|
222
245
|
export interface STMultipartForm<T extends STProps = STProps> extends STSchema {
|
|
223
246
|
[Kind]: 'multipartForm'
|
|
224
|
-
static: T extends undefined
|
|
247
|
+
static: T extends undefined
|
|
248
|
+
? {
|
|
249
|
+
[k: string]: {
|
|
250
|
+
headers: { type?: string; name: string; filename?: string }
|
|
251
|
+
content: Static<STMultipartFormValues>
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
: {
|
|
255
|
+
[K in keyof T]: {
|
|
256
|
+
headers: { type?: string; name: K; filename?: string }
|
|
257
|
+
content: Static<T[K]>
|
|
258
|
+
}
|
|
259
|
+
}
|
|
225
260
|
props: T
|
|
226
261
|
}
|
|
227
262
|
function _MultipartForm<T extends STProps>(properties?: T, options: Options = {}): STMultipartForm<T> {
|
|
@@ -241,12 +276,12 @@ export interface STArray<T extends STSchema = STSchema> extends STSchema {
|
|
|
241
276
|
static: Static<T>[]
|
|
242
277
|
items: T
|
|
243
278
|
}
|
|
244
|
-
export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T
|
|
279
|
+
export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T> {
|
|
245
280
|
return {
|
|
246
281
|
...options,
|
|
247
282
|
[Kind]: 'array',
|
|
248
283
|
items: schema ?? _Any()
|
|
249
|
-
} as unknown as
|
|
284
|
+
} as unknown as STArray<T>
|
|
250
285
|
}
|
|
251
286
|
|
|
252
287
|
// Union
|
|
@@ -313,9 +348,12 @@ export class SchemaType {
|
|
|
313
348
|
public object<T extends STProps>(properties?: T, options: Options = {}): STObject<T> {
|
|
314
349
|
return _Object(properties, options)
|
|
315
350
|
}
|
|
316
|
-
/**
|
|
317
|
-
public json<T extends
|
|
318
|
-
|
|
351
|
+
/** Creates a JSON Schema Type */
|
|
352
|
+
public json<T extends STString | STBoolean | STNumber | STObject<STProps>>(
|
|
353
|
+
value: T,
|
|
354
|
+
options: Options = {}
|
|
355
|
+
): STJson<T> {
|
|
356
|
+
return _Json(value, options)
|
|
319
357
|
}
|
|
320
358
|
/** Creates an UrlForm Schema Type */
|
|
321
359
|
public urlForm<T extends STUrlFormProps>(properties?: T, options: Options = {}): STUrlForm<T> {
|
|
@@ -326,7 +364,7 @@ export class SchemaType {
|
|
|
326
364
|
return _MultipartForm(properties, options)
|
|
327
365
|
}
|
|
328
366
|
/** Crates an Array Schema Type */
|
|
329
|
-
public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T
|
|
367
|
+
public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T> {
|
|
330
368
|
return _Array(schema, options)
|
|
331
369
|
}
|
|
332
370
|
/** Crates an Union Schema Type */
|
|
@@ -376,3 +414,36 @@ export class SchemaType {
|
|
|
376
414
|
return _Stream(schema)
|
|
377
415
|
}
|
|
378
416
|
}
|
|
417
|
+
|
|
418
|
+
export const schemaToTypeStr = (schema: STSchema): string => {
|
|
419
|
+
let type = 'unknown'
|
|
420
|
+
let kind = schema[Kind]
|
|
421
|
+
|
|
422
|
+
if (kind === 'boolean') type = 'boolean'
|
|
423
|
+
else if (kind === 'byteArray') type = 'Uint8Array'
|
|
424
|
+
else if (kind === 'number') type = 'number'
|
|
425
|
+
else if (kind === 'integer') type = 'number'
|
|
426
|
+
else if (kind === 'string') type = 'string'
|
|
427
|
+
else if (kind === 'any') type = 'any'
|
|
428
|
+
else if (kind === 'literal') {
|
|
429
|
+
let value = (schema as STLiteral).value
|
|
430
|
+
if (typeof value === 'string') type = `'${value}'`
|
|
431
|
+
else type = String(value)
|
|
432
|
+
} else if (kind === 'array') {
|
|
433
|
+
type = `Array<${schemaToTypeStr((schema as STArray).items)}>`
|
|
434
|
+
} else if (kind === 'object') {
|
|
435
|
+
let props = (schema as STObject).props
|
|
436
|
+
type = `{${Object.entries(props)
|
|
437
|
+
.map(([k, v]) => `${typeof k === 'string' ? `'${k}'` : k}:${schemaToTypeStr(v)}`)
|
|
438
|
+
.join(';')}}`
|
|
439
|
+
} else if (kind === 'json') {
|
|
440
|
+
type = `Json<${schemaToTypeStr({ ...schema, [Kind]: schema.type })}>`
|
|
441
|
+
} else if (kind === 'union') {
|
|
442
|
+
let anyOf = (schema as STUnion).anyOf
|
|
443
|
+
type = anyOf.map(s => schemaToTypeStr(s)).join('|')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (schema[Optional]) type = `${type}|undefined`
|
|
447
|
+
|
|
448
|
+
return type
|
|
449
|
+
}
|