@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10
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/CHANGELOG.md +450 -0
- package/README.md +1754 -156
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +98 -0
- package/dist/next/attrs.js +125 -0
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -3
- package/dist/next/index.js +34 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +704 -9
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +32 -0
- package/dist/overlay/index.js +596 -0
- package/dist/overlay/protocol.d.ts +187 -0
- package/dist/overlay/protocol.js +253 -0
- package/package.json +70 -15
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadGraphQL = loadGraphQL;
|
|
4
|
+
exports.cookTemplate = cookTemplate;
|
|
5
|
+
exports.extractDocuments = extractDocuments;
|
|
6
|
+
exports.generateGraphQLModule = generateGraphQLModule;
|
|
7
|
+
const sha256_1 = require("./next/graphql/sha256");
|
|
8
|
+
const summary_1 = require("./next/graphql/summary");
|
|
9
|
+
const tree_layout_1 = require("./next/graphql/tree-layout");
|
|
10
|
+
let loaded = null;
|
|
11
|
+
/** graphql-js, or an error that says how to get it. */
|
|
12
|
+
function loadGraphQL() {
|
|
13
|
+
if (loaded)
|
|
14
|
+
return loaded;
|
|
15
|
+
try {
|
|
16
|
+
loaded = require("graphql");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error("capa-codegen --graphql and capa persist need the graphql package: pnpm add -D graphql");
|
|
20
|
+
}
|
|
21
|
+
return loaded;
|
|
22
|
+
}
|
|
23
|
+
const INTERPOLATED = "a template with ${} in it cannot be checked ahead of time, since its text is only known at run time";
|
|
24
|
+
/** Why a literal's `${NAME}` is not read: NAME is no literal of the project, as a fragment literal is. */
|
|
25
|
+
const unknownInterpolation = (name) => `a template with \${} in it is read only when each \${} names a #graphql literal of this project, and ${name} is not one, so its text is only known at run time`;
|
|
26
|
+
const NEEDS_AS_CONST = "Add `as const` after this template: TypeScript keeps the text of a template with ${} only then, and client.graphql types the call by that text.";
|
|
27
|
+
/** `${NAME}`, a bare identifier: the one interpolation codegen resolves, from another literal of the project. */
|
|
28
|
+
const NAMED_INTERPOLATION = /\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
29
|
+
/** The variable a template is assigned to, from the text just before it: `export const CARD = `, `const Q = /* capa *\/ `, `const Q = gql(`. */
|
|
30
|
+
const ASSIGNED_TO = /(?:^|[\s;{(,])(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(?:\/\*\s*capa\s*\*\/\s*|gql\s*\(\s*)?$/;
|
|
31
|
+
const UNMARKED = "it looks like a GraphQL document but is not marked as one: start it with #graphql, put /* capa */ before it, or write gql(`...`)";
|
|
32
|
+
const FETCH_TAG = "graphql`...` is /nextjs's request helper, not a tag: write gql`...`, or start the template with #graphql";
|
|
33
|
+
/** Any template literal, and one whose text opens like a GraphQL operation or fragment. */
|
|
34
|
+
const ANY_TEMPLATE = /`((?:[^`\\]|\\[\s\S])*)`/g;
|
|
35
|
+
const LOOKS_LIKE_GRAPHQL = /^\s*(?:(?:query|fragment|mutation|subscription)\s+[_A-Za-z]\w*\s*[({@]|query\s*\{|\{\s*[_A-Za-z]\w*\s*[({])/;
|
|
36
|
+
/**
|
|
37
|
+
* The template forms codegen reads, in order of precedence for a template two
|
|
38
|
+
* of them match. A template's body is anything but a backtick, or an escaped
|
|
39
|
+
* character. `gql` as a function keeps the literal's type; as a tag it returns
|
|
40
|
+
* a plain string. `graphql` is not one of them: it is `/nextjs`'s request
|
|
41
|
+
* helper, and graphql`...` would run a request, not declare a document.
|
|
42
|
+
*/
|
|
43
|
+
const TEMPLATES = [
|
|
44
|
+
{ pattern: /\bgql\s*\(\s*`((?:[^`\\]|\\[\s\S])*)`/g, literal: true },
|
|
45
|
+
{ pattern: /\/\*\s*capa\s*\*\/\s*`((?:[^`\\]|\\[\s\S])*)`/g, literal: true },
|
|
46
|
+
{ pattern: /\bgql\s*`((?:[^`\\]|\\[\s\S])*)`/g, literal: false },
|
|
47
|
+
{ pattern: /`(\s*#graphql\b(?:[^`\\]|\\[\s\S])*)`/g, literal: true },
|
|
48
|
+
];
|
|
49
|
+
/** A template's text at run time, from its source text: line ends as LF, escapes applied. */
|
|
50
|
+
function cookTemplate(raw) {
|
|
51
|
+
const simple = { n: "\n", t: "\t", r: "\r", b: "\b", f: "\f", v: "\v", "0": "\0" };
|
|
52
|
+
return raw
|
|
53
|
+
.replace(/\r\n?/g, "\n")
|
|
54
|
+
.replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\n|[\s\S])/g, (_, escape) => {
|
|
55
|
+
if (escape === "\n")
|
|
56
|
+
return "";
|
|
57
|
+
if (escape.length > 1)
|
|
58
|
+
return String.fromCodePoint(parseInt(escape.replace(/^[ux]\{?|\}$/g, ""), 16));
|
|
59
|
+
return simple[escape] ?? escape;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Whether the match at `at` sits on a comment line (// or a JSDoc *): an example, not code. */
|
|
63
|
+
function onCommentLine(text, at) {
|
|
64
|
+
return /^\s*(\/\/|\/?\*)/.test(text.slice(text.lastIndexOf("\n", at - 1) + 1, at));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The GraphQL documents in one file: the whole file for `.graphql` and `.gql`,
|
|
68
|
+
* and elsewhere every template that is marked as one: a `#graphql` first
|
|
69
|
+
* line, a `/* capa *\/` comment before it, or `gql` as a tag or a function.
|
|
70
|
+
* One on a comment line is an example, not a document.
|
|
71
|
+
*
|
|
72
|
+
* `skipped` names every template codegen will not read that looks meant for
|
|
73
|
+
* it, with the reason: one with `${}` inside (its text is only known at run
|
|
74
|
+
* time, so it cannot be validated or hashed ahead of it), a named operation
|
|
75
|
+
* left unmarked, and `/nextjs`'s `graphql` used as a tag.
|
|
76
|
+
*/
|
|
77
|
+
function extractDocuments(file, text) {
|
|
78
|
+
if (/\.(graphql|gql)$/i.test(file)) {
|
|
79
|
+
return { documents: text.trim() ? [{ file, line: 1, text }] : [], skipped: [] };
|
|
80
|
+
}
|
|
81
|
+
const lineAt = (offset) => text.slice(0, offset).split("\n").length;
|
|
82
|
+
const found = new Map();
|
|
83
|
+
for (const { pattern, literal } of TEMPLATES) {
|
|
84
|
+
for (const match of text.matchAll(pattern)) {
|
|
85
|
+
const at = match.index ?? 0;
|
|
86
|
+
if (onCommentLine(text, at))
|
|
87
|
+
continue;
|
|
88
|
+
const start = at + match[0].indexOf("`") + 1;
|
|
89
|
+
if (!found.has(start))
|
|
90
|
+
found.set(start, { raw: match[1], literal, end: at + match[0].length, at });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const documents = [];
|
|
94
|
+
const skipped = [];
|
|
95
|
+
for (const [start, { raw, literal, end, at }] of [...found].sort(([a], [b]) => a - b)) {
|
|
96
|
+
const source = { file, line: lineAt(start), text: cookTemplate(raw) };
|
|
97
|
+
if (literal) {
|
|
98
|
+
source.literal = true;
|
|
99
|
+
const name = ASSIGNED_TO.exec(text.slice(Math.max(0, at - 200), at))?.[1];
|
|
100
|
+
if (name)
|
|
101
|
+
source.name = name;
|
|
102
|
+
}
|
|
103
|
+
if (!/(^|[^\\])\$\{/.test(raw)) {
|
|
104
|
+
documents.push(source);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// A literal whose every ${} names a variable may be another literal of the
|
|
108
|
+
// project, a fragment most often: generateGraphQLModule resolves it.
|
|
109
|
+
const parts = raw.split(NAMED_INTERPOLATION);
|
|
110
|
+
const segments = parts.filter((_, i) => i % 2 === 0);
|
|
111
|
+
if (!literal || segments.some((segment) => /(^|[^\\])\$\{/.test(segment))) {
|
|
112
|
+
skipped.push({ ...source, reason: INTERPOLATED });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
source.template = {
|
|
116
|
+
segments: segments.map(cookTemplate),
|
|
117
|
+
names: parts.filter((_, i) => i % 2 === 1),
|
|
118
|
+
asConst: /^\s*as\s+const\b/.test(text.slice(end)),
|
|
119
|
+
};
|
|
120
|
+
documents.push(source);
|
|
121
|
+
}
|
|
122
|
+
for (const match of text.matchAll(ANY_TEMPLATE)) {
|
|
123
|
+
const at = match.index ?? 0;
|
|
124
|
+
if (found.has(at + 1) || onCommentLine(text, at) || !LOOKS_LIKE_GRAPHQL.test(match[1]))
|
|
125
|
+
continue;
|
|
126
|
+
const tag = /\bgraphql\s*$/.test(text.slice(Math.max(0, at - 20), at));
|
|
127
|
+
skipped.push({ file, line: lineAt(at + 1), text: cookTemplate(match[1]), reason: tag ? FETCH_TAG : UNMARKED });
|
|
128
|
+
}
|
|
129
|
+
skipped.sort((a, b) => a.line - b.line);
|
|
130
|
+
return { documents, skipped };
|
|
131
|
+
}
|
|
132
|
+
// ------------------------------------------------------------- schema types ---
|
|
133
|
+
const SCALARS = {
|
|
134
|
+
ID: "string",
|
|
135
|
+
String: "string",
|
|
136
|
+
Int: "number",
|
|
137
|
+
Float: "number",
|
|
138
|
+
Boolean: "boolean",
|
|
139
|
+
DateTime: "string",
|
|
140
|
+
JSON: "unknown",
|
|
141
|
+
};
|
|
142
|
+
function tsNamed(g, type) {
|
|
143
|
+
if (g.isScalarType(type))
|
|
144
|
+
return SCALARS[type.name] ?? "unknown";
|
|
145
|
+
return type.name;
|
|
146
|
+
}
|
|
147
|
+
/** An output or input type as TS, `null` included where GraphQL allows it. */
|
|
148
|
+
function tsType(g, type, inner) {
|
|
149
|
+
const print = (t, nullable) => {
|
|
150
|
+
if (g.isNonNullType(t))
|
|
151
|
+
return print(t.ofType, false);
|
|
152
|
+
const body = g.isListType(t) ? `Array<${print(t.ofType, true)}>` : (inner ?? ((n) => tsNamed(g, n)))(t);
|
|
153
|
+
return nullable ? `${body} | null` : body;
|
|
154
|
+
};
|
|
155
|
+
return print(type, true);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A JSDoc block: the description, then `@deprecated <reason>` for a field a
|
|
159
|
+
* later Capa-Version phases out, which editors strike through at every use.
|
|
160
|
+
*/
|
|
161
|
+
function docComment(description, indent = "", deprecationReason) {
|
|
162
|
+
const lines = [...(description ? description.split("\n") : []), ...(deprecationReason != null ? [`@deprecated ${deprecationReason}`] : [])];
|
|
163
|
+
if (lines.length === 0)
|
|
164
|
+
return [];
|
|
165
|
+
const safe = (line) => line.replace(/\*\//g, "* /");
|
|
166
|
+
if (lines.length === 1)
|
|
167
|
+
return [`${indent}/** ${safe(lines[0])} */`];
|
|
168
|
+
return [`${indent}/**`, ...lines.map((l) => `${indent} * ${safe(l)}`), `${indent} */`];
|
|
169
|
+
}
|
|
170
|
+
/** An enum's description, with a line per deprecated value: a union member carries no JSDoc of its own. */
|
|
171
|
+
function enumDescription(type) {
|
|
172
|
+
const deprecated = type.getValues().filter((v) => v.deprecationReason != null);
|
|
173
|
+
const lines = [type.description ?? "", ...deprecated.map((v) => `${v.name} is deprecated: ${v.deprecationReason}`)].filter(Boolean);
|
|
174
|
+
return lines.length ? lines.join("\n") : null;
|
|
175
|
+
}
|
|
176
|
+
/** `/** @deprecated <reason> *\/ ` before a deprecated argument written inline, else nothing. */
|
|
177
|
+
function inlineDeprecation(reason) {
|
|
178
|
+
return reason != null ? `/** @deprecated ${reason.replace(/\*\//g, "* /")} */ ` : "";
|
|
179
|
+
}
|
|
180
|
+
function argsType(g, args) {
|
|
181
|
+
const parts = args.map((arg) => {
|
|
182
|
+
const optional = !g.isNonNullType(arg.type) || arg.defaultValue !== undefined;
|
|
183
|
+
return `${inlineDeprecation(arg.deprecationReason)}${arg.name}${optional ? "?" : ""}: ${tsType(g, arg.type)}`;
|
|
184
|
+
});
|
|
185
|
+
return `{ ${parts.join("; ")} }`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A model type's `TreeLayout`, from the N9 descriptions the API writes: each
|
|
189
|
+
* customer field's namespace, and the relations typed as ids. Null for any
|
|
190
|
+
* other object type.
|
|
191
|
+
*/
|
|
192
|
+
function treeLayoutMember(g, type) {
|
|
193
|
+
if (!(0, summary_1.modelNamespaceOf)(type.description))
|
|
194
|
+
return null;
|
|
195
|
+
const namespaces = [];
|
|
196
|
+
const ids = [];
|
|
197
|
+
for (const field of Object.values(type.getFields())) {
|
|
198
|
+
const parsed = (0, summary_1.parseFieldDescription)(field.description);
|
|
199
|
+
if (!parsed)
|
|
200
|
+
continue;
|
|
201
|
+
namespaces.push(` ${field.name}: ${JSON.stringify(parsed.namespace)};`);
|
|
202
|
+
if (g.getNamedType(field.type).name === "ID")
|
|
203
|
+
ids.push(JSON.stringify(field.name));
|
|
204
|
+
}
|
|
205
|
+
return [
|
|
206
|
+
" /** How toTree lays these entries out in REST's shape: each field's namespace, and the relations typed as ids. Not a field. */",
|
|
207
|
+
" readonly [treeLayout]?: {",
|
|
208
|
+
...(namespaces.length ? [" fields: {", ...namespaces, " };"] : [" fields: {};"]),
|
|
209
|
+
` ids: ${ids.length ? ids.join(" | ") : "never"};`,
|
|
210
|
+
" };",
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
/** `capaTreeLayout` as TypeScript: one line per model's root fields, one per field it lays out. */
|
|
214
|
+
function treeLayoutConstant(layout) {
|
|
215
|
+
const text = (value) => (value === null ? "null" : JSON.stringify(value));
|
|
216
|
+
const out = [
|
|
217
|
+
"/** What toTree reads of this schema: toTree(data, selection, capaTreeLayout) reads no schema from the API. */",
|
|
218
|
+
"export const capaTreeLayout: CapaTreeLayout<CapaQuery> = {",
|
|
219
|
+
" models: [",
|
|
220
|
+
];
|
|
221
|
+
for (const model of layout.models) {
|
|
222
|
+
out.push(` {`, ` namespace: ${text(model.namespace)},`, ` listField: ${text(model.listField)},`, ` singleField: ${text(model.singleField)},`);
|
|
223
|
+
if (model.fields.length === 0)
|
|
224
|
+
out.push(" fields: [],");
|
|
225
|
+
else {
|
|
226
|
+
out.push(" fields: [");
|
|
227
|
+
for (const field of model.fields) {
|
|
228
|
+
out.push(` { name: ${text(field.name)}, namespace: ${text(field.namespace)}, kind: ${text(field.kind)}, target: ${text(field.target)} },`);
|
|
229
|
+
}
|
|
230
|
+
out.push(" ],");
|
|
231
|
+
}
|
|
232
|
+
out.push(" },");
|
|
233
|
+
}
|
|
234
|
+
out.push(" ],", "};", "");
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function schemaTypes(g, schema) {
|
|
238
|
+
const out = [];
|
|
239
|
+
const names = Object.keys(schema.getTypeMap())
|
|
240
|
+
.filter((name) => !name.startsWith("__") && !(name in SCALARS))
|
|
241
|
+
.sort();
|
|
242
|
+
for (const name of names) {
|
|
243
|
+
const type = schema.getType(name);
|
|
244
|
+
if (g.isEnumType(type)) {
|
|
245
|
+
out.push(...docComment(enumDescription(type)));
|
|
246
|
+
out.push(`export type ${name} = ${type.getValues().map((v) => JSON.stringify(v.name)).join(" | ")};`, "");
|
|
247
|
+
}
|
|
248
|
+
else if (g.isObjectType(type) || g.isInterfaceType(type)) {
|
|
249
|
+
out.push(...docComment(type.description), `export interface ${name} {`);
|
|
250
|
+
for (const field of Object.values(type.getFields())) {
|
|
251
|
+
out.push(...docComment(field.description, " ", field.deprecationReason));
|
|
252
|
+
const returns = tsType(g, field.type);
|
|
253
|
+
out.push(field.args.length
|
|
254
|
+
? ` ${field.name}: CapaField<${returns}, ${argsType(g, field.args)}>;`
|
|
255
|
+
: ` ${field.name}: ${returns};`);
|
|
256
|
+
}
|
|
257
|
+
if (g.isObjectType(type))
|
|
258
|
+
out.push(...(treeLayoutMember(g, type) ?? []));
|
|
259
|
+
out.push("}", "");
|
|
260
|
+
}
|
|
261
|
+
else if (g.isInputObjectType(type)) {
|
|
262
|
+
out.push(...docComment(type.description), `export interface ${name} {`);
|
|
263
|
+
for (const field of Object.values(type.getFields())) {
|
|
264
|
+
const optional = !g.isNonNullType(field.type) || field.defaultValue !== undefined;
|
|
265
|
+
out.push(...docComment(null, " ", field.deprecationReason));
|
|
266
|
+
out.push(` ${field.name}${optional ? "?" : ""}: ${tsType(g, field.type)};`);
|
|
267
|
+
}
|
|
268
|
+
out.push("}", "");
|
|
269
|
+
}
|
|
270
|
+
else if (g.isUnionType(type)) {
|
|
271
|
+
out.push(`export type ${name} = ${type.getTypes().map((t) => t.name).join(" | ")};`, "");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
/** Fields selected on `parent`, fragments folded in; fields under another type condition are optional. */
|
|
277
|
+
function collect(g, schema, parent, selections, fragments, optional, into) {
|
|
278
|
+
for (const selection of selections) {
|
|
279
|
+
if (selection.kind === g.Kind.FIELD) {
|
|
280
|
+
const key = selection.alias?.value ?? selection.name.value;
|
|
281
|
+
const fields = g.isUnionType(parent) ? {} : parent.getFields();
|
|
282
|
+
const existing = into.get(key);
|
|
283
|
+
if (existing) {
|
|
284
|
+
existing.nodes.push(selection);
|
|
285
|
+
existing.optional = existing.optional && optional;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
into.set(key, { key, field: fields[selection.name.value] ?? null, nodes: [selection], optional });
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const fragment = selection.kind === g.Kind.FRAGMENT_SPREAD ? fragments.get(selection.name.value) : selection;
|
|
293
|
+
if (!fragment)
|
|
294
|
+
continue;
|
|
295
|
+
const condition = fragment.typeCondition?.name.value;
|
|
296
|
+
const narrowed = condition ? schema.getType(condition) : parent;
|
|
297
|
+
const scope = narrowed && (g.isObjectType(narrowed) || g.isInterfaceType(narrowed) || g.isUnionType(narrowed)) ? narrowed : parent;
|
|
298
|
+
collect(g, schema, scope, fragment.selectionSet.selections, fragments, optional || scope.name !== parent.name, into);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function resultType(g, schema, parent, selections, fragments, indent) {
|
|
302
|
+
const collected = new Map();
|
|
303
|
+
collect(g, schema, parent, selections, fragments, false, collected);
|
|
304
|
+
const lines = ["{"];
|
|
305
|
+
for (const entry of collected.values()) {
|
|
306
|
+
const name = entry.nodes[0].name.value;
|
|
307
|
+
let type;
|
|
308
|
+
if (name === "__typename") {
|
|
309
|
+
type = g.isObjectType(parent) ? JSON.stringify(parent.name) : "string";
|
|
310
|
+
}
|
|
311
|
+
else if (!entry.field) {
|
|
312
|
+
type = "unknown";
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const children = entry.nodes.flatMap((node) => node.selectionSet?.selections ?? []);
|
|
316
|
+
type = tsType(g, entry.field.type, (named) => g.isObjectType(named) || g.isInterfaceType(named) || g.isUnionType(named)
|
|
317
|
+
? resultType(g, schema, named, children, fragments, `${indent} `)
|
|
318
|
+
: tsNamed(g, named));
|
|
319
|
+
}
|
|
320
|
+
lines.push(...docComment(null, `${indent} `, entry.field?.deprecationReason));
|
|
321
|
+
lines.push(`${indent} ${entry.key}${entry.optional ? "?" : ""}: ${type};`);
|
|
322
|
+
}
|
|
323
|
+
lines.push(`${indent}}`);
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
function variablesType(g, schema, operation) {
|
|
327
|
+
const parts = (operation.variableDefinitions ?? []).map((definition) => {
|
|
328
|
+
const type = g.typeFromAST(schema, definition.type);
|
|
329
|
+
const optional = definition.type.kind !== g.Kind.NON_NULL_TYPE || definition.defaultValue !== undefined;
|
|
330
|
+
return ` ${definition.variable.name.value}${optional ? "?" : ""}: ${type ? tsType(g, type) : "unknown"};`;
|
|
331
|
+
});
|
|
332
|
+
return parts.length ? `{\n${parts.join("\n")}\n}` : "Record<string, never>";
|
|
333
|
+
}
|
|
334
|
+
/** Every fragment an operation reaches, transitively, in first-use order. */
|
|
335
|
+
function usedFragments(g, node, fragments, seen = new Set()) {
|
|
336
|
+
const out = [];
|
|
337
|
+
g.visit(node, {
|
|
338
|
+
FragmentSpread(spread) {
|
|
339
|
+
const name = spread.name.value;
|
|
340
|
+
const fragment = fragments.get(name);
|
|
341
|
+
if (!fragment || seen.has(name))
|
|
342
|
+
return;
|
|
343
|
+
seen.add(name);
|
|
344
|
+
out.push(fragment, ...usedFragments(g, fragment, fragments, seen));
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The namespaces of the models an operation reads, in the order it first
|
|
351
|
+
* reaches them: each model whose entries it selects (every model for
|
|
352
|
+
* `entry(id:)`, which reads an entry of any), and each model a filter or sort
|
|
353
|
+
* reaches through a relation (`author: { name: ... }`, `author__name_ASC`),
|
|
354
|
+
* since a publish there changes which entries match or their order. A filter
|
|
355
|
+
* or sort passed as a variable is known only at run time, so every model its
|
|
356
|
+
* type can reach counts. These are the models whose publish must revalidate
|
|
357
|
+
* a cached read of it: `tagsFor({ namespace: <Name>Models })`.
|
|
358
|
+
*/
|
|
359
|
+
function modelsRead(g, schema, summary, document) {
|
|
360
|
+
const byType = new Map(summary.models.map((model) => [model.typeName, model]));
|
|
361
|
+
const bySort = new Map(summary.models.map((model) => [model.sortType, model]));
|
|
362
|
+
/** Whether a relation's filter input takes hop fields (`name: StringFilter`) beside its operators. */
|
|
363
|
+
const hasHops = (input) => g.isInputObjectType(input) && Object.values(input.getFields()).some((field) => g.isInputObjectType(g.getNamedType(field.type)));
|
|
364
|
+
/** A relation's filter input (`AuthorsRelationFilter`), to the model its hop fields read. */
|
|
365
|
+
const hopTargets = new Map();
|
|
366
|
+
/** A model's filter input (`ArticlesFilter`), to every model its hops can read. */
|
|
367
|
+
const filterReach = new Map();
|
|
368
|
+
for (const model of summary.models) {
|
|
369
|
+
const filter = schema.getType(model.filterType);
|
|
370
|
+
if (!g.isInputObjectType(filter))
|
|
371
|
+
continue;
|
|
372
|
+
const reach = [];
|
|
373
|
+
for (const field of model.fields) {
|
|
374
|
+
const input = field.target ? filter.getFields()[field.name] : undefined;
|
|
375
|
+
const relationFilter = input ? g.getNamedType(input.type) : undefined;
|
|
376
|
+
if (!field.target || !relationFilter || !hasHops(relationFilter))
|
|
377
|
+
continue;
|
|
378
|
+
hopTargets.set(relationFilter.name, field.target);
|
|
379
|
+
reach.push(field.target);
|
|
380
|
+
}
|
|
381
|
+
filterReach.set(model.filterType, reach);
|
|
382
|
+
}
|
|
383
|
+
/** The model a sort value reads through a relation (`author__name_ASC`), or null for the model's own field. */
|
|
384
|
+
const sortTarget = (model, value) => {
|
|
385
|
+
const hop = /^(.+?)__.+_(?:ASC|DESC)$/.exec(value);
|
|
386
|
+
return hop ? (model.fields.find((field) => field.name === hop[1])?.target ?? null) : null;
|
|
387
|
+
};
|
|
388
|
+
const read = [];
|
|
389
|
+
const add = (namespace) => {
|
|
390
|
+
if (namespace && !read.includes(namespace))
|
|
391
|
+
read.push(namespace);
|
|
392
|
+
};
|
|
393
|
+
const addEntries = (type) => {
|
|
394
|
+
const types = g.isAbstractType(type) ? schema.getPossibleTypes(type) : [type];
|
|
395
|
+
for (const each of types)
|
|
396
|
+
add(byType.get(each.name)?.namespace);
|
|
397
|
+
// A connection read for its count or page alone still reads its model.
|
|
398
|
+
if (g.isObjectType(type) && type.getFields().nodes)
|
|
399
|
+
addEntries(g.getNamedType(type.getFields().nodes.type));
|
|
400
|
+
};
|
|
401
|
+
const typeInfo = new g.TypeInfo(schema);
|
|
402
|
+
g.visit(document, g.visitWithTypeInfo(typeInfo, {
|
|
403
|
+
// Every variable is used in an argument, where its type is read below,
|
|
404
|
+
// after the field it filters, so the root model comes first.
|
|
405
|
+
VariableDefinition: () => false,
|
|
406
|
+
Field() {
|
|
407
|
+
const type = typeInfo.getType();
|
|
408
|
+
if (type)
|
|
409
|
+
addEntries(g.getNamedType(type));
|
|
410
|
+
},
|
|
411
|
+
ObjectField() {
|
|
412
|
+
const parent = typeInfo.getParentInputType();
|
|
413
|
+
const input = typeInfo.getInputType();
|
|
414
|
+
if (!parent || !input)
|
|
415
|
+
return;
|
|
416
|
+
const target = hopTargets.get(g.getNamedType(parent).name);
|
|
417
|
+
if (target && g.isInputObjectType(g.getNamedType(input)))
|
|
418
|
+
add(target);
|
|
419
|
+
},
|
|
420
|
+
EnumValue(node) {
|
|
421
|
+
const input = typeInfo.getInputType();
|
|
422
|
+
const model = input ? bySort.get(g.getNamedType(input).name) : undefined;
|
|
423
|
+
if (model)
|
|
424
|
+
add(sortTarget(model, node.value));
|
|
425
|
+
},
|
|
426
|
+
Variable() {
|
|
427
|
+
const input = typeInfo.getInputType();
|
|
428
|
+
if (!input)
|
|
429
|
+
return;
|
|
430
|
+
const name = g.getNamedType(input).name;
|
|
431
|
+
for (const target of filterReach.get(name) ?? [])
|
|
432
|
+
add(target);
|
|
433
|
+
add(hopTargets.get(name));
|
|
434
|
+
const sorted = bySort.get(name);
|
|
435
|
+
if (sorted)
|
|
436
|
+
for (const value of sorted.sortValues)
|
|
437
|
+
add(sortTarget(sorted, value));
|
|
438
|
+
},
|
|
439
|
+
}));
|
|
440
|
+
return read;
|
|
441
|
+
}
|
|
442
|
+
function pascal(name) {
|
|
443
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Each literal's `${NAME}`s filled in with the text of the literal assigned to
|
|
447
|
+
* NAME, as the program fills them in when it runs: the one in the same file
|
|
448
|
+
* when there is one, else the one literal of that name in the project. A name
|
|
449
|
+
* no literal takes leaves the template to run time (skipped, with the name); a
|
|
450
|
+
* name two files take, a cycle, and a template without `as const` (whose
|
|
451
|
+
* text TypeScript then forgets) are problems, each with its fix.
|
|
452
|
+
*/
|
|
453
|
+
function resolveTemplates(sources) {
|
|
454
|
+
const byName = new Map();
|
|
455
|
+
for (const source of sources) {
|
|
456
|
+
if (source.literal && source.name)
|
|
457
|
+
byName.set(source.name, [...(byName.get(source.name) ?? []), source]);
|
|
458
|
+
}
|
|
459
|
+
const done = new Map();
|
|
460
|
+
const resolve = (source, path) => {
|
|
461
|
+
const known = done.get(source);
|
|
462
|
+
if (known)
|
|
463
|
+
return known;
|
|
464
|
+
if (!source.template)
|
|
465
|
+
return { text: source.text };
|
|
466
|
+
if (path.includes(source))
|
|
467
|
+
return { problem: `${source.name ?? "This template"} spreads itself through \${}, so its text never ends.` };
|
|
468
|
+
const { segments, names, asConst } = source.template;
|
|
469
|
+
let text = segments[0];
|
|
470
|
+
let outcome = null;
|
|
471
|
+
for (const [i, name] of names.entries()) {
|
|
472
|
+
const all = byName.get(name) ?? [];
|
|
473
|
+
const here = all.filter((candidate) => candidate.file === source.file);
|
|
474
|
+
const candidates = here.length ? here : all;
|
|
475
|
+
if (candidates.length === 0) {
|
|
476
|
+
outcome = { skip: unknownInterpolation(name) };
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
if (candidates.length > 1) {
|
|
480
|
+
const where = candidates.map((c) => `${c.file}:${c.line}`).join(", ");
|
|
481
|
+
outcome = { problem: `\${${name}} names ${candidates.length} literals (${where}): rename one, so this template spreads the one it means.` };
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const inner = resolve(candidates[0], [...path, source]);
|
|
485
|
+
if (!("text" in inner)) {
|
|
486
|
+
outcome = inner;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
text += inner.text + segments[i + 1];
|
|
490
|
+
}
|
|
491
|
+
const result = outcome ?? (asConst ? { text } : { problem: NEEDS_AS_CONST });
|
|
492
|
+
done.set(source, result);
|
|
493
|
+
return result;
|
|
494
|
+
};
|
|
495
|
+
for (const source of sources)
|
|
496
|
+
done.set(source, resolve(source, []));
|
|
497
|
+
return done;
|
|
498
|
+
}
|
|
499
|
+
/** Whether `node` sits inside one of `definitions`, by its place in the same text. */
|
|
500
|
+
function insideAny(node, definitions) {
|
|
501
|
+
return definitions.some((d) => d.loc && node.loc && d.loc.source === node.loc.source && node.loc.start >= d.loc.start && node.loc.end <= d.loc.end);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The generated module for a schema and a project's documents. Problems (a
|
|
505
|
+
* syntax error, a field the key cannot read, an unnamed operation, a name used
|
|
506
|
+
* twice) are returned with their file and line, and no module is produced
|
|
507
|
+
* while any remain.
|
|
508
|
+
*/
|
|
509
|
+
function generateGraphQLModule(introspection, sources) {
|
|
510
|
+
const g = loadGraphQL();
|
|
511
|
+
const schema = g.buildClientSchema(introspection);
|
|
512
|
+
const summary = (0, summary_1.summarizeIntrospection)(introspection);
|
|
513
|
+
const problems = [];
|
|
514
|
+
const at = (source, message, location) => ({
|
|
515
|
+
file: source.file,
|
|
516
|
+
line: source.line + (location ? location.line - 1 : 0),
|
|
517
|
+
column: location?.column ?? 1,
|
|
518
|
+
message,
|
|
519
|
+
});
|
|
520
|
+
const problem = (source, message, location) => problems.push(at(source, message, location));
|
|
521
|
+
const warnings = new Map();
|
|
522
|
+
/** Every use of a deprecated field, argument, input field or enum value in `document`, once each. */
|
|
523
|
+
const warnDeprecated = (source, document) => {
|
|
524
|
+
for (const use of g.validate(schema, document, [g.NoDeprecatedCustomRule])) {
|
|
525
|
+
const location = use.locations?.[0];
|
|
526
|
+
const warning = at(source, use.message, location && use.source?.body === source.text ? location : undefined);
|
|
527
|
+
warnings.set(`${warning.file}:${warning.line}:${warning.column}:${warning.message}`, warning);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const skipped = [];
|
|
531
|
+
const readable = [];
|
|
532
|
+
const resolved = resolveTemplates(sources);
|
|
533
|
+
for (const source of sources) {
|
|
534
|
+
const outcome = resolved.get(source);
|
|
535
|
+
if ("skip" in outcome)
|
|
536
|
+
skipped.push({ file: source.file, line: source.line, text: source.text, literal: true, reason: outcome.skip });
|
|
537
|
+
else if ("problem" in outcome)
|
|
538
|
+
problem(source, outcome.problem);
|
|
539
|
+
else
|
|
540
|
+
readable.push(outcome.text === source.text ? source : { ...source, text: outcome.text, template: undefined });
|
|
541
|
+
}
|
|
542
|
+
const parsed = [];
|
|
543
|
+
const fragments = new Map();
|
|
544
|
+
const fragmentSources = new Map();
|
|
545
|
+
const lineOf = (source, node) => node.loc ? source.line + g.getLocation(node.loc.source, node.loc.start).line - 1 : source.line;
|
|
546
|
+
for (const source of readable) {
|
|
547
|
+
let document;
|
|
548
|
+
try {
|
|
549
|
+
document = g.parse(source.text);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
const located = error;
|
|
553
|
+
problem(source, located.message, located.locations?.[0]);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
parsed.push({ source, document });
|
|
557
|
+
for (const definition of document.definitions) {
|
|
558
|
+
if (definition.kind !== g.Kind.FRAGMENT_DEFINITION)
|
|
559
|
+
continue;
|
|
560
|
+
const name = definition.name.value;
|
|
561
|
+
const first = fragments.get(name);
|
|
562
|
+
if (!first) {
|
|
563
|
+
fragments.set(name, definition);
|
|
564
|
+
fragmentSources.set(name, source);
|
|
565
|
+
}
|
|
566
|
+
else if (g.print(first) !== g.print(definition)) {
|
|
567
|
+
// The same fragment spread into several literals with ${} is one fragment; two texts under one name are not.
|
|
568
|
+
const also = fragmentSources.get(name);
|
|
569
|
+
problem(source, `Fragment ${name} is defined twice, differently (also ${also.file}:${lineOf(also, first)}). Fragment names must be unique across the project.`, definition.loc ? g.getLocation(definition.loc.source, definition.loc.start) : undefined);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Every fragment is checked on its own and typed, so a component can take
|
|
574
|
+
// its data as props (`ArticleCardFragment`) wherever the fragment is spread.
|
|
575
|
+
const standaloneRules = g.specifiedRules.filter((rule) => rule !== g.NoUnusedFragmentsRule);
|
|
576
|
+
const invalidFragments = [];
|
|
577
|
+
const fragmentBlocks = [];
|
|
578
|
+
for (const [name, definition] of fragments) {
|
|
579
|
+
const source = fragmentSources.get(name);
|
|
580
|
+
const alone = { kind: g.Kind.DOCUMENT, definitions: [definition, ...usedFragments(g, definition, fragments)] };
|
|
581
|
+
const errors = g.validate(schema, alone, standaloneRules);
|
|
582
|
+
if (errors.length) {
|
|
583
|
+
invalidFragments.push(definition);
|
|
584
|
+
for (const error of errors) {
|
|
585
|
+
const location = error.locations?.[0];
|
|
586
|
+
problem(source, error.message, location && error.source?.body === source.text ? location : undefined);
|
|
587
|
+
}
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const condition = schema.getType(definition.typeCondition.name.value);
|
|
591
|
+
if (!condition || !(g.isObjectType(condition) || g.isInterfaceType(condition) || g.isUnionType(condition)))
|
|
592
|
+
continue;
|
|
593
|
+
fragmentBlocks.push(`export type ${pascal(name)}Fragment = ${resultType(g, schema, condition, definition.selectionSet.selections, fragments, "")};`, "");
|
|
594
|
+
}
|
|
595
|
+
/** An error in a fragment already reported on its own is not reported again for each operation that spreads it. */
|
|
596
|
+
const reportedInFragment = (error) => invalidFragments.length > 0 && (error.nodes?.length ?? 0) > 0 && error.nodes.every((node) => insideAny(node, invalidFragments));
|
|
597
|
+
const operations = [];
|
|
598
|
+
const blocks = [];
|
|
599
|
+
const recorded = [];
|
|
600
|
+
const names = new Set();
|
|
601
|
+
const hash = (text) => (0, sha256_1.sha256Pure)(new TextEncoder().encode(text));
|
|
602
|
+
for (const { source, document } of parsed) {
|
|
603
|
+
// A literal is sent exactly as written and typed by its whole text, so it
|
|
604
|
+
// holds one operation and everything that operation needs, and is checked
|
|
605
|
+
// whole, as the API will check it.
|
|
606
|
+
if (source.literal) {
|
|
607
|
+
const count = document.definitions.filter((d) => d.kind === g.Kind.OPERATION_DEFINITION).length;
|
|
608
|
+
// A literal of fragments only is a fragment source for other literals' ${}: checked and typed above.
|
|
609
|
+
if (count === 0 && document.definitions.length > 0)
|
|
610
|
+
continue;
|
|
611
|
+
if (count !== 1) {
|
|
612
|
+
problem(source, `A #graphql, /* capa */ or gql() literal holds exactly one operation, since its text is what types it; this one holds ${count}.`);
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const errors = g.validate(schema, document).filter((error) => !reportedInFragment(error));
|
|
616
|
+
if (errors.length) {
|
|
617
|
+
for (const error of errors) {
|
|
618
|
+
const unknownFragment = /^Unknown fragment "/.test(error.message);
|
|
619
|
+
problem(source, unknownFragment
|
|
620
|
+
? `${error.message} A literal is sent as written, so define the fragment inside it or spread a fragment literal into it with \${NAME}, or use the <Name>Document codegen writes.`
|
|
621
|
+
: error.message, error.locations?.[0]);
|
|
622
|
+
}
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
for (const definition of document.definitions) {
|
|
627
|
+
if (definition.kind !== g.Kind.OPERATION_DEFINITION)
|
|
628
|
+
continue;
|
|
629
|
+
const name = definition.name?.value;
|
|
630
|
+
if (!name) {
|
|
631
|
+
problem(source, "Name every operation (query ArticlesPage { ... }): the name is what codegen exports.", definition.loc ? g.getLocation(definition.loc.source, definition.loc.start) : undefined);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (names.has(name)) {
|
|
635
|
+
problem(source, `Operation name ${name} is used twice. Operation names must be unique across the project.`);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
names.add(name);
|
|
639
|
+
const standalone = {
|
|
640
|
+
kind: g.Kind.DOCUMENT,
|
|
641
|
+
definitions: [definition, ...usedFragments(g, definition, fragments)],
|
|
642
|
+
};
|
|
643
|
+
const errors = g.validate(schema, standalone).filter((error) => !reportedInFragment(error));
|
|
644
|
+
if (errors.length) {
|
|
645
|
+
for (const error of errors) {
|
|
646
|
+
const at = error.locations?.[0];
|
|
647
|
+
problem(source, error.message, at && error.source?.body === source.text ? at : undefined);
|
|
648
|
+
}
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (invalidFragments.some((fragment) => standalone.definitions.includes(fragment)))
|
|
652
|
+
continue;
|
|
653
|
+
warnDeprecated(source, standalone);
|
|
654
|
+
const text = standalone.definitions.map((d) => g.print(d)).join("\n\n");
|
|
655
|
+
const root = definition.operation === "query" ? schema.getQueryType() : null;
|
|
656
|
+
if (!root) {
|
|
657
|
+
problem(source, `${name} is a ${definition.operation}; Capa's GraphQL API serves queries only.`);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const typeName = `${pascal(name)}Query`;
|
|
661
|
+
const models = modelsRead(g, schema, summary, standalone);
|
|
662
|
+
blocks.push(`export type ${typeName} = ${resultType(g, schema, root, definition.selectionSet.selections, fragments, "")};`, "", `export type ${typeName}Variables = ${variablesType(g, schema, definition)};`, "", `export const ${pascal(name)}Document = ${JSON.stringify(text)} as TypedDocument<${typeName}, ${typeName}Variables>;`, "", `/** The models ${name} reads, for its cache tags: tagsFor({ namespace: ${pascal(name)}Models }). */`, `export const ${pascal(name)}Models = ${JSON.stringify(models).replace(/","/g, '", "')} as const;`, "");
|
|
663
|
+
const operation = { name, document: text, sha256: hash(text), models };
|
|
664
|
+
if (source.literal) {
|
|
665
|
+
operation.literal = { document: source.text, sha256: hash(source.text) };
|
|
666
|
+
recorded.push(` ${JSON.stringify(source.text)}: { result: ${typeName}; variables: ${typeName}Variables };`);
|
|
667
|
+
}
|
|
668
|
+
operations.push(operation);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const deprecatedUses = [...warnings.values()];
|
|
672
|
+
const unique = [...new Map(problems.map((p) => [`${p.file}:${p.line}:${p.column}:${p.message}`, p])).values()];
|
|
673
|
+
if (unique.length)
|
|
674
|
+
return { source: null, operations, problems: unique, warnings: deprecatedUses, skipped };
|
|
675
|
+
const sdl = g.printSchema(schema);
|
|
676
|
+
const header = [
|
|
677
|
+
"// Generated by capa-codegen --graphql. Do not edit by hand.",
|
|
678
|
+
`// Schema: version ${introspection.version ?? "unknown"}, sha256 ${(0, sha256_1.sha256Pure)(new TextEncoder().encode(sdl)).slice(0, 16)}.`,
|
|
679
|
+
"// Re-run it after a model changes in Capa or a document changes here.",
|
|
680
|
+
'import type { CapaField, CapaTreeLayout, treeLayout, TypedDocument } from "@capacms/sdk/next";',
|
|
681
|
+
"",
|
|
682
|
+
];
|
|
683
|
+
const query = schema.getQueryType()?.name ?? "Query";
|
|
684
|
+
const body = [
|
|
685
|
+
...schemaTypes(g, schema),
|
|
686
|
+
"/** Pass to createClient<CapaQuery>() to type client.graphql.query(). */",
|
|
687
|
+
`export type CapaQuery = ${query};`,
|
|
688
|
+
"",
|
|
689
|
+
...treeLayoutConstant((0, tree_layout_1.treeLayoutOf)(summary)),
|
|
690
|
+
...fragmentBlocks,
|
|
691
|
+
...blocks,
|
|
692
|
+
...(recorded.length
|
|
693
|
+
? [
|
|
694
|
+
'declare module "@capacms/sdk/next" {',
|
|
695
|
+
" /** The documents written as literals in this project, by their exact text: client.graphql(text) is typed from here. */",
|
|
696
|
+
" interface CapaDocuments {",
|
|
697
|
+
...recorded,
|
|
698
|
+
" }",
|
|
699
|
+
"}",
|
|
700
|
+
"",
|
|
701
|
+
]
|
|
702
|
+
: []),
|
|
703
|
+
];
|
|
704
|
+
return { source: [...header, ...body].join("\n").trimEnd() + "\n", operations, problems: unique, warnings: deprecatedUses, skipped };
|
|
705
|
+
}
|