UNPKG

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
"use strict"; var _require = require('@babel/helper-plugin-utils'), declare = _require.declare; var _require2 = require('@babel/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]; }