UNPKG

14.2 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
5exports.__esModule = true;
6exports.default = void 0;
7
8var _traverse = _interopRequireDefault(require("@babel/traverse"));
9
10var _babelParseToAst = require("../utils/babel-parse-to-ast");
11
12var _codeFrame = require("@babel/code-frame");
13
14var _errorParser = require("./error-parser");
15
16const fs = require(`fs-extra`);
17
18const crypto = require(`crypto`);
19
20const _ = require(`lodash`);
21
22const slugify = require(`slugify`); // Traverse is a es6 module...
23
24
25const {
26 getGraphQLTag,
27 StringInterpolationNotAllowedError,
28 EmptyGraphQLTagError,
29 GraphQLSyntaxError
30} = require(`babel-plugin-remove-graphql-queries`);
31
32const report = require(`gatsby-cli/lib/reporter`);
33
34const apiRunnerNode = require(`../utils/api-runner-node`);
35
36const {
37 boundActionCreators
38} = require(`../redux/actions`);
39
40const generateQueryName = ({
41 def,
42 hash,
43 file
44}) => {
45 if (!def.name || !def.name.value) {
46 const slugified = slugify(file, {
47 replacement: ` `,
48 lower: false
49 });
50 def.name = {
51 value: `${_.camelCase(slugified)}${hash}`,
52 kind: `Name`
53 };
54 }
55
56 return def;
57};
58
59function isUseStaticQuery(path) {
60 return path.node.callee.type === `MemberExpression` && path.node.callee.property.name === `useStaticQuery` && path.get(`callee`).get(`object`).referencesImport(`gatsby`) || path.node.callee.name === `useStaticQuery` && path.get(`callee`).referencesImport(`gatsby`);
61}
62
63const warnForUnknownQueryVariable = (varName, file, usageFunction) => report.warn(`\nWe were unable to find the declaration of variable "${varName}", which you passed as the "query" prop into the ${usageFunction} declaration in "${file}".
64
65Perhaps the variable name has a typo?
66
67Also note that we are currently unable to use queries defined in files other than the file where the ${usageFunction} is defined. If you're attempting to import the query, please move it into "${file}". If being able to import queries from another file is an important capability for you, we invite your help fixing it.\n`);
68
69async function parseToAst(filePath, fileStr, {
70 parentSpan,
71 addError
72} = {}) {
73 let ast; // Preprocess and attempt to parse source; return an AST if we can, log an
74 // error if we can't.
75
76 const transpiled = await apiRunnerNode(`preprocessSource`, {
77 filename: filePath,
78 contents: fileStr,
79 parentSpan: parentSpan
80 });
81
82 if (transpiled && transpiled.length) {
83 for (const item of transpiled) {
84 try {
85 const tmp = (0, _babelParseToAst.babelParseToAst)(item, filePath);
86 ast = tmp;
87 break;
88 } catch (error) {
89 boundActionCreators.queryExtractionGraphQLError({
90 componentPath: filePath
91 });
92 continue;
93 }
94 }
95
96 if (ast === undefined) {
97 addError({
98 id: `85912`,
99 filePath,
100 context: {
101 filePath
102 }
103 });
104 boundActionCreators.queryExtractionGraphQLError({
105 componentPath: filePath
106 });
107 return null;
108 }
109 } else {
110 try {
111 ast = (0, _babelParseToAst.babelParseToAst)(fileStr, filePath);
112 } catch (error) {
113 boundActionCreators.queryExtractionBabelError({
114 componentPath: filePath,
115 error
116 });
117 addError({
118 id: `85911`,
119 filePath,
120 context: {
121 filePath
122 }
123 });
124 return null;
125 }
126 }
127
128 return ast;
129}
130
131const warnForGlobalTag = file => report.warn(`Using the global \`graphql\` tag is deprecated, and will not be supported in v3.\n` + `Import it instead like: import { graphql } from 'gatsby' in file:\n` + file);
132
133async function findGraphQLTags(file, text, {
134 parentSpan,
135 addError
136} = {}) {
137 return new Promise((resolve, reject) => {
138 parseToAst(file, text, {
139 parentSpan,
140 addError
141 }).then(ast => {
142 const documents = [];
143
144 if (!ast) {
145 resolve(documents);
146 return;
147 }
148 /**
149 * A map of graphql documents to unique locations.
150 *
151 * A graphql document's unique location is made of:
152 *
153 * - the location of the graphql template literal that contains the document, and
154 * - the document's location within the graphql template literal
155 *
156 * This is used to prevent returning duplicated documents.
157 */
158
159
160 const documentLocations = new WeakMap();
161
162 const extractStaticQuery = (taggedTemplateExpressPath, isHook = false) => {
163 const {
164 ast: gqlAst,
165 text,
166 hash,
167 isGlobal
168 } = getGraphQLTag(taggedTemplateExpressPath);
169 if (!gqlAst) return;
170 if (isGlobal) warnForGlobalTag(file);
171 gqlAst.definitions.forEach(def => {
172 generateQueryName({
173 def,
174 hash,
175 file
176 });
177 });
178 let templateLoc;
179 taggedTemplateExpressPath.traverse({
180 TemplateElement(templateElementPath) {
181 templateLoc = templateElementPath.node.loc;
182 }
183
184 });
185 const docInFile = {
186 filePath: file,
187 doc: gqlAst,
188 text: text,
189 hash: hash,
190 isStaticQuery: true,
191 isHook,
192 templateLoc
193 };
194 documentLocations.set(docInFile, `${taggedTemplateExpressPath.node.start}-${gqlAst.loc.start}`);
195 documents.push(docInFile);
196 }; // Look for queries in <StaticQuery /> elements.
197
198
199 (0, _traverse.default)(ast, {
200 JSXElement(path) {
201 if (path.node.openingElement.name.name !== `StaticQuery`) {
202 return;
203 } // astexplorer.com link I (@kyleamathews) used when prototyping this algorithm
204 // https://astexplorer.net/#/gist/ab5d71c0f08f287fbb840bf1dd8b85ff/2f188345d8e5a4152fe7c96f0d52dbcc6e9da466
205
206
207 path.traverse({
208 JSXAttribute(jsxPath) {
209 if (jsxPath.node.name.name !== `query`) {
210 return;
211 }
212
213 jsxPath.traverse({
214 // Assume the query is inline in the component and extract that.
215 TaggedTemplateExpression(templatePath) {
216 extractStaticQuery(templatePath);
217 },
218
219 // Also see if it's a variable that's passed in as a prop
220 // and if it is, go find it.
221 Identifier(identifierPath) {
222 if (identifierPath.node.name !== `graphql`) {
223 const varName = identifierPath.node.name;
224 let found = false;
225 (0, _traverse.default)(ast, {
226 VariableDeclarator(varPath) {
227 if (varPath.node.id.name === varName && varPath.node.init.type === `TaggedTemplateExpression`) {
228 varPath.traverse({
229 TaggedTemplateExpression(templatePath) {
230 found = true;
231 extractStaticQuery(templatePath);
232 }
233
234 });
235 }
236 }
237
238 });
239
240 if (!found) {
241 warnForUnknownQueryVariable(varName, file, `<StaticQuery>`);
242 }
243 }
244 }
245
246 });
247 }
248
249 });
250 return;
251 }
252
253 }); // Look for queries in useStaticQuery hooks.
254
255 (0, _traverse.default)(ast, {
256 CallExpression(hookPath) {
257 if (!isUseStaticQuery(hookPath)) return;
258 const firstArg = hookPath.get(`arguments`)[0]; // Assume the query is inline in the component and extract that.
259
260 if (firstArg.isTaggedTemplateExpression()) {
261 extractStaticQuery(firstArg, true); // Also see if it's a variable that's passed in as a prop
262 // and if it is, go find it.
263 } else if (firstArg.isIdentifier()) {
264 if (firstArg.node.name !== `graphql` && firstArg.node.name !== `useStaticQuery`) {
265 const varName = firstArg.node.name;
266 let found = false;
267 (0, _traverse.default)(ast, {
268 VariableDeclarator(varPath) {
269 if (varPath.node.id.name === varName && varPath.node.init.type === `TaggedTemplateExpression`) {
270 varPath.traverse({
271 TaggedTemplateExpression(templatePath) {
272 found = true;
273 extractStaticQuery(templatePath, true);
274 }
275
276 });
277 }
278 }
279
280 });
281
282 if (!found) {
283 warnForUnknownQueryVariable(varName, file, `useStaticQuery`);
284 }
285 }
286 }
287 }
288
289 });
290
291 function TaggedTemplateExpression(innerPath) {
292 const {
293 ast: gqlAst,
294 isGlobal,
295 hash,
296 text
297 } = getGraphQLTag(innerPath);
298 if (!gqlAst) return;
299 if (isGlobal) warnForGlobalTag(file);
300 gqlAst.definitions.forEach(def => {
301 generateQueryName({
302 def,
303 hash,
304 file
305 });
306 });
307 let templateLoc;
308 innerPath.traverse({
309 TemplateElement(templateElementPath) {
310 templateLoc = templateElementPath.node.loc;
311 }
312
313 });
314 const docInFile = {
315 filePath: file,
316 doc: gqlAst,
317 text: text,
318 hash: hash,
319 isStaticQuery: false,
320 isHook: false,
321 templateLoc
322 };
323 documentLocations.set(docInFile, `${innerPath.node.start}-${gqlAst.loc.start}`);
324 documents.push(docInFile);
325 }
326
327 function followVariableDeclarations(binding) {
328 var _binding$path;
329
330 const node = (_binding$path = binding.path) === null || _binding$path === void 0 ? void 0 : _binding$path.node;
331
332 if (node && node.type === `VariableDeclarator` && node.id.type === `Identifier` && node.init.type === `Identifier`) {
333 return followVariableDeclarations(binding.path.scope.getBinding(node.init.name));
334 }
335
336 return binding;
337 } // Look for exported page queries
338
339
340 (0, _traverse.default)(ast, {
341 ExportNamedDeclaration(path, state) {
342 path.traverse({
343 TaggedTemplateExpression,
344
345 ExportSpecifier(path) {
346 const binding = followVariableDeclarations(path.scope.getBinding(path.node.local.name));
347 binding.path.traverse({
348 TaggedTemplateExpression
349 });
350 }
351
352 });
353 }
354
355 }); // Remove duplicate queries
356
357 const uniqueQueries = _.uniqBy(documents, q => documentLocations.get(q));
358
359 resolve(uniqueQueries);
360 }).catch(reject);
361 });
362}
363
364const cache = {};
365
366class FileParser {
367 constructor({
368 parentSpan
369 } = {}) {
370 this.parentSpan = parentSpan;
371 }
372
373 async parseFile(file, addError) {
374 let text;
375
376 try {
377 text = await fs.readFile(file, `utf8`);
378 } catch (err) {
379 addError({
380 id: `85913`,
381 filePath: file,
382 context: {
383 filePath: file
384 },
385 error: err
386 });
387 boundActionCreators.queryExtractionGraphQLError({
388 componentPath: file
389 });
390 return null;
391 }
392
393 if (!text.includes(`graphql`)) return null;
394 const hash = crypto.createHash(`md5`).update(file).update(text).digest(`hex`);
395
396 try {
397 const astDefinitions = cache[hash] || (cache[hash] = await findGraphQLTags(file, text, {
398 parentSpan: this.parentSpan,
399 addError
400 })); // If any AST definitions were extracted, report success.
401 // This can mean there is none or there was a babel error when
402 // we tried to extract the graphql AST.
403
404 if (astDefinitions.length > 0) {
405 boundActionCreators.queryExtractedBabelSuccess({
406 componentPath: file
407 });
408 }
409
410 return astDefinitions;
411 } catch (err) {
412 // default error
413 let structuredError = {
414 id: `85915`,
415 context: {
416 filePath: file
417 }
418 };
419
420 if (err instanceof StringInterpolationNotAllowedError) {
421 const location = {
422 start: err.interpolationStart,
423 end: err.interpolationEnd
424 };
425 structuredError = {
426 id: `85916`,
427 location,
428 context: {
429 codeFrame: (0, _codeFrame.codeFrameColumns)(text, location, {
430 highlightCode: process.env.FORCE_COLOR !== `0`
431 })
432 }
433 };
434 } else if (err instanceof EmptyGraphQLTagError) {
435 const location = err.templateLoc ? {
436 start: err.templateLoc.start,
437 end: err.templateLoc.end
438 } : null;
439 structuredError = {
440 id: `85917`,
441 location,
442 context: {
443 codeFrame: location ? (0, _codeFrame.codeFrameColumns)(text, location, {
444 highlightCode: process.env.FORCE_COLOR !== `0`
445 }) : null
446 }
447 };
448 } else if (err instanceof GraphQLSyntaxError) {
449 const location = {
450 start: (0, _errorParser.locInGraphQlToLocInFile)(err.templateLoc, err.originalError.locations[0])
451 };
452 structuredError = {
453 id: `85918`,
454 location,
455 context: {
456 codeFrame: location ? (0, _codeFrame.codeFrameColumns)(text, location, {
457 highlightCode: process.env.FORCE_COLOR !== `0`,
458 message: err.originalError.message
459 }) : null,
460 sourceMessage: err.originalError.message
461 }
462 };
463 }
464
465 addError(Object.assign({}, structuredError, {
466 filePath: file
467 }));
468 boundActionCreators.queryExtractionGraphQLError({
469 componentPath: file
470 });
471 return null;
472 }
473 }
474
475 async parseFiles(files, addError) {
476 const documents = [];
477 return Promise.all(files.map(file => this.parseFile(file, addError).then(docs => {
478 documents.push(...(docs || []));
479 }))).then(() => documents);
480 }
481
482}
483
484exports.default = FileParser;
485//# sourceMappingURL=file-parser.js.map
\No newline at end of file