@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.
- package/README.md +228 -2
- package/dist/bin/contentMapper.d.ts +1 -0
- package/dist/bin/contentMapper.js +11 -0
- package/dist/bin/contentMapper.js.map +1 -0
- package/dist/contentMapper-BMNwbuMF.js +896 -0
- package/dist/contentMapper-BMNwbuMF.js.map +1 -0
- package/dist/index.d.ts +236 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/jest.d.ts +30 -0
- package/dist/jest.d.ts.map +1 -0
- package/dist/jest.js +30 -0
- package/dist/jest.js.map +1 -0
- package/dist/next.d.ts +46 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +122 -0
- package/dist/next.js.map +1 -0
- package/dist/nodeHooks-CRMKGlwf.js +21 -0
- package/dist/nodeHooks-CRMKGlwf.js.map +1 -0
- package/dist/nodeHooks.d.ts +8 -0
- package/dist/nodeHooks.d.ts.map +1 -0
- package/dist/nodeHooks.js +1 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +6 -0
- package/dist/register.js.map +1 -0
- package/dist/runtime-DNC5x51t.js +23 -0
- package/dist/runtime-DNC5x51t.js.map +1 -0
- package/dist/vite.d.ts +32 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +34 -0
- package/dist/vite.js.map +1 -0
- package/package.json +73 -4
|
@@ -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
|