@sanity/codegen
Version:
Codegen toolkit for Sanity.io
507 lines (506 loc) • 16.6 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import { traverse } from '@babel/core';
import { Scope } from '@babel/traverse';
import * as babelTypes from '@babel/types';
import createDebug from 'debug';
import { formatPath } from '../utils/formatPath.js';
import { parseSourceFile } from './parseSource.js';
const debug = createDebug('sanity:codegen:findQueries:debug');
const TAGGED_TEMPLATE_ALLOW_LIST = new Set([
'groq'
]);
const FUNCTION_WRAPPER_ALLOW_LIST = new Set([
'defineQuery'
]);
/**
* resolveExpression takes a node and returns the resolved value of the expression.
* @beta
* @internal
*/ export function resolveExpression({ babelConfig, file, filename, fnArguments = [], node, params = [], resolver, scope }) {
debug(`Resolving node ${node.type} in ${filename}:${node.loc?.start.line}:${node.loc?.start.column}`);
if (babelTypes.isTaggedTemplateExpression(node) && babelTypes.isIdentifier(node.tag) && TAGGED_TEMPLATE_ALLOW_LIST.has(node.tag.name)) {
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: node.quasi,
params,
resolver,
scope
});
}
if (babelTypes.isTemplateLiteral(node)) {
const resolvedExpressions = node.expressions.map((expression)=>resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: expression,
params,
resolver,
scope
}));
return node.quasis.map((quasi, idx)=>{
return (quasi.value.cooked || '') + (resolvedExpressions[idx] || '');
}).join('');
}
if (babelTypes.isLiteral(node)) {
if (node.type === 'NullLiteral' || node.type === 'RegExpLiteral') {
throw new Error(`Unsupported literal type: ${node.type}`);
}
return node.value.toString();
}
if (babelTypes.isIdentifier(node)) {
return resolveIdentifier({
babelConfig,
file,
filename,
fnArguments,
node,
params,
resolver,
scope
});
}
if (babelTypes.isVariableDeclarator(node)) {
const init = node.init ?? (babelTypes.isAssignmentPattern(node.id) && node.id.right);
if (!init) {
throw new Error(`Unsupported variable declarator`);
}
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: init,
resolver,
scope
});
}
if (babelTypes.isCallExpression(node) && babelTypes.isIdentifier(node.callee) && FUNCTION_WRAPPER_ALLOW_LIST.has(node.callee.name)) {
return resolveExpression({
babelConfig,
file,
filename,
node: node.arguments[0],
params,
resolver,
scope
});
}
if (babelTypes.isCallExpression(node)) {
return resolveCallExpression({
babelConfig,
file,
filename,
fnArguments,
node,
params,
resolver,
scope
});
}
if (babelTypes.isArrowFunctionExpression(node) || babelTypes.isFunctionDeclaration(node) || babelTypes.isFunctionExpression(node)) {
const newScope = new Scope(scope.path, scope);
for (const [i, param] of params.entries()){
newScope.push({
id: param,
init: fnArguments[i]
});
}
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: node.body,
params: node.params,
resolver,
scope: newScope
});
}
if (babelTypes.isNewExpression(node)) {
return resolveExpression({
babelConfig,
file,
filename,
node: node.callee,
resolver,
scope
});
}
if (babelTypes.isImportDefaultSpecifier(node) || babelTypes.isImportSpecifier(node)) {
return resolveImportSpecifier({
babelConfig,
file,
filename,
fnArguments,
node,
resolver
});
}
if (babelTypes.isAssignmentPattern(node)) {
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: node.right,
params,
resolver,
scope
});
}
// Handle TypeScript type assertions (e.g., `'foo' as string`)
if (babelTypes.isTSAsExpression(node)) {
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: node.expression,
params,
resolver,
scope
});
}
if (babelTypes.isMemberExpression(node)) {
const propertyName = getMemberPropertyName(node);
const objExpr = resolveToObjectExpression({
babelConfig,
file,
filename,
node: node.object,
resolver,
scope
});
const prop = findObjectProperty(objExpr, propertyName, filename, node);
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: prop.value,
params,
resolver,
scope
});
}
throw new Error(`Unsupported expression type: ${node.type} in ${filename}:${node.loc?.start.line}:${node.loc?.start.column}`);
}
function resolveIdentifier({ babelConfig, file, filename, fnArguments, node, params, resolver, scope }) {
const paramIndex = params.findIndex((param)=>babelTypes.isIdentifier(param) && node.name === param.name || babelTypes.isAssignmentPattern(param) && babelTypes.isIdentifier(param.left) && node.name === param.left.name);
let argument = fnArguments[paramIndex];
if (!argument && paramIndex !== -1 && babelTypes.isAssignmentPattern(params[paramIndex])) {
argument = params[paramIndex].right;
}
if (argument && babelTypes.isLiteral(argument)) {
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: argument,
params,
resolver,
scope
});
}
const binding = scope.getBinding(node.name);
if (binding) {
if (babelTypes.isIdentifier(binding.path.node)) {
const isSame = binding.path.node.name === node.name;
if (isSame) {
throw new Error(`Could not resolve same identifier "${node.name}" in "${filename}:${node.loc?.start.line}:${node.loc?.start.column}"`);
}
}
return resolveExpression({
babelConfig,
file,
filename,
fnArguments,
node: binding.path.node,
params,
resolver,
scope
});
}
throw new Error(`Could not find binding for node "${node.name}" in ${filename}:${node.loc?.start.line}:${node.loc?.start.column}`);
}
function resolveCallExpression({ babelConfig, file, filename, node, params, resolver, scope }) {
const { callee } = node;
return resolveExpression({
babelConfig,
file,
filename,
fnArguments: node.arguments,
node: callee,
params,
resolver,
scope
});
}
function resolveImportBinding({ babelConfig, file, filename, node, resolver }) {
let importDeclaration;
traverse(file, {
ImportDeclaration (n) {
if (!babelTypes.isImportDeclaration(n.node)) {
return;
}
for (const specifier of n.node.specifiers){
if (babelTypes.isImportDefaultSpecifier(specifier) && specifier.local.loc?.identifierName === node.local.name) {
importDeclaration = n.node;
break;
}
if (specifier.local.name === node.local.name) {
importDeclaration = n.node;
}
}
}
});
if (!importDeclaration) {
throw new Error(`Could not find import declaration for ${node.local.name}`);
}
const importName = node.local.name;
const importFileName = importDeclaration.source.value;
const importPath = importFileName.startsWith('./') || importFileName.startsWith('../') ? path.resolve(path.dirname(filename), importFileName) : importFileName;
const resolvedFile = resolver(formatPath(importPath));
const source = fs.readFileSync(resolvedFile);
const tree = parseSourceFile(source.toString(), resolvedFile, babelConfig);
let scope;
traverse(tree, {
Program (p) {
scope = p.scope;
}
});
if (!scope) {
throw new Error(`Could not find scope for ${filename}`);
}
const binding = scope.getBinding(importName);
return {
binding,
importFileName,
importName,
resolvedFile,
scope,
tree
};
}
function resolveImportSpecifier({ babelConfig, file, filename, fnArguments, node, resolver }) {
const { binding, importFileName, importName, resolvedFile, scope, tree } = resolveImportBinding({
babelConfig,
file,
filename,
node,
resolver
});
if (binding) {
return resolveExpression({
babelConfig,
file: tree,
filename: resolvedFile,
fnArguments,
node: binding.path.node,
resolver,
scope
});
}
// It's not a global binding, but it might be a named export
let namedExport;
let newImportName;
traverse(tree, {
ExportDeclaration (p) {
if (p.node.type === 'ExportNamedDeclaration') {
for (const specifier of p.node.specifiers){
if (specifier.type === 'ExportSpecifier' && specifier.exported.type === 'Identifier' && specifier.exported.name === importName) {
namedExport = p.node;
newImportName = specifier.exported.name;
}
}
}
}
});
if (namedExport && newImportName) {
return resolveExportSpecifier({
babelConfig,
filename: resolvedFile,
fnArguments,
importName: newImportName,
node: namedExport,
resolver
});
}
let result;
traverse(tree, {
ExportDeclaration (p) {
if (p.node.type === 'ExportAllDeclaration') {
try {
result = resolveExportSpecifier({
babelConfig,
filename: resolvedFile,
fnArguments,
importName,
node: p.node,
resolver
});
} catch (e) {
if (e.cause !== `noBinding:${importName}`) throw e;
}
}
}
});
if (result) return result;
throw new Error(`Could not find binding for import "${importName}" in ${importFileName}`);
}
function resolveExportSpecifier({ babelConfig, filename, fnArguments, importName, node, resolver }) {
if (!node.source) {
throw new Error(`Could not find source for export "${importName}" in ${filename}`);
}
const importFileName = node.source.value;
const importPath = path.resolve(path.dirname(filename), importFileName);
const resolvedFile = resolver(formatPath(importPath));
const source = fs.readFileSync(resolvedFile);
const tree = parseSourceFile(source.toString(), resolvedFile, babelConfig);
let newScope;
traverse(tree, {
Program (p) {
newScope = p.scope;
}
});
if (!newScope) {
throw new Error(`Could not find scope for ${filename}`);
}
const binding = newScope.getBinding(importName);
if (binding) {
return resolveExpression({
babelConfig,
file: tree,
filename: resolvedFile,
fnArguments,
node: binding.path.node,
resolver,
scope: newScope
});
}
throw new Error(`Could not find binding for export "${importName}" in ${importFileName}`, {
cause: `noBinding:${importName}`
});
}
function getMemberPropertyName(node) {
const { computed, loc, property } = node;
if (!computed && babelTypes.isIdentifier(property)) {
return property.name;
}
if (computed && babelTypes.isStringLiteral(property)) {
return property.value;
}
const locInfo = loc ? `${loc.filename}:${loc.start.line}:${loc.start.column}` : 'unknown location';
throw new Error(`Unsupported MemberExpression property type: ${property.type} @ ${locInfo}`);
}
function findObjectProperty(objExpr, propertyName, filename, node) {
for (const prop of objExpr.properties){
if (!babelTypes.isObjectProperty(prop)) continue;
if (babelTypes.isIdentifier(prop.key) && prop.key.name === propertyName) {
return prop;
}
if (babelTypes.isStringLiteral(prop.key) && prop.key.value === propertyName) {
return prop;
}
}
throw new Error(`Could not find property "${propertyName}" in object expression in ${filename}:${node.loc?.start.line}:${node.loc?.start.column}`);
}
function resolveToObjectExpression({ babelConfig, file, filename, node, resolver, scope }) {
if (babelTypes.isObjectExpression(node)) {
return node;
}
if (babelTypes.isTSAsExpression(node)) {
return resolveToObjectExpression({
babelConfig,
file,
filename,
node: node.expression,
resolver,
scope
});
}
if (babelTypes.isIdentifier(node)) {
const binding = scope.getBinding(node.name);
if (!binding) {
throw new Error(`Could not find binding for "${node.name}" in ${filename}`);
}
return resolveToObjectExpression({
babelConfig,
file,
filename,
node: binding.path.node,
resolver,
scope
});
}
if (babelTypes.isVariableDeclarator(node)) {
if (!node.init) {
throw new Error(`Variable declarator has no init`);
}
return resolveToObjectExpression({
babelConfig,
file,
filename,
node: node.init,
resolver,
scope
});
}
if (babelTypes.isMemberExpression(node)) {
const propertyName = getMemberPropertyName(node);
const objExpr = resolveToObjectExpression({
babelConfig,
file,
filename,
node: node.object,
resolver,
scope
});
const prop = findObjectProperty(objExpr, propertyName, filename, node);
return resolveToObjectExpression({
babelConfig,
file,
filename,
node: prop.value,
resolver,
scope
});
}
if (babelTypes.isImportDefaultSpecifier(node) || babelTypes.isImportSpecifier(node)) {
return resolveImportToObjectExpression({
babelConfig,
file,
filename,
node,
resolver
});
}
throw new Error(`Cannot resolve node type "${node.type}" to ObjectExpression in ${filename}:${node.loc?.start.line}:${node.loc?.start.column}`);
}
function resolveImportToObjectExpression({ babelConfig, file, filename, node, resolver }) {
const { binding, importFileName, importName, resolvedFile, scope, tree } = resolveImportBinding({
babelConfig,
file,
filename,
node,
resolver
});
if (!binding) {
throw new Error(`Could not find binding for import "${importName}" in ${importFileName}`);
}
return resolveToObjectExpression({
babelConfig,
file: tree,
filename: resolvedFile,
node: binding.path.node,
resolver,
scope
});
}
//# sourceMappingURL=expressionResolvers.js.map