diff --git a/transformations/__tests__/render-to-resolveComponent.spec.ts b/transformations/__tests__/render-to-resolveComponent.spec.ts new file mode 100644 index 0000000..ffa7426 --- /dev/null +++ b/transformations/__tests__/render-to-resolveComponent.spec.ts @@ -0,0 +1,21 @@ +import { defineInlineTest } from 'jscodeshift/src/testUtils' +const transform = require('../render-to-resolveComponent') + +defineInlineTest( + transform, + {}, + `export default { + render(h){ + return h('button-counter') + } +}`, + ` +import { resolveComponent } from "vue"; +export default { + render() { + const buttonCounter = resolveComponent('button-counter') + return buttonCounter; + } +}`, + 'transform render-to-resolveComponent' +) diff --git a/transformations/index.ts b/transformations/index.ts index 8ec4e18..6372f6f 100644 --- a/transformations/index.ts +++ b/transformations/index.ts @@ -17,6 +17,7 @@ const transformationMap: { 'scoped-slots-to-slots': require('./scoped-slots-to-slots'), 'new-directive-api': require('./new-directive-api'), 'remove-vue-set-and-delete': require('./remove-vue-set-and-delete'), + 'render-to-resolveComponent': require('./render-to-resolveComponent'), // atomic ones 'remove-contextual-h-from-render': require('./remove-contextual-h-from-render'), diff --git a/transformations/render-to-resolveComponent.ts b/transformations/render-to-resolveComponent.ts new file mode 100644 index 0000000..3f9b3ab --- /dev/null +++ b/transformations/render-to-resolveComponent.ts @@ -0,0 +1,70 @@ +import wrap from '../src/wrapAstTransformation' +import type { ASTTransformation } from '../src/wrapAstTransformation' +import { transformAST as addImport } from './add-import' + +export const transformAST: ASTTransformation = (context) => { + + const { root, j } = context + // find render function + const renderCollections = root.find(j.ObjectMethod, node => { + return node.key.name === 'render' + && node.params.length === 1 + }) + .filter(nodePath => nodePath.parent.parent.node.type === 'ExportDefaultDeclaration') + if (!renderCollections) return + + // add import + addImport(context, { + specifier: { type: 'named', imported: 'resolveComponent' }, + source: 'vue' + }) + + renderCollections.forEach(({ node }) => { + // @ts-ignore + const paramName = node.params[0].name + // remove render function param + node.params = [] + const callExpressionCollection = j(node).find(j.CallExpression, node => { + return node.callee.name === paramName + && node.arguments.length === 1 + }) + + if (!callExpressionCollection.length) return + // find the component name + const componentName = callExpressionCollection.get(0).node.arguments[0].value + // remove non-letter for complying variable name rules + const componentVariableName = removeNonLetter(componentName) + callExpressionCollection.get(0).parent.insertBefore(j(`const ${componentVariableName} = resolveComponent('${componentName}')`).find(j.VariableDeclaration).get().node) + // replace h('xxx') with resolveComponent('xxx') + // @ts-ignore + callExpressionCollection.replaceWith(nodePath => nodePath.node.callee.name = componentVariableName) + }) +} + +/** + * remove non-letter and uppercase the first letter after non-letter + * button-component => buttonComponent + * @param str + */ +function removeNonLetter(str: string): string | undefined { + + if (str) { + let returnValue: string = '' + for (let i = 0; i < str.length; i++) { + // letter + if ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) { + returnValue += str[i] + } else { + // non-letter , remove and uppercase the first letter after non-letter + i++ + if (str[i] >= 'a' && str[i] <= 'z') { + returnValue += String.fromCharCode(str[i].charCodeAt(0) - 32) + } + } + } + return returnValue + } +} + +export default wrap(transformAST) +export const parser = 'babylon'