babel-plugin-react-generate-property
Version:
A plugin to automatically generate properties (for example data attributes) for all JSX open tags, using user specified convention
157 lines (135 loc) • 8.19 kB
JavaScript
"use strict";
var _require = require('/helper-plugin-utils'),
declare = _require.declare;
var _require2 = require('/core'),
t = _require2.types;
module.exports = declare(function (api) {
api.assertVersion(7);
return {
// name: 'react-generate-data-id',
visitor: {
Program: function Program(programPath, state) {
// Get user configs
var _state$opts = state.opts,
_state$opts$customPro = _state$opts.customProperty,
customProperty = _state$opts$customPro === void 0 ? 'data-id' : _state$opts$customPro,
_state$opts$customSep = _state$opts.customSeparator,
customSeparator = _state$opts$customSep === void 0 ? '_' : _state$opts$customSep,
_state$opts$slashChar = _state$opts.slashChar,
slashChar = _state$opts$slashChar === void 0 ? '/' : _state$opts$slashChar,
_state$opts$dirLevel = _state$opts.dirLevel,
dirLevel = _state$opts$dirLevel === void 0 ? 1 : _state$opts$dirLevel,
_state$opts$addModule = _state$opts.addModuleClassNames,
addModuleClassNames = _state$opts$addModule === void 0 ? false : _state$opts$addModule,
_state$opts$prefix = _state$opts.prefix,
prefix = _state$opts$prefix === void 0 ? '' : _state$opts$prefix,
_state$opts$ignoreTre = _state$opts.ignoreTreeDepth,
ignoreTreeDepth = _state$opts$ignoreTre === void 0 ? false : _state$opts$ignoreTre,
_state$opts$ignoreNod = _state$opts.ignoreNodeNames,
ignoreNodeNames = _state$opts$ignoreNod === void 0 ? false : _state$opts$ignoreNod,
_state$opts$firstChil = _state$opts.firstChildOnly,
firstChildOnly = _state$opts$firstChil === void 0 ? false : _state$opts$firstChil,
_state$opts$omitFileN = _state$opts.omitFileName,
omitFileName = _state$opts$omitFileN === void 0 ? false : _state$opts$omitFileN,
_state$opts$match = _state$opts.match,
match = _state$opts$match === void 0 ? null : _state$opts$match;
var filename = state.file.opts.filename || ''; // filename missing in test env, see tests
var rootDir = state.file.opts.root;
var relativePath = filename.slice(rootDir.length);
var splits = filename.split(slashChar);
if (!splits || !splits.length) {
console.error('babel-plugin-react-generate-property plugin error: File path is not valid. If you are on Windows, you might need to specify backslash as slashChar in options.');
return;
}
var relativePathSplits = relativePath.split(slashChar); // User may specify negative dirLevel, to STRIP the first x layers instead of KEEPING last x layers
var dirNames = dirLevel >= 0 ? splits.slice(-1 - dirLevel, -1) : relativePathSplits.slice(-dirLevel, -1);
var fileName = splits[splits.length - 1].split('.')[0];
var fileIdentifier = "".concat(dirNames.join(customSeparator)).concat(omitFileName ? '' : "".concat(customSeparator).concat(fileName));
var previousNodeName = '';
var index = 0;
programPath.traverse({
JSXElement: function JSXElement(jsxPath) {
var _jsxPath$parent$openi, _jsxPath$parent$openi2;
var nodeName = '',
className = '',
dataIDDefined = false;
var parentNodeName = (_jsxPath$parent$openi = jsxPath.parent.openingElement) === null || _jsxPath$parent$openi === void 0 ? void 0 : (_jsxPath$parent$openi2 = _jsxPath$parent$openi.name) === null || _jsxPath$parent$openi2 === void 0 ? void 0 : _jsxPath$parent$openi2.name; // we traverse to find css module classes names, works like:
// `<div className={s.foo}>` -> `foo`
if (addModuleClassNames) {
var classNodes = jsxPath.node.openingElement.attributes.filter(function (x) {
var _x$name;
return (x === null || x === void 0 ? void 0 : (_x$name = x.name) === null || _x$name === void 0 ? void 0 : _x$name.name) == 'className';
});
var classNames = classNodes.map(function (x) {
var _x$value, _x$value$expression, _x$value$expression$p;
return x === null || x === void 0 ? void 0 : (_x$value = x.value) === null || _x$value === void 0 ? void 0 : (_x$value$expression = _x$value.expression) === null || _x$value$expression === void 0 ? void 0 : (_x$value$expression$p = _x$value$expression.property) === null || _x$value$expression$p === void 0 ? void 0 : _x$value$expression$p.name;
}).filter(Boolean);
className = classNames.length > 0 ? classNames.join(customSeparator) : '';
} // Traverse once to get the element node name (div, Header, span, etc)
jsxPath.traverse({
JSXOpeningElement: function JSXOpeningElement(openingPath) {
openingPath.stop(); // Do not visit child nodes again
var identifierNode = openingPath.get('name').node;
nodeName = identifierNode.name;
openingPath.traverse({
JSXAttribute: function JSXAttribute(attributePath) {
// If the data attribute doesn't exist, then we append the data attribute
var attributeName = attributePath.get('name').node.name;
if (!dataIDDefined) {
dataIDDefined = attributeName === customProperty;
}
}
});
}
}); // Detect if parent is React component or DOM node
// Option adds attrs to first DOM node in the component
// and ignores inner nodes
var matchFirstChildRule = firstChildOnly // only use filter if option passed
? !previousNodeName || // case of top node in a component: `let A = () => <main ><div /></main>` -> main matches
startsWithUpperCase(previousNodeName) && // case of first child: `<A><main /><div /></A>` -> main matches
previousNodeName == parentNodeName // but not if previous node is not parent: `<A><main /><B /><div /></A>` -> div not matches
: true; // do not filter anything in case option is missing
// option to append data-attrs only to certain components
// matches filename/filepath by RegExp
var matchRegex = match ? match.test(filename) : true;
var filteringOptionsCheck = matchFirstChildRule && matchRegex;
if (!dataIDDefined && nodeName && nodeName !== 'Fragment') {
var params = {
path: fileIdentifier,
customSeparator: customSeparator,
nodeName: nodeName,
previousNodeName: previousNodeName,
index: index,
className: className,
regex: match && filename.match(match) != null ? filename.match(match)[0] : null
};
filteringOptionsCheck && jsxPath.node.openingElement.attributes.push(t.jSXAttribute(t.jSXIdentifier(customProperty), t.stringLiteral(nameGenerator(params, state.opts))));
previousNodeName = nodeName;
if (previousNodeName === nodeName) {
index++;
} else {
index = 0;
}
}
}
});
}
}
};
});
function nameGenerator(params, options) {
var prefix = options.prefix || null;
var regexPrefix = params.regex || null;
var separator = params.customSeparator || '_';
var path = params.path || null;
var nodeName = options.ignoreNodeNames ? null : params.nodeName || null;
var index = params.nodeName == params.previousNodeName && !options.ignoreTreeDepth ? params.index : null;
var className = options.addModuleClassNames && params.className.length > 0 ? params.className : null;
return [prefix, regexPrefix, path, nodeName, index, className].filter(Boolean).join(separator);
}
function startsWithUpperCase(s) {
if (s.length == 0) {
return false;
}
return s[0].toUpperCase() == s[0];
}