353 lines
11 KiB
JavaScript
353 lines
11 KiB
JavaScript
"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 <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 (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
|