300 lines
13 KiB
JavaScript
300 lines
13 KiB
JavaScript
import { DATA_COLLECTION_ITEM_FIELD, DATA_COLLECTION_ITEM_ID, EXCLUDED_FIELDS, } from "../consts.js";
|
|
import { JSXUtils } from "../jsx-utils.js";
|
|
import { JSXAttributeUtils, ExpressionAnalysisUtils, StaticValueUtils, } from "./utils/shared-utils.js";
|
|
export class DataItemFieldProcessor {
|
|
types;
|
|
attributeUtils;
|
|
expressionUtils;
|
|
staticUtils;
|
|
constructor(types) {
|
|
this.types = types;
|
|
this.attributeUtils = new JSXAttributeUtils(types);
|
|
this.expressionUtils = new ExpressionAnalysisUtils(types);
|
|
this.staticUtils = new StaticValueUtils(types);
|
|
}
|
|
process(path) {
|
|
if (this.attributeUtils.hasAttribute(path, DATA_COLLECTION_ITEM_FIELD)) {
|
|
return;
|
|
}
|
|
const jsxElement = path.parentPath;
|
|
if (!jsxElement?.isJSXElement())
|
|
return;
|
|
const result = this.extractFieldFromChildren(jsxElement);
|
|
if (!result)
|
|
return;
|
|
this.attributeUtils.addStringAttribute(path, DATA_COLLECTION_ITEM_FIELD, result.fieldPath);
|
|
this.tryAddItemId(path, result.rootIdentifier, result.fieldPath);
|
|
}
|
|
extractFieldFromChildren(jsxElement) {
|
|
const children = jsxElement.get("children");
|
|
for (const child of children) {
|
|
const result = this.extractFieldFromChild(child);
|
|
if (result)
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
extractFieldFromChild(child) {
|
|
if (child.isJSXExpressionContainer()) {
|
|
return this.extractFromExpressionContainer(child);
|
|
}
|
|
if (child.isJSXElement()) {
|
|
return this.extractFromChildElement(child);
|
|
}
|
|
return null;
|
|
}
|
|
extractFromExpressionContainer(container) {
|
|
const expression = container.get("expression");
|
|
if (expression.isJSXEmptyExpression())
|
|
return null;
|
|
const expr = expression;
|
|
if (expr.isMemberExpression() || expr.isOptionalMemberExpression()) {
|
|
return this.extractFromMemberExpression(expr);
|
|
}
|
|
if (expr.isLogicalExpression() && expr.node.operator === "&&") {
|
|
const left = expr.get("left");
|
|
if (left.isMemberExpression() ||
|
|
left.isOptionalMemberExpression()) {
|
|
return this.extractFromMemberExpression(left);
|
|
}
|
|
}
|
|
if (expr.isCallExpression()) {
|
|
const callee = expr.get("callee");
|
|
if (callee.isMemberExpression()) {
|
|
const obj = callee.get("object");
|
|
if (obj.isMemberExpression() || obj.isOptionalMemberExpression()) {
|
|
return this.extractFromMemberExpression(obj);
|
|
}
|
|
}
|
|
}
|
|
if (expr.isIdentifier()) {
|
|
return this.extractFromSimpleIdentifier(expr);
|
|
}
|
|
return null;
|
|
}
|
|
extractFromMemberExpression(expr) {
|
|
const rootId = this.expressionUtils.extractRootIdentifier(expr.node);
|
|
if (!rootId)
|
|
return null;
|
|
if (this.staticUtils.isDerivedFromStaticData(rootId.name, expr)) {
|
|
return null;
|
|
}
|
|
const parts = this.expressionUtils.collectMemberExpressionPath(expr.node);
|
|
if (parts.length < 2)
|
|
return null;
|
|
const fieldParts = parts.slice(1);
|
|
const fieldPath = fieldParts.join(".");
|
|
if (EXCLUDED_FIELDS.includes(fieldPath))
|
|
return null;
|
|
if (fieldParts.some((p) => EXCLUDED_FIELDS.includes(p)))
|
|
return null;
|
|
return { fieldPath, rootIdentifier: rootId };
|
|
}
|
|
extractFromSimpleIdentifier(expr) {
|
|
const name = expr.node.name;
|
|
if (EXCLUDED_FIELDS.includes(name))
|
|
return null;
|
|
if (this.staticUtils.isDerivedFromStaticData(name, expr))
|
|
return null;
|
|
return { fieldPath: name, rootIdentifier: expr.node };
|
|
}
|
|
extractFromChildElement(child) {
|
|
const elementName = JSXUtils.getElementName(child.node.openingElement);
|
|
if (!elementName)
|
|
return null;
|
|
if (elementName === "Image" || elementName === "img") {
|
|
return this.extractFromImageElement(child);
|
|
}
|
|
const logicalParent = child.parentPath;
|
|
if (logicalParent?.isJSXExpressionContainer()) {
|
|
const logicalExpr = logicalParent.parentPath;
|
|
if (logicalExpr?.isLogicalExpression()) {
|
|
return this.extractFromConditionalImage(child);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
extractFromImageElement(imageElement) {
|
|
const openingElement = imageElement.get("openingElement");
|
|
const attrs = openingElement.node.attributes;
|
|
for (const attr of attrs) {
|
|
if (this.types.isJSXAttribute(attr) &&
|
|
this.types.isJSXIdentifier(attr.name) &&
|
|
attr.name.name === "src") {
|
|
if (attr.value &&
|
|
this.types.isJSXExpressionContainer(attr.value) &&
|
|
this.types.isExpression(attr.value.expression) &&
|
|
!this.types.isJSXEmptyExpression(attr.value.expression)) {
|
|
const expr = attr.value.expression;
|
|
if (this.types.isMemberExpression(expr) ||
|
|
this.types.isOptionalMemberExpression(expr)) {
|
|
const rootId = this.expressionUtils.extractRootIdentifier(expr);
|
|
if (!rootId)
|
|
return null;
|
|
const parts = this.expressionUtils.collectMemberExpressionPath(expr);
|
|
if (parts.length < 2)
|
|
return null;
|
|
const fieldPath = parts.slice(1).join(".");
|
|
if (EXCLUDED_FIELDS.includes(fieldPath))
|
|
return null;
|
|
return { fieldPath, rootIdentifier: rootId };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
extractFromConditionalImage(child) {
|
|
const elementName = JSXUtils.getElementName(child.node.openingElement);
|
|
if (elementName === "Image" || elementName === "img") {
|
|
return this.extractFromImageElement(child);
|
|
}
|
|
return null;
|
|
}
|
|
tryAddItemId(path, rootIdentifier, fieldPath) {
|
|
if (this.attributeUtils.hasAttribute(path, DATA_COLLECTION_ITEM_ID)) {
|
|
return;
|
|
}
|
|
const idField = this.resolveIdFieldName(path);
|
|
if (rootIdentifier.name === fieldPath) {
|
|
this.tryAddItemIdFromDestructuredParams(path, idField);
|
|
return;
|
|
}
|
|
const idExpr = this.buildIdExpression(rootIdentifier.name, idField);
|
|
const binding = path.scope.getBinding(rootIdentifier.name);
|
|
if (!binding) {
|
|
this.tryAddItemIdFromParentComponent(path, rootIdentifier, idField);
|
|
return;
|
|
}
|
|
if (binding.path.isVariableDeclarator() ||
|
|
binding.kind === "param") {
|
|
this.attributeUtils.addExpressionAttribute(path, DATA_COLLECTION_ITEM_ID, idExpr);
|
|
}
|
|
}
|
|
/**
|
|
* Build the ID expression for a given root object name.
|
|
* If a specific idField was resolved from a key attribute, generates root?.id (or root?._id).
|
|
* If unknown (cross-file component), generates root?.id || root?._id to cover both conventions.
|
|
*/
|
|
buildIdExpression(rootName, idField) {
|
|
if (idField) {
|
|
return this.types.optionalMemberExpression(this.types.identifier(rootName), this.types.identifier(idField), false, true);
|
|
}
|
|
return this.types.logicalExpression("||", this.types.optionalMemberExpression(this.types.identifier(rootName), this.types.identifier("id"), false, true), this.types.optionalMemberExpression(this.types.identifier(rootName), this.types.identifier("_id"), false, true));
|
|
}
|
|
/**
|
|
* Walk up the JSX tree to find a key attribute that accesses .id or ._id.
|
|
* Returns null when no key is found (e.g. cross-file component receiving props).
|
|
*/
|
|
resolveIdFieldName(path) {
|
|
let current = path.parentPath;
|
|
while (current) {
|
|
if (current.isJSXElement()) {
|
|
const opening = current.get("openingElement");
|
|
const field = this.extractIdFieldFromKey(opening);
|
|
if (field)
|
|
return field;
|
|
}
|
|
current = current.parentPath;
|
|
}
|
|
return null;
|
|
}
|
|
extractIdFieldFromKey(path) {
|
|
for (const attr of path.node.attributes) {
|
|
if (this.types.isJSXAttribute(attr) &&
|
|
this.types.isJSXIdentifier(attr.name) &&
|
|
attr.name.name === "key" &&
|
|
attr.value &&
|
|
this.types.isJSXExpressionContainer(attr.value)) {
|
|
const expr = attr.value.expression;
|
|
if (this.types.isMemberExpression(expr) &&
|
|
this.types.isIdentifier(expr.property)) {
|
|
const name = expr.property.name;
|
|
if (name === "id" || name === "_id")
|
|
return name;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* For simple identifiers from destructured component props like ({ name, price }),
|
|
* find or inject the id field in the ObjectPattern so we can reference it directly.
|
|
*/
|
|
tryAddItemIdFromDestructuredParams(path, idField) {
|
|
const fn = path.getFunctionParent();
|
|
if (!fn)
|
|
return;
|
|
const params = fn.get("params");
|
|
for (const param of Array.isArray(params) ? params : [params]) {
|
|
if (!param.isObjectPattern())
|
|
continue;
|
|
for (const prop of param.get("properties")) {
|
|
if (!prop.isObjectProperty())
|
|
continue;
|
|
const key = prop.node.key;
|
|
const val = prop.node.value;
|
|
// Reuse a forwarding param already injected by processRootElement
|
|
if (this.types.isStringLiteral(key) &&
|
|
key.value === DATA_COLLECTION_ITEM_ID &&
|
|
this.types.isIdentifier(val)) {
|
|
this.attributeUtils.addExpressionAttribute(path, DATA_COLLECTION_ITEM_ID, this.types.identifier(val.name));
|
|
return;
|
|
}
|
|
// Use an existing id / _id destructured prop
|
|
if (this.types.isIdentifier(key) &&
|
|
(key.name === "id" || key.name === "_id") &&
|
|
this.types.isIdentifier(val)) {
|
|
this.attributeUtils.addExpressionAttribute(path, DATA_COLLECTION_ITEM_ID, this.types.identifier(val.name));
|
|
return;
|
|
}
|
|
}
|
|
const fieldToInject = idField ?? "id";
|
|
const newProp = this.types.objectProperty(this.types.identifier(fieldToInject), this.types.identifier(fieldToInject), false, true);
|
|
const props = param.node.properties;
|
|
const lastProp = props[props.length - 1];
|
|
if (lastProp && this.types.isRestElement(lastProp)) {
|
|
props.splice(props.length - 1, 0, newProp);
|
|
}
|
|
else {
|
|
props.push(newProp);
|
|
}
|
|
this.attributeUtils.addExpressionAttribute(path, DATA_COLLECTION_ITEM_ID, this.types.identifier(fieldToInject));
|
|
return;
|
|
}
|
|
}
|
|
tryAddItemIdFromParentComponent(path, rootIdentifier, idField) {
|
|
let current = path.parentPath;
|
|
while (current) {
|
|
if (current.isJSXElement()) {
|
|
const opening = current.get("openingElement");
|
|
const name = JSXUtils.getElementName(opening.node);
|
|
if (name && JSXUtils.isCustomComponent(name)) {
|
|
const keyAttr = this.findKeyWithId(opening);
|
|
if (keyAttr) {
|
|
const idExpr = this.buildIdExpression(rootIdentifier.name, idField);
|
|
this.attributeUtils.addExpressionAttribute(path, DATA_COLLECTION_ITEM_ID, idExpr);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
current = current.parentPath;
|
|
}
|
|
}
|
|
findKeyWithId(path) {
|
|
for (const attr of path.node.attributes) {
|
|
if (this.types.isJSXAttribute(attr) &&
|
|
this.types.isJSXIdentifier(attr.name) &&
|
|
attr.name.name === "key" &&
|
|
attr.value &&
|
|
this.types.isJSXExpressionContainer(attr.value)) {
|
|
const expr = attr.value.expression;
|
|
if (this.types.isExpression(expr)) {
|
|
return this.expressionUtils.isIdAccess(expr);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
//# sourceMappingURL=collection-item-field-processor.js.map
|