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/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,
@@ -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 { InternalError, RequestError } from './index'
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?: STBodyType
40
+ contentType?: string
40
41
  ) => {
41
- let schema: STBodyValue =
42
- (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)?.['*/*']
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 contentType only
52
- if (contentType === 'byteArray') {
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 (contentType === 'json') {
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 (contentType === 'text') {
72
+ } else if (parseMode === 'text') {
66
73
  if (body === null) return ''
67
74
  return streamToString(body)
68
- } else if (contentType === 'urlForm') {
75
+ } else if (parseMode === 'urlForm') {
69
76
  if (body === null) return {}
70
77
  return await streamToUrlForm(body)
71
- } else if (contentType === 'multipart') {
78
+ } else if (parseMode === 'multipart') {
72
79
  if (body === null) return {}
73
- const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
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 (contentType === 'default' && (schema || Object.values(schemas).every(s => s?.[Optional]))) {
79
- if (kind === 'byteArray') contentType = 'byteArray'
80
- 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'
81
88
  else return body === null ? null : rsToAsyncIterator(body)
82
89
  }
83
- if (contentType === 'byteArray') {
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 (contentType === 'text') {
98
- 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))
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 === 'union') {
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 (contentType === 'json') {
122
+ } else if (parseMode === 'json') {
116
123
  if (
117
- !['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union', 'intersection'].includes(kind)
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 === 'union') {
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 (contentType === 'urlForm') {
142
- 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))
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 === 'union') {
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 (contentType === 'multipart') {
168
- 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` } })
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'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
179
- if (kind === 'union') {
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 (contentType === 'default') {
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
- 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
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
- 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
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 = parseInt(value, 10)
572
- 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`
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 (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid number`
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 type.value === 'boolean') val = value === 'true' ? true : value === 'false' ? false : value
587
- if (typeof type.value === 'number') val = Number(value)
588
- 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`
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] === 'union') {
603
- const union = Object.values(type.anyOf)
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
- 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
+ }
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
- 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) {
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 InternalError()
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 discirminants = schema.anyOf.reduce(
748
- (acc, obj) =>
749
- acc.filter(k => obj?.props && k in obj.props && obj.props[k]?.[Kind] === 'literal' && !obj.props[k]?.Optional),
750
- Object.keys(schema.anyOf[0]?.props || {})
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 (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
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, 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,