@sanity/client 7.2.2 → 7.4.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.
Files changed (54) hide show
  1. package/README.md +126 -1
  2. package/dist/_chunks-cjs/isRecord.cjs +6 -0
  3. package/dist/_chunks-cjs/isRecord.cjs.map +1 -0
  4. package/dist/_chunks-cjs/resolveEditInfo.cjs +3 -5
  5. package/dist/_chunks-cjs/resolveEditInfo.cjs.map +1 -1
  6. package/dist/_chunks-cjs/stegaClean.cjs +4 -0
  7. package/dist/_chunks-cjs/stegaClean.cjs.map +1 -1
  8. package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs +2 -5
  9. package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs.map +1 -1
  10. package/dist/_chunks-es/isRecord.js +7 -0
  11. package/dist/_chunks-es/isRecord.js.map +1 -0
  12. package/dist/_chunks-es/resolveEditInfo.js +1 -3
  13. package/dist/_chunks-es/resolveEditInfo.js.map +1 -1
  14. package/dist/_chunks-es/stegaClean.js +4 -0
  15. package/dist/_chunks-es/stegaClean.js.map +1 -1
  16. package/dist/_chunks-es/stegaEncodeSourceMap.js +1 -4
  17. package/dist/_chunks-es/stegaEncodeSourceMap.js.map +1 -1
  18. package/dist/index.browser.cjs +158 -33
  19. package/dist/index.browser.cjs.map +1 -1
  20. package/dist/index.browser.d.cts +485 -68
  21. package/dist/index.browser.d.ts +485 -68
  22. package/dist/index.browser.js +159 -34
  23. package/dist/index.browser.js.map +1 -1
  24. package/dist/index.cjs +160 -35
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +485 -68
  27. package/dist/index.d.ts +485 -68
  28. package/dist/index.js +160 -34
  29. package/dist/index.js.map +1 -1
  30. package/dist/stega.browser.d.cts +485 -68
  31. package/dist/stega.browser.d.ts +485 -68
  32. package/dist/stega.d.cts +485 -68
  33. package/dist/stega.d.ts +485 -68
  34. package/package.json +1 -1
  35. package/src/agent/actions/AgentActionsClient.ts +29 -2
  36. package/src/agent/actions/commonTypes.ts +57 -17
  37. package/src/agent/actions/generate.ts +36 -2
  38. package/src/agent/actions/patch.ts +136 -0
  39. package/src/agent/actions/prompt.ts +145 -0
  40. package/src/agent/actions/transform.ts +27 -4
  41. package/src/agent/actions/translate.ts +5 -2
  42. package/src/csm/walkMap.ts +1 -1
  43. package/src/data/eventsource.ts +16 -7
  44. package/src/data/listen.ts +10 -4
  45. package/src/data/live.ts +13 -5
  46. package/src/defineCreateClient.ts +7 -1
  47. package/src/http/errors.ts +92 -27
  48. package/src/http/request.ts +3 -3
  49. package/src/http/requestOptions.ts +4 -0
  50. package/src/types.ts +39 -10
  51. package/src/util/codeFrame.ts +174 -0
  52. package/src/{csm → util}/isRecord.ts +1 -1
  53. package/umd/sanityClient.js +161 -36
  54. package/umd/sanityClient.min.js +2 -2
@@ -1,5 +1,6 @@
1
1
  import {defer, isObservable, mergeMap, Observable, of} from 'rxjs'
2
2
 
3
+ import {formatQueryParseError, isQueryParseError} from '../http/errors'
3
4
  import {type Any} from '../types'
4
5
 
