galbe 0.13.0 → 0.14.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 +18 -1
- package/bin/commands/build.ts +10 -4
- package/bin/commands/generate/cli/index.ts +156 -0
- package/bin/commands/generate/cli/targets/cac.ts +535 -0
- package/bin/commands/generate/client.ts +32 -109
- package/bin/commands/generate/code/openapi.parser.ts +428 -133
- package/bin/commands/generate/code/route-merge.ts +248 -0
- package/bin/commands/generate/code.ts +110 -23
- package/bin/commands/generate/index.ts +2 -0
- package/package.json +4 -1
- package/src/cookies.ts +87 -0
- package/src/extras/spec/openapi.serializer.ts +236 -101
- package/src/extras.ts +1 -0
- package/src/index.ts +4 -7
- package/src/parser.ts +104 -63
- package/src/router.ts +34 -20
- package/src/routes.ts +14 -10
- package/src/schema.ts +82 -95
- package/src/server.ts +35 -21
- package/src/types.ts +180 -61
- package/src/util.ts +10 -18
- package/src/validator.ts +62 -32
- package/bin/res/cli.template.js +0 -122
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaybeArray, STBody, Context, STResponse,
|
|
1
|
+
import type { MaybeArray, STBody, Context, STResponse, STBodyContent, STBodyValue } from './index'
|
|
2
2
|
import type {
|
|
3
3
|
STStream,
|
|
4
4
|
STMultipartForm,
|
|
@@ -12,13 +12,15 @@ import type {
|
|
|
12
12
|
STNull,
|
|
13
13
|
STPropsValue,
|
|
14
14
|
STUnion,
|
|
15
|
+
STIntersection,
|
|
16
|
+
STArray,
|
|
15
17
|
} from './schema'
|
|
16
18
|
|
|
17
19
|
import { readableStreamToArrayBuffer } from 'bun'
|
|
18
20
|
import { Kind, Optional, Stream } from './schema'
|
|
19
21
|
import { validate } from './validator'
|
|
20
|
-
import {
|
|
21
|
-
import { isIterator } from './util'
|
|
22
|
+
import { InternalServerError, RequestError } from './index'
|
|
23
|
+
import { isIterator, inferBodyType, type ParseMode } from './util'
|
|
22
24
|
|
|
23
25
|
const textDecoder = new TextDecoder()
|
|
24
26
|
const textEncoder = new TextEncoder()
|
|
@@ -35,10 +37,16 @@ export const requestBodyParser = async (
|
|
|
35
37
|
body: ReadableStream | null,
|
|
36
38
|
headers: Record<string, string>,
|
|
37
39
|
schemas?: STBody | STNull,
|
|
38
|
-
contentType?:
|
|
40
|
+
contentType?: string
|
|
39
41
|
) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const normalizedCT = contentType?.split(';')[0]?.trim()
|
|
43
|
+
let parseMode: ParseMode = inferBodyType(contentType)
|
|
44
|
+
let schema: STBodyValue | STNull | undefined =
|
|
45
|
+
(schemas as STNull)?.[Kind] === 'null'
|
|
46
|
+
? (schemas as STNull)
|
|
47
|
+
: normalizedCT
|
|
48
|
+
? ((schemas as STBodyContent)?.[normalizedCT as `${string}/${string}`] ?? (schemas as STBodyContent)?.['*/*'])
|
|
49
|
+
: (schemas as STBodyContent)?.['*/*']
|
|
42
50
|
let kind = schema?.[Kind]
|
|
43
51
|
let isStream = schema && Stream in schema
|
|
44
52
|
try {
|
|
@@ -47,11 +55,11 @@ export const requestBodyParser = async (
|
|
|
47
55
|
throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
|
|
48
56
|
}
|
|
49
57
|
if (!schemas || !Object.keys(schemas).length) {
|
|
50
|
-
// No schema defined, we base parsing on
|
|
51
|
-
if (
|
|
58
|
+
// No schema defined, we base parsing on parseMode only
|
|
59
|
+
if (parseMode === 'byteArray') {
|
|
52
60
|
if (body === null) return new Uint8Array()
|
|
53
61
|
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
54
|
-
} else if (
|
|
62
|
+
} else if (parseMode === 'json') {
|
|
55
63
|
if (body === null) return null
|
|
56
64
|
try {
|
|
57
65
|
return JSON.parse(await streamToString(body))
|
|
@@ -61,25 +69,25 @@ export const requestBodyParser = async (
|
|
|
61
69
|
payload: { body: err?.message ?? 'Parsing error' },
|
|
62
70
|
})
|
|
63
71
|
}
|
|
64
|
-
} else if (
|
|
72
|
+
} else if (parseMode === 'text') {
|
|
65
73
|
if (body === null) return ''
|
|
66
74
|
return streamToString(body)
|
|
67
|
-
} else if (
|
|
75
|
+
} else if (parseMode === 'urlForm') {
|
|
68
76
|
if (body === null) return {}
|
|
69
77
|
return await streamToUrlForm(body)
|
|
70
|
-
} else if (
|
|
78
|
+
} else if (parseMode === 'multipart') {
|
|
71
79
|
if (body === null) return {}
|
|
72
|
-
const boundary = headers?.['content-type']
|
|
80
|
+
const boundary = headers?.['content-type']?.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
73
81
|
return await streamToMultipartForm(body, boundary)
|
|
74
82
|
} else return body === null ? null : rsToAsyncIterator(body)
|
|
75
83
|
} else {
|
|
76
84
|
// Schemas found
|
|
77
|
-
if (
|
|
78
|
-
if (kind === 'byteArray')
|
|
79
|
-
else if (kind === 'string')
|
|
85
|
+
if (parseMode === 'default' && (schema || Object.values(schemas).every(s => s?.[Optional]))) {
|
|
86
|
+
if (kind === 'byteArray') parseMode = 'byteArray'
|
|
87
|
+
else if (kind === 'string') parseMode = 'text'
|
|
80
88
|
else return body === null ? null : rsToAsyncIterator(body)
|
|
81
89
|
}
|
|
82
|
-
if (
|
|
90
|
+
if (parseMode === 'byteArray') {
|
|
83
91
|
if (kind !== 'byteArray') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
84
92
|
if (body === null) {
|
|
85
93
|
return isStream
|
|
@@ -93,8 +101,8 @@ export const requestBodyParser = async (
|
|
|
93
101
|
}
|
|
94
102
|
if (isStream) return rsToAsyncIterator(body)
|
|
95
103
|
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
96
|
-
} else if (
|
|
97
|
-
if (!['string', 'boolean', 'number', 'integer', '
|
|
104
|
+
} else if (parseMode === 'text') {
|
|
105
|
+
if (!kind || !['string', 'boolean', 'number', 'integer', 'anyOf', 'oneOf', 'literal'].includes(kind))
|
|
98
106
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
99
107
|
if (body === null)
|
|
100
108
|
return isStream
|
|
@@ -104,20 +112,28 @@ export const requestBodyParser = async (
|
|
|
104
112
|
controller.close()
|
|
105
113
|
},
|
|
106
114
|
})
|
|
107
|
-
: validate('', schema, { parse: true })
|
|
115
|
+
: validate('', schema as STSchema, { parse: true })
|
|
108
116
|
if (isStream) return $streamToString(body)
|
|
109
|
-
if (kind === '
|
|
117
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
110
118
|
let str = await streamToString(body)
|
|
111
|
-
return unionize(str, schema)
|
|
119
|
+
return unionize(str, schema as STUnion)
|
|
112
120
|
}
|
|
113
121
|
return await streamToString(body, schema as STBodyValue)
|
|
114
|
-
} else if (
|
|
115
|
-
if (
|
|
122
|
+
} else if (parseMode === 'json') {
|
|
123
|
+
if (
|
|
124
|
+
!kind ||
|
|
125
|
+
!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'anyOf', 'oneOf', 'intersection'].includes(kind)
|
|
126
|
+
)
|
|
116
127
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
117
|
-
if (kind === '
|
|
128
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
118
129
|
let str = body === null ? 'null' : await streamToString(body)
|
|
119
130
|
let json = JSON.parse(str)
|
|
120
|
-
return unionize(json, schema)
|
|
131
|
+
return unionize(json, schema as STUnion)
|
|
132
|
+
}
|
|
133
|
+
if (kind === 'intersection') {
|
|
134
|
+
let str = body === null ? 'null' : await streamToString(body)
|
|
135
|
+
let json = JSON.parse(str)
|
|
136
|
+
return intersectionize(json, schema as STIntersection<any>)
|
|
121
137
|
}
|
|
122
138
|
const str = body === null ? 'null' : await streamToString(body)
|
|
123
139
|
let json
|
|
@@ -129,9 +145,9 @@ export const requestBodyParser = async (
|
|
|
129
145
|
payload: { body: err?.message ?? 'Parsing error' },
|
|
130
146
|
})
|
|
131
147
|
}
|
|
132
|
-
return validate(json, schema, { parse: true })
|
|
133
|
-
} else if (
|
|
134
|
-
if (!['object', '
|
|
148
|
+
return validate(json, schema as STSchema, { parse: true })
|
|
149
|
+
} else if (parseMode === 'urlForm') {
|
|
150
|
+
if (!kind || !['object', 'anyOf', 'oneOf'].includes(kind))
|
|
135
151
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
136
152
|
if (body === null)
|
|
137
153
|
return isStream
|
|
@@ -148,16 +164,17 @@ export const requestBodyParser = async (
|
|
|
148
164
|
controller.close()
|
|
149
165
|
},
|
|
150
166
|
}),
|
|
151
|
-
schema
|
|
167
|
+
schema as STObject
|
|
152
168
|
)
|
|
153
|
-
if (kind === '
|
|
169
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
154
170
|
const b = await streamToUrlForm(body)
|
|
155
|
-
return unionize(b, schema)
|
|
171
|
+
return unionize(b, schema as STUnion)
|
|
156
172
|
}
|
|
157
173
|
if (isStream) return $streamToUrlForm(body, schema as STStream<STObject>)
|
|
158
174
|
else return await streamToUrlForm(body, schema as STObject)
|
|
159
|
-
} else if (
|
|
160
|
-
if (kind !== 'multipartForm'
|
|
175
|
+
} else if (parseMode === 'multipart') {
|
|
176
|
+
if (kind !== 'multipartForm' && kind !== 'anyOf' && kind !== 'oneOf')
|
|
177
|
+
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
161
178
|
if (body === null)
|
|
162
179
|
return isStream
|
|
163
180
|
? new ReadableStream({
|
|
@@ -167,14 +184,14 @@ export const requestBodyParser = async (
|
|
|
167
184
|
},
|
|
168
185
|
})
|
|
169
186
|
: {}
|
|
170
|
-
const boundary = headers?.['content-type']
|
|
171
|
-
if (kind === '
|
|
187
|
+
const boundary = headers?.['content-type']?.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
188
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
172
189
|
let mp = await streamToMultipartForm(body, boundary)
|
|
173
|
-
return unionize(mp, schema)
|
|
190
|
+
return unionize(mp, schema as STUnion)
|
|
174
191
|
}
|
|
175
192
|
if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
|
|
176
193
|
return streamToMultipartForm(body, boundary, schema as STMultipartForm)
|
|
177
|
-
} else if (
|
|
194
|
+
} else if (parseMode === 'default') {
|
|
178
195
|
throw new RequestError({ status: 400, payload: { body: `Not a valid content-type` } })
|
|
179
196
|
}
|
|
180
197
|
}
|
|
@@ -215,7 +232,8 @@ async function* $streamToUrlForm(
|
|
|
215
232
|
decodeURIComponent(textDecoder.decode(bV)),
|
|
216
233
|
]
|
|
217
234
|
try {
|
|
218
|
-
|
|
235
|
+
const propSchema = schema?.props?.[key]
|
|
236
|
+
let s = propSchema?.[Kind] === 'array' ? (propSchema as STArray).items : propSchema
|
|
219
237
|
val = s ? paramParser(val, s) : val
|
|
220
238
|
} catch (error) {
|
|
221
239
|
throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
|
|
@@ -246,7 +264,8 @@ async function* $streamToUrlForm(
|
|
|
246
264
|
decodeURIComponent(textDecoder.decode(rest)),
|
|
247
265
|
]
|
|
248
266
|
try {
|
|
249
|
-
|
|
267
|
+
const propSchema = schema?.props?.[key]
|
|
268
|
+
let s = propSchema?.[Kind] === 'array' ? (propSchema as STArray).items : propSchema
|
|
250
269
|
val = s ? paramParser(val, s) : val
|
|
251
270
|
} catch (error) {
|
|
252
271
|
throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
|
|
@@ -545,7 +564,7 @@ const paramParser = (
|
|
|
545
564
|
let errors: Record<number, any> = {}
|
|
546
565
|
for (let [idx, v] of value.entries()) {
|
|
547
566
|
try {
|
|
548
|
-
pv.push(paramParser(v, type.items as STMultipartFormValues) as Static<STPropsValue>)
|
|
567
|
+
pv.push(paramParser(v, (type as STArray).items as STMultipartFormValues) as Static<STPropsValue>)
|
|
549
568
|
} catch (error) {
|
|
550
569
|
errors[idx] = error
|
|
551
570
|
}
|
|
@@ -559,25 +578,26 @@ const paramParser = (
|
|
|
559
578
|
if (value === 'false') return false
|
|
560
579
|
else throw `Not a valid boolean. Should be 'true' or 'false'`
|
|
561
580
|
} else if (type[Kind] === 'integer') {
|
|
562
|
-
if (value === null || value === undefined) throw `Not a valid integer`
|
|
563
|
-
const parsedValue =
|
|
564
|
-
if (
|
|
581
|
+
if (value === null || value === undefined || value === '') throw `Not a valid integer`
|
|
582
|
+
const parsedValue = Number(value)
|
|
583
|
+
if (!Number.isFinite(parsedValue) || !Number.isInteger(parsedValue)) throw `Not a valid integer`
|
|
565
584
|
validate(parsedValue, type)
|
|
566
585
|
return parsedValue
|
|
567
586
|
} else if (type[Kind] === 'number') {
|
|
568
|
-
if (value === null || value === undefined) throw `Not a valid number`
|
|
587
|
+
if (value === null || value === undefined || value === '') throw `Not a valid number`
|
|
569
588
|
const parsedValue = Number(value)
|
|
570
|
-
if (
|
|
589
|
+
if (!Number.isFinite(parsedValue)) throw `Not a valid number`
|
|
571
590
|
validate(parsedValue, type)
|
|
572
591
|
return parsedValue
|
|
573
592
|
} else if (type[Kind] === 'string') {
|
|
574
593
|
validate(value, type)
|
|
575
594
|
return value
|
|
576
595
|
} else if (type[Kind] === 'literal') {
|
|
596
|
+
const lit = type as STLiteral
|
|
577
597
|
let val: any = value
|
|
578
|
-
if (typeof
|
|
579
|
-
if (typeof
|
|
580
|
-
if (val !==
|
|
598
|
+
if (typeof lit.value === 'boolean') val = value === 'true' ? true : value === 'false' ? false : value
|
|
599
|
+
if (typeof lit.value === 'number') val = Number(value)
|
|
600
|
+
if (val !== lit.value) throw `Not a valid value`
|
|
581
601
|
return val
|
|
582
602
|
} else if (type[Kind] === 'object') {
|
|
583
603
|
let json
|
|
@@ -588,11 +608,11 @@ const paramParser = (
|
|
|
588
608
|
}
|
|
589
609
|
return validate(json, type)
|
|
590
610
|
} else if (type[Kind] === 'array') {
|
|
591
|
-
return [paramParser(value, type.items as STMultipartFormValues) as Static<STPropsValue>]
|
|
611
|
+
return [paramParser(value, (type as STArray).items as STMultipartFormValues) as Static<STPropsValue>]
|
|
592
612
|
} else if (type[Kind] === 'byteArray') {
|
|
593
613
|
return Uint8Array.from(value, c => c.charCodeAt(0))
|
|
594
|
-
} else if (type[Kind] === '
|
|
595
|
-
const union = Object.values(type.
|
|
614
|
+
} else if (type[Kind] === 'anyOf' || type[Kind] === 'oneOf') {
|
|
615
|
+
const union = Object.values((type as STUnion).members)
|
|
596
616
|
for (const elt of union) {
|
|
597
617
|
try {
|
|
598
618
|
return paramParser(value, elt as STMultipartFormValues)
|
|
@@ -631,7 +651,12 @@ export const requestPathParser = (input: string, path: string) => {
|
|
|
631
651
|
}
|
|
632
652
|
name += c
|
|
633
653
|
}
|
|
634
|
-
|
|
654
|
+
const raw = pInput[idx]
|
|
655
|
+
try {
|
|
656
|
+
params[name] = raw === undefined ? raw : decodeURIComponent(raw)
|
|
657
|
+
} catch {
|
|
658
|
+
params[name] = raw
|
|
659
|
+
}
|
|
635
660
|
}
|
|
636
661
|
}
|
|
637
662
|
return params
|
|
@@ -673,11 +698,14 @@ export const parseEntry = <T extends STProps>(
|
|
|
673
698
|
return parsedParams as Static<STObject<T>>
|
|
674
699
|
}
|
|
675
700
|
|
|
676
|
-
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
701
|
+
export const responseParser = (response: any, ctx: Context, cookies: string[], schema?: STResponse) => {
|
|
677
702
|
const details = {
|
|
678
703
|
status: ctx.set.status || 200,
|
|
679
704
|
headers: new Headers(),
|
|
680
705
|
}
|
|
706
|
+
for (const cookie of cookies) {
|
|
707
|
+
details.headers.append('set-cookie', cookie)
|
|
708
|
+
}
|
|
681
709
|
for (const [key, value] of Object.entries(ctx.set.headers)) {
|
|
682
710
|
if (Array.isArray(value)) {
|
|
683
711
|
value.forEach(v => details.headers.append(key, v))
|
|
@@ -686,7 +714,11 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
686
714
|
if (response instanceof Response) return response
|
|
687
715
|
else if (typeof response === 'string') {
|
|
688
716
|
if (!details?.headers?.has('content-type')) {
|
|
689
|
-
|
|
717
|
+
const statusEntry: any = schema?.[details.status]
|
|
718
|
+
const isJson = statusEntry?.[Kind]
|
|
719
|
+
? statusEntry[Kind] === 'json'
|
|
720
|
+
: statusEntry?.['application/json'] && !statusEntry?.['text/plain']
|
|
721
|
+
if (isJson) {
|
|
690
722
|
details?.headers?.set('content-type', 'application/json')
|
|
691
723
|
response = `"${response}"`
|
|
692
724
|
} else details?.headers?.set('content-type', 'text/plain')
|
|
@@ -728,7 +760,7 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
728
760
|
return new Response(response, details)
|
|
729
761
|
} catch (error) {
|
|
730
762
|
console.error(error)
|
|
731
|
-
throw new
|
|
763
|
+
throw new InternalServerError()
|
|
732
764
|
}
|
|
733
765
|
}
|
|
734
766
|
}
|
|
@@ -736,20 +768,29 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
736
768
|
const unionize = (b: any, schema: STUnion) => {
|
|
737
769
|
let res
|
|
738
770
|
let error
|
|
739
|
-
const
|
|
740
|
-
(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
)
|
|
744
|
-
for (let s of schema.anyOf) {
|
|
771
|
+
const discriminants = schema.members.reduce((acc, obj) => {
|
|
772
|
+
const props = (obj as STObject).props
|
|
773
|
+
return acc.filter(k => props && k in props && props[k]?.[Kind] === 'literal' && !props[k]?.[Optional])
|
|
774
|
+
}, Object.keys((schema.members[0] as STObject)?.props || {}))
|
|
775
|
+
for (let s of schema.members) {
|
|
745
776
|
try {
|
|
746
777
|
res = validate(b, s, { parse: true })
|
|
747
778
|
if (res !== undefined) break
|
|
748
779
|
} catch (err: any) {
|
|
749
|
-
if (
|
|
780
|
+
if (discriminants.every(d => !err?.[d]?.startsWith('Not a valid value'))) error = err
|
|
750
781
|
}
|
|
751
782
|
}
|
|
752
783
|
if (res !== undefined) return res
|
|
753
784
|
else if (error) throw new RequestError({ status: 400, payload: { body: error } })
|
|
754
785
|
else throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
755
786
|
}
|
|
787
|
+
|
|
788
|
+
const intersectionize = (b: any, schema: STIntersection<any>) => {
|
|
789
|
+
let res
|
|
790
|
+
try {
|
|
791
|
+
for (let s of schema.allOf) res = validate(b, s, { parse: true })
|
|
792
|
+
return res
|
|
793
|
+
} catch (e) {
|
|
794
|
+
throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
795
|
+
}
|
|
796
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -3,33 +3,47 @@ import { MethodNotAllowedError, NotFoundError } from './types'
|
|
|
3
3
|
|
|
4
4
|
const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d.][\w-.]+[\w\d]))*\/?$/
|
|
5
5
|
|
|
6
|
-
const walk = (path: string[], node: RouteNode,
|
|
7
|
-
if (path.length
|
|
8
|
-
|
|
6
|
+
const walk = (path: string[], node: RouteNode, index: number = 0): RouteNode => {
|
|
7
|
+
if (index === path.length - 1) {
|
|
8
|
+
if (node.routes && !!Object.keys(node.routes).length) return node
|
|
9
|
+
// /a/* should match /a — fall back to a wildcard child if the node has no
|
|
10
|
+
// routes of its own.
|
|
11
|
+
const wc = node.children?.['*']
|
|
12
|
+
if (wc?.routes && !!Object.keys(wc.routes).length) return wc
|
|
13
|
+
throw new NotFoundError()
|
|
14
|
+
}
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
if (node.param) alts.push(node.param)
|
|
16
|
+
const nextSegment = path[index + 1]
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
// 1. Exact Match
|
|
19
|
+
if (node.children && nextSegment in node.children) {
|
|
20
|
+
try {
|
|
21
|
+
return walk(path, node.children[nextSegment], index + 1)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (!(error instanceof NotFoundError)) throw error
|
|
24
|
+
}
|
|
16
25
|
}
|
|
26
|
+
|
|
27
|
+
// 2. Param Match
|
|
17
28
|
if (node.param) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return walk(path, alts.pop() as RouteNode, alts)
|
|
29
|
+
try {
|
|
30
|
+
return walk(path, node.param, index + 1)
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!(error instanceof NotFoundError)) throw error
|
|
33
|
+
}
|
|
24
34
|
}
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
|
|
36
|
+
// 3. Wildcard Match
|
|
37
|
+
if (node.children && '*' in node.children) {
|
|
27
38
|
try {
|
|
28
|
-
return walk(path,
|
|
39
|
+
return walk(path, node.children['*'], index + 1)
|
|
29
40
|
} catch (error) {
|
|
30
41
|
if (error instanceof NotFoundError) {
|
|
31
|
-
if (
|
|
32
|
-
|
|
42
|
+
if (node.children['*'].routes && !!Object.keys(node.children['*'].routes).length) {
|
|
43
|
+
return node.children['*']
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!(error instanceof NotFoundError)) throw error
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
|
|
@@ -53,8 +67,8 @@ export class GalbeRouter {
|
|
|
53
67
|
route.path = route?.path?.[0] === '/' ? route.path : `/${route.path}`
|
|
54
68
|
if (!route.path.match(ROUTE_REGEX)) throw new SyntaxError(`${route.path} is not a valid route path.`)
|
|
55
69
|
const isStatic = !route.path.match(/(:[\w\d-]+|\*)/)
|
|
56
|
-
if (isStatic) this.cachedRoutes.set(`[${route.method.toUpperCase()}]${route.path}`, route)
|
|
57
70
|
route.path = `${this.prefix || ''}${route.path}`
|
|
71
|
+
if (isStatic) this.cachedRoutes.set(`[${route.method}]${route.path}`, route)
|
|
58
72
|
let path = route.path.replace(/^\/+|\/+$/g, '').split('/')
|
|
59
73
|
if (path[0] === '') path.shift()
|
|
60
74
|
let r = this.routes
|
package/src/routes.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { cpSync } from 'fs'
|
|
2
1
|
import type { GalbeConfig, GalbePlugin, Method, Route } from './types'
|
|
3
2
|
|
|
4
3
|
import { readdir, lstat } from 'fs/promises'
|
|
@@ -39,6 +38,7 @@ export class GalbeProxy {
|
|
|
39
38
|
_metaTmp?: RoutesMeta
|
|
40
39
|
_filepath?: string
|
|
41
40
|
_meta: Array<RouteFileMeta> = []
|
|
41
|
+
_staticTargets: Array<{ path: string; target: string }> = []
|
|
42
42
|
constructor(g: Galbe, cb?: RouteInstanciationCallback) {
|
|
43
43
|
this.#g = g
|
|
44
44
|
this._cb = cb
|
|
@@ -110,23 +110,27 @@ export class GalbeProxy {
|
|
|
110
110
|
return this.handleRoute('head', ...args)
|
|
111
111
|
}
|
|
112
112
|
async static(...args: any[]) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const [path, target] = args
|
|
114
|
+
if (typeof path === 'string' && typeof target === 'string') {
|
|
115
|
+
this._staticTargets.push({ path, target })
|
|
116
116
|
}
|
|
117
117
|
return this.handleRoute('static', ...args)
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const parseComment = (comment: string): Record<string, string | string[]> => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
// Find the first JSDoc-tag line (a line whose first non-whitespace/star
|
|
123
|
+
// character is `@`). Anything before it is the head; from it onwards is tags.
|
|
124
|
+
// We can't naively split on `@` because descriptions legitimately contain
|
|
125
|
+
// `@` (e.g. `@scope/name` package identifiers).
|
|
126
|
+
const tagLineRe = /^\s*\*?\s*@[a-zA-Z_][0-9a-zA-Z_]*(?:\s|$)/m
|
|
127
|
+
const m = comment.match(tagLineRe)
|
|
128
|
+
const headRaw = m && m.index !== undefined ? comment.slice(0, m.index) : comment
|
|
129
|
+
const head = headRaw.replace(/^ *\* */gm, '').trim() || ''
|
|
130
|
+
const tagsSrc = m && m.index !== undefined ? comment.slice(m.index) : ''
|
|
127
131
|
const refs = {
|
|
128
132
|
...(head ? { head } : {}),
|
|
129
|
-
...[...
|
|
133
|
+
...[...tagsSrc.matchAll(new RegExp(`^\\s*\\*\\s*@([a-zA-Z_][0-9a-zA-Z_]*)(?:$|\\s+([^\\n]*)\\s*$)`, 'gm'))].reduce(
|
|
130
134
|
(acc, n) => {
|
|
131
135
|
return {
|
|
132
136
|
...acc,
|