/*** IMPORT ------------------------------------------- ***/ import { parse, type DefinitionNode, type DocumentNode, type Location } from "graphql"; /*** UTILITY ------------------------------------------ ***/ const docCache = new Map(); const fragmentSourceMap = new Map>(); 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(); 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>(doc.definitions as unknown as Record[]); 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); }); }); 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)!; }