You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
108 lines
3.0 KiB
108 lines
3.0 KiB
module.exports = function addDataLocator({ types: t }) {
|
|
function getElementName(node) {
|
|
if (!node) return "";
|
|
if (t.isJSXIdentifier(node)) {
|
|
return node.name;
|
|
}
|
|
if (t.isJSXMemberExpression(node)) {
|
|
return `${getElementName(node.object)}.${node.property.name}`;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
const FRAGMENT_DEFAULT_COMPONENTS = new Set([
|
|
"Fragment",
|
|
"React.Fragment",
|
|
"Tab.Group",
|
|
"Tabs",
|
|
"Transition",
|
|
"Transition.Root",
|
|
"Transition.Child",
|
|
"Menu",
|
|
"Menu.Item",
|
|
"Switch.Group",
|
|
"Popover.Group",
|
|
]);
|
|
|
|
return {
|
|
name: "add-data-locator",
|
|
visitor: {
|
|
JSXOpeningElement(path, state) {
|
|
if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") {
|
|
return;
|
|
}
|
|
|
|
const filePath = state.file.opts.filename;
|
|
|
|
if (!filePath || filePath.includes("node_modules")) {
|
|
return;
|
|
}
|
|
|
|
const elementName = getElementName(path.node.name);
|
|
|
|
// 1. Skip React Fragment and known Headless UI components that render Fragment
|
|
if (
|
|
!elementName ||
|
|
elementName === "Fragment" ||
|
|
elementName.endsWith(".Fragment") ||
|
|
FRAGMENT_DEFAULT_COMPONENTS.has(elementName)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// 2. Skip any component explicitly rendered as a Fragment (e.g. as={Fragment} or as="Fragment")
|
|
const hasFragmentAs = path.node.attributes.some((attribute) => {
|
|
if (
|
|
t.isJSXAttribute(attribute) &&
|
|
t.isJSXIdentifier(attribute.name) &&
|
|
attribute.name.name === "as"
|
|
) {
|
|
if (t.isJSXExpressionContainer(attribute.value)) {
|
|
const expr = attribute.value.expression;
|
|
if (t.isIdentifier(expr, { name: "Fragment" })) return true;
|
|
if (
|
|
t.isMemberExpression(expr) &&
|
|
t.isIdentifier(expr.property, { name: "Fragment" })
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
if (
|
|
t.isStringLiteral(attribute.value) &&
|
|
attribute.value.value === "Fragment"
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (hasFragmentAs) {
|
|
return;
|
|
}
|
|
|
|
const attributeExists = path.node.attributes.some(
|
|
(attribute) =>
|
|
t.isJSXAttribute(attribute) &&
|
|
t.isJSXIdentifier(attribute.name) &&
|
|
attribute.name.name === "data-locator",
|
|
);
|
|
|
|
if (attributeExists) {
|
|
return;
|
|
}
|
|
|
|
const lineNumber = path.node.loc?.start.line ?? "unknown";
|
|
const columnNumber = path.node.loc?.start.column ?? "unknown";
|
|
const locatorValue = `${filePath}:${lineNumber}:${columnNumber}`;
|
|
|
|
path.node.attributes.push(
|
|
t.jsxAttribute(
|
|
t.jsxIdentifier("data-locator"),
|
|
t.stringLiteral(locatorValue),
|
|
),
|
|
);
|
|
},
|
|
},
|
|
};
|
|
};
|