1/* eslint-disable prefer-rest-params */
2import { Kind } from '@0no-co/graphql.web';
3import type { DocumentNode, DefinitionNode } from './utils/graphql';
4import type { AnyVariables, TypedDocumentNode } from './types';
5import { keyDocument, stringifyDocument } from './utils';
6
7/** A GraphQL parse function, which may be called as a tagged template literal, returning a parsed {@link DocumentNode}.
8 *
9 * @remarks
10 * The `gql` tag or function is used to parse a GraphQL query document into a {@link DocumentNode}.
11 *
12 * When used as a tagged template, `gql` will automatically merge fragment definitions into the resulting
13 * document and deduplicate them.
14 *
15 * It enforces that all fragments have a unique name. When fragments with different definitions share a name,
16 * it will log a warning in development.
17 *
18 * Hint: It’s recommended to use this `gql` function over other GraphQL parse functions, since it puts the parsed
19 * results directly into `@urql/core`’s internal caches and prevents further unnecessary work.
20 *
21 * @example
22 * ```ts
23 * const AuthorFragment = gql`
24 * fragment AuthorDisplayComponent on Author {
25 * id
26 * name
27 * }
28 * `;
29 *
30 * const BookFragment = gql`
31 * fragment ListBookComponent on Book {
32 * id
33 * title
34 * author {
35 * ...AuthorDisplayComponent
36 * }
37 * }
38 *
39 * ${AuthorFragment}
40 * `;
41 *
42 * const BookQuery = gql`
43 * query Book($id: ID!) {
44 * book(id: $id) {
45 * ...BookFragment
46 * }
47 * }
48 *
49 * ${BookFragment}
50 * `;
51 * ```
52 */
53function gql<Data = any, Variables extends AnyVariables = AnyVariables>(
54 strings: TemplateStringsArray,
55 ...interpolations: Array<TypedDocumentNode | DocumentNode | string>
56): TypedDocumentNode<Data, Variables>;
57
58function gql<Data = any, Variables extends AnyVariables = AnyVariables>(
59 string: string
60): TypedDocumentNode<Data, Variables>;
61
62function gql(parts: string | TemplateStringsArray /* arguments */) {
63 const fragmentNames = new Map<string, string>();
64 const definitions: DefinitionNode[] = [];
65 const source: DocumentNode[] = [];
66
67 // Apply the entire tagged template body's definitions
68 let body: string = Array.isArray(parts) ? parts[0] : parts || '';
69 for (let i = 1; i < arguments.length; i++) {
70 const value = arguments[i];
71 if (value && value.definitions) {
72 source.push(value);
73 } else {
74 body += value;
75 }
76
77 body += arguments[0][i];
78 }
79
80 source.unshift(keyDocument(body));
81 for (let i = 0; i < source.length; i++) {
82 for (let j = 0; j < source[i].definitions.length; j++) {
83 const definition = source[i].definitions[j];
84 if (definition.kind === Kind.FRAGMENT_DEFINITION) {
85 const name = definition.name.value;
86 const value = stringifyDocument(definition);
87 // Fragments will be deduplicated according to this Map
88 if (!fragmentNames.has(name)) {
89 fragmentNames.set(name, value);
90 definitions.push(definition);
91 } else if (
92 process.env.NODE_ENV !== 'production' &&
93 fragmentNames.get(name) !== value
94 ) {
95 // Fragments with the same names is expected to have the same contents
96 console.warn(
97 '[WARNING: Duplicate Fragment] A fragment with name `' +
98 name +
99 '` already exists in this document.\n' +
100 'While fragment names may not be unique across your source, each name must be unique per document.'
101 );
102 }
103 } else {
104 definitions.push(definition);
105 }
106 }
107 }
108
109 return keyDocument({
110 kind: Kind.DOCUMENT,
111 definitions,
112 });
113}
114
115export { gql };