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/src/parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MaybeArray, STBody, Context, STResponse, STBodyValue, STBodyType } from './index'
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 { InternalError, RequestError } from './index'
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?: STBodyType
40
+ contentType?: string
39
41
  ) => {
40
- let schema: STBodyValue =
41
- (schemas as STNull)?.[Kind] === 'null' ? schemas : contentType ? schemas?.[contentType] : undefined
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 contentType only
51
- if (contentType === 'byteArray') {
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 (contentType === 'json') {
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 (contentType === 'text') {
72
+ } else if (parseMode === 'text') {
65
73
  if (body === null) return ''
66
74
  return streamToString(body)
67
- } else if (contentType === 'urlForm') {
75
+ } else if (parseMode === 'urlForm') {
68
76
  if (body === null) return {}
69
77
  return await streamToUrlForm(body)
70
- } else if (contentType === 'multipart') {
78
+ } else if (parseMode === 'multipart') {
71
79
  if (body === null) return {}
72
- const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
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 (contentType === 'default' && (schema || Object.values(schemas).every(s => s?.[Optional]))) {
78
- if (kind === 'byteArray') contentType = 'byteArray'
79
- else if (kind === 'string') contentType = 'text'
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 (contentType === 'byteArray') {
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 (contentType === 'text') {
97
- if (!['string', 'boolean', 'number', 'integer', 'union', 'literal'].includes(kind))
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 === 'union') {
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 (contentType === 'json') {
115
- if (!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union'].includes(kind))
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 === 'union') {
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 (contentType === 'urlForm') {
134
- if (!['object', 'union'].includes(kind))
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 === 'union') {
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 (contentType === 'multipart') {
160
- if (kind !== 'multipartForm') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
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'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
171
- if (kind === 'union') {
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 (contentType === 'default') {
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
- let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
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
- let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
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 = parseInt(value, 10)
564
- if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid integer`
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 (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid number`
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 type.value === 'boolean') val = value === 'true' ? true : value === 'false' ? false : value
579
- if (typeof type.value === 'number') val = Number(value)
580
- if (val !== type.value) throw `Not a valid value`
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] === 'union') {
595
- const union = Object.values(type.anyOf)
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
- params[name] = pInput[idx]
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
- if (schema?.[details.status]?.[Kind] === 'json') {
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 InternalError()
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 discirminants = schema.anyOf.reduce(
740
- (acc, obj) =>
741
- acc.filter(k => obj?.props && k in obj.props && obj.props[k]?.[Kind] === 'literal' && !obj.props[k]?.Optional),
742
- Object.keys(schema.anyOf[0]?.props || {})
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 (discirminants.every(d => !err?.[d]?.startsWith('Not a valid value'))) error = err
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, alts: RouteNode[] = []): RouteNode => {
7
- if (path.length < 1) throw new NotFoundError()
8
- if (path.length === 1 && node.routes && !!Object.keys(node.routes).length) return node
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
- if (node.children?.['*']) alts.push(node.children['*'])
11
- if (node.param) alts.push(node.param)
16
+ const nextSegment = path[index + 1]
12
17
 
13
- if (node.children && path[1] in node.children) {
14
- path.shift()
15
- return walk(path, node.children[path[0]], alts)
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
- path.shift()
19
- alts.pop()
20
- return walk(path, node.param, alts)
21
- }
22
- if (alts.length > 1) {
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
- if (alts.length === 1) {
26
- let lastAlt = alts.pop() as RouteNode
35
+
36
+ // 3. Wildcard Match
37
+ if (node.children && '*' in node.children) {
27
38
  try {
28
- return walk(path, lastAlt, alts)
39
+ return walk(path, node.children['*'], index + 1)
29
40
  } catch (error) {
30
41
  if (error instanceof NotFoundError) {
31
- if (lastAlt?.routes) return lastAlt
32
- } else throw error
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
- if (!!Bun.env.GALBE_BUILD_OUT) {
114
- let [_, target] = args
115
- cpSync(target, `${Bun.env.GALBE_BUILD_OUT}/static-${Bun.env.GALBE_BUILD}/${target}`, { recursive: true, dereference: true })
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
- const head =
123
- comment
124
- .match(/^([^@]*)/)?.[1]
125
- .replace(/^ *\* */gm, '')
126
- .trim() || ''
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
- ...[...comment.matchAll(new RegExp(`^\\s*\\*\\s*@([a-zA-Z_][0-9a-zA-Z_]*)(?:$|\\s+([^\\n]*)\\s*$)`, 'gm'))].reduce(
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,