@sanity/codegen
Version:
Codegen toolkit for Sanity.io
175 lines (174 loc) • 7.27 kB
JavaScript
import { createRequire } from 'node:module';
import { traverse } from '@babel/core';
import * as babelTypes from '@babel/types';
import { getBabelConfig } from '../getBabelConfig.js';
import { resolveExpression } from './expressionResolvers.js';
import { parseSourceFile } from './parseSource.js';
import { QueryExtractionError } from './types.js';
const require = createRequire(import.meta.url);
const groqTagName = 'groq';
const defineQueryFunctionName = 'defineQuery';
const groqModuleName = 'groq';
const nextSanityModuleName = 'next-sanity';
const sveltekitModuleName = '@sanity/sveltekit';
const ignoreValue = '@sanity-typegen-ignore';
/**
* findQueriesInSource takes a source string and returns all GROQ queries in it.
* @param source - The source code to search for queries
* @param filename - The filename of the source code
* @param babelConfig - The babel configuration to use when parsing the source
* @param resolver - A resolver function to use when resolving module imports
* @returns
* @beta
* @internal
*/ export function findQueriesInSource(source, filename, babelConfig = getBabelConfig(), resolver = require.resolve) {
const queries = [];
const errors = [];
const file = parseSourceFile(source, filename, babelConfig);
traverse(file, {
// Look for variable declarations, e.g. `const myQuery = groq`... and extract the query.
// The variable name is used as the name of the query result type
VariableDeclarator (path) {
const { node, scope } = path;
const init = node.init;
// Look for tagged template expressions that are called with the `groq` tag
const isGroqTemplateTag = babelTypes.isTaggedTemplateExpression(init) && babelTypes.isIdentifier(init.tag) && init.tag.name === groqTagName;
// Look for strings wrapped in a defineQuery function call
const isDefineQueryCall = babelTypes.isCallExpression(init) && (isImportFrom(groqModuleName, defineQueryFunctionName, scope, init.callee) || isImportFrom(nextSanityModuleName, defineQueryFunctionName, scope, init.callee) || isImportFrom(sveltekitModuleName, defineQueryFunctionName, scope, init.callee));
if (babelTypes.isIdentifier(node.id) && (isGroqTemplateTag || isDefineQueryCall)) {
// If we find a comment leading the decleration which macthes with ignoreValue we don't add
// the query
if (declarationLeadingCommentContains(path, ignoreValue)) {
return;
}
const { end, id, start } = node;
const variable = {
id,
...start && {
start
},
...end && {
end
}
};
try {
const query = resolveExpression({
babelConfig,
file,
filename,
node: init,
resolver,
scope
});
queries.push({
filename,
query,
variable
});
} catch (cause) {
errors.push(new QueryExtractionError({
cause,
filename,
variable
}));
}
}
}
});
return {
errors,
filename,
queries
};
}
function declarationLeadingCommentContains(path, comment) {
/*
* We have to consider these cases:
*
* // @sanity-typegen-ignore
* const query = groq`...`
*
* // AST
* VariableDeclaration {
* declarations: [
* VariableDeclarator: {init: tag: {name: "groq"}}
* ],
* leadingComments: ...
* }
*
* // @sanity-typegen-ignore
* const query1 = groq`...`, query2 = groq`...`
*
* // AST
* VariableDeclaration {
* declarations: [
* VariableDeclarator: {init: tag: {name: "groq"}}
* VariableDeclarator: {init: tag: {name: "groq"}}
* ],
* leadingComments: ...
* }
*
* // @sanity-typegen-ignore
* export const query = groq`...`
*
* // AST
* ExportNamedDeclaration {
* declaration: VariableDeclaration {
* declarations: [
* VariableDeclarator: {init: tag: {name: "groq"}}
* VariableDeclarator: {init: tag: {name: "groq"}}
* ],
* },
* leadingComments: ...
* }
*
* In the case where multiple variables are under the same VariableDeclaration the leadingComments
* will still be on the VariableDeclaration
*
* In the case where the variable is exported, the leadingComments are on the
* ExportNamedDeclaration which includes the VariableDeclaration in its own declaration property
*/ const variableDeclaration = path.find((node)=>node.isVariableDeclaration());
if (!variableDeclaration) return false;
if (variableDeclaration.node.leadingComments?.find((commentItem)=>commentItem.value.trim() === comment)) {
return true;
}
// If the declaration is exported, the comment lies on the parent of the export declaration
if (variableDeclaration.parent.leadingComments?.find((commentItem)=>commentItem.value.trim() === comment)) {
return true;
}
return false;
}
function isImportFrom(moduleName, importName, scope, node) {
if (babelTypes.isIdentifier(node)) {
const binding = scope.getBinding(node.name);
if (!binding) {
return false;
}
const { path } = binding;
// import { foo } from 'groq'
if (babelTypes.isImportSpecifier(path.node)) {
return path.node.importKind === 'value' && path.parentPath && babelTypes.isImportDeclaration(path.parentPath.node) && path.parentPath.node.source.value === moduleName && babelTypes.isIdentifier(path.node.imported) && path.node.imported.name === importName;
}
// const { defineQuery } = require('groq')
if (babelTypes.isVariableDeclarator(path.node)) {
const { init } = path.node;
return babelTypes.isCallExpression(init) && babelTypes.isIdentifier(init.callee) && init.callee.name === 'require' && babelTypes.isStringLiteral(init.arguments[0]) && init.arguments[0].value === moduleName;
}
}
// import * as foo from 'groq'
// foo.defineQuery(...)
if (babelTypes.isMemberExpression(node)) {
const { object, property } = node;
if (!babelTypes.isIdentifier(object)) {
return false;
}
const binding = scope.getBinding(object.name);
if (!binding) {
return false;
}
const { path } = binding;
return babelTypes.isIdentifier(object) && babelTypes.isIdentifier(property) && property.name === importName && babelTypes.isImportNamespaceSpecifier(path.node) && path.parentPath && babelTypes.isImportDeclaration(path.parentPath.node) && path.parentPath.node.source.value === moduleName;
}
return false;
}
//# sourceMappingURL=findQueriesInSource.js.map