@sanity-labs/groq-content-mapper 0.0.0 → 0.1.1

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.
@@ -0,0 +1,896 @@
1
+ import { createHash } from "node:crypto";
2
+ import path, { dirname, isAbsolute, resolve, sep } from "node:path";
3
+ import "node:stream";
4
+ import * as v from "valibot";
5
+ import * as t from "@babel/types";
6
+ import { GroqSyntaxError, parse, typeEvaluate } from "groq-js";
7
+ import { CodeGenerator } from "@babel/generator";
8
+ import * as codegen from "@sanity/codegen";
9
+ import { readFile } from "node:fs/promises";
10
+ /**
11
+ * Names `@sanity/codegen` gives the helpers a schema type can refer to. A module that refers to
12
+ * one declares it locally, as `sanity.types.ts` does.
13
+ */
14
+ const ARRAY_OF = "ArrayOf", INTERNAL_REFERENCE_SYMBOL = "internalGroqTypeReferenceTo", RESERVED_IDENTIFIERS = /* @__PURE__ */ new Set([
15
+ "AllSanitySchemaTypes",
16
+ ARRAY_OF,
17
+ INTERNAL_REFERENCE_SYMBOL,
18
+ "SanityQueries"
19
+ ]);
20
+ /** `filename` relative to `root`, with forward slashes on every platform. */
21
+ function normalizePrintablePath(root, filename) {
22
+ return path.relative(root, path.resolve(root, filename)).replaceAll("\\", "/");
23
+ }
24
+ /** Whether a string is a valid ECMAScript IdentifierName. */
25
+ function isIdentifierName(input) {
26
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(input);
27
+ }
28
+ /** A PascalCase identifier for `name` that is not in `taken`, suffixed `_2`, `_3`… as needed. */
29
+ function getUniqueIdentifierForName(name, taken) {
30
+ let sanitized = name.replace(/^\d/, "_").replaceAll(/[^$\w]+(.)/g, (_, char) => char.toUpperCase()), desired = `${sanitized.charAt(0).toUpperCase()}${sanitized.slice(1)}`, resulting = desired;
31
+ for (let index = 2; taken.has(resulting) || RESERVED_IDENTIFIERS.has(resulting); index++) resulting = `${desired}_${index}`;
32
+ return t.identifier(resulting);
33
+ }
34
+ function generateCode(node) {
35
+ return `${new CodeGenerator(node).generate().code.trim()}\n\n`;
36
+ }
37
+ function getMessage(error) {
38
+ return typeof error == "object" && error && "message" in error && typeof error.message == "string" ? error.message : "Unknown error";
39
+ }
40
+ /**
41
+ * Parses a GROQ query for type evaluation. groq-js only accepts constant slice bounds, so a
42
+ * `$param` used as one is bound to `0` first: the value does not matter for its type. Not for
43
+ * queries that are executed.
44
+ */
45
+ function safeParseQuery(query) {
46
+ let params = {};
47
+ for (let param of extractSliceParams(query)) params[param] = 0;
48
+ return parse(query, { params });
49
+ }
50
+ /** The names of the `$params` used as slice bounds, as in `[$start...$end]` or `[10..$end]`. */
51
+ function* extractSliceParams(query) {
52
+ for (let match of query.matchAll(/\[\s*(\$(\w+)|\d+)\s*\.\.\.?\s*(\$(\w+)|\d+)\s*\]/g)) match[1] === `$${match[2]}` && (yield match[2]), match[3] === `$${match[4]}` && (yield match[4]);
53
+ }
54
+ /** The attribute a probe projection writes the counterpart's type to. */
55
+ const PROBE = "__sanityGroqParam", UNKNOWN = { type: "unknown" }, PARAMETER = /\$([A-Za-z_]\w*)/g, ROOT = {
56
+ depth: 0,
57
+ wrap: (expr) => expr
58
+ };
59
+ /**
60
+ * The type of every `$param` in a query, in order of first appearance.
61
+ *
62
+ * A placeholder compared with a field takes the field's type: `slug.current == $slug` is
63
+ * `string`, `role == $role` keeps a string enum, `tags == $tags` is `Array<string>`,
64
+ * `$tag in tags` is `string`, `_id in $ids` is `Array<string>`, and `title match $term` is
65
+ * `string`. Slice bounds are `number`. Field types come from groq-js's own type evaluation, so
66
+ * dereferences, `_type` narrowing in a filter, nested scopes and inline types resolve as they do
67
+ * for the result type. A placeholder compared with a literal widens to the literal's primitive.
68
+ * One that is only passed to a function or projected is `unknown`, and still required.
69
+ *
70
+ * @param ast - The query as `safeParseQuery` parsed it, so slice bounds are already constants
71
+ */
72
+ function inferParams({ ast, query, schema }) {
73
+ let found = {
74
+ comparisons: [],
75
+ names: /* @__PURE__ */ new Set()
76
+ };
77
+ walk(ast, ROOT, found);
78
+ let code = withoutLiterals(query), types = /* @__PURE__ */ new Map();
79
+ for (let name of extractSliceParams(code)) types.set(name, [{ type: "number" }]);
80
+ for (let name of found.names) types.set(name, types.get(name) ?? []);
81
+ for (let comparison of found.comparisons) {
82
+ let type = resolve$1(comparison, schema);
83
+ type && types.get(comparison.name)?.push(type);
84
+ }
85
+ let params = /* @__PURE__ */ new Map(), inOrder = [...code.matchAll(PARAMETER)].map((match) => match[1]);
86
+ for (let name of [...inOrder, ...types.keys()]) {
87
+ let candidates = types.get(name);
88
+ candidates && !params.has(name) && params.set(name, combine(candidates));
89
+ }
90
+ return params;
91
+ }
92
+ function walk(node, scope, found) {
93
+ switch (node.type) {
94
+ case "Filter":
95
+ walk(node.base, scope, found), walk(node.expr, nested(scope, (probe) => mapElements(node, probe)), found);
96
+ return;
97
+ case "FlatMap":
98
+ case "Map":
99
+ walk(node.base, scope, found), walk(node.expr, nested(scope, (probe) => ({
100
+ ...node,
101
+ expr: probe
102
+ })), found);
103
+ return;
104
+ case "InRange":
105
+ noteRange(node, scope, found);
106
+ break;
107
+ case "OpCall":
108
+ noteComparison(node, scope, found);
109
+ break;
110
+ case "Parameter":
111
+ found.names.add(node.name);
112
+ return;
113
+ case "PipeFuncCall": {
114
+ walk(node.base, scope, found);
115
+ let inner = nested(scope, (probe) => mapElements(node.base, probe));
116
+ for (let arg of node.args) walk(arg, inner, found);
117
+ return;
118
+ }
119
+ case "Projection": {
120
+ walk(node.base, scope, found);
121
+ let inner = node.base.type === "This" ? scope : nested(scope, (probe) => ({
122
+ ...node,
123
+ expr: probe
124
+ }));
125
+ walk(node.expr, inner, found);
126
+ return;
127
+ }
128
+ }
129
+ for (let child of children(node)) walk(child, scope, found);
130
+ }
131
+ function nested(scope, build) {
132
+ return {
133
+ depth: scope.depth + 1,
134
+ wrap: (expr) => scope.wrap(build({
135
+ attributes: [{
136
+ name: PROBE,
137
+ type: "ObjectAttributeValue",
138
+ value: expr
139
+ }],
140
+ type: "Object"
141
+ }))
142
+ };
143
+ }
144
+ /** The probe evaluated once per element of `base`, whether the elements are objects or not. */
145
+ function mapElements(base, probe) {
146
+ return {
147
+ base,
148
+ expr: probe,
149
+ type: "Map"
150
+ };
151
+ }
152
+ function noteComparison(node, scope, found) {
153
+ let left = asParameter(node.left), right = asParameter(node.right);
154
+ !!left != !!right && found.comparisons.push({
155
+ counterpart: left ? node.right : node.left,
156
+ name: (left ?? right).name,
157
+ op: node.op,
158
+ paramOnLeft: !!left,
159
+ scope
160
+ });
161
+ }
162
+ function noteRange(node, scope, found) {
163
+ for (let bound of [node.left, node.right]) {
164
+ let param = asParameter(bound);
165
+ param && found.comparisons.push({
166
+ counterpart: node.base,
167
+ name: param.name,
168
+ op: "range",
169
+ paramOnLeft: !0,
170
+ scope
171
+ });
172
+ }
173
+ }
174
+ function asParameter(node) {
175
+ return node.type === "Group" ? asParameter(node.base) : node.type === "Parameter" ? node : void 0;
176
+ }
177
+ function resolve$1({ counterpart, op, paramOnLeft, scope }, schema) {
178
+ if (op === "match") return { type: "string" };
179
+ let expr = unwrapGroups(counterpart);
180
+ if (some(expr, (node) => node.type === "Parent")) return;
181
+ let probed = probe(scope, expr, schema);
182
+ if (!probed) return;
183
+ let type = expr.type === "Value" ? widen(probed) : withoutNull(probed);
184
+ if (type.type !== "null" || expr.type === "Value") return op === "in" ? paramOnLeft ? withoutNull(elementOf(type)) : {
185
+ of: type,
186
+ type: "array"
187
+ } : type;
188
+ }
189
+ /** Evaluates `expr` in `scope`, or `undefined` when the probe cannot be typed. */
190
+ function probe(scope, expr, schema) {
191
+ let evaluated;
192
+ try {
193
+ evaluated = typeEvaluate(scope.wrap(expr), schema);
194
+ } catch {
195
+ return;
196
+ }
197
+ let types = dig(evaluated, scope.depth);
198
+ return types.length > 0 ? union(types) : void 0;
199
+ }
200
+ /** Reads the probe attribute `depth` levels down, through the arrays and unions on the way. */
201
+ function dig(type, depth) {
202
+ if (depth === 0) return [type];
203
+ switch (type.type) {
204
+ case "array": return dig(type.of, depth);
205
+ case "boolean":
206
+ case "inline":
207
+ case "null":
208
+ case "number":
209
+ case "string": return [];
210
+ case "object": {
211
+ let attribute = type.attributes[PROBE];
212
+ return attribute ? dig(attribute.value, depth - 1) : [];
213
+ }
214
+ case "union": return type.of.flatMap((member) => dig(member, depth));
215
+ case "unknown": return [type];
216
+ default: throw Error(`Unexpected type node ${JSON.stringify(type)}`);
217
+ }
218
+ }
219
+ /** One type per parameter: the union of what its comparisons say, or `unknown` if nothing does. */
220
+ function combine(types) {
221
+ let known = types.flatMap((type) => members(type)).filter((type) => type.type !== "unknown");
222
+ return known.length > 0 ? union(known) : UNKNOWN;
223
+ }
224
+ function members(type) {
225
+ return type.type === "union" ? type.of.flatMap((member) => members(member)) : [type];
226
+ }
227
+ function union(types) {
228
+ let unique = /* @__PURE__ */ new Map();
229
+ for (let type of types.flatMap((member) => members(member))) unique.set(JSON.stringify(type), type);
230
+ let of = [...unique.values()];
231
+ return of.length === 1 ? of[0] : {
232
+ of,
233
+ type: "union"
234
+ };
235
+ }
236
+ function withoutNull(type) {
237
+ let of = members(type).filter((member) => member.type !== "null");
238
+ return of.length > 0 ? union(of) : type;
239
+ }
240
+ function elementOf(type) {
241
+ return union(members(type).map((member) => member.type === "array" ? member.of : member));
242
+ }
243
+ function widen(type) {
244
+ return union(members(type).map((member) => member.type === "string" || member.type === "number" || member.type === "boolean" ? { type: member.type } : member));
245
+ }
246
+ function unwrapGroups(node) {
247
+ return node.type === "Group" ? unwrapGroups(node.base) : node;
248
+ }
249
+ function some(node, predicate) {
250
+ return predicate(node) || children(node).some((child) => some(child, predicate));
251
+ }
252
+ function children(node) {
253
+ switch (node.type) {
254
+ case "AccessAttribute": return node.base ? [node.base] : [];
255
+ case "AccessElement":
256
+ case "ArrayCoerce":
257
+ case "Asc":
258
+ case "Deref":
259
+ case "Desc":
260
+ case "Group":
261
+ case "Neg":
262
+ case "Not":
263
+ case "Pos":
264
+ case "Slice": return [node.base];
265
+ case "And":
266
+ case "OpCall":
267
+ case "Or": return [node.left, node.right];
268
+ case "Array": return node.elements.map((element) => element.value);
269
+ case "Context":
270
+ case "Everything":
271
+ case "Parameter":
272
+ case "Parent":
273
+ case "This":
274
+ case "Value": return [];
275
+ case "Filter":
276
+ case "FlatMap":
277
+ case "Map":
278
+ case "Projection": return [node.base, node.expr];
279
+ case "FuncCall": return node.args;
280
+ case "InRange": return [
281
+ node.base,
282
+ node.left,
283
+ node.right
284
+ ];
285
+ case "Object": return node.attributes.flatMap((attribute) => attribute.type === "ObjectConditionalSplat" ? [attribute.condition, attribute.value] : [attribute.value]);
286
+ case "PipeFuncCall": return [node.base, ...node.args];
287
+ case "Select": return [...node.alternatives.flatMap((alternative) => [alternative.condition, alternative.value]), ...node.fallback ? [node.fallback] : []];
288
+ case "SelectorFuncCall": return [node.arg];
289
+ case "SelectorNested": return [node.base, node.nested];
290
+ case "Tuple": return node.members;
291
+ default: throw Error(`Unexpected GROQ node ${JSON.stringify(node)}`);
292
+ }
293
+ }
294
+ /**
295
+ * The query with the contents of string literals and comments blanked out, at the same
296
+ * positions, so a `$name` or a slice inside them is neither read nor hides a real one.
297
+ */
298
+ function withoutLiterals(query) {
299
+ let code = "", quote;
300
+ for (let index = 0; index < query.length; index++) {
301
+ let char = query[index];
302
+ if (quote) char === "\\" ? (code += " ", index++) : char === quote ? (quote = void 0, code += char) : code += " ";
303
+ else if (char === "\"" || char === "'") quote = char, code += char;
304
+ else if (char === "/" && query[index + 1] === "/") {
305
+ let end = query.indexOf("\n", index), length = (end === -1 ? query.length : end) - index;
306
+ code += " ".repeat(length), index += length - 1;
307
+ } else code += char;
308
+ }
309
+ return code;
310
+ }
311
+ const cache = /* @__PURE__ */ new WeakMap();
312
+ /** The schema's types, computed once per schema object. */
313
+ function schemaTypesFor(schema) {
314
+ let schemaTypes = cache.get(schema);
315
+ return schemaTypes || cache.set(schema, schemaTypes = createSchemaTypes(schema)), schemaTypes;
316
+ }
317
+ /**
318
+ * `@sanity/codegen` 8.2 does not export `SchemaTypeGenerator`, and keeps `generateTsType` and
319
+ * the selector that builds one private to TypeScript only. Until it exports the class, the
320
+ * instance `TypeGenerator` builds for `generateTypes` is used, which is the same object.
321
+ */
322
+ function createSchemaTypes(schema) {
323
+ let exported = codegen.SchemaTypeGenerator;
324
+ if (exported) return new exported(schema);
325
+ let schemaTypes = new codegen.TypeGenerator().getSchemaTypeGenerator?.({ schema });
326
+ if (typeof schemaTypes?.generateTsType != "function") throw TypeError("This version of @sanity/codegen does not provide SchemaTypeGenerator. Install @sanity/codegen 8.2 or later.");
327
+ return schemaTypes;
328
+ }
329
+ /** The type exports of every typed `.groq` module. */
330
+ const QUERY_RESPONSE = "QueryResponse", QUERY_PARAMS = "QueryParams", MODULE_NAMES = /* @__PURE__ */ new Set([
331
+ "query",
332
+ QUERY_PARAMS,
333
+ QUERY_RESPONSE
334
+ ]), GroqFileDiagnosticCode = {
335
+ /** The file holds no query. */
336
+ EmptyFile: 1,
337
+ /** groq-js could not evaluate the query against the schema. */
338
+ EvaluationFailed: 4,
339
+ /** No schema to type the query against. */
340
+ SchemaUnavailable: 3,
341
+ /** The query does not parse. `start` points at the error. */
342
+ SyntaxError: 2
343
+ };
344
+ /**
345
+ * Types one `.groq` file as a TypeScript module, so a text import of the file,
346
+ * `import query from './file.groq' with {type: 'text'}`, gives a query string that carries
347
+ * its own types:
348
+ *
349
+ * ```ts
350
+ * export type QueryResponse = Array<{title: string | null}>
351
+ * export type QueryParams = {slug: string}
352
+ * declare const query: '*[_type == "post" && slug.current == $slug]{title}\n' & {
353
+ * readonly __groqResult?: QueryResponse
354
+ * readonly __groqParams?: QueryParams
355
+ * }
356
+ * export default query
357
+ * ```
358
+ *
359
+ * The literal is the file's exact content, whitespace and all, because that is what a text
360
+ * import evaluates to at run time; GROQ does not mind the whitespace. The result type is
361
+ * evaluated as `@sanity/codegen` evaluates a `defineQuery` call, so `[0]` becomes `T | null`.
362
+ * Params are inferred from the `$` placeholders: a placeholder compared with a field takes the
363
+ * field's type, slice bounds are numbers, and anything else is `unknown`.
364
+ *
365
+ * A file that cannot be typed still yields a complete module, with `unknown` for what could
366
+ * not be inferred, plus a diagnostic saying why. The schema's type aliases are computed once
367
+ * per schema object.
368
+ * @beta
369
+ */
370
+ function generateGroqModule(options) {
371
+ let { content, filename, root, schema, schemaPath } = options, query = content, diagnostics = [], notes = [], report = (code, messageText, start, length) => {
372
+ let from = Math.min(start, content.length);
373
+ diagnostics.push({
374
+ code,
375
+ length: Math.min(length, content.length - from),
376
+ messageText,
377
+ start: from
378
+ });
379
+ }, render = (parts) => ({
380
+ ...renderGroqModule({
381
+ filename,
382
+ notes,
383
+ query,
384
+ root,
385
+ schemaPath,
386
+ ...parts
387
+ }),
388
+ diagnostics,
389
+ query
390
+ });
391
+ if (query.trim().length === 0) return report(GroqFileDiagnosticCode.EmptyFile, "This .groq file has no query.", 0, content.length), render({});
392
+ let ast;
393
+ try {
394
+ ast = safeParseQuery(query);
395
+ } catch (error) {
396
+ let position = error instanceof GroqSyntaxError ? error.position : 0;
397
+ return report(GroqFileDiagnosticCode.SyntaxError, getMessage(error), position, 1), render({});
398
+ }
399
+ try {
400
+ let schemaTypes = schemaTypesFor(schema), params = /* @__PURE__ */ new Map();
401
+ for (let [name, type] of inferParams({
402
+ ast,
403
+ query,
404
+ schema
405
+ })) params.set(name, schemaTypes.generateTsType(type));
406
+ let tsType = schemaTypes.generateTsType(typeEvaluate(ast, schema));
407
+ return render({
408
+ params: paramsType(params),
409
+ result: {
410
+ schemaTypes,
411
+ tsType
412
+ }
413
+ });
414
+ } catch (error) {
415
+ let message = `Could not type this query against the schema: ${getMessage(error)}`;
416
+ return report(GroqFileDiagnosticCode.EvaluationFailed, message, 0, query.length), notes.push(message), render({});
417
+ }
418
+ }
419
+ /**
420
+ * The TypeScript module for a `.groq` file.
421
+ *
422
+ * The module is self-contained. Only the schema type aliases the result and params refer to are
423
+ * declared, as module-local types, so a projection carries no schema at all and two files with
424
+ * the same query text are interchangeable.
425
+ */
426
+ function renderGroqModule(options) {
427
+ let { filename, notes, query, result, root = process.cwd(), schemaPath } = options, response = result?.tsType ?? t.tsUnknownKeyword(), params = options.params ?? recordOf(t.tsUnknownKeyword()), referenced = /* @__PURE__ */ new Set();
428
+ collectReferences(response, referenced), collectReferences(params, referenced);
429
+ let body = [];
430
+ if (result) {
431
+ let aliases = referencedSchemaTypes(result.schemaTypes, referenced), renames = renamesFor(aliases.map(({ id }) => id.name));
432
+ renames.size > 0 && (response = renamed(response, renames), params = renamed(params, renames), aliases = aliases.map(({ id, tsType }) => ({
433
+ id: t.identifier(renames.get(id.name) ?? id.name),
434
+ tsType: renamed(tsType, renames)
435
+ }))), referenced.has("internalGroqTypeReferenceTo") && body.push(internalReferenceSymbol()), referenced.has("ArrayOf") && body.push(arrayOf());
436
+ for (let { id, tsType } of aliases) body.push(t.tsTypeAliasDeclaration(id, null, tsType));
437
+ }
438
+ body.push(documented(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier(QUERY_RESPONSE), null, response)), " The result of this query, as `client.fetch` resolves it. "), documented(t.exportNamedDeclaration(t.tsTypeAliasDeclaration(t.identifier(QUERY_PARAMS), null, params)), " The values for the `$` placeholders in this query. "), queryDeclaration(query), t.exportDefaultDeclaration(t.identifier("query")));
439
+ let comments = [
440
+ ` Source: ${normalizePrintablePath(root, filename)}`,
441
+ ...schemaPath ? [` Schema: ${normalizePrintablePath(root, schemaPath)}`] : [],
442
+ ...notes.flatMap((note) => note.split("\n").map((line) => ` ${line}`))
443
+ ];
444
+ return t.addComments(body[0], "leading", comments.map((value) => ({
445
+ type: "CommentLine",
446
+ value
447
+ }))), {
448
+ ast: t.program(body),
449
+ code: body.map((statement) => generateCode(statement)).join("")
450
+ };
451
+ }
452
+ /** `{name: type}` for the inferred params, or `Record<string, never>` for a query without any. */
453
+ function paramsType(params) {
454
+ return params.size === 0 ? recordOf(t.tsNeverKeyword()) : t.tsTypeLiteral([...params].map(([name, type]) => t.tsPropertySignature(isIdentifierName(name) ? t.identifier(name) : t.stringLiteral(name), t.tsTypeAnnotation(type))));
455
+ }
456
+ /**
457
+ * The default export's declaration: the query's literal type intersected with an object of two
458
+ * optional, readonly phantom properties, `__groqResult` and `__groqParams`. The properties never
459
+ * exist at runtime, and because they are optional the value is still assignable to `string` and
460
+ * to its own literal.
461
+ */
462
+ function queryDeclaration(query) {
463
+ let brand = t.tsTypeLiteral([phantomProperty("__groqResult", QUERY_RESPONSE), phantomProperty("__groqParams", QUERY_PARAMS)]), id = t.identifier("query");
464
+ id.typeAnnotation = t.tsTypeAnnotation(t.tsIntersectionType([t.tsLiteralType(t.stringLiteral(query)), brand]));
465
+ let declaration = t.variableDeclaration("const", [t.variableDeclarator(id)]);
466
+ return declaration.declare = !0, declaration;
467
+ }
468
+ function phantomProperty(name, type) {
469
+ let property = t.tsPropertySignature(t.identifier(name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(type))));
470
+ return property.optional = !0, property.readonly = !0, property;
471
+ }
472
+ function recordOf(value) {
473
+ return t.tsTypeReference(t.identifier("Record"), t.tsTypeParameterInstantiation([t.tsStringKeyword(), value]));
474
+ }
475
+ function documented(node, comment) {
476
+ return t.addComment(node, "leading", `*${comment}`, !1);
477
+ }
478
+ /** `declare const internalGroqTypeReferenceTo: unique symbol`, as `sanity.types.ts` declares it. */
479
+ function internalReferenceSymbol() {
480
+ let id = t.identifier(INTERNAL_REFERENCE_SYMBOL);
481
+ id.typeAnnotation = t.tsTypeAnnotation(t.tsTypeOperator(t.tsSymbolKeyword(), "unique"));
482
+ let declaration = t.variableDeclaration("const", [t.variableDeclarator(id)]);
483
+ return declaration.declare = !0, declaration;
484
+ }
485
+ /** `type ArrayOf<T> = Array<T & {_key: string}>`, as `sanity.types.ts` declares it. */
486
+ function arrayOf() {
487
+ return t.tsTypeAliasDeclaration(t.identifier(ARRAY_OF), t.tsTypeParameterDeclaration([t.tsTypeParameter(null, null, "T")]), t.tsTypeReference(t.identifier("Array"), t.tsTypeParameterInstantiation([t.tsIntersectionType([t.tsTypeReference(t.identifier("T")), t.tsTypeLiteral([t.tsPropertySignature(t.identifier("_key"), t.tsTypeAnnotation(t.tsStringKeyword()))])])])));
488
+ }
489
+ /**
490
+ * The schema aliases `referenced` names, and the ones they name in turn, in schema order. Every
491
+ * name found on the way is added to `referenced`.
492
+ */
493
+ function referencedSchemaTypes(schemaTypes, referenced) {
494
+ let byName = new Map([...schemaTypes].map((schemaType) => [schemaType.id.name, schemaType])), included = /* @__PURE__ */ new Set(), queue = [...referenced];
495
+ for (let name = queue.shift(); name !== void 0; name = queue.shift()) {
496
+ let schemaType = byName.get(name);
497
+ if (!schemaType || included.has(name)) continue;
498
+ included.add(name);
499
+ let inner = /* @__PURE__ */ new Set();
500
+ collectReferences(schemaType.tsType, inner);
501
+ for (let innerName of inner) referenced.add(innerName), queue.push(innerName);
502
+ }
503
+ return [...byName.values()].filter(({ id }) => included.has(id.name));
504
+ }
505
+ /** New names for the aliases that collide with the module's own exports. */
506
+ function renamesFor(aliasNames) {
507
+ let renames = /* @__PURE__ */ new Map(), taken = /* @__PURE__ */ new Set([...MODULE_NAMES, ...aliasNames]);
508
+ for (let name of aliasNames) {
509
+ if (!MODULE_NAMES.has(name)) continue;
510
+ let id = getUniqueIdentifierForName(name, taken);
511
+ taken.add(id.name), renames.set(name, id.name);
512
+ }
513
+ return renames;
514
+ }
515
+ /** A copy of `node` with the identifiers in `renames` renamed. The original is shared and kept. */
516
+ function renamed(node, renames) {
517
+ let copy = t.cloneNode(node, !0);
518
+ return visitIdentifiers(copy, (identifier) => {
519
+ identifier.name = renames.get(identifier.name) ?? identifier.name;
520
+ }), copy;
521
+ }
522
+ /**
523
+ * Adds the identifiers a type mentions: the names of the types it refers to, and the computed
524
+ * keys of its properties, which name a symbol. Plain property names are not references.
525
+ */
526
+ function collectReferences(node, into) {
527
+ visitIdentifiers(node, (identifier) => into.add(identifier.name));
528
+ }
529
+ function visitIdentifiers(node, visit) {
530
+ if (t.isIdentifier(node)) visit(node);
531
+ else for (let key of t.VISITOR_KEYS[node.type] ?? []) {
532
+ if (key === "key" && t.isTSPropertySignature(node) && !node.computed) continue;
533
+ let child = node[key];
534
+ for (let item of Array.isArray(child) ? child : [child]) item && visitIdentifiers(item, visit);
535
+ }
536
+ }
537
+ /** @beta */
538
+ const JsonRpcErrorCode = {
539
+ InternalError: -32603,
540
+ InvalidParams: -32602,
541
+ InvalidRequest: -32600,
542
+ MethodNotFound: -32601,
543
+ ParseError: -32700
544
+ };
545
+ /**
546
+ * An error a request handler throws to answer with a specific JSON-RPC error code. Any other
547
+ * error is answered as an internal error with its message.
548
+ * @beta
549
+ */
550
+ var JsonRpcError = class extends Error {
551
+ code;
552
+ constructor(code, message) {
553
+ super(message), this.name = "JsonRpcError", this.code = code;
554
+ }
555
+ };
556
+ /**
557
+ * Serves JSON-RPC 2.0 over a pair of streams, framed with `Content-Length` headers the way the
558
+ * LSP base protocol and TypeScript's content mapper host do. Requests are answered one at a
559
+ * time, in order. Resolves once `input` ends and the last answer is written, and rejects if
560
+ * `input` errors.
561
+ * @beta
562
+ */
563
+ async function serveJsonRpc({ handle, input, output }) {
564
+ let buffer = Buffer.alloc(0);
565
+ for await (let chunk of input) {
566
+ buffer = Buffer.concat([buffer, typeof chunk == "string" ? Buffer.from(chunk) : chunk]);
567
+ for (let frame = readFrame(buffer); frame; frame = readFrame(buffer)) buffer = buffer.subarray(frame.consumed), await respond(frame.body, handle, output);
568
+ }
569
+ }
570
+ /** The next complete frame in `buffer`, or `undefined` while it is still arriving. */
571
+ function readFrame(buffer) {
572
+ let headerEnd = buffer.indexOf("\r\n\r\n");
573
+ if (headerEnd === -1) return;
574
+ let length = Number(/content-length:\s*(\d+)/i.exec(buffer.toString("utf8", 0, headerEnd))?.[1]);
575
+ if (!Number.isSafeInteger(length)) return {
576
+ body: void 0,
577
+ consumed: headerEnd + 4
578
+ };
579
+ let bodyStart = headerEnd + 4;
580
+ if (!(buffer.length < bodyStart + length)) return {
581
+ body: buffer.subarray(bodyStart, bodyStart + length),
582
+ consumed: bodyStart + length
583
+ };
584
+ }
585
+ async function respond(body, handle, output) {
586
+ if (body === void 0) {
587
+ writeError(output, null, JsonRpcErrorCode.InvalidRequest, "Invalid Request");
588
+ return;
589
+ }
590
+ let message;
591
+ try {
592
+ message = JSON.parse(body.toString("utf8"));
593
+ } catch {
594
+ writeError(output, null, JsonRpcErrorCode.ParseError, "Parse error");
595
+ return;
596
+ }
597
+ if (!isRequest(message)) writeError(output, idOf(message), JsonRpcErrorCode.InvalidRequest, "Invalid Request");
598
+ else try {
599
+ let result = await handle(message);
600
+ message.id !== void 0 && write(output, {
601
+ id: message.id,
602
+ jsonrpc: "2.0",
603
+ result
604
+ });
605
+ } catch (error) {
606
+ if (message.id === void 0) return;
607
+ let code = error instanceof JsonRpcError ? error.code : JsonRpcErrorCode.InternalError, text = error instanceof Error ? error.message : "Internal error";
608
+ writeError(output, message.id, code, text);
609
+ }
610
+ }
611
+ function isRequest(value) {
612
+ if (typeof value != "object" || !value || Array.isArray(value)) return !1;
613
+ let record = value;
614
+ return record.jsonrpc === "2.0" && typeof record.method == "string" && (!("id" in record) || isId(record.id));
615
+ }
616
+ function idOf(value) {
617
+ return typeof value != "object" || !value || !("id" in value) ? null : isId(value.id) ? value.id : null;
618
+ }
619
+ function isId(value) {
620
+ return value === null || typeof value == "string" || typeof value == "number";
621
+ }
622
+ function writeError(output, id, code, message) {
623
+ write(output, {
624
+ error: {
625
+ code,
626
+ message
627
+ },
628
+ id,
629
+ jsonrpc: "2.0"
630
+ });
631
+ }
632
+ function write(output, message) {
633
+ let body = Buffer.from(JSON.stringify(message), "utf8");
634
+ output.write(Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`), body]));
635
+ }
636
+ const pathString = v.pipe(v.string(), v.nonEmpty("Must not be empty")), schemaEntry = v.strictObject({
637
+ include: v.union([pathString, v.pipe(v.array(pathString), v.minLength(1, "List a folder"))]),
638
+ schema: pathString
639
+ });
640
+ /**
641
+ * Validates the options object of a `contentMappers` entry. What is well-formed is kept, so one
642
+ * bad entry does not stop the others from typing their files, and each problem is reported
643
+ * with its path into the object.
644
+ */
645
+ function parseGroqFilesOptions(input) {
646
+ let issues = [], options = {};
647
+ if (input !== void 0 && !isRecord(input)) return issues.push({
648
+ message: "The options must be an object.",
649
+ path: []
650
+ }), {
651
+ issues,
652
+ options
653
+ };
654
+ let record = input ?? {};
655
+ for (let key of Object.keys(record)) key !== "schema" && key !== "schemas" && issues.push({
656
+ message: `Unknown option "${key}". The options are "schema" and "schemas".`,
657
+ path: [key]
658
+ });
659
+ if (record.schema !== void 0 && (typeof record.schema == "string" && record.schema.length > 0 ? options.schema = record.schema : issues.push({
660
+ message: "\"schema\" must be the path of a schema file.",
661
+ path: ["schema"]
662
+ })), record.schemas !== void 0) {
663
+ if (Array.isArray(record.schemas)) {
664
+ options.schemas = [];
665
+ for (let [index, item] of record.schemas.entries()) {
666
+ let parsed = v.safeParse(schemaEntry, item);
667
+ if (parsed.success) options.schemas.push(parsed.output);
668
+ else for (let issue of parsed.issues) issues.push({
669
+ message: issue.message,
670
+ path: [
671
+ "schemas",
672
+ index,
673
+ ...(issue.path ?? []).map((item) => item.key)
674
+ ]
675
+ });
676
+ }
677
+ } else issues.push({
678
+ message: "\"schemas\" must be a list of {include, schema} entries.",
679
+ path: ["schemas"]
680
+ });
681
+ }
682
+ return options.schema === void 0 && !options.schemas?.length && issues.push({
683
+ message: "Set \"schema\" to the file \"sanity schema extract\" writes, or \"schemas\" to one such file per folder.",
684
+ path: []
685
+ }), {
686
+ issues,
687
+ options
688
+ };
689
+ }
690
+ /**
691
+ * Resolves `options` against `root`, the tsconfig's directory, and reads every schema it names.
692
+ * A schema that cannot be read is kept with its `error`, which the mapper reports on every
693
+ * `.groq` file that needed it, so the imports keep resolving while the schema is fixed.
694
+ *
695
+ * Without a `root`, as for a TypeScript project that has no tsconfig, only absolute paths can
696
+ * be resolved; a relative one is an error on the schema it was meant to locate.
697
+ */
698
+ async function resolveGroqFiles({ options, root }) {
699
+ let locate = (path) => root === void 0 ? isAbsolute(path) ? path : void 0 : resolve(root, path), load = async (path) => {
700
+ let located = locate(path);
701
+ return located === void 0 ? { error: `Without a tsconfig.json to resolve it against, "${path}" must be an absolute path.` } : loadSchema(located);
702
+ }, fallback = options.schema === void 0 ? void 0 : await load(options.schema), scopes = await Promise.all((options.schemas ?? []).map(async (entry) => {
703
+ let folders = (Array.isArray(entry.include) ? entry.include : [entry.include]).flatMap((folder) => {
704
+ let located = locate(folder);
705
+ return located === void 0 ? [] : [normalizePath(located)];
706
+ });
707
+ return Object.assign(await load(entry.schema), { folders });
708
+ })), watchedFiles = [fallback, ...scopes].flatMap((located) => located?.schemaPath === void 0 ? [] : [located.schemaPath]);
709
+ return {
710
+ fallback,
711
+ root,
712
+ scopes,
713
+ watchedFiles: [...new Set(watchedFiles)]
714
+ };
715
+ }
716
+ /**
717
+ * The schema for a `.groq` file: that of the `schemas` entry whose longest folder contains it,
718
+ * else `schema`, else an error saying neither covers the file.
719
+ */
720
+ function schemaFor(resolved, file) {
721
+ let path = normalizePath(file), best, depth = -1;
722
+ for (let scope of resolved.scopes) for (let folder of scope.folders) folder.length > depth && path.startsWith(`${folder}${sep}`) && (best = scope, depth = folder.length);
723
+ return best || (resolved.fallback ? resolved.fallback : { error: resolved.scopes.length > 0 ? "This file is outside every \"schemas\" folder, and no \"schema\" is set for the rest." : "No schema is configured. Set the content mapper option \"schema\" to the file \"sanity schema extract\" writes." });
724
+ }
725
+ async function loadSchema(schemaPath) {
726
+ let bytes;
727
+ try {
728
+ bytes = await readFile(schemaPath);
729
+ } catch (error) {
730
+ return {
731
+ error: typeof error == "object" && error && "code" in error && error.code === "ENOENT" ? `No schema file at ${schemaPath}. Run "sanity schema extract" to create it.` : `Could not read the schema at ${schemaPath}: ${getMessage(error)}`,
732
+ schemaPath
733
+ };
734
+ }
735
+ let schema;
736
+ try {
737
+ schema = JSON.parse(bytes.toString("utf8"));
738
+ } catch (error) {
739
+ return {
740
+ bytes,
741
+ error: `Could not parse the schema at ${schemaPath}: ${getMessage(error)}`,
742
+ schemaPath
743
+ };
744
+ }
745
+ return Array.isArray(schema) ? {
746
+ bytes,
747
+ schema,
748
+ schemaPath
749
+ } : {
750
+ bytes,
751
+ error: `${schemaPath} is not the array "sanity schema extract" writes.`,
752
+ schemaPath
753
+ };
754
+ }
755
+ /** Resolved, without a trailing separator, and case-folded where the file system is. */
756
+ function normalizePath(path) {
757
+ let resolved = resolve(path);
758
+ return process.platform === "win32" || process.platform === "darwin" ? resolved.toLowerCase() : resolved;
759
+ }
760
+ function isRecord(value) {
761
+ return typeof value == "object" && !!value && !Array.isArray(value);
762
+ }
763
+ /**
764
+ * The source TypeScript shows for the mapper's diagnostics, as in `error groq2`.
765
+ * @beta
766
+ */
767
+ const DIAGNOSTIC_SOURCE = "groq", initializeParams = v.looseObject({
768
+ positionEncodings: v.array(v.string()),
769
+ protocolVersion: v.optional(v.number())
770
+ }), openProjectParams = v.looseObject({
771
+ configFileName: v.string(),
772
+ options: v.optional(v.unknown()),
773
+ projectHandle: v.string()
774
+ }), transformParams = v.looseObject({
775
+ content: v.string(),
776
+ fileName: v.string(),
777
+ projectHandle: v.string()
778
+ }), closeProjectParams = v.looseObject({ projectHandle: v.string() });
779
+ /**
780
+ * A TypeScript 7.1 content mapper that types `.groq` files from their live content, so
781
+ * `import query from './posts.groq'` is typed with nothing generated to disk. TypeScript
782
+ * starts it from the `typescript.contentMapper` field of this package's `package.json` when a
783
+ * `tsconfig.json` registers it:
784
+ *
785
+ * ```jsonc
786
+ * {
787
+ * "contentMappers": [
788
+ * {"package": "@sanity-labs/groq-content-mapper", "extensions": [".groq"], "options": {"schema": "./schema.json"}}
789
+ * ]
790
+ * }
791
+ * ```
792
+ *
793
+ * The tsconfig is the only place the mapper is configured. `options` is a `GroqFilesOptions`
794
+ * object: `schema`, the file `sanity schema extract` writes, or `schemas`, one such file per
795
+ * folder for a codebase that queries more than one project or dataset. The mapper declares
796
+ * `dynamicConfig`, so every schema is watched and the project is reopened when one changes.
797
+ *
798
+ * One instance serves any number of projects, each under the handle TypeScript assigns it.
799
+ * @beta
800
+ */
801
+ var ContentMapper = class ContentMapper {
802
+ projects = /* @__PURE__ */ new Map();
803
+ static initialize({ positionEncodings, protocolVersion }) {
804
+ if (protocolVersion !== void 0 && protocolVersion !== 1) throw new JsonRpcError(JsonRpcErrorCode.InvalidParams, `This content mapper speaks protocol version 1, not ${protocolVersion}.`);
805
+ if (!positionEncodings.includes("utf-16")) throw new JsonRpcError(JsonRpcErrorCode.InvalidParams, "This content mapper reports positions in utf-16, which the host does not accept.");
806
+ return {
807
+ diagnosticSource: DIAGNOSTIC_SOURCE,
808
+ positionEncoding: "utf-16",
809
+ protocolVersion: 1
810
+ };
811
+ }
812
+ /**
813
+ * Answers one request of the protocol: `initialize`, `openProject`, `transform` or
814
+ * `closeProject`. Throws a `JsonRpcError` for a request the mapper cannot answer.
815
+ */
816
+ async handle(method, params) {
817
+ switch (method) {
818
+ case "closeProject": return this.projects.delete(parse$1(closeProjectParams, params, method).projectHandle), {};
819
+ case "initialize": return ContentMapper.initialize(parse$1(initializeParams, params, method));
820
+ case "openProject": return this.openProject(parse$1(openProjectParams, params, method));
821
+ case "transform": return this.transform(parse$1(transformParams, params, method));
822
+ default: throw new JsonRpcError(JsonRpcErrorCode.MethodNotFound, `Unknown method "${method}".`);
823
+ }
824
+ }
825
+ /** Serves the protocol over the streams until `input` ends. */
826
+ serve({ input, output }) {
827
+ return serveJsonRpc({
828
+ handle: (request) => this.handle(request.method, request.params),
829
+ input,
830
+ output
831
+ });
832
+ }
833
+ async openProject({ configFileName, options, projectHandle }) {
834
+ let root = configFileName === "" ? void 0 : dirname(configFileName), parsed = parseGroqFilesOptions(options), optionDiagnostics = parsed.issues.map(({ message, path }) => ({
835
+ code: 1,
836
+ messageText: message,
837
+ path
838
+ })), resolved = await resolveGroqFiles({
839
+ options: parsed.options,
840
+ root
841
+ });
842
+ this.projects.set(projectHandle, { resolved });
843
+ let identity = createHash("sha256");
844
+ for (let located of [resolved.fallback, ...resolved.scopes]) identity.update(`${located?.schemaPath ?? ""}\0`), identity.update(located?.bytes ?? "missing"), identity.update("\0");
845
+ return {
846
+ configIdentity: identity.digest("hex"),
847
+ ...optionDiagnostics.length > 0 ? { optionDiagnostics } : {},
848
+ watchedFiles: resolved.watchedFiles
849
+ };
850
+ }
851
+ transform({ content, fileName, projectHandle }) {
852
+ let project = this.projects.get(projectHandle);
853
+ if (!project) throw new JsonRpcError(JsonRpcErrorCode.InvalidParams, `Unknown project "${projectHandle}".`);
854
+ let { resolved } = project, root = resolved.root ?? dirname(fileName), located = schemaFor(resolved, fileName);
855
+ if (!located.schema) {
856
+ let messageText = located.error ?? "No schema is configured.", { code } = renderGroqModule({
857
+ filename: fileName,
858
+ notes: [messageText],
859
+ query: content,
860
+ root,
861
+ schemaPath: located.schemaPath
862
+ });
863
+ return {
864
+ diagnostics: [{
865
+ code: GroqFileDiagnosticCode.SchemaUnavailable,
866
+ length: Math.min(1, content.length),
867
+ messageText,
868
+ start: 0
869
+ }],
870
+ extension: ".mts",
871
+ text: code
872
+ };
873
+ }
874
+ let { code, diagnostics } = generateGroqModule({
875
+ content,
876
+ filename: fileName,
877
+ root,
878
+ schema: located.schema,
879
+ schemaPath: located.schemaPath
880
+ });
881
+ return {
882
+ ...diagnostics.length > 0 ? { diagnostics } : {},
883
+ extension: ".mts",
884
+ text: code
885
+ };
886
+ }
887
+ };
888
+ function parse$1(schema, params, method) {
889
+ let result = v.safeParse(schema, params);
890
+ if (result.success) return result.output;
891
+ let issues = result.issues.map((issue) => `${v.getDotPath(issue) ?? "params"}: ${issue.message}`);
892
+ throw new JsonRpcError(JsonRpcErrorCode.InvalidParams, `Invalid params for ${method}: ${issues.join("; ")}`);
893
+ }
894
+ export { serveJsonRpc as a, JsonRpcErrorCode as i, DIAGNOSTIC_SOURCE as n, GroqFileDiagnosticCode as o, JsonRpcError as r, generateGroqModule as s, ContentMapper as t };
895
+
896
+ //# sourceMappingURL=contentMapper-BMNwbuMF.js.map