1 | import "apollo-env";
|
2 |
|
3 | import { fs } from "./localfs";
|
4 | import { stripIndents } from "common-tags";
|
5 | const astTypes = require("ast-types");
|
6 | const recast = require("recast");
|
7 |
|
8 | import {
|
9 | buildClientSchema,
|
10 | Source,
|
11 | concatAST,
|
12 | parse,
|
13 | DocumentNode,
|
14 | GraphQLSchema,
|
15 | visit,
|
16 | Kind,
|
17 | OperationDefinitionNode,
|
18 | FragmentDefinitionNode
|
19 | } from "graphql";
|
20 |
|
21 | import { ToolError } from "apollo-language-server";
|
22 |
|
23 | export function loadSchema(schemaPath: string): GraphQLSchema {
|
24 | if (!fs.existsSync(schemaPath)) {
|
25 | throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`);
|
26 | }
|
27 | const schemaData = require(schemaPath);
|
28 |
|
29 | if (!schemaData.data && !schemaData.__schema) {
|
30 | throw new ToolError(
|
31 | "GraphQL schema file should contain a valid GraphQL introspection query result"
|
32 | );
|
33 | }
|
34 | return buildClientSchema(schemaData.data ? schemaData.data : schemaData);
|
35 | }
|
36 |
|
37 | function maybeCommentedOut(content: string) {
|
38 | return (
|
39 | (content.indexOf("/*") > -1 && content.indexOf("*/") > -1) ||
|
40 | content.split("//").length > 1
|
41 | );
|
42 | }
|
43 |
|
44 | function filterValidDocuments(documents: string[]) {
|
45 | return documents.filter(document => {
|
46 | const source = new Source(document);
|
47 | try {
|
48 | parse(source);
|
49 | return true;
|
50 | } catch (e) {
|
51 | if (!maybeCommentedOut(document)) {
|
52 | console.warn(
|
53 | stripIndents`
|
54 | Failed to parse:
|
55 |
|
56 | ${document.trim().split("\n")[0]}...
|
57 | `
|
58 | );
|
59 | }
|
60 |
|
61 | return false;
|
62 | }
|
63 | });
|
64 | }
|
65 |
|
66 | function extractDocumentsWithAST(
|
67 | content: string,
|
68 | options: {
|
69 | tagName?: string;
|
70 | parser?: any;
|
71 | }
|
72 | ): string[] {
|
73 | let tagName = options.tagName || "gql";
|
74 |
|
75 |
|
76 | const ast = recast.parse(content, {
|
77 | parser: options.parser || require("recast/parsers/babylon")
|
78 | });
|
79 |
|
80 | const finished: string[] = [];
|
81 |
|
82 |
|
83 | astTypes.visit(ast, {
|
84 | visitTaggedTemplateExpression(path: any) {
|
85 | const tag = path.value.tag;
|
86 | if (tag.name === tagName) {
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | finished.push(
|
92 | (path.value.quasi.quasis as Array<{
|
93 | value: { cooked: string; raw: string };
|
94 | }>)
|
95 | .map(({ value }) => value.cooked)
|
96 | .join("")
|
97 | );
|
98 | }
|
99 | return this.traverse(path);
|
100 | }
|
101 | });
|
102 |
|
103 | return finished;
|
104 | }
|
105 |
|
106 | export function extractDocumentFromJavascript(
|
107 | content: string,
|
108 | options: {
|
109 | tagName?: string;
|
110 | parser?: any;
|
111 | inputPath?: string;
|
112 | } = {}
|
113 | ): string | null {
|
114 | let matches: string[] = [];
|
115 |
|
116 | try {
|
117 | matches = extractDocumentsWithAST(content, options);
|
118 | } catch (e) {
|
119 | e.message =
|
120 | "Operation extraction " +
|
121 | (options.inputPath ? "from file " + options.inputPath + " " : "") +
|
122 | "failed with \n" +
|
123 | e.message;
|
124 |
|
125 | throw e;
|
126 | }
|
127 |
|
128 | matches = filterValidDocuments(matches);
|
129 | const doc = matches.join("\n");
|
130 | return doc.length ? doc : null;
|
131 | }
|
132 |
|
133 | export function loadQueryDocuments(
|
134 | inputPaths: string[],
|
135 | tagName: string = "gql"
|
136 | ): DocumentNode[] {
|
137 | const sources = inputPaths
|
138 | .map(inputPath => {
|
139 | if (fs.lstatSync(inputPath).isDirectory()) {
|
140 | return null;
|
141 | }
|
142 |
|
143 | const body = fs.readFileSync(inputPath, "utf8");
|
144 | if (!body) {
|
145 | return null;
|
146 | }
|
147 |
|
148 | if (
|
149 | inputPath.endsWith(".jsx") ||
|
150 | inputPath.endsWith(".js") ||
|
151 | inputPath.endsWith(".tsx") ||
|
152 | inputPath.endsWith(".ts")
|
153 | ) {
|
154 | let parser;
|
155 | if (inputPath.endsWith(".ts")) {
|
156 | parser = require("recast/parsers/typescript");
|
157 | } else if (inputPath.endsWith(".tsx")) {
|
158 | parser = {
|
159 | parse: (source: any, options: any) => {
|
160 | const babelParser = require("@babel/parser");
|
161 | options = require("recast/parsers/_babylon_options.js")(options);
|
162 | options.plugins.push("jsx", "typescript");
|
163 | return babelParser.parse(source, options);
|
164 | }
|
165 | };
|
166 | } else {
|
167 | parser = require("recast/parsers/babylon");
|
168 | }
|
169 |
|
170 | const doc = extractDocumentFromJavascript(body.toString(), {
|
171 | tagName,
|
172 | parser,
|
173 | inputPath
|
174 | });
|
175 | return doc ? new Source(doc, inputPath) : null;
|
176 | }
|
177 |
|
178 | if (
|
179 | inputPath.endsWith(".graphql") ||
|
180 | inputPath.endsWith(".graphqls") ||
|
181 | inputPath.endsWith(".gql")
|
182 | ) {
|
183 | return new Source(body, inputPath);
|
184 | }
|
185 |
|
186 | return null;
|
187 | })
|
188 | .filter(source => source)
|
189 | .map(source => {
|
190 | try {
|
191 | return parse(source!);
|
192 | } catch (e) {
|
193 | const name = (source && source.name) || "";
|
194 | console.warn(stripIndents`
|
195 | Warning: error parsing GraphQL file ${name}
|
196 | ${e.stack}`);
|
197 | return null;
|
198 | }
|
199 | })
|
200 | .filter(source => source);
|
201 |
|
202 | return sources as DocumentNode[];
|
203 | }
|
204 |
|
205 | export function loadAndMergeQueryDocuments(
|
206 | inputPaths: string[],
|
207 | tagName: string = "gql"
|
208 | ): DocumentNode {
|
209 | return concatAST(loadQueryDocuments(inputPaths, tagName));
|
210 | }
|
211 |
|
212 | export function extractOperationsAndFragments(
|
213 | documents: Array<DocumentNode>,
|
214 | errorLogger?: (message: string) => void
|
215 | ) {
|
216 | const fragments: Record<string, FragmentDefinitionNode> = {};
|
217 | const operations: Array<OperationDefinitionNode> = [];
|
218 |
|
219 | documents.forEach(operation => {
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | visit(operation, {
|
225 | [Kind.FRAGMENT_DEFINITION]: node => {
|
226 | if (!node.name || node.name.kind !== "Name") {
|
227 | (errorLogger || console.warn)(
|
228 | `Fragment Definition must have a name ${node}`
|
229 | );
|
230 | }
|
231 |
|
232 | if (fragments[node.name.value]) {
|
233 | (errorLogger || console.warn)(
|
234 | `Duplicate definition of fragment ${node.name.value}. Please rename one of them or use the same fragment`
|
235 | );
|
236 | }
|
237 | fragments[node.name.value] = node;
|
238 | },
|
239 | [Kind.OPERATION_DEFINITION]: node => {
|
240 | operations.push(node);
|
241 | }
|
242 | });
|
243 | });
|
244 |
|
245 | return { fragments, operations };
|
246 | }
|
247 |
|
248 | export function combineOperationsAndFragments(
|
249 | operations: Array<OperationDefinitionNode>,
|
250 | fragments: Record<string, FragmentDefinitionNode>,
|
251 | errorLogger?: (message: string) => void
|
252 | ) {
|
253 | const fullOperations: Array<DocumentNode> = [];
|
254 | operations.forEach(operation => {
|
255 | const completeOperation: Array<
|
256 | OperationDefinitionNode | FragmentDefinitionNode
|
257 | > = [
|
258 | operation,
|
259 | ...Object.values(getNestedFragments(operation, fragments, errorLogger))
|
260 | ];
|
261 |
|
262 | fullOperations.push({
|
263 | kind: "Document",
|
264 | definitions: completeOperation
|
265 | });
|
266 | });
|
267 | return fullOperations;
|
268 | }
|
269 |
|
270 | function getNestedFragments(
|
271 | operation: OperationDefinitionNode | FragmentDefinitionNode,
|
272 | fragments: Record<string, FragmentDefinitionNode>,
|
273 | errorLogger?: (message: string) => void
|
274 | ) {
|
275 |
|
276 |
|
277 |
|
278 | const combination: Record<string, FragmentDefinitionNode> = {};
|
279 | visit(operation, {
|
280 | [Kind.FRAGMENT_SPREAD]: node => {
|
281 | if (!node.name || node.name.kind !== "Name") {
|
282 | (errorLogger || console.warn)(
|
283 | `Fragment Spread must have a name ${node}`
|
284 | );
|
285 | }
|
286 | if (!fragments[node.name.value]) {
|
287 | (errorLogger || console.warn)(
|
288 | `Fragment ${node.name.value} is not defined. Please add the file containing the fragment to the set of included paths`
|
289 | );
|
290 | }
|
291 | Object.assign(
|
292 | combination,
|
293 | getNestedFragments(fragments[node.name.value], fragments, errorLogger),
|
294 | { [node.name.value]: fragments[node.name.value] }
|
295 | );
|
296 | }
|
297 | });
|
298 | return combination;
|
299 | }
|