UNPKG

8.21 kBPlain TextView Raw
1import "apollo-env";
2
3import { fs } from "./localfs";
4import { stripIndents } from "common-tags";
5const astTypes = require("ast-types");
6const recast = require("recast");
7
8import {
9 buildClientSchema,
10 Source,
11 concatAST,
12 parse,
13 DocumentNode,
14 GraphQLSchema,
15 visit,
16 Kind,
17 OperationDefinitionNode,
18 FragmentDefinitionNode
19} from "graphql";
20
21import { ToolError } from "apollo-language-server";
22
23export 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
37function maybeCommentedOut(content: string) {
38 return (
39 (content.indexOf("/*") > -1 && content.indexOf("*/") > -1) ||
40 content.split("//").length > 1
41 );
42}
43
44function 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
66function extractDocumentsWithAST(
67 content: string,
68 options: {
69 tagName?: string;
70 parser?: any;
71 }
72): string[] {
73 let tagName = options.tagName || "gql";
74
75 // Sometimes the js is unparsable, so this function will throw
76 const ast = recast.parse(content, {
77 parser: options.parser || require("recast/parsers/babylon")
78 });
79
80 const finished: string[] = [];
81
82 // isolate the template literals tagged with gql
83 astTypes.visit(ast, {
84 visitTaggedTemplateExpression(path: any) {
85 const tag = path.value.tag;
86 if (tag.name === tagName) {
87 // This currently ignores the anti-pattern of including an interpolated
88 // string as anything other than a fragment definition, for example a
89 // literal(these cases could be covered during the replacement of
90 // literals in the signature calculation)
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
106export 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
133export 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
205export function loadAndMergeQueryDocuments(
206 inputPaths: string[],
207 tagName: string = "gql"
208): DocumentNode {
209 return concatAST(loadQueryDocuments(inputPaths, tagName));
210}
211
212export 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 // We could use separateOperations from graphql-js in the case that
221 // all fragments are defined in the same file. Currently this
222 // solution duplicates much of the logic, adding the ability to pull
223 // fragments from separate files
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
248export 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
270function getNestedFragments(
271 operation: OperationDefinitionNode | FragmentDefinitionNode,
272 fragments: Record<string, FragmentDefinitionNode>,
273 errorLogger?: (message: string) => void
274) {
275 // Using an object ensures that we only include each fragment definition once.
276 // We are assured that there will be no duplicate fragment names during the
277 // extraction step
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}