5
6
  /**
@@ -169,8 +170,10 @@ function connectWithESInstance<EventTypeName extends string>(
169
170
  }
170
171
  if (message.type === 'channelError') {
171
172
  // An error occurred. This is different from a network-level error (which will be emitted as 'error').
172
- // Possible causes are things such as malformed filters, non-existant datasets or similar.
173
- observer.error(new ChannelError(extractErrorMessage(event?.data), event.data))
173
+ // Possible causes are things such as malformed filters, non-existant datasets
174
+ // or similar.
175
+ const tag = new URL(es.url).searchParams.get('tag')
176
+ observer.error(new ChannelError(extractErrorMessage(event?.data, tag), event.data))
174
177
  return
175
178
  }
176
179
  if (message.type === 'disconnect') {
@@ -235,16 +238,22 @@ function parseEvent(
235
238
  }
236
239
  }
237
240
 
238
- function extractErrorMessage(err: Any) {
239
- if (!err.error) {
241
+ function extractErrorMessage(err: Any, tag?: string | null) {
242
+ const error = err.error
243
+
244
+ if (!error) {
240
245
  return err.message || 'Unknown listener error'
241
246
  }
242
247
 
243
- if (err.error.description) {
244
- return err.error.description
248
+ if (isQueryParseError(error)) {
249
+ return formatQueryParseError(error, tag)
250
+ }
251
+
252
+ if (error.description) {
253
+ return error.description
245
254
  }
246
255
 
247
- return typeof err.error === 'string' ? err.error : JSON.stringify(err.error, null, 2)
256
+ return typeof error === 'string' ? error : JSON.stringify(error, null, 2)
248
257
  }
249
258
 
250
259
  function isEmptyObject(data: object) {
@@ -70,7 +70,7 @@ export function _listen<R extends Record<string, Any> = Record<string, Any>>(
70
70
  params?: ListenParams,
71
71
  opts: ListenOptions = {},
72
72
  ): Observable<MutationEvent<R> | ListenEvent<R>> {
73
- const {url, token, withCredentials, requestTagPrefix} = this.config()
73
+ const {url, token, withCredentials, requestTagPrefix, headers: configHeaders} = this.config()
74
74
  const tag = opts.tag && requestTagPrefix ? [requestTagPrefix, opts.tag].join('.') : opts.tag
75
75
  const options = {...defaults(opts, defaultOptions), tag}
76
76
  const listenOpts = pick(options, possibleOptions)
@@ -88,9 +88,15 @@ export function _listen<R extends Record<string, Any> = Record<string, Any>>(
88
88
  esOptions.withCredentials = true
89
89
  }
90
90
 
91
- if (token) {
92
- esOptions.headers = {
93
- Authorization: `Bearer ${token}`,
91
+ if (token || configHeaders) {
92
+ esOptions.headers = {}
93
+
94
+ if (token) {
95
+ esOptions.headers.Authorization = `Bearer ${token}`
96
+ }
97
+
98
+ if (configHeaders) {
99
+ Object.assign(esOptions.headers, configHeaders)
94
100
  }
95
101
  }
96
102
 
package/src/data/live.ts CHANGED
@@ -52,6 +52,7 @@ export class LiveClient {
52
52
  token,
53
53
  withCredentials,
54
54
  requestTagPrefix,
55
+ headers: configHeaders,
55
56
  } = this.#client.config()
56
57
  const apiVersion = _apiVersion.replace(/^v/, '')
57
58
  if (apiVersion !== 'X' && apiVersion < requiredApiVersion) {
@@ -76,15 +77,22 @@ export class LiveClient {
76
77
  url.searchParams.set('includeDrafts', 'true')
77
78
  }
78
79
  const esOptions: EventSourceInit & {headers?: Record<string, string>} = {}
79
- if (includeDrafts && token) {
80
- esOptions.headers = {
81
- Authorization: `Bearer ${token}`,
82
- }
83
- }
84
80
  if (includeDrafts && withCredentials) {
85
81
  esOptions.withCredentials = true
86
82
  }
87
83
 
84
+ if ((includeDrafts && token) || configHeaders) {
85
+ esOptions.headers = {}
86
+
87
+ if (includeDrafts && token) {
88
+ esOptions.headers.Authorization = `Bearer ${token}`
89
+ }
90
+
91
+ if (configHeaders) {
92
+ Object.assign(esOptions.headers, configHeaders)
93
+ }
94
+ }
95
+
88
96
  const key = `${url.href}::${JSON.stringify(esOptions)}`
89
97
  const existing = eventsCache.get(key)
90
98
 
@@ -17,7 +17,13 @@ export {
17
17
  } from './data/eventsource'
18
18
  export * from './data/patch'
19
19
  export * from './data/transaction'
20
- export {ClientError, CorsOriginError, ServerError} from './http/errors'
20
+ export {
21
+ ClientError,
22
+ CorsOriginError,
23
+ formatQueryParseError,
24
+ isQueryParseError,
25
+ ServerError,
26
+ } from './http/errors'
21
27
  export * from './SanityClient'
22
28
  export * from './types'
23
29
 
@@ -1,4 +1,8 @@
1
- import type {ActionError, Any, ErrorProps, MutationError} from '../types'
1
+ import type {HttpContext} from 'get-it'
2
+
3
+ import type {ActionError, Any, ErrorProps, MutationError, QueryParseError} from '../types'
4
+ import {codeFrame} from '../util/codeFrame'
5
+ import {isRecord} from '../util/isRecord'
2
6
 
3
7
  const MAX_ITEMS_IN_ERROR_MESSAGE = 5
4
8
 
@@ -9,8 +13,8 @@ export class ClientError extends Error {
9
13
  responseBody: ErrorProps['responseBody']
10
14
  details: ErrorProps['details']
11
15
 
12
- constructor(res: Any) {
13
- const props = extractErrorProps(res)
16
+ constructor(res: Any, context?: HttpContext) {
17
+ const props = extractErrorProps(res, context)
14
18
  super(props.message)
15
19
  Object.assign(this, props)
16
20
  }
@@ -30,7 +34,7 @@ export class ServerError extends Error {
30
34
  }
31
35
  }
32
36
 
33
- function extractErrorProps(res: Any): ErrorProps {
37
+ function extractErrorProps(res: Any, context?: HttpContext): ErrorProps {
34
38
  const body = res.body
35
39
  const props = {
36
40
  response: res,
@@ -40,15 +44,35 @@ function extractErrorProps(res: Any): ErrorProps {
40
44
  details: undefined as Any,
41
45
  }
42
46
 
47
+ // Fall back early if we didn't get a JSON object returned as expected
48
+ if (!isRecord(body)) {
49
+ props.message = httpErrorMessage(res, body)
50
+ return props
51
+ }
52
+
53
+ const error = body.error
54
+
43
55
  // API/Boom style errors ({statusCode, error, message})
44
- if (body.error && body.message) {
45
- props.message = `${body.error} - ${body.message}`
56
+ if (typeof error === 'string' && typeof body.message === 'string') {
57
+ props.message = `${error} - ${body.message}`
58
+ return props
59
+ }
60
+
61
+ // Content Lake errors with a `error` prop being an object
62
+ if (typeof error !== 'object' || error === null) {
63
+ if (typeof error === 'string') {
64
+ props.message = error
65
+ } else if (typeof body.message === 'string') {
66
+ props.message = body.message
67
+ } else {
68
+ props.message = httpErrorMessage(res, body)
69
+ }
46
70
  return props
47
71
  }
48
72
 
49
73
  // Mutation errors (specifically)
50
- if (isMutationError(body) || isActionError(body)) {
51
- const allItems = body.error.items || []
74
+ if (isMutationError(error) || isActionError(error)) {
75
+ const allItems = error.items || []
52
76
  const items = allItems
53
77
  .slice(0, MAX_ITEMS_IN_ERROR_MESSAGE)
54
78
  .map((item) => item.error?.description)
@@ -57,48 +81,85 @@ function extractErrorProps(res: Any): ErrorProps {
57
81
  if (allItems.length > MAX_ITEMS_IN_ERROR_MESSAGE) {
58
82
  itemsStr += `\n...and ${allItems.length - MAX_ITEMS_IN_ERROR_MESSAGE} more`
59
83
  }
60
- props.message = `${body.error.description}${itemsStr}`
84
+ props.message = `${error.description}${itemsStr}`
61
85
  props.details = body.error
62
86
  return props
63
87
  }
64
88
 
65
- // Query/database errors ({error: {description, other, arb, props}})
66
- if (body.error && body.error.description) {
67
- props.message = body.error.description
89
+ // Query parse errors
90
+ if (isQueryParseError(error)) {
91
+ const tag = context?.options?.query?.tag
92
+ props.message = formatQueryParseError(error, tag)
68
93
  props.details = body.error
69
94
  return props
70
95
  }
71
96
 
97
+ if ('description' in error && typeof error.description === 'string') {
98
+ // Query/database errors ({error: {description, other, arb, props}})
99
+ props.message = error.description
100
+ props.details = error
101
+ return props
102
+ }
103
+
72
104
  // Other, more arbitrary errors
73
- props.message = body.error || body.message || httpErrorMessage(res)
105
+ props.message = httpErrorMessage(res, body)
74
106
  return props
75
107
  }
76
108
 
77
- function isMutationError(body: Any): body is MutationError {
109
+ function isMutationError(error: object): error is MutationError {
110
+ return (
111
+ 'type' in error &&
112
+ error.type === 'mutationError' &&
113
+ 'description' in error &&
114
+ typeof error.description === 'string'
115
+ )
116
+ }
117
+
118
+ function isActionError(error: object): error is ActionError {
78
119
  return (
79
- isPlainObject(body) &&
80
- isPlainObject(body.error) &&
81
- body.error.type === 'mutationError' &&
82
- typeof body.error.description === 'string'
120
+ 'type' in error &&
121
+ error.type === 'actionError' &&
122
+ 'description' in error &&
123
+ typeof error.description === 'string'
83
124
  )
84
125
  }
85
126
 
86
- function isActionError(body: Any): body is ActionError {
127
+ /** @internal */
128
+ export function isQueryParseError(error: object): error is QueryParseError {
87
129
  return (
88
- isPlainObject(body) &&
89
- isPlainObject(body.error) &&
90
- body.error.type === 'actionError' &&
91
- typeof body.error.description === 'string'
130
+ isRecord(error) &&
131
+ error.type === 'queryParseError' &&
132
+ typeof error.query === 'string' &&
133
+ typeof error.start === 'number' &&
134
+ typeof error.end === 'number'
92
135
  )
93
136
  }
94
137
 
95
- function isPlainObject(obj: Any): obj is Record<string, unknown> {
96
- return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
138
+ /**
139
+ * Formats a GROQ query parse error into a human-readable string.
140
+ *
141
+ * @param error - The error object containing details about the parse error.
142
+ * @param tag - An optional tag to include in the error message.
143
+ * @returns A formatted error message string.
144
+ * @public
145
+ */
146
+ export function formatQueryParseError(error: QueryParseError, tag?: string | null) {
147
+ const {query, start, end, description} = error
148
+
149
+ if (!query || typeof start === 'undefined') {
150
+ return `GROQ query parse error: ${description}`
151
+ }
152
+
153
+ const withTag = tag ? `\n\nTag: ${tag}` : ''
154
+ const framed = codeFrame(query, {start, end}, description)
155
+
156
+ return `GROQ query parse error:\n${framed}${withTag}`
97
157
  }
98
158
 
99
- function httpErrorMessage(res: Any) {
159
+ function httpErrorMessage(res: Any, body: unknown) {
160
+ const details = typeof body === 'string' ? ` (${sliceWithEllipsis(body, 100)})` : ''
100
161
  const statusMessage = res.statusMessage ? ` ${res.statusMessage}` : ''
101
- return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}`
162
+ return `${res.method}-request to ${res.url} resulted in HTTP ${res.statusCode}${statusMessage}${details}`
102
163
  }
103
164
 
104
165
  function stringifyBody(body: Any, res: Any) {
@@ -107,6 +168,10 @@ function stringifyBody(body: Any, res: Any) {
107
168
  return isJson ? JSON.stringify(body, null, 2) : body
108
169
  }
109
170
 
171
+ function sliceWithEllipsis(str: string, max: number) {
172
+ return str.length > max ? `${str.slice(0, max)}…` : str
173
+ }
174
+
110
175
  /** @public */
111
176
  export class CorsOriginError extends Error {
112
177
  projectId: string
@@ -1,4 +1,4 @@
1
- import {getIt, type Middlewares, type Requester} from 'get-it'
1
+ import {getIt, type HttpContext, type Middlewares, type Requester} from 'get-it'
2
2
  import {jsonRequest, jsonResponse, observable, progress, retry} from 'get-it/middleware'
3
3
  import {Observable} from 'rxjs'
4
4
 
@@ -6,11 +6,11 @@ import type {Any} from '../types'
6
6
  import {ClientError, ServerError} from './errors'
7
7
 
8
8
  const httpError = {
9
- onResponse: (res: Any) => {
9
+ onResponse: (res: Any, context: HttpContext) => {
10
10
  if (res.statusCode >= 500) {
11
11
  throw new ServerError(res)
12
12
  } else if (res.statusCode >= 400) {
13
- throw new ClientError(res)
13
+ throw new ClientError(res, context)
14
14
  }
15
15
 
16
16
  return res
@@ -7,6 +7,10 @@ const projectHeader = 'X-Sanity-Project-ID'
7
7
  export function requestOptions(config: Any, overrides: Any = {}): Omit<RequestOptions, 'url'> {
8
8
  const headers: Any = {}
9
9
 
10
+ if (config.headers) {
11
+ Object.assign(headers, config.headers)
12
+ }
13
+
10
14
  const token = overrides.token || config.token
11
15
  if (token) {
12
16
  headers.Authorization = `Bearer ${token}`
package/src/types.ts CHANGED
@@ -102,6 +102,14 @@ export interface ClientConfig {
102
102
  * Optional request tag prefix for all request tags
103
103
  */
104
104
  requestTagPrefix?: string
105
+
106
+ /**
107
+ * Optional default headers to include with all requests
108
+ *
109
+ * @remarks request-specific headers will override any default headers with the same name.
110
+ */
111
+ headers?: Record<string, string>
112
+
105
113
  ignoreBrowserTokenWarning?: boolean
106
114
  withCredentials?: boolean
107
115
  allowReconfigure?: boolean
@@ -173,6 +181,12 @@ export interface InitializedClientConfig extends ClientConfig {
173
181
  * The fully initialized stega config, can be used to check if stega is enabled
174
182
  */
175
183
  stega: InitializedStegaConfig
184
+ /**
185
+ * Default headers to include with all requests
186
+ *
187
+ * @remarks request-specific headers will override any default headers with the same name.
188
+ */
189
+ headers?: Record<string, string>
176
190
  }
177
191
 
178
192
  /** @public */
@@ -1359,11 +1373,26 @@ export interface ApiError {
1359
1373
 
1360
1374
  /** @internal */
1361
1375
  export interface MutationError {
1362
- error: {
1363
- type: 'mutationError'
1364
- description: string
1365
- items?: MutationErrorItem[]
1366
- }
1376
+ type: 'mutationError'
1377
+ description: string
1378
+ items?: MutationErrorItem[]
1379
+ }
1380
+
1381
+ /**
1382
+ * Returned from the Content Lake API when a query is malformed, usually with a start
1383
+ * and end column to indicate where the error occurred, but not always. Can we used to
1384
+ * provide a more structured error message to the user.
1385
+ *
1386
+ * This will be located under the response `error` property.
1387
+ *
1388
+ * @public
1389
+ */
1390
+ export interface QueryParseError {
1391
+ type: 'queryParseError'
1392
+ description: string
1393
+ start?: number
1394
+ end?: number
1395
+ query?: string
1367
1396
  }
1368
1397
 
1369
1398
  /** @internal */
@@ -1377,11 +1406,9 @@ export interface MutationErrorItem {
1377
1406
 
1378
1407
  /** @internal */
1379
1408
  export interface ActionError {
1380
- error: {
1381
- type: 'actionError'
1382
- description: string
1383
- items?: ActionErrorItem[]
1384
- }
1409
+ type: 'actionError'
1410
+ description: string
1411
+ items?: ActionErrorItem[]
1385
1412
  }
1386
1413
 
1387
1414
  /** @internal */
@@ -1605,6 +1632,8 @@ export type {
1605
1632
  GenerateTargetDocument,
1606
1633
  GenerateTargetInclude,
1607
1634
  } from './agent/actions/generate'
1635
+ export type {PatchDocument, PatchOperation, PatchTarget} from './agent/actions/patch'
1636
+ export type {PromptRequest} from './agent/actions/prompt'
1608
1637
  export type {
1609
1638
  TransformDocument,
1610
1639
  TransformTarget,
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Inlined, modified version of the `codeFrameColumns` function from `@babel/code-frame`.
3
+ * MIT-licensed - https://github.com/babel/babel/blob/main/LICENSE
4
+ * Copyright (c) 2014-present Sebastian McKenzie and other contributors.
5
+ */
6
+ type Location = {
7
+ column: number
8
+ line: number
9
+ }
10
+
11
+ type NodeLocation = {
12
+ start: Location
13
+ end?: Location
14
+ }
15
+
16
+ type GroqLocation = {
17
+ start: number
18
+ end?: number
19
+ }
20
+
21
+ /**
22
+ * RegExp to test for newlines.
23
+ */
24
+
25
+ const NEWLINE = /\r\n|[\n\r\u2028\u2029]/
26
+
27
+ /**
28
+ * Extract what lines should be marked and highlighted.
29
+ */
30
+
31
+ type MarkerLines = Record<number, true | [number, number]>
32
+
33
+ /**
34
+ * Highlight a code frame with the given location and message.
35
+ *
36
+ * @param query - The query to be highlighted.
37
+ * @param location - The location of the error in the code/query.
38
+ * @param message - Message to be displayed inline (if possible) next to the highlighted
39
+ * location in the code. If it can't be positioned inline, it will be placed above the
40
+ * code frame.
41
+ * @returns The highlighted code frame.
42
+ */
43
+ export function codeFrame(query: string, location: GroqLocation, message?: string): string {
44
+ const lines = query.split(NEWLINE)
45
+ const loc = {
46
+ start: columnToLine(location.start, lines),
47
+ end: location.end ? columnToLine(location.end, lines) : undefined,
48
+ }
49
+
50
+ const {start, end, markerLines} = getMarkerLines(loc, lines)
51
+
52
+ const numberMaxWidth = `${end}`.length
53
+
54
+ return query
55
+ .split(NEWLINE, end)
56
+ .slice(start, end)
57
+ .map((line, index) => {
58
+ const number = start + 1 + index
59
+ const paddedNumber = ` ${number}`.slice(-numberMaxWidth)
60
+ const gutter = ` ${paddedNumber} |`
61
+ const hasMarker = markerLines[number]
62
+ const lastMarkerLine = !markerLines[number + 1]
63
+ if (!hasMarker) {
64
+ return ` ${gutter}${line.length > 0 ? ` ${line}` : ''}`
65
+ }
66
+
67
+ let markerLine = ''
68
+ if (Array.isArray(hasMarker)) {
69
+ const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, ' ')
70
+ const numberOfMarkers = hasMarker[1] || 1
71
+
72
+ markerLine = [
73
+ '\n ',
74
+ gutter.replace(/\d/g, ' '),
75
+ ' ',
76
+ markerSpacing,
77
+ '^'.repeat(numberOfMarkers),
78
+ ].join('')
79
+
80
+ if (lastMarkerLine && message) {
81
+ markerLine += ' ' + message
82
+ }
83
+ }
84
+ return ['>', gutter, line.length > 0 ? ` ${line}` : '', markerLine].join('')
85
+ })
86
+ .join('\n')
87
+ }
88
+
89
+ function getMarkerLines(
90
+ loc: NodeLocation,
91
+ source: Array<string>,
92
+ ): {
93
+ start: number
94
+ end: number
95
+ markerLines: MarkerLines
96
+ } {
97
+ const startLoc: Location = {...loc.start}
98
+ const endLoc: Location = {...startLoc, ...loc.end}
99
+ const linesAbove = 2
100
+ const linesBelow = 3
101
+ const startLine = startLoc.line ?? -1
102
+ const startColumn = startLoc.column ?? 0
103
+ const endLine = endLoc.line
104
+ const endColumn = endLoc.column
105
+
106
+ let start = Math.max(startLine - (linesAbove + 1), 0)
107
+ let end = Math.min(source.length, endLine + linesBelow)
108
+
109
+ if (startLine === -1) {
110
+ start = 0
111
+ }
112
+
113
+ if (endLine === -1) {
114
+ end = source.length
115
+ }
116
+
117
+ const lineDiff = endLine - startLine
118
+ const markerLines: MarkerLines = {}
119
+
120
+ if (lineDiff) {
121
+ for (let i = 0; i <= lineDiff; i++) {
122
+ const lineNumber = i + startLine
123
+
124
+ if (!startColumn) {
125
+ markerLines[lineNumber] = true
126
+ } else if (i === 0) {
127
+ const sourceLength = source[lineNumber - 1].length
128
+
129
+ markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1]
130
+ } else if (i === lineDiff) {
131
+ markerLines[lineNumber] = [0, endColumn]
132
+ } else {
133
+ const sourceLength = source[lineNumber - i].length
134
+
135
+ markerLines[lineNumber] = [0, sourceLength]
136
+ }
137
+ }
138
+ } else {
139
+ if (startColumn === endColumn) {
140
+ if (startColumn) {
141
+ markerLines[startLine] = [startColumn, 0]
142
+ } else {
143
+ markerLines[startLine] = true
144
+ }
145
+ } else {
146
+ markerLines[startLine] = [startColumn, endColumn - startColumn]
147
+ }
148
+ }
149
+
150
+ return {start, end, markerLines}
151
+ }
152
+
153
+ function columnToLine(column: number, lines: string[]): Location {
154
+ let offset = 0
155
+
156
+ for (let i = 0; i < lines.length; i++) {
157
+ const lineLength = lines[i].length + 1 // assume '\n' after each line
158
+
159
+ if (offset + lineLength > column) {
160
+ return {
161
+ line: i + 1, // 1-based line
162
+ column: column - offset, // 0-based column
163
+ }
164
+ }
165
+
166
+ offset += lineLength
167
+ }
168
+
169
+ // Fallback: beyond last line
170
+ return {
171
+ line: lines.length,
172
+ column: lines[lines.length - 1]?.length ?? 0,
173
+ }
174
+ }
@@ -1,4 +1,4 @@
1
1
  /** @internal */
2
2
  export function isRecord(value: unknown): value is Record<string, unknown> {
3
- return typeof value === 'object' && value !== null
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
4
4
  }