Mirror: The spec-compliant minimum of client-side GraphQL.
1import { ASTNode } from './ast';
2
3export const BREAK = {};
4
5export function visit<N extends ASTNode>(root: N, visitor: ASTVisitor): N;
6export function visit<R>(root: ASTNode, visitor: ASTReducer<R>): R;
7
8export function visit(node: ASTNode, visitor: ASTVisitor | ASTReducer<any>) {
9 const ancestors: Array<ASTNode | ReadonlyArray<ASTNode>> = [];
10 const path: Array<string | number> = [];
11
12 function traverse(
13 node: ASTNode,
14 key?: string | number | undefined,
15 parent?: ASTNode | ReadonlyArray<ASTNode> | undefined
16 ) {
17 let hasEdited = false;
18
19 const enter =
20 (visitor[node.kind] && visitor[node.kind].enter) ||
21 visitor[node.kind] ||
22 (visitor as EnterLeaveVisitor<ASTNode>).enter;
23 const resultEnter = enter && enter.call(visitor, node, key, parent, path, ancestors);
24 if (resultEnter === false) {
25 return node;
26 } else if (resultEnter === null) {
27 return null;
28 } else if (resultEnter === BREAK) {
29 throw BREAK;
30 } else if (resultEnter && typeof resultEnter.kind === 'string') {
31 hasEdited = resultEnter !== node;
32 node = resultEnter;
33 }
34
35 if (parent) ancestors.push(parent);
36
37 let result: any;
38 const copy = { ...node };
39 for (const nodeKey in node) {
40 path.push(nodeKey);
41 let value = node[nodeKey];
42 if (Array.isArray(value)) {
43 const newValue: any[] = [];
44 for (let index = 0; index < value.length; index++) {
45 if (value[index] != null && typeof value[index].kind === 'string') {
46 ancestors.push(node);
47 path.push(index);
48 result = traverse(value[index], index, value);
49 path.pop();
50 ancestors.pop();
51 if (result === undefined) {
52 newValue.push(value[index]);
53 } else if (result === null) {
54 hasEdited = true;
55 } else {
56 hasEdited = hasEdited || result !== value[index];
57 newValue.push(result);
58 }
59 }
60 }
61 value = newValue;
62 } else if (value != null && typeof value.kind === 'string') {
63 result = traverse(value, nodeKey, node);
64 if (result !== undefined) {
65 hasEdited = hasEdited || value !== result;
66 value = result;
67 }
68 }
69
70 path.pop();
71 if (hasEdited) copy[nodeKey] = value;
72 }
73
74 if (parent) ancestors.pop();
75 const leave =
76 (visitor[node.kind] && visitor[node.kind].leave) ||
77 (visitor as EnterLeaveVisitor<ASTNode>).leave;
78 const resultLeave = leave && leave.call(visitor, node, key, parent, path, ancestors);
79 if (resultLeave === BREAK) {
80 throw BREAK;
81 } else if (resultLeave !== undefined) {
82 return resultLeave;
83 } else if (resultEnter !== undefined) {
84 return hasEdited ? copy : resultEnter;
85 } else {
86 return hasEdited ? copy : node;
87 }
88 }
89
90 try {
91 const result = traverse(node);
92 return result !== undefined && result !== false ? result : node;
93 } catch (error) {
94 if (error !== BREAK) throw error;
95 return node;
96 }
97}
98
99export type ASTVisitor = EnterLeaveVisitor<ASTNode> | KindVisitor;
100
101type KindVisitor = {
102 readonly [NodeT in ASTNode as NodeT['kind']]?: ASTVisitFn<NodeT> | EnterLeaveVisitor<NodeT>;
103};
104
105interface EnterLeaveVisitor<TVisitedNode extends ASTNode> {
106 readonly enter?: ASTVisitFn<TVisitedNode> | undefined;
107 readonly leave?: ASTVisitFn<TVisitedNode> | undefined;
108}
109
110export type ASTVisitFn<Node extends ASTNode> = (
111 node: Node,
112 key: string | number | undefined,
113 parent: ASTNode | ReadonlyArray<ASTNode> | undefined,
114 path: ReadonlyArray<string | number>,
115 ancestors: ReadonlyArray<ASTNode | ReadonlyArray<ASTNode>>
116) => any;
117
118export type ASTReducer<R> = {
119 readonly [NodeT in ASTNode as NodeT['kind']]?: {
120 readonly enter?: ASTVisitFn<NodeT>;
121 readonly leave: ASTReducerFn<NodeT, R>;
122 };
123};
124
125type ASTReducerFn<TReducedNode extends ASTNode, R> = (
126 node: { [K in keyof TReducedNode]: ReducedField<TReducedNode[K], R> },
127 key: string | number | undefined,
128 parent: ASTNode | ReadonlyArray<ASTNode> | undefined,
129 path: ReadonlyArray<string | number>,
130 ancestors: ReadonlyArray<ASTNode | ReadonlyArray<ASTNode>>
131) => R;
132
133type ReducedField<T, R> = T extends null | undefined
134 ? T
135 : T extends ReadonlyArray<any>
136 ? ReadonlyArray<R>
137 : R;