diff --git a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/js/handlers/optionParserHandler.js b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/js/handlers/optionParserHandler.js index 76221ff92b93a13631f9336523db48b2446aac90..07c64f385c6f05d13776b10969457ee36330e17e 100644 --- a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/js/handlers/optionParserHandler.js +++ b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/js/handlers/optionParserHandler.js @@ -1,11 +1,12 @@ -import t from '@babel/types'; -import LOG from '../../../logHelper.js'; -import { globalLibPaths } from '../../defaultConfig.js'; -import { addInstance } from '../../jsx/handlers/instanceHandler.js'; -import { JSErrors } from '../../../errors.js'; -import { DATA_REACTIVE, INSTANCE } from '../../jsx/consts.js'; -import { watchParser } from './watchHandler.js'; -import { propsParser } from './propsHandler.js'; +import t from '@babel/types' +import LOG from '../../../logHelper.js' +import { globalLibPaths } from '../../defaultConfig.js' +import { addInstance } from '../../jsx/handlers/instanceHandler.js' +import { JSErrors } from '../../../errors.js' +import { DATA_REACTIVE, INSTANCE } from '../../jsx/consts.js' +import { watchParser } from './watchHandler.js' +import { propsParser } from './propsHandler.js' +import { getIsPinia } from '../jsUtils.js'; /** * 找到Object的key和value @@ -116,6 +117,7 @@ function computedParser(ast, reactCovert) { let computedBody = null; if (prop.type === 'SpreadElement') { /* + vuex中需要讲mapState转成useMapState, pinia不需要 computed: { ...mapState('counter', ['count']), } @@ -124,7 +126,8 @@ function computedParser(ast, reactCovert) { */ if (prop.argument.type === 'CallExpression') { const callName = prop.argument.callee.name; - const ags0 = prop.argument.arguments[0]; + const isPinia = getIsPinia(reactCovert); + const ags0 = prop.argument.arguments[isPinia ? 1 : 0]; let outKeys = []; let outputsNode = null; // 存在vuex的模块定义 @@ -143,10 +146,10 @@ function computedParser(ast, reactCovert) { }); } } - + const newCallName = isPinia ? callName : globalLibPaths.vuex.imports[callName]; // 创建函数调用表达式 xx('') const functionCallExpression = t.callExpression( - t.identifier(globalLibPaths.vuex.imports[callName] || callName), + t.identifier(newCallName), prop.argument.arguments ); @@ -221,24 +224,26 @@ function computedParser(ast, reactCovert) { } function methodsParser(ast, reactCovert) { + // vuex中需要讲mapState转成useMapState, pinia不需要 const methodNames = []; ast?.properties.forEach(prop => { let computedBody = null; if (prop.type === 'SpreadElement') { if (prop.argument.type === 'CallExpression') { + const isPinia = getIsPinia(reactCovert); const callName = prop.argument.callee.name; - const ags0 = prop.argument.arguments[0]; + const mapAttrParam = prop.argument.arguments[isPinia ? 1 : 0]; let outKeys = []; let outputsNode = null; // 存在vuex的模块定义 - if (ags0.type === 'ObjectExpression') { - const { keys } = findObjectExpressionKeyAndValue(ags0); + if (mapAttrParam.type === 'ObjectExpression') { + const { keys } = findObjectExpressionKeyAndValue(mapAttrParam); outKeys = keys; } else { - if (ags0.type === 'StringLiteral') { + if (mapAttrParam.type === 'StringLiteral') { outputsNode = prop.argument.arguments?.[1]; - } else if (ags0.type === 'ArrayExpression') { - outputsNode = ags0; + } else if (mapAttrParam.type === 'ArrayExpression') { + outputsNode = mapAttrParam; } if (outputsNode && outputsNode.type === 'ArrayExpression') { outputsNode.elements.forEach(v => { @@ -247,10 +252,10 @@ function methodsParser(ast, reactCovert) { }); } } - + const newCallName = isPinia ? callName : globalLibPaths.vuex.imports[callName] // 创建函数调用表达式 xx('') const functionCallExpression = t.callExpression( - t.identifier(globalLibPaths.vuex.imports[callName] || callName), + t.identifier(newCallName), prop.argument.arguments ); @@ -298,11 +303,16 @@ function methodsParser(ast, reactCovert) { // 创建 methods 对象 const methodsObject = t.objectExpression( - methodNames.map(name => t.objectProperty(t.identifier(name), t.identifier(name), false, true)) + methodNames.map(name => + t.objectProperty(t.identifier(name), t.identifier(name), false, true) + ) ); // 创建 setToInstance 函数调用 - const setToInstanceCall = t.callExpression(t.identifier('setToInstance'), [t.identifier(INSTANCE), methodsObject]); + const setToInstanceCall = t.callExpression(t.identifier('setToInstance'), [ + t.identifier(INSTANCE), + methodsObject + ]); // 创建表达式语句 const expressionStatement = t.expressionStatement(setToInstanceCall); @@ -382,8 +392,10 @@ function componentsParser(ast, reactCovert) { t.identifier('components'), t.objectExpression( ast.properties.map(node => { - const name = reactCovert.sourceCodeContext.optionTypeRegistComponentMap.get(node.key.name); - return t.objectProperty(t.identifier(node.key.name), t.identifier(name)); + const keyName = node.key.name || node.key.value; + const mapped = reactCovert.sourceCodeContext.optionTypeRegistComponentMap.get(keyName); + const targetName = mapped || keyName; + return t.objectProperty(t.identifier(keyName), t.identifier(targetName)); }) ) ), diff --git a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/handlers/expressionHandler.js b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/handlers/expressionHandler.js index c9c462ca79a9a8e0380c18c4c03789e7867e999a..ec35f13fc0e3e889affe9587b95fd31a75141540 100644 --- a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/handlers/expressionHandler.js +++ b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/handlers/expressionHandler.js @@ -1,13 +1,13 @@ import t from '@babel/types'; import { globalLibPaths } from '../../defaultConfig.js'; -import { addInstance, addUsedGlobalProperties } from './instanceHandler.js'; +import { addInstance, addUsedGlobalProperties } from './instanceHandler.js' import LOG from '../../../logHelper.js'; import parser from '@babel/parser'; import { traverse } from '@babel/core'; -import { isFunctionParameter, processIdentifier, processIdentifierPath } from './identifierHandler.js'; -import { JSWarnings } from '../../../errors.js'; -import { INSTANCE } from '../consts.js'; +import { isFunctionParameter, processIdentifier, processIdentifierPath } from './identifierHandler.js' +import { JSWarnings } from '../../../errors.js' +import { INSTANCE } from '../consts.js' /** * 处理this 表达式 @@ -22,7 +22,7 @@ import { INSTANCE } from '../consts.js'; * @param {*} path */ export function handlerThisExpression(path, reactCovert) { - if (isThisNode(path.node.object)) { + if (isThisNode(path.node.object, path)) { // 将 this.xxx 替换为一个不含 this 的标识符 xxx let name = getThisProperty(path.node); if (name === '') { @@ -30,7 +30,7 @@ export function handlerThisExpression(path, reactCovert) { return; } - let replaceNode = createNodeByVueVariable(name, reactCovert); + let replaceNode = createNodeByVueVariable(name, reactCovert, path); if (replaceNode) { path.replaceWith(replaceNode); } else { @@ -64,36 +64,103 @@ export function getThisProperty(node) { return name; } -export const THIS_ALIASES = [ - '_this', - 'this_', - '$this', - 'this$', - 'self', - '_self', - 'self_', - 'that', - '_that', - 'that', - '_serf', -]; +export const THIS_ALIASES = ['_this', 'this_', '$this', 'this$', 'self', '_self', 'self_', 'that', '_that', 'that', '_serf', 'seft']; /** * 检查给定的节点是否表示 this 或其别名(如 self, _that) * @param {Object} node - Babel 的 AST 节点 + * @param path * @returns {boolean} - 如果节点表示 this 或其别名则返回 true,否则返回 false */ -export function isThisNode(node) { +export function isThisNode(node, path) { // 检查 this if (t.isThisExpression(node)) { return true; } // 检查 this 的别名(如 self, _that) - if (t.isIdentifier(node)) { - return THIS_ALIASES.includes(node.name); + if (!t.isIdentifier(node) || !THIS_ALIASES.includes(node.name)) { + return false; } + // 查找该变量的声明节点(定义处) + const binding = path.scope.getBinding(node.name); + if (!binding) { + return false; + } + + // 检查该绑定的值是否来自 `this` + const declaration = binding.path.node; + if (!t.isVariableDeclarator(declaration)) { + return false; + } + + const init = declaration.init; + if (!init) { + return false; + } + + // 判断初始化是否綁定的 `this` 表达式 + if (t.isThisExpression(init) || init.name === 'instance') { + return true; + } + + return false; +} + +/** + * 查找当前node是否在reactive里面使用的 + * + * 用于处理reactive里面调用reactive的参数的场景 + * 示例转换: + * const dataReactive = reactive({ + * subId: [], + * obj: { + * taskId: this.subId + * } + * }); + * 转换后: + * const dataReactive = reactive({ + * subId: [], + * obj: { + * taskId: undefined + * } + * }); + * @returns {boolean} - 如果节点在reactive里面被调用,返回true,否则返回false + */ +function isThisInReactiveScope(path) { + if (!path) { + return false; + } + + let currentPath = path.parentPath; + let depth = 0; + const MAX_DEPTH = 10; // 最多向上遍历 10 层 + + while (currentPath && depth < MAX_DEPTH) { + const node = currentPath.node; + + // 1. 遇到函数体节点,立即停止遍历 + if ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ClassMethod' + ) { + return false; + } + + // 遇到 CallExpression 且其 callee 是 'reactive',返回true + if (node.type === 'CallExpression' && node.callee?.name === 'reactive') { + return true; + } + + // 移动到下一个祖先节点 + currentPath = currentPath.parentPath; + depth++; + } + + // 遍历了指定层数或者到达了根节点都没有找到 return false; } @@ -109,7 +176,7 @@ export function isThisNode(node) { * 5. store: this.$store -> useStore() * 6. instance: this.$parent -> instance.$parent */ -export function createNodeByVueVariable(name, reactCovert) { +export function createNodeByVueVariable(name, reactCovert, path) { // 处理 props if (reactCovert.sourceCodeContext.findKeyInProps(name)) { return t.memberExpression(t.identifier(reactCovert.sourceCodeContext.propsName), t.identifier(name)); @@ -124,7 +191,8 @@ export function createNodeByVueVariable(name, reactCovert) { } // 处理 reactive else if (reactCovert.sourceCodeContext.findKeyInReactive(name)) { - return t.memberExpression(t.identifier(reactCovert.sourceCodeContext.reactive[name]), t.identifier(name)); + const isInReactive = isThisInReactiveScope(path); + return isInReactive ? t.identifier('undefined') : t.memberExpression(t.identifier(reactCovert.sourceCodeContext.reactive[name]), t.identifier(name)); } // 处理全局属性 else if (reactCovert.sourceCodeContext.findKeyInGlobalProperties(name)) { @@ -133,14 +201,29 @@ export function createNodeByVueVariable(name, reactCovert) { } // 处理自定义的 this 定义 else if (reactCovert.sourceCodeContext.findKeyInSelfThisDefines(name)) { - return t.memberExpression( - t.identifier(reactCovert.sourceCodeContext.selfThisDefines.get(name)), - t.identifier('value') - ); + return t.memberExpression(t.identifier(reactCovert.sourceCodeContext.selfThisDefines.get(name)), t.identifier('value')); } // 处理特殊的 $ 开头的属性 else if (name.startsWith('$')) { switch (name) { + case '$event': { + // 将模板中的 $event 识别为当前事件参数 e + return t.identifier('e'); + } + case '$emit': { + // 将模板中的 $emit(...) 统一映射为 emit(...) + // 若不存在 emit,则自动注入: const emit = defineEmits(props); + reactCovert.sourceCodeContext.addExtrasImport('defineEmits', globalLibPaths.vue); + reactCovert.addCodeAstToHorizonForOnce('emit', () => { + return t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('emit'), + t.callExpression(t.identifier('defineEmits'), [t.identifier(reactCovert.sourceCodeContext.propsName)]) + ), + ]); + }); + return t.identifier('emit'); + } case '$store': // 处理 Vuex store reactCovert.sourceCodeContext.addExtrasImport('useStore', globalLibPaths.vuex.path); @@ -161,10 +244,6 @@ export function createNodeByVueVariable(name, reactCovert) { addInstance(reactCovert); return t.memberExpression(t.identifier(INSTANCE), t.identifier(name)); - // 处理 Vue 3 的 $attrs - case '$attrs': - return t.identifier('attrs'); - // 处理国际化相关方法 case '$t': case '$l': @@ -185,9 +264,9 @@ export function createNodeByVueVariable(name, reactCovert) { * @returns */ export function memberExpressionValueReplaceHandler(expression, reactCovert, path) { - const processNode = node => { + const processNode = (node) => { if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { - if (isThisNode(node.object)) { + if (isThisNode(node.object, path)) { const name = getThisProperty(node); if (name === '') { return node.property; @@ -196,7 +275,7 @@ export function memberExpressionValueReplaceHandler(expression, reactCovert, pat // {{this.xxx}} ==> {{dataReactive.xxx}} return createNodeByVueVariable(name, reactCovert); } - if (node.computed && (t.isIdentifier(node.property) || t.isBinaryExpression(node.property))) { + if (node.computed && (t.isIdentifier(node.property) || t.isBinaryExpression(node.property)) || t.isMemberExpression(node.property)) { node.property = processNode(node.property); } // 递归处理对象和属性 @@ -229,20 +308,15 @@ export function memberExpressionValueReplaceHandler(expression, reactCovert, pat if (nodeAst && nodeAst.type !== 'Identifier') { // 如果解析成功且结果不是简单的标识符,进行进一步处理 - traverse( - nodeAst, - { - Identifier(path) { - processIdentifierPath(path, reactCovert); - }, - MemberExpression(path) { - path.replaceWith(processNode(path.node)); - path.skip(); - }, + traverse(nodeAst, { + Identifier(path) { + processIdentifierPath(path, reactCovert); }, - path.scope, - path - ); + MemberExpression(path) { + path.replaceWith(processNode(path.node)); + path.skip(); + } + }, path.scope, path); return nodeAst; } else { if (!isFunctionParameter(path)) { @@ -255,3 +329,5 @@ export function memberExpressionValueReplaceHandler(expression, reactCovert, pat return processNode(expression); } + + diff --git a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/stringRegexHandler.js b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/stringRegexHandler.js index 22b5aa57ea5a29ecb935bd8c9994744d59b260f0..02c515bff3d3591c03c103b7c6c0962b32cece08 100644 --- a/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/stringRegexHandler.js +++ b/packages/vue-to-horizon/libs/horizon-vue/src/next/convert/jsx/stringRegexHandler.js @@ -196,6 +196,20 @@ const convertJsxNoCloseTag = input => { return input.replaceAll(/
/g, '
'); }; +// 支持动态事件/指令参数归一化 +const normalizeDynamicDirectiveArgs = input => { + // @[] 动态事件到 v-on: + let result = input.replace(/@\[(.*?)\]((\.[a-z0-9]+)*)(="[^"]*")?/gi, (m, name, modifiers, _, handler = '') => { + const modStr = (modifiers || '').replace(/\./g, '__'); + return `v-on:${name}${modStr}${handler || ''}`; + }); + // v-on:[] -> v-on: + result = result.replace(/v-on:\[([^\]]+)\]/gi, (m, name) => `v-on:${name}`); + // 通用 v-xxx:[arg] -> v-xxx:arg + result = result.replace(/v-([a-zA-Z-]+):\[([^\]]+)\]/g, (m, dir, arg) => `v-${dir}:${arg}`); + return result; +}; + // 正则表达式匹配 v-model.xxx 语法 const regex = /v-model\.(\w+)/g; const removeModelModifiers = input => { @@ -246,6 +260,7 @@ const DefaultHardCodeHandler = { JSXNotes: convertJSXNotes, DynamicBinding: convertVueBindingToReact, ShortBinding: convertVBindShorthand, + DynamicArgs: normalizeDynamicDirectiveArgs, ShortOn: convertVueShortEventToReact, ShortSlot: convertVueShortSlotToReact, vShow: transformVShowSyntax,