UNPKG

gatsby

Version:
584 lines (566 loc) • 20.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = void 0; var _traverse = _interopRequireDefault(require("@babel/traverse")); var t = _interopRequireWildcard(require("@babel/types")); var _findSlices = require("../utils/babel/find-slices"); var _babelParseToAst = require("../utils/babel-parse-to-ast"); var _codeFrame = require("@babel/code-frame"); var _gatsbyCoreUtils = require("gatsby-core-utils"); var _errorParser = require("./error-parser"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* eslint-disable no-unused-expressions */const fs = require(`fs-extra`); const crypto = require(`crypto`); const _ = require(`lodash`); const slugify = require(`slugify`); // Traverse is a es6 module... const { getGraphQLTag, StringInterpolationNotAllowedError, EmptyGraphQLTagError, GraphQLSyntaxError, ExportIsNotAsyncError, isWithinConfigExport } = require(`babel-plugin-remove-graphql-queries`); const report = require(`gatsby-cli/lib/reporter`); const apiRunnerNode = require(`../utils/api-runner-node`); const { actions } = require(`../redux/actions`); const { store } = require(`../redux`); /** * Add autogenerated query name if it wasn't defined by user. */ const generateQueryName = ({ def, hash, file, queryType }) => { if (!def.name || !def.name.value) { const slugified = slugify(file, { replacement: ` `, lower: false }); def.name = { value: `${_.camelCase(`${queryType}-${slugified}-${hash}`)}`, kind: `Name` }; } return def; }; // taken from `babel-plugin-remove-graphql-queries`, in the future import from // there function followVariableDeclarations(binding) { var _binding$path, _node$init; const node = binding === null || binding === void 0 ? void 0 : (_binding$path = binding.path) === null || _binding$path === void 0 ? void 0 : _binding$path.node; if ((node === null || node === void 0 ? void 0 : node.type) === `VariableDeclarator` && (node === null || node === void 0 ? void 0 : node.id.type) === `Identifier` && (node === null || node === void 0 ? void 0 : (_node$init = node.init) === null || _node$init === void 0 ? void 0 : _node$init.type) === `Identifier`) { return followVariableDeclarations(binding.path.scope.getBinding(node.init.name)); } return binding; } function referencesGatsby(path, callee, calleeName) { // This works for es6 imports if (callee.referencesImport(`gatsby`, ``)) { return true; } else { var _declaration$path$nod; // This finds where userStaticQuery was declared and then checks // if it is a "require" and "gatsby" is the argument. const declaration = path.scope.getBinding(calleeName); if (declaration && ((_declaration$path$nod = declaration.path.node.init) === null || _declaration$path$nod === void 0 ? void 0 : _declaration$path$nod.callee.name) === `require` && declaration.path.node.init.arguments[0].value === `gatsby`) { return true; } else { return false; } } } function isUseStaticQuery(path) { const callee = path.node.callee; if (callee.type === `MemberExpression`) { const property = callee.property; if (property.name === `useStaticQuery`) { var _path$node; return referencesGatsby(path, path.get(`callee`).get(`object`), (_path$node = path.node) === null || _path$node === void 0 ? void 0 : _path$node.callee.object.name); } return false; } if (callee.name === `useStaticQuery`) { var _path$node2; return referencesGatsby(path, path.get(`callee`), (_path$node2 = path.node) === null || _path$node2 === void 0 ? void 0 : _path$node2.callee.name); } return false; } 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, { parentSpan, addError } = {}) { let ast; // Since gatsby-plugin-mdx v4, we are using the resourceQuery feature of webpack's loaders to inject a content file into a page component. const cleanFilePath = (0, _gatsbyCoreUtils.getPathToLayoutComponent)(filePath); // 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: cleanFilePath, contents: fileStr, parentSpan }); if (transpiled && transpiled.length) { for (const item of transpiled) { try { const tmp = (0, _babelParseToAst.babelParseToAst)(item, cleanFilePath); ast = tmp; break; } catch (error) { // We emit the actual error below if every transpiled variant fails to parse } } if (ast === undefined) { addError({ id: `85912`, filePath: cleanFilePath, context: { filePath } }); store.dispatch(actions.queryExtractionGraphQLError({ componentPath: cleanFilePath })); return null; } } else { try { ast = (0, _babelParseToAst.babelParseToAst)(fileStr, cleanFilePath); } catch (error) { store.dispatch(actions.queryExtractionBabelError({ componentPath: cleanFilePath, error })); addError({ id: `85911`, filePath: cleanFilePath, context: { filePath: cleanFilePath } }); return null; } } return ast; } const panicOnGlobalTag = file => report.panicOnBuild(`Using the global \`graphql\` tag for Gatsby's queries isn't supported as of v3.\n` + `Import it instead like: import { graphql } from 'gatsby' in file:\n` + file); // Adapted from gatsby/src/utils/babel/babel-plugin-remove-api function findApiExport(ast, api) { let hasExport = false; const apiToFind = api !== null && api !== void 0 ? api : ``; (0, _traverse.default)(ast, { ExportNamedDeclaration(path) { const declaration = path.node.declaration; if (t.isExportNamedDeclaration(path.node) && !hasExport) { hasExport = path.node.specifiers.some(specifier => t.isExportSpecifier(specifier) && t.isIdentifier(specifier.exported) && specifier.exported.name === apiToFind); } let apiToCheck; if (t.isFunctionDeclaration(declaration) && declaration.id) { apiToCheck = declaration.id.name; } if (t.isVariableDeclaration(declaration) && t.isIdentifier(declaration.declarations[0].id)) { apiToCheck = declaration.declarations[0].id.name; } if (apiToCheck && apiToCheck === apiToFind) { hasExport = true; } } }); return hasExport; } async function findGraphQLTags(file, ast, { parentSpan, addError } = {}) { const documents = []; if (!ast) { return documents; } /** * 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) { panicOnGlobalTag(file); return; } gqlAst.definitions.forEach(def => { generateQueryName({ def, hash, file, queryType: `static` }); }); let templateLoc; taggedTemplateExpressPath.traverse({ TemplateElement(templateElementPath) { templateLoc = templateElementPath.node.loc; } }); const docInFile = { filePath: file, doc: gqlAst, text: text, hash: hash, isStaticQuery: true, isConfigQuery: false, isHook, templateLoc }; documentLocations.set(docInFile, `${taggedTemplateExpressPath.node.start}-${gqlAst.loc.start}`); documents.push(docInFile); }; // Look for queries in <StaticQuery /> 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, `<StaticQuery>`); } } } }); } }); return; } }); // Look for queries in useStaticQuery hooks. (0, _traverse.default)(ast, { CallExpression(hookPath) { if (!isUseStaticQuery(hookPath)) 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`); } } } } }); function TaggedTemplateExpression(innerPath) { const { ast: gqlAst, isGlobal, hash, text } = getGraphQLTag(innerPath); if (!gqlAst) return; if (isGlobal) { panicOnGlobalTag(file); return; } const isConfigQuery = isWithinConfigExport(innerPath); gqlAst.definitions.forEach(def => { generateQueryName({ def, hash, file, queryType: isConfigQuery ? `config` : `page` }); }); let templateLoc; innerPath.traverse({ TemplateElement(templateElementPath) { templateLoc = templateElementPath.node.loc; } }); const docInFile = { filePath: file, doc: gqlAst, text: text, hash: hash, isStaticQuery: false, isConfigQuery, isHook: false, templateLoc }; documentLocations.set(docInFile, `${innerPath.node.start}-${gqlAst.loc.start}`); documents.push(docInFile); } // When a component has a StaticQuery we scan all of its exports and follow those exported variables // to determine if they lead to this static query (via tagged template literal) (0, _traverse.default)(ast, { ExportNamedDeclaration(path, state) { // Skipping the edge case of re-exporting (i.e. "export { bar } from 'Bar'") // (it is handled elsewhere for queries, see usages of warnForUnknownQueryVariable) if (path.node.source) { return; } path.traverse({ TaggedTemplateExpression, ExportSpecifier(path) { var _binding$path2; const binding = followVariableDeclarations(path.scope.getBinding(path.node.local.name)); binding === null || binding === void 0 ? void 0 : (_binding$path2 = binding.path) === null || _binding$path2 === void 0 ? void 0 : _binding$path2.traverse({ TaggedTemplateExpression }); } }); } }); // Remove duplicate queries const uniqueQueries = _.uniqBy(documents, q => documentLocations.get(q)); return uniqueQueries; } const cache = {}; class FileParser { constructor({ parentSpan } = {}) { this.parentSpan = parentSpan; } async parseFile(file, addError) { let text; const cleanFilepath = (0, _gatsbyCoreUtils.getPathToLayoutComponent)(file); try { text = await fs.readFile(cleanFilepath, `utf8`); } catch (err) { addError({ id: `85913`, filePath: file, context: { filePath: file }, error: err }); store.dispatch(actions.queryExtractionGraphQLError({ componentPath: file })); return null; } // We do a quick check so we can exit early if this is a file we're not interested in. // We only process files that include the APIs below if (!text.includes(`graphql`) && !text.includes(`gatsby-plugin-image`) && !text.includes(`getServerData`) && !text.includes(`config`) && !text.includes(`Slice`) && !text.includes(`Head`)) { return null; } const hash = await (0, _gatsbyCoreUtils.md5)(file + text); try { if (!cache[hash]) { const ast = await parseToAst(file, text, { parentSpan: this.parentSpan, addError }); cache[hash] = { astDefinitions: await findGraphQLTags(file, ast, { parentSpan: this.parentSpan, addError }), serverData: findApiExport(ast, `getServerData`), config: findApiExport(ast, `config`), Head: findApiExport(ast, `Head`), pageSlices: (0, _findSlices.collectSlices)(ast, file) }; } const { astDefinitions, serverData, config, Head, pageSlices } = cache[hash]; // Note: we should dispatch this action even when getServerData is not found // (maybe it was set before, so now we need to reset renderMode from SSR to the default one) store.dispatch({ type: `SET_COMPONENT_FEATURES`, payload: { componentPath: file, serverData, config, Head } }); // 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) { store.dispatch(actions.queryExtractedBabelSuccess({ componentPath: file })); } return { astDefinitions, pageSlices }; } catch (err) { // default error let structuredError = { id: `85915`, context: { filePath: file } }; if (err instanceof StringInterpolationNotAllowedError) { const location = { start: err.interpolationStart, end: err.interpolationEnd }; structuredError = { id: `85916`, location, context: { codeFrame: (0, _codeFrame.codeFrameColumns)(text, location, { highlightCode: process.env.FORCE_COLOR !== `0` }) } }; } else if (err instanceof EmptyGraphQLTagError) { const location = err.templateLoc ? { start: err.templateLoc.start, end: err.templateLoc.end } : null; structuredError = { id: `85917`, location, context: { codeFrame: location ? (0, _codeFrame.codeFrameColumns)(text, location, { highlightCode: process.env.FORCE_COLOR !== `0` }) : null } }; } else if (err instanceof GraphQLSyntaxError) { const location = { start: (0, _errorParser.locInGraphQlToLocInFile)(err.templateLoc, err.originalError.locations[0]) }; structuredError = { id: `85918`, location, context: { codeFrame: location ? (0, _codeFrame.codeFrameColumns)(text, location, { highlightCode: process.env.FORCE_COLOR !== `0`, message: err.originalError.message }) : null, sourceMessage: err.originalError.message } }; } else if (err instanceof ExportIsNotAsyncError) { const location = { start: err.exportStart, end: err.exportStart }; structuredError = { id: `85929`, location, context: { exportName: err.exportName, codeFrame: (0, _codeFrame.codeFrameColumns)(text, location, { highlightCode: process.env.FORCE_COLOR !== `0` }) } }; } addError({ ...structuredError, filePath: file }); store.dispatch(actions.queryExtractionGraphQLError({ componentPath: file })); return null; } } async parseFiles(files, addError) { const documents = []; const pageSliceUsage = new Map(); return Promise.all(files.map(file => this.parseFile(file, addError).then(results => { if (results) { const { astDefinitions, pageSlices } = results; documents.push(...(astDefinitions || [])); if (pageSlices) { pageSliceUsage.set(file, pageSlices); } } }))).then(() => { store.dispatch({ type: `SET_COMPONENTS_USING_PAGE_SLICES`, payload: pageSliceUsage }); return documents; }); } } exports.default = FileParser; //# sourceMappingURL=file-parser.js.map