akfun
Version:
前端脚手架:支持Vue技术栈和react技术栈
484 lines (437 loc) • 15.2 kB
JavaScript
/**
* Babel 插件:自动在 React 组件的最外层元素添加 scopeKey 属性
*
* 使用场景:
* - 在构建 React 组件时,自动为组件的根元素添加 scopeKey 属性
* - 支持函数组件和类组件
* - 如果组件返回 Fragment 或数组,会在 Fragment 的第一个子元素或数组的第一个元素上添加
* - scopeKey 的值默认为当前组件所在文件目录的名称
*/
const path = require('path');
/**
* @param {*} babelParams
* babelTypes(即 @babel/types)提供:
* 节点类型检查:babelTypes.isJSXElement(node)
* 创建节点:babelTypes.jsxAttribute(...)
* 节点构造器:babelTypes.stringLiteral('value')
* 节点操作:path.get('body')、path.traverse({})
*
* @param {*} options 插件配置对象
* scopeKey 数据作用域的键名,默认为 'data-scope'
*/
module.exports = function (babelParams, options) {
const { types: babelTypes } = babelParams;
const scopeKey = (options || {}).scopeKey || 'data-scope';
/**
* 获取文件所在目录的名称
*/
function getDirectoryName(filePath) {
if (!filePath) {
return '';
}
try {
const dirPath = path.dirname(filePath);
const dirName = path.basename(dirPath);
return dirName || '';
} catch (e) {
return '';
}
}
/**
* 判断 callee 是否是 React.createElement
*/
function isReactCreateElementCallee(calleePath) {
if (!calleePath) {
return false;
}
if (
calleePath.isMemberExpression() &&
babelTypes.isIdentifier(calleePath.get('object').node, { name: 'React' }) &&
babelTypes.isIdentifier(calleePath.get('property').node, { name: 'createElement' })
) {
return true;
}
return calleePath.isIdentifier({ name: 'createElement' });
}
/**
* 判断 callee 是否是 React 17 自动 JSX 运行时生成的 jsx/jsxs/jsxDEV 调用
* 支持 _jsx/_jsxs 形式(ts 编译后的别名)
*/
const jsxRuntimeCalleeNames = new Set(['jsx', 'jsxs', 'jsxDEV', '_jsx', '_jsxs', '_jsxDEV']);
function isReactJSXRuntimeCallee(calleePath) {
return (
calleePath && calleePath.isIdentifier() && jsxRuntimeCalleeNames.has(calleePath.node.name)
);
}
/**
* 判断是否是 React 元素生成调用(createElement 或 jsx/jsxs/jsxDEV)
*/
function isReactElementCall(callPath) {
if (!callPath || !callPath.isCallExpression()) {
return false;
}
const calleePath = callPath.get('callee');
return isReactCreateElementCallee(calleePath) || isReactJSXRuntimeCallee(calleePath);
}
/**
* 判断元素类型是否是 Fragment
*/
function isFragmentType(typePath) {
if (!typePath || !typePath.node) {
return false;
}
if (babelTypes.isIdentifier(typePath.node, { name: 'Fragment' })) {
return true;
}
if (
babelTypes.isMemberExpression(typePath.node) &&
babelTypes.isIdentifier(typePath.node.object, { name: 'React' }) &&
babelTypes.isIdentifier(typePath.node.property, { name: 'Fragment' })
) {
return true;
}
return false;
}
/**
* 判断 ObjectExpression 中是否已有 scopeKey
*/
function hasScopeKeyInProps(objectExpressionNode) {
return objectExpressionNode.properties.some((prop) => {
if (!babelTypes.isObjectProperty(prop) || prop.computed) {
return false;
}
if (babelTypes.isIdentifier(prop.key) && prop.key.name === scopeKey) {
return true;
}
return babelTypes.isStringLiteral(prop.key) && prop.key.value === scopeKey;
});
}
/**
* 创建一个 scopeKey 的 object property
*/
function createScopeProperty(scopeValue) {
const key = babelTypes.isValidIdentifier(scopeKey)
? babelTypes.identifier(scopeKey)
: babelTypes.stringLiteral(scopeKey);
return babelTypes.objectProperty(key, babelTypes.stringLiteral(scopeValue));
}
/**
* 为 props 对象添加 scopeKey
*/
function addScopeToPropsObject(propsPath, scopeValue) {
if (!propsPath) {
return;
}
// props 为 null/undefined 时直接替换为对象
if (propsPath.isNullLiteral() || propsPath.isIdentifier({ name: 'undefined' })) {
propsPath.replaceWith(babelTypes.objectExpression([createScopeProperty(scopeValue)]));
return;
}
if (propsPath.isObjectExpression()) {
if (hasScopeKeyInProps(propsPath.node)) {
return;
}
propsPath.node.properties.unshift(createScopeProperty(scopeValue));
return;
}
// 其他类型的 props,使用 Object.assign 合并
propsPath.replaceWith(
babelTypes.callExpression(
babelTypes.memberExpression(
babelTypes.identifier('Object'),
babelTypes.identifier('assign')
),
[babelTypes.objectExpression([createScopeProperty(scopeValue)]), propsPath.node]
)
);
}
/**
* 处理 Fragment 的子元素,找到首个可注入的元素
*/
function processFragmentChildren(callPath, scopeValue) {
const args = callPath.get('arguments');
// jsx/jsxs 调用的 children 在第二个参数(props.children)
const propsPath = args[1];
if (propsPath && propsPath.isObjectExpression()) {
const propPaths = propsPath.get('properties');
for (const propPath of propPaths) {
if (
propPath.isObjectProperty() &&
!propPath.node.computed &&
((babelTypes.isIdentifier(propPath.node.key) && propPath.node.key.name === 'children') ||
(babelTypes.isStringLiteral(propPath.node.key) &&
propPath.node.key.value === 'children'))
) {
const valuePath = propPath.get('value');
if (Array.isArray(valuePath)) {
// safety: ObjectProperty value returns NodePath, not array
continue;
}
if (processJSXLikePath(valuePath, scopeValue)) {
return true;
}
}
}
}
// createElement 形式的 children 在第三个参数开始
for (let i = 2; i < args.length; i++) {
if (processJSXLikePath(args[i], scopeValue)) {
return true;
}
}
return false;
}
/**
* 处理 createElement/jsx 调用,向 props 注入 scopeKey
*/
function processCreateElementCall(callPath, scopeValue) {
if (!isReactElementCall(callPath)) {
return false;
}
const args = callPath.get('arguments');
const typePath = args[0];
const isFragment = isFragmentType(typePath);
if (isFragment) {
// Fragment 需要把 scopeKey 添加到首个子元素
const handled = processFragmentChildren(callPath, scopeValue);
if (handled) {
return true;
}
// 如果没有子元素可处理,则回退到为 Fragment props 添加
}
// jsx/jsxs/createElement 的 props 都在第二个参数
const propsIndex = 1;
if (!args[propsIndex]) {
callPath.node.arguments[propsIndex] = babelTypes.objectExpression([]);
}
const propsPath = callPath.get('arguments')[propsIndex];
addScopeToPropsObject(propsPath, scopeValue);
return true;
}
/**
* 递归判断表达式中是否包含 JSX 或 React 元素调用
*/
function containsJSXLike(nodePath) {
if (!nodePath || !nodePath.node) {
return false;
}
if (nodePath.isJSXElement() || nodePath.isJSXFragment()) {
return true;
}
if (nodePath.isArrayExpression()) {
return nodePath.get('elements').some((element) => containsJSXLike(element));
}
if (nodePath.isConditionalExpression()) {
return (
containsJSXLike(nodePath.get('consequent')) || containsJSXLike(nodePath.get('alternate'))
);
}
if (nodePath.isLogicalExpression()) {
return containsJSXLike(nodePath.get('left')) || containsJSXLike(nodePath.get('right'));
}
if (nodePath.isCallExpression()) {
if (isReactElementCall(nodePath)) {
return true;
}
// 进一步查看参数里是否有 JSX 结构
return nodePath.get('arguments').some((argPath) => containsJSXLike(argPath));
}
return false;
}
/**
* 递归处理可能的 JSX/React 元素表达式
*/
function processJSXLikePath(nodePath, scopeValue) {
if (!nodePath || !nodePath.node) {
return false;
}
if (nodePath.isJSXElement()) {
addDataScopeAttribute(nodePath.node, scopeValue);
return true;
}
if (nodePath.isJSXFragment()) {
const children = nodePath.get('children');
for (const child of children) {
if (processJSXLikePath(child, scopeValue)) {
return true;
}
}
return false;
}
if (nodePath.isArrayExpression()) {
const elements = nodePath.get('elements');
for (const element of elements) {
if (processJSXLikePath(element, scopeValue)) {
return true;
}
}
return false;
}
if (nodePath.isConditionalExpression()) {
const handledConsequent = processJSXLikePath(nodePath.get('consequent'), scopeValue);
const handledAlternate = processJSXLikePath(nodePath.get('alternate'), scopeValue);
return handledConsequent || handledAlternate;
}
if (nodePath.isLogicalExpression()) {
const handledLeft = processJSXLikePath(nodePath.get('left'), scopeValue);
const handledRight = processJSXLikePath(nodePath.get('right'), scopeValue);
return handledLeft || handledRight;
}
if (nodePath.isCallExpression()) {
return processCreateElementCall(nodePath, scopeValue);
}
return false;
}
/**
* 检查函数是否返回 JSX(用于判断是否是 React 组件)
*/
function returnsJSX(path) {
const body = path.get('body');
if (body.isBlockStatement()) {
// 检查函数体中是否有返回 JSX 的语句
let hasJSXReturn = false;
body.traverse({
ReturnStatement(returnPath) {
const argument = returnPath.get('argument');
if (argument.node && containsJSXLike(argument)) {
hasJSXReturn = true;
returnPath.stop();
}
}
});
return hasJSXReturn;
}
if (containsJSXLike(body)) {
// 箭头函数或表达式直接返回 JSX/React 元素调用
return true;
}
return false;
}
/**
* 在 JSX 元素上添加 scopeKey 属性
* @param {Object} jsxElement - JSX 元素节点
* @param {string} scopeValue - scopeKey 的值,默认为空字符串
*/
function addDataScopeAttribute(jsxElement, scopeValue = '') {
const openingElement = jsxElement.openingElement;
const attributes = openingElement.attributes;
// 检查是否已经存在 scopeKey 属性
const hasDataScope = attributes.some(
(attr) => babelTypes.isJSXAttribute(attr) && attr.name.name === scopeKey
);
if (!hasDataScope) {
// 创建 scopeKey 属性,值为目录名
const dataScopeAttr = babelTypes.jsxAttribute(
babelTypes.jsxIdentifier(scopeKey),
babelTypes.stringLiteral(scopeValue)
);
// 添加到属性列表的开头
attributes.unshift(dataScopeAttr);
}
}
/**
* 处理返回语句中的 JSX
* @param {Object} path - Babel path 对象
* @param {string} scopeValue - scopeKey 的值
*/
function processReturnStatement(path, scopeValue) {
const argument = path.get('argument');
if (!argument.node) {
return;
}
processJSXLikePath(argument, scopeValue);
}
return {
visitor: {
// 处理函数组件和箭头函数组件
Function(functionPath) {
// 只处理返回 JSX 的函数(即 React 组件)
if (!returnsJSX(functionPath)) {
return;
}
// 获取当前文件路径并提取目录名
const filePath = functionPath.hub?.file?.opts?.filename || '';
const scopeValue = getDirectoryName(filePath);
const body = functionPath.get('body');
// 处理函数体中的返回语句
if (body.isBlockStatement()) {
body.traverse({
ReturnStatement(returnPath) {
processReturnStatement(returnPath, scopeValue);
}
});
} else {
// 箭头函数或表达式体直接返回 React 元素调用
processJSXLikePath(body, scopeValue);
}
},
// 处理类组件(推荐方式:使用 Class visitor 更可靠)
Class(classPath) {
// 检查是否是 React 组件类(继承自 React.Component、React.PureComponent 或 BaseCmp)
const superClass = classPath.node.superClass;
if (!superClass) {
return;
}
// 检查是否是 React 组件相关的类
// 支持:React.Component、React.PureComponent、BaseCmp 等
const isReactComponent =
(babelTypes.isIdentifier(superClass) &&
(superClass.name === 'Component' ||
superClass.name === 'PureComponent' ||
superClass.name === 'BaseCmp')) ||
(babelTypes.isMemberExpression(superClass) &&
babelTypes.isIdentifier(superClass.object) &&
superClass.object.name === 'React');
if (!isReactComponent) {
return;
}
// 获取当前文件路径并提取目录名
const filePath = classPath.hub?.file?.opts?.filename || '';
const scopeValue = getDirectoryName(filePath);
// 遍历类的方法,查找 render 方法
classPath.traverse({
ClassMethod(methodPath) {
const methodKey = methodPath.node.key;
// 检查是否是 render 方法(排除 getter/setter)
if (
methodPath.node.kind === 'method' &&
babelTypes.isIdentifier(methodKey) &&
methodKey.name === 'render'
) {
const body = methodPath.get('body');
if (body.isBlockStatement()) {
body.traverse({
ReturnStatement(returnPath) {
processReturnStatement(returnPath, scopeValue);
}
});
}
}
}
});
},
// 处理类组件的 render 方法(备选方案:直接使用 ClassMethod visitor)
ClassMethod(classMethodPath) {
// 放宽条件判断,只排除 getter/setter
const methodKey = classMethodPath.node.key;
const isRenderMethod =
classMethodPath.node.kind === 'method' &&
babelTypes.isIdentifier(methodKey) &&
methodKey.name === 'render';
if (isRenderMethod) {
// 获取当前文件路径并提取目录名
const filePath = classMethodPath.hub?.file?.opts?.filename || '';
const scopeValue = getDirectoryName(filePath);
const body = classMethodPath.get('body');
if (body.isBlockStatement()) {
body.traverse({
ReturnStatement(returnPath) {
processReturnStatement(returnPath, scopeValue);
}
});
}
}
}
}
};
};