galbe 0.13.1 → 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 +88 -65
- 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 +179 -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,
|
|
@@ -13,13 +13,14 @@ import type {
|
|
|
13
13
|
STPropsValue,
|
|
14
14
|
STUnion,
|
|
15
15
|
STIntersection,
|
|
16
|
+
STArray,
|
|
16
17
|
} from './schema'
|
|
17
18
|
|
|
18
19
|
import { readableStreamToArrayBuffer } from 'bun'
|
|
19
20
|
import { Kind, Optional, Stream } from './schema'
|
|
20
21
|
import { validate } from './validator'
|
|
21
|
-
import {
|
|
22
|
-
import { isIterator } from './util'
|
|
22
|
+
import { InternalServerError, RequestError } from './index'
|
|
23
|
+
import { isIterator, inferBodyType, type ParseMode } from './util'
|
|
23
24
|
|
|
24
25
|
const textDecoder = new TextDecoder()
|
|
25
26
|
const textEncoder = new TextEncoder()
|
|
@@ -36,10 +37,16 @@ export const requestBodyParser = async (
|
|
|
36
37
|
body: ReadableStream | null,
|
|
37
38
|
headers: Record<string, string>,
|
|
38
39
|
schemas?: STBody | STNull,
|
|
39
|
-
contentType?:
|
|
40
|
+
contentType?: string
|
|
40
41
|
) => {
|
|
41
|
-
|
|
42
|
-
|
|
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)?.['*/*']
|
|
43
50
|
let kind = schema?.[Kind]
|
|
44
51
|
let isStream = schema && Stream in schema
|
|
45
52
|
try {
|
|
@@ -48,11 +55,11 @@ export const requestBodyParser = async (
|
|
|
48
55
|
throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
|
|
49
56
|
}
|
|
50
57
|
if (!schemas || !Object.keys(schemas).length) {
|
|
51
|
-
// No schema defined, we base parsing on
|
|
52
|
-
if (
|
|
58
|
+
// No schema defined, we base parsing on parseMode only
|
|
59
|
+
if (parseMode === 'byteArray') {
|
|
53
60
|
if (body === null) return new Uint8Array()
|
|
54
61
|
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
55
|
-
} else if (
|
|
62
|
+
} else if (parseMode === 'json') {
|
|
56
63
|
if (body === null) return null
|
|
57
64
|
try {
|
|
58
65
|
return JSON.parse(await streamToString(body))
|
|
@@ -62,25 +69,25 @@ export const requestBodyParser = async (
|
|
|
62
69
|
payload: { body: err?.message ?? 'Parsing error' },
|
|
63
70
|
})
|
|
64
71
|
}
|
|
65
|
-
} else if (
|
|
72
|
+
} else if (parseMode === 'text') {
|
|
66
73
|
if (body === null) return ''
|
|
67
74
|
return streamToString(body)
|
|
68
|
-
} else if (
|
|
75
|
+
} else if (parseMode === 'urlForm') {
|
|
69
76
|
if (body === null) return {}
|
|
70
77
|
return await streamToUrlForm(body)
|
|
71
|
-
} else if (
|
|
78
|
+
} else if (parseMode === 'multipart') {
|
|
72
79
|
if (body === null) return {}
|
|
73
|
-
const boundary = headers?.['content-type']
|
|
80
|
+
const boundary = headers?.['content-type']?.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
74
81
|
return await streamToMultipartForm(body, boundary)
|
|
75
82
|
} else return body === null ? null : rsToAsyncIterator(body)
|
|
76
83
|
} else {
|
|
77
84
|
// Schemas found
|
|
78
|
-
if (
|
|
79
|
-
if (kind === 'byteArray')
|
|
80
|
-
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'
|
|
81
88
|
else return body === null ? null : rsToAsyncIterator(body)
|
|
82
89
|
}
|
|
83
|
-
if (
|
|
90
|
+
if (parseMode === 'byteArray') {
|
|
84
91
|
if (kind !== 'byteArray') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
85
92
|
if (body === null) {
|
|
86
93
|
return isStream
|
|
@@ -94,8 +101,8 @@ export const requestBodyParser = async (
|
|
|
94
101
|
}
|
|
95
102
|
if (isStream) return rsToAsyncIterator(body)
|
|
96
103
|
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
97
|
-
} else if (
|
|
98
|
-
if (!['string', 'boolean', 'number', 'integer', '
|
|
104
|
+
} else if (parseMode === 'text') {
|
|
105
|
+
if (!kind || !['string', 'boolean', 'number', 'integer', 'anyOf', 'oneOf', 'literal'].includes(kind))
|
|
99
106
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
100
107
|
if (body === null)
|
|
101
108
|
return isStream
|
|
@@ -105,27 +112,28 @@ export const requestBodyParser = async (
|
|
|
105
112
|
controller.close()
|
|
106
113
|
},
|
|
107
114
|
})
|
|
108
|
-
: validate('', schema, { parse: true })
|
|
115
|
+
: validate('', schema as STSchema, { parse: true })
|
|
109
116
|
if (isStream) return $streamToString(body)
|
|
110
|
-
if (kind === '
|
|
117
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
111
118
|
let str = await streamToString(body)
|
|
112
|
-
return unionize(str, schema)
|
|
119
|
+
return unionize(str, schema as STUnion)
|
|
113
120
|
}
|
|
114
121
|
return await streamToString(body, schema as STBodyValue)
|
|
115
|
-
} else if (
|
|
122
|
+
} else if (parseMode === 'json') {
|
|
116
123
|
if (
|
|
117
|
-
!
|
|
124
|
+
!kind ||
|
|
125
|
+
!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'anyOf', 'oneOf', 'intersection'].includes(kind)
|
|
118
126
|
)
|
|
119
127
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
120
|
-
if (kind === '
|
|
128
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
121
129
|
let str = body === null ? 'null' : await streamToString(body)
|
|
122
130
|
let json = JSON.parse(str)
|
|
123
|
-
return unionize(json, schema)
|
|
131
|
+
return unionize(json, schema as STUnion)
|
|
124
132
|
}
|
|
125
133
|
if (kind === 'intersection') {
|
|
126
134
|
let str = body === null ? 'null' : await streamToString(body)
|
|
127
135
|
let json = JSON.parse(str)
|
|
128
|
-
return intersectionize(json, schema)
|
|
136
|
+
return intersectionize(json, schema as STIntersection<any>)
|
|
129
137
|
}
|
|
130
138
|
const str = body === null ? 'null' : await streamToString(body)
|
|
131
139
|
let json
|
|
@@ -137,9 +145,9 @@ export const requestBodyParser = async (
|
|
|
137
145
|
payload: { body: err?.message ?? 'Parsing error' },
|
|
138
146
|
})
|
|
139
147
|
}
|
|
140
|
-
return validate(json, schema, { parse: true })
|
|
141
|
-
} else if (
|
|
142
|
-
if (!['object', '
|
|
148
|
+
return validate(json, schema as STSchema, { parse: true })
|
|
149
|
+
} else if (parseMode === 'urlForm') {
|
|
150
|
+
if (!kind || !['object', 'anyOf', 'oneOf'].includes(kind))
|
|
143
151
|
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
144
152
|
if (body === null)
|
|
145
153
|
return isStream
|
|
@@ -156,16 +164,17 @@ export const requestBodyParser = async (
|
|
|
156
164
|
controller.close()
|
|
157
165
|
},
|
|
158
166
|
}),
|
|
159
|
-
schema
|
|
167
|
+
schema as STObject
|
|
160
168
|
)
|
|
161
|
-
if (kind === '
|
|
169
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
162
170
|
const b = await streamToUrlForm(body)
|
|
163
|
-
return unionize(b, schema)
|
|
171
|
+
return unionize(b, schema as STUnion)
|
|
164
172
|
}
|
|
165
173
|
if (isStream) return $streamToUrlForm(body, schema as STStream<STObject>)
|
|
166
174
|
else return await streamToUrlForm(body, schema as STObject)
|
|
167
|
-
} else if (
|
|
168
|
-
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` } })
|
|
169
178
|
if (body === null)
|
|
170
179
|
return isStream
|
|
171
180
|
? new ReadableStream({
|
|
@@ -175,14 +184,14 @@ export const requestBodyParser = async (
|
|
|
175
184
|
},
|
|
176
185
|
})
|
|
177
186
|
: {}
|
|
178
|
-
const boundary = headers?.['content-type']
|
|
179
|
-
if (kind === '
|
|
187
|
+
const boundary = headers?.['content-type']?.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
188
|
+
if (kind === 'anyOf' || kind === 'oneOf') {
|
|
180
189
|
let mp = await streamToMultipartForm(body, boundary)
|
|
181
|
-
return unionize(mp, schema)
|
|
190
|
+
return unionize(mp, schema as STUnion)
|
|
182
191
|
}
|
|
183
192
|
if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
|
|
184
193
|
return streamToMultipartForm(body, boundary, schema as STMultipartForm)
|
|
185
|
-
} else if (
|
|
194
|
+
} else if (parseMode === 'default') {
|
|
186
195
|
throw new RequestError({ status: 400, payload: { body: `Not a valid content-type` } })
|
|
187
196
|
}
|
|
188
197
|
}
|
|
@@ -223,7 +232,8 @@ async function* $streamToUrlForm(
|
|
|
223
232
|
decodeURIComponent(textDecoder.decode(bV)),
|
|
224
233
|
]
|
|
225
234
|
try {
|
|
226
|
-
|
|
235
|
+
const propSchema = schema?.props?.[key]
|
|
236
|
+
let s = propSchema?.[Kind] === 'array' ? (propSchema as STArray).items : propSchema
|
|
227
237
|
val = s ? paramParser(val, s) : val
|
|
228
238
|
} catch (error) {
|
|
229
239
|
throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
|
|
@@ -254,7 +264,8 @@ async function* $streamToUrlForm(
|
|
|
254
264
|
decodeURIComponent(textDecoder.decode(rest)),
|
|
255
265
|
]
|
|
256
266
|
try {
|
|
257
|
-
|
|
267
|
+
const propSchema = schema?.props?.[key]
|
|
268
|
+
let s = propSchema?.[Kind] === 'array' ? (propSchema as STArray).items : propSchema
|
|
258
269
|
val = s ? paramParser(val, s) : val
|
|
259
270
|
} catch (error) {
|
|
260
271
|
throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
|
|
@@ -553,7 +564,7 @@ const paramParser = (
|
|
|
553
564
|
let errors: Record<number, any> = {}
|
|
554
565
|
for (let [idx, v] of value.entries()) {
|
|
555
566
|
try {
|
|
556
|
-
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>)
|
|
557
568
|
} catch (error) {
|
|
558
569
|
errors[idx] = error
|
|
559
570
|
}
|
|
@@ -567,25 +578,26 @@ const paramParser = (
|
|
|
567
578
|
if (value === 'false') return false
|
|
568
579
|
else throw `Not a valid boolean. Should be 'true' or 'false'`
|
|
569
580
|
} else if (type[Kind] === 'integer') {
|
|
570
|
-
if (value === null || value === undefined) throw `Not a valid integer`
|
|
571
|
-
const parsedValue =
|
|
572
|
-
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`
|
|
573
584
|
validate(parsedValue, type)
|
|
574
585
|
return parsedValue
|
|
575
586
|
} else if (type[Kind] === 'number') {
|
|
576
|
-
if (value === null || value === undefined) throw `Not a valid number`
|
|
587
|
+
if (value === null || value === undefined || value === '') throw `Not a valid number`
|
|
577
588
|
const parsedValue = Number(value)
|
|
578
|
-
if (
|
|
589
|
+
if (!Number.isFinite(parsedValue)) throw `Not a valid number`
|
|
579
590
|
validate(parsedValue, type)
|
|
580
591
|
return parsedValue
|
|
581
592
|
} else if (type[Kind] === 'string') {
|
|
582
593
|
validate(value, type)
|
|
583
594
|
return value
|
|
584
595
|
} else if (type[Kind] === 'literal') {
|
|
596
|
+
const lit = type as STLiteral
|
|
585
597
|
let val: any = value
|
|
586
|
-
if (typeof
|
|
587
|
-
if (typeof
|
|
588
|
-
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`
|
|
589
601
|
return val
|
|
590
602
|
} else if (type[Kind] === 'object') {
|
|
591
603
|
let json
|
|
@@ -596,11 +608,11 @@ const paramParser = (
|
|
|
596
608
|
}
|
|
597
609
|
return validate(json, type)
|
|
598
610
|
} else if (type[Kind] === 'array') {
|
|
599
|
-
return [paramParser(value, type.items as STMultipartFormValues) as Static<STPropsValue>]
|
|
611
|
+
return [paramParser(value, (type as STArray).items as STMultipartFormValues) as Static<STPropsValue>]
|
|
600
612
|
} else if (type[Kind] === 'byteArray') {
|
|
601
613
|
return Uint8Array.from(value, c => c.charCodeAt(0))
|
|
602
|
-
} else if (type[Kind] === '
|
|
603
|
-
const union = Object.values(type.
|
|
614
|
+
} else if (type[Kind] === 'anyOf' || type[Kind] === 'oneOf') {
|
|
615
|
+
const union = Object.values((type as STUnion).members)
|
|
604
616
|
for (const elt of union) {
|
|
605
617
|
try {
|
|
606
618
|
return paramParser(value, elt as STMultipartFormValues)
|
|
@@ -639,7 +651,12 @@ export const requestPathParser = (input: string, path: string) => {
|
|
|
639
651
|
}
|
|
640
652
|
name += c
|
|
641
653
|
}
|
|
642
|
-
|
|
654
|
+
const raw = pInput[idx]
|
|
655
|
+
try {
|
|
656
|
+
params[name] = raw === undefined ? raw : decodeURIComponent(raw)
|
|
657
|
+
} catch {
|
|
658
|
+
params[name] = raw
|
|
659
|
+
}
|
|
643
660
|
}
|
|
644
661
|
}
|
|
645
662
|
return params
|
|
@@ -681,11 +698,14 @@ export const parseEntry = <T extends STProps>(
|
|
|
681
698
|
return parsedParams as Static<STObject<T>>
|
|
682
699
|
}
|
|
683
700
|
|
|
684
|
-
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
701
|
+
export const responseParser = (response: any, ctx: Context, cookies: string[], schema?: STResponse) => {
|
|
685
702
|
const details = {
|
|
686
703
|
status: ctx.set.status || 200,
|
|
687
704
|
headers: new Headers(),
|
|
688
705
|
}
|
|
706
|
+
for (const cookie of cookies) {
|
|
707
|
+
details.headers.append('set-cookie', cookie)
|
|
708
|
+
}
|
|
689
709
|
for (const [key, value] of Object.entries(ctx.set.headers)) {
|
|
690
710
|
if (Array.isArray(value)) {
|
|
691
711
|
value.forEach(v => details.headers.append(key, v))
|
|
@@ -694,7 +714,11 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
694
714
|
if (response instanceof Response) return response
|
|
695
715
|
else if (typeof response === 'string') {
|
|
696
716
|
if (!details?.headers?.has('content-type')) {
|
|
697
|
-
|
|
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) {
|
|
698
722
|
details?.headers?.set('content-type', 'application/json')
|
|
699
723
|
response = `"${response}"`
|
|
700
724
|
} else details?.headers?.set('content-type', 'text/plain')
|
|
@@ -736,7 +760,7 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
736
760
|
return new Response(response, details)
|
|
737
761
|
} catch (error) {
|
|
738
762
|
console.error(error)
|
|
739
|
-
throw new
|
|
763
|
+
throw new InternalServerError()
|
|
740
764
|
}
|
|
741
765
|
}
|
|
742
766
|
}
|
|
@@ -744,17 +768,16 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
744
768
|
const unionize = (b: any, schema: STUnion) => {
|
|
745
769
|
let res
|
|
746
770
|
let error
|
|
747
|
-
const
|
|
748
|
-
(
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
)
|
|
752
|
-
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) {
|
|
753
776
|
try {
|
|
754
777
|
res = validate(b, s, { parse: true })
|
|
755
778
|
if (res !== undefined) break
|
|
756
779
|
} catch (err: any) {
|
|
757
|
-
if (
|
|
780
|
+
if (discriminants.every(d => !err?.[d]?.startsWith('Not a valid value'))) error = err
|
|
758
781
|
}
|
|
759
782
|
}
|
|
760
783
|
if (res !== undefined) return res
|
|
@@ -762,7 +785,7 @@ const unionize = (b: any, schema: STUnion) => {
|
|
|
762
785
|
else throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
763
786
|
}
|
|
764
787
|
|
|
765
|
-
const intersectionize = (b: any, schema: STIntersection) => {
|
|
788
|
+
const intersectionize = (b: any, schema: STIntersection<any>) => {
|
|
766
789
|
let res
|
|
767
790
|
try {
|
|
768
791
|
for (let s of schema.allOf) res = validate(b, s, { parse: true })
|
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,
|