@sanity/client 7.3.0 → 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.
- package/README.md +122 -1
- package/dist/_chunks-cjs/isRecord.cjs +6 -0
- package/dist/_chunks-cjs/isRecord.cjs.map +1 -0
- package/dist/_chunks-cjs/resolveEditInfo.cjs +3 -5
- package/dist/_chunks-cjs/resolveEditInfo.cjs.map +1 -1
- package/dist/_chunks-cjs/stegaClean.cjs +4 -0
- package/dist/_chunks-cjs/stegaClean.cjs.map +1 -1
- package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs +2 -5
- package/dist/_chunks-cjs/stegaEncodeSourceMap.cjs.map +1 -1
- package/dist/_chunks-es/isRecord.js +7 -0
- package/dist/_chunks-es/isRecord.js.map +1 -0
- package/dist/_chunks-es/resolveEditInfo.js +1 -3
- package/dist/_chunks-es/resolveEditInfo.js.map +1 -1
- package/dist/_chunks-es/stegaClean.js +4 -0
- package/dist/_chunks-es/stegaClean.js.map +1 -1
- package/dist/_chunks-es/stegaEncodeSourceMap.js +1 -4
- package/dist/_chunks-es/stegaEncodeSourceMap.js.map +1 -1
- package/dist/index.browser.cjs +155 -32
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.d.cts +473 -68
- package/dist/index.browser.d.ts +473 -68
- package/dist/index.browser.js +156 -33
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +157 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +473 -68
- package/dist/index.d.ts +473 -68
- package/dist/index.js +157 -33
- package/dist/index.js.map +1 -1
- package/dist/stega.browser.d.cts +473 -68
- package/dist/stega.browser.d.ts +473 -68
- package/dist/stega.d.cts +473 -68
- package/dist/stega.d.ts +473 -68
- package/package.json +1 -1
- package/src/agent/actions/AgentActionsClient.ts +29 -2
- package/src/agent/actions/commonTypes.ts +57 -17
- package/src/agent/actions/generate.ts +36 -2
- package/src/agent/actions/patch.ts +136 -0
- package/src/agent/actions/prompt.ts +145 -0
- package/src/agent/actions/transform.ts +27 -4
- package/src/agent/actions/translate.ts +5 -2
- package/src/csm/walkMap.ts +1 -1
- package/src/data/eventsource.ts +16 -7
- package/src/data/listen.ts +10 -4
- package/src/data/live.ts +13 -5
- package/src/defineCreateClient.ts +7 -1
- package/src/http/errors.ts +92 -27
- package/src/http/request.ts +3 -3
- package/src/types.ts +25 -10
- package/src/util/codeFrame.ts +174 -0
- package/src/{csm → util}/isRecord.ts +1 -1
- package/umd/sanityClient.js +158 -35
- package/umd/sanityClient.min.js +2 -2
package/src/data/eventsource.ts
CHANGED
|
@@ -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
|
|
173
|
-
|
|
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
|
-
|
|
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 (
|
|
244
|
-
return
|
|
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
|
|
256
|
+
return typeof error === 'string' ? error : JSON.stringify(error, null, 2)
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
function isEmptyObject(data: object) {
|
package/src/data/listen.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
|
package/src/http/errors.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
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 (
|
|
45
|
-
props.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(
|
|
51
|
-
const allItems =
|
|
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 = `${
|
|
84
|
+
props.message = `${error.description}${itemsStr}`
|
|
61
85
|
props.details = body.error
|
|
62
86
|
return props
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
// Query
|
|
66
|
-
if (
|
|
67
|
-
|
|
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 =
|
|
105
|
+
props.message = httpErrorMessage(res, body)
|
|
74
106
|
return props
|
|
75
107
|
}
|
|
76
108
|
|
|
77
|
-
function isMutationError(
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
typeof
|
|
120
|
+
'type' in error &&
|
|
121
|
+
error.type === 'actionError' &&
|
|
122
|
+
'description' in error &&
|
|
123
|
+
typeof error.description === 'string'
|
|
83
124
|
)
|
|
84
125
|
}
|
|
85
126
|
|
|
86
|
-
|
|
127
|
+
/** @internal */
|
|
128
|
+
export function isQueryParseError(error: object): error is QueryParseError {
|
|
87
129
|
return (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
typeof
|
|
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
|
-
|
|
96
|
-
|
|
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
|
package/src/http/request.ts
CHANGED
|
@@ -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
|
package/src/types.ts
CHANGED
|
@@ -1373,11 +1373,26 @@ export interface ApiError {
|
|
|
1373
1373
|
|
|
1374
1374
|
/** @internal */
|
|
1375
1375
|
export interface MutationError {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
|
1381
1396
|
}
|
|
1382
1397
|
|
|
1383
1398
|
/** @internal */
|
|
@@ -1391,11 +1406,9 @@ export interface MutationErrorItem {
|
|
|
1391
1406
|
|
|
1392
1407
|
/** @internal */
|
|
1393
1408
|
export interface ActionError {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
items?: ActionErrorItem[]
|
|
1398
|
-
}
|
|
1409
|
+
type: 'actionError'
|
|
1410
|
+
description: string
|
|
1411
|
+
items?: ActionErrorItem[]
|
|
1399
1412
|
}
|
|
1400
1413
|
|
|
1401
1414
|
/** @internal */
|
|
@@ -1619,6 +1632,8 @@ export type {
|
|
|
1619
1632
|
GenerateTargetDocument,
|
|
1620
1633
|
GenerateTargetInclude,
|
|
1621
1634
|
} from './agent/actions/generate'
|
|
1635
|
+
export type {PatchDocument, PatchOperation, PatchTarget} from './agent/actions/patch'
|
|
1636
|
+
export type {PromptRequest} from './agent/actions/prompt'
|
|
1622
1637
|
export type {
|
|
1623
1638
|
TransformDocument,
|
|
1624
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
|
+
}
|