"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = void 0; var _traverse = _interopRequireDefault(require("@babel/traverse")); var _babelParseToAst = require("../utils/babel-parse-to-ast"); const fs = require(`fs-extra`); const crypto = require(`crypto`); const _ = require(`lodash`); // Traverse is a es6 module... const getGraphQLTag = require(`babel-plugin-remove-graphql-queries`).getGraphQLTag; const report = require(`gatsby-cli/lib/reporter`); const apiRunnerNode = require(`../utils/api-runner-node`); const { boundActionCreators } = require(`../redux/actions`); /** * Add autogenerated query name if it wasn't defined by user. */ const generateQueryName = ({ def, hash, file }) => { if (!def.name || !def.name.value) { def.name = { value: `${_.camelCase(file)}${hash}`, kind: `Name` }; } return def; }; const 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}". Perhaps the variable name has a typo? Also 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`); async function parseToAst(filePath, fileStr) { let ast; // Preprocess and attempt to parse source; return an AST if we can, log an // error if we can't. const transpiled = await apiRunnerNode(`preprocessSource`, { filename: filePath, contents: fileStr }); if (transpiled && transpiled.length) { for (const item of transpiled) { try { const tmp = (0, _babelParseToAst.babelParseToAst)(item, filePath); ast = tmp; break; } catch (error) { report.error(error); boundActionCreators.queryExtractionGraphQLError({ componentPath: filePath }); continue; } } if (ast === undefined) { report.error(`Failed to parse preprocessed file ${filePath}`); boundActionCreators.queryExtractionGraphQLError({ componentPath: filePath }); return null; } } else { try { ast = (0, _babelParseToAst.babelParseToAst)(fileStr, filePath); } catch (error) { boundActionCreators.queryExtractionBabelError({ componentPath: filePath, error }); report.error(`There was a problem parsing "${filePath}"; any GraphQL ` + `fragments or queries in this file were not processed. \n` + `This may indicate a syntax error in the code, or it may be a file type ` + `that Gatsby does not know how to parse.`); return null; } } return ast; } const 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); async function findGraphQLTags(file, text) { return new Promise((resolve, reject) => { parseToAst(file, text).then(ast => { let queries = []; if (!ast) { resolve(queries); return; } /** * A map of graphql documents to unique locations. * * A graphql document's unique location is made of: * * - the location of the graphql template literal that contains the document, and * - the document's location within the graphql template literal * * This is used to prevent returning duplicated documents. */ const documentLocations = new WeakMap(); const extractStaticQuery = (taggedTemplateExpressPath, isHook = false) => { const { ast: gqlAst, text, hash, isGlobal } = getGraphQLTag(taggedTemplateExpressPath); if (!gqlAst) return; if (isGlobal) warnForGlobalTag(file); gqlAst.definitions.forEach(def => { documentLocations.set(def, `${taggedTemplateExpressPath.node.start}-${def.loc.start}`); generateQueryName({ def, hash, file }); }); const definitions = [...gqlAst.definitions].map(d => { d.isStaticQuery = true; d.isHook = isHook; d.text = text; d.hash = hash; taggedTemplateExpressPath.traverse({ TemplateElement(templateElementPath) { d.templateLoc = templateElementPath.node.loc; } }); return d; }); queries.push(...definitions); }; // Look for queries in elements. (0, _traverse.default)(ast, { JSXElement(path) { if (path.node.openingElement.name.name !== `StaticQuery`) { return; } // astexplorer.com link I (@kyleamathews) used when prototyping this algorithm // https://astexplorer.net/#/gist/ab5d71c0f08f287fbb840bf1dd8b85ff/2f188345d8e5a4152fe7c96f0d52dbcc6e9da466 path.traverse({ JSXAttribute(jsxPath) { if (jsxPath.node.name.name !== `query`) { return; } jsxPath.traverse({ // Assume the query is inline in the component and extract that. TaggedTemplateExpression(templatePath) { extractStaticQuery(templatePath); }, // Also see if it's a variable that's passed in as a prop // and if it is, go find it. Identifier(identifierPath) { if (identifierPath.node.name !== `graphql`) { const varName = identifierPath.node.name; let found = false; (0, _traverse.default)(ast, { VariableDeclarator(varPath) { if (varPath.node.id.name === varName && varPath.node.init.type === `TaggedTemplateExpression`) { varPath.traverse({ TaggedTemplateExpression(templatePath) { found = true; extractStaticQuery(templatePath); } }); } } }); if (!found) { warnForUnknownQueryVariable(varName, file, ``); } } } }); } }); return; } }); // Look for queries in useStaticQuery hooks. (0, _traverse.default)(ast, { CallExpression(hookPath) { if (hookPath.node.callee.name !== `useStaticQuery` || !hookPath.get(`callee`).referencesImport(`gatsby`)) { return; } const firstArg = hookPath.get(`arguments`)[0]; // Assume the query is inline in the component and extract that. if (firstArg.isTaggedTemplateExpression()) { extractStaticQuery(firstArg, true); // Also see if it's a variable that's passed in as a prop // and if it is, go find it. } else if (firstArg.isIdentifier()) { if (firstArg.node.name !== `graphql` && firstArg.node.name !== `useStaticQuery`) { const varName = firstArg.node.name; let found = false; (0, _traverse.default)(ast, { VariableDeclarator(varPath) { if (varPath.node.id.name === varName && varPath.node.init.type === `TaggedTemplateExpression`) { varPath.traverse({ TaggedTemplateExpression(templatePath) { found = true; extractStaticQuery(templatePath, true); } }); } } }); if (!found) { warnForUnknownQueryVariable(varName, file, `useStaticQuery`); } } } } }); // Look for exported page queries (0, _traverse.default)(ast, { ExportNamedDeclaration(path, state) { path.traverse({ TaggedTemplateExpression(innerPath) { const { ast: gqlAst, isGlobal, hash, text } = getGraphQLTag(innerPath); if (!gqlAst) return; if (isGlobal) warnForGlobalTag(file); gqlAst.definitions.forEach(def => { documentLocations.set(def, `${innerPath.node.start}-${def.loc.start}`); generateQueryName({ def, hash, file }); }); queries.push(...gqlAst.definitions.map(d => { d.text = text; innerPath.traverse({ TemplateElement(templateElementPath) { d.templateLoc = templateElementPath.node.loc; } }); return d; })); } }); } }); // Remove duplicate queries const uniqueQueries = _.uniqBy(queries, q => documentLocations.get(q)); resolve(uniqueQueries); }).catch(reject); }); } const cache = {}; class FileParser { async parseFile(file) { let text; try { text = await fs.readFile(file, `utf8`); } catch (err) { report.error(`There was a problem reading the file: ${file}`, err); boundActionCreators.queryExtractionGraphQLError({ componentPath: file }); return null; } if (text.indexOf(`graphql`) === -1) return null; const hash = crypto.createHash(`md5`).update(file).update(text).digest(`hex`); try { let astDefinitions = cache[hash] || (cache[hash] = await findGraphQLTags(file, text)); // If any AST definitions were extracted, report success. // This can mean there is none or there was a babel error when // we tried to extract the graphql AST. if (astDefinitions.length > 0) { boundActionCreators.queryExtractedBabelSuccess({ componentPath: file }); } return astDefinitions.length ? { kind: `Document`, definitions: astDefinitions } : null; } catch (err) { report.error(`There was a problem parsing the GraphQL query in file: ${file}`, err); boundActionCreators.queryExtractionGraphQLError({ componentPath: file }); return null; } } async parseFiles(files) { const documents = new Map(); return Promise.all(files.map(file => this.parseFile(file).then(doc => { if (!doc) return; documents.set(file, doc); }))).then(() => documents); } } exports.default = FileParser; //# sourceMappingURL=file-parser.js.map