gatsby
Version:
Blazing fast modern site generator for React
584 lines (566 loc) • 20.3 kB
JavaScript
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
;