1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
/*** IMPORT ------------------------------------------- ***/
import {
parse,
type DefinitionNode,
type DocumentNode,
type Location
} from "graphql";
/*** UTILITY ------------------------------------------ ***/
const docCache = new Map<string, DocumentNode>();
const fragmentSourceMap = new Map<string, Set<string>>();
let allowLegacyFragmentVariables = false;
let printFragmentWarnings = true;
/*** EXPORT ------------------------------------------- ***/
/** Turns off support for legacy/experimental fragment variables (the default). */
export function disableExperimentalFragmentVariables() {
allowLegacyFragmentVariables = false;
}
/** Silences `console.warn` calls when duplicate fragment names are detected. */
export function disableFragmentWarnings() {
printFragmentWarnings = false;
}
/** Opts into parsing experimental fragment variables via `graphql-js`. */
export function enableExperimentalFragmentVariables() {
allowLegacyFragmentVariables = true;
}
/**
* Tagged-template literal that parses a GraphQL string into a `DocumentNode`.
*
* Parsed documents are cached by normalized source, so repeated calls with the
* same query return the same AST. Embedded `DocumentNode` interpolations are
* inlined from their original source.
*
* @example
* ```ts
* const Hello = gql`type Query { hello: String }`;
* ```
*/
export function gql(literals: string | readonly string[], ...args: unknown[]): DocumentNode {
if (typeof literals === "string")
literals = [literals];
let result = literals[0];
args.forEach((arg, i) => {
if (arg && typeof arg === "object" && "kind" in arg && arg.kind === "Document")
result += (arg as DocumentNode).loc?.source.body ?? "";
else
result += arg;
result += literals[i + 1];
});
return parseDocument(result);
}
/** Clears the internal document and fragment caches used by {@link gql}. */
export function resetCaches() {
docCache.clear();
fragmentSourceMap.clear();
}
/*** HELPER ------------------------------------------- ***/
function normalize(string: string) {
return string.replace(/[\s,]+/g, " ").trim();
}
function cacheKeyFromLoc(loc: Location) {
return normalize(loc.source.body.substring(loc.start, loc.end));
}
function processFragments(ast: DocumentNode) {
const definitions: DefinitionNode[] = [];
const seenKeys = new Set<string>();
ast.definitions.forEach((fragmentDefinition) => {
if (fragmentDefinition.kind === "FragmentDefinition") {
const fragmentName = fragmentDefinition.name.value;
const sourceKey = cacheKeyFromLoc(fragmentDefinition.loc!);
let sourceKeySet = fragmentSourceMap.get(fragmentName)!;
if (sourceKeySet && !sourceKeySet.has(sourceKey)) {
if (printFragmentWarnings)
console.warn(`Warning: fragment with name ${fragmentName} already exists.`);
} else if (!sourceKeySet) {
fragmentSourceMap.set(fragmentName, (sourceKeySet = new Set()));
}
sourceKeySet.add(sourceKey);
if (!seenKeys.has(sourceKey)) {
seenKeys.add(sourceKey);
definitions.push(fragmentDefinition);
}
} else {
definitions.push(fragmentDefinition);
}
})
return {
...ast,
definitions
};
}
function stripLoc(doc: DocumentNode) {
const workSet = new Set<Record<string, unknown>>(doc.definitions as unknown as Record<string, unknown>[]);
workSet.forEach((node) => {
if (node.loc)
delete node.loc;
Object.keys(node).forEach((key) => {
const value = node[key];
if (value && typeof value === "object")
workSet.add(value as Record<string, unknown>);
});
});
const loc = doc.loc as { endToken: unknown; startToken: unknown; };
if (loc) {
delete loc.startToken;
delete loc.endToken;
}
return doc;
}
function parseDocument(source: string) {
const cacheKey = normalize(source);
if (!docCache.has(cacheKey)) {
const parsed = parse(source, { allowLegacyFragmentVariables });
if (!parsed || parsed.kind !== "Document")
throw new Error("Not a valid GraphQL document.");
docCache.set(
cacheKey,
stripLoc(processFragments(parsed))
);
}
return docCache.get(cacheKey)!;
}
|