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