From b9279717b7ca9f1eef21f989514866dbe7c7ea87 Mon Sep 17 00:00:00 2001 From: Allex Wang Date: Fri, 13 Nov 2020 11:41:55 +0800 Subject: [PATCH] feat(babel-sugar-inject-h): add support for functional jsx injections --- packages/babel-sugar-inject-h/src/index.js | 205 +++++++++++---- .../test/rules/funcional.js | 242 ++++++++++++++++++ packages/babel-sugar-inject-h/test/test.js | 14 +- 3 files changed, 406 insertions(+), 55 deletions(-) create mode 100644 packages/babel-sugar-inject-h/test/rules/funcional.js diff --git a/packages/babel-sugar-inject-h/src/index.js b/packages/babel-sugar-inject-h/src/index.js index 43ad368..94fbc24 100644 --- a/packages/babel-sugar-inject-h/src/index.js +++ b/packages/babel-sugar-inject-h/src/index.js @@ -1,35 +1,61 @@ import syntaxJsx from '@babel/plugin-syntax-jsx' +const PLUGIN_DATA_PREFIX = `@vue/babel-sugar-inject-h_${Date.now()}` + +// helpers for ast custom data get/set +const getv = (p, k) => p.getData(`${PLUGIN_DATA_PREFIX}_${k}`) +const setv = (p, k, v) => p.setData(`${PLUGIN_DATA_PREFIX}_${k}`, v) + /** - * Check if first parameter is `h` + * Get the index of the parameter `h` * @param t - * @param path ObjectMethod | ClassMethod - * @returns boolean + * @param path ObjectMethod | ClassMethod | Function + * @returns number -1 if not found */ -const firstParamIsH = (t, path) => { +const indexOfParamH = (t, path) => { const params = path.get('params') - return params.length && t.isIdentifier(params[0]) && params[0].node.name === 'h' + return params.length ? params.findIndex(p => t.isIdentifier(p) && p.node.name === 'h') : -1 } /** - * Check if body contains JSX - * @param t - * @param path ObjectMethod | ClassMethod - * @returns boolean + * Check if expression is an object member function */ -const hasJSX = (t, path) => { - const JSXChecker = { - hasJSX: false, +const isObjectMemberFunc = (t, path) => t.isFunction(path) && t.isObjectMember(path.parentPath) + +/** + * Check if expression is an object function-typed member + */ +const isMemberFunction = (t, path) => t.isObjectMethod(path) || t.isClassMethod(path) || isObjectMemberFunc(t, path) + +// find JSX, returns the first walked jsx expression +const findJSXElement = (t, path) => { + let elem = null + path.traverse({ + JSXElement (p) { + elem = p + p.stop() + } + }) + return elem +} + +/** + * Get function-typed ancestry of the specific node range + * @param path JSXElement + * @param root The last boundary node + * @returns the ancestry paths + */ +const getFuncAncestry = (t, path, root) => { + const paths = [] + if (path !== root) { + while (path = path.parentPath) { + if (t.isFunction(path)) { + paths.push(path) + } + if (path === root) break + } } - path.traverse( - { - JSXElement() { - this.hasJSX = true - }, - }, - JSXChecker, - ) - return JSXChecker.hasJSX + return paths } /** @@ -38,14 +64,56 @@ const hasJSX = (t, path) => { * @param path ObjectMethod | ClassMethod * @returns boolean */ -const isInsideJSXExpression = (t, path) => { - if (!path.parentPath) { - return false - } - if (t.isJSXExpressionContainer(path.parentPath)) { - return true +const isInsideJSXExpression = (t, path) => path.findParent(p => p && t.isJSX(p)) !== null + +/** + * Cleanup and reduce stack that `distance` gt than reference's + * @param stack + * @param reference The reference node to match + */ +const cleanupStack = (stack, path) => { + const ref = getv(path, 'distance') + stack.forEach(p => { + if (getv(p, 'distance') > ref) setv(p, 'hasJSX', false) + }) +} + +/** + * Prepend parameter `h` to function params (ensure as the first parameter) + * + * @param path {ArrowFunctionExpression|FunctionExpression} + */ +const addParamH = (t, path) => { + path.node.params = [t.identifier('h')].concat(path.node.params) +} + +/** + * Prepend `h` variable to function body, i.e. const h = xxx; + * + * @param path {ObjectMethod|ClassMethod|FunctionExpression} + */ +const patchVariableH = (t, path) => { + const funcName = path.isFunctionExpression() + ? path.parent.key.name + : path.node.key.name + const isRender = funcName === 'render' + + if (isRender) { + addParamH(t, path) + return } - return isInsideJSXExpression(t, path.parentPath) + + path + .get('body') + .unshiftContainer( + 'body', + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('h'), + t.memberExpression(t.thisExpression(), t.identifier('$createElement')), + ), + ]), + ) } export default babel => { @@ -54,28 +122,67 @@ export default babel => { return { inherits: syntaxJsx, visitor: { - Program(path1) { - path1.traverse({ - 'ObjectMethod|ClassMethod'(path) { - if (firstParamIsH(t, path) || !hasJSX(t, path) || isInsideJSXExpression(t, path)) { - return - } + Program (p) { + const stack = [] + p.traverse({ + 'ObjectMethod|ClassMethod|FunctionDeclaration|FunctionExpression|ArrowFunctionExpression': { + enter (path) { + const jsxElem = findJSXElement(t, path) + if (!jsxElem || isInsideJSXExpression(t, path)) { + return + } - const isRender = path.node.key.name === 'render' - - path - .get('body') - .unshiftContainer( - 'body', - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier('h'), - isRender - ? t.memberExpression(t.identifier('arguments'), t.numericLiteral(0), true) - : t.memberExpression(t.thisExpression(), t.identifier('$createElement')), - ), - ]), - ) + const ancestry = getFuncAncestry(t, jsxElem, path) + const isObjFn = isMemberFunction(t, path) + + // check if JSX expression is in method + if (!isObjFn && ancestry.some(p => isMemberFunction(t, p))) { + return + } + + // add to pending stack + stack.push(path) + + setv(path, 'hasJSX', true) + setv(path, 'distance', ancestry.length) + + // check params already has param `h` + if (indexOfParamH(t, path) !== -1) { + setv(path, 'fixed', true) + return + } + + if (isObjFn) { + if (path.isArrowFunctionExpression()) { + addParamH(t, path) + } else { + patchVariableH(t, path) + } + setv(path, 'fixed', true) + } + }, + exit (path) { + if (!getv(path, 'hasJSX')) { + return + } + stack.pop() + + // skip and cancel remaining nodes if `h` has fixed + if (getv(path, 'fixed')) { + cleanupStack(stack, path) + return + } + + // skip, functional JSX `h` should to be fixed in top of pending stack + if (!isObjectMemberFunc(t, path) && stack.some(p => getv(p, 'hasJSX'))) { + return + } + + addParamH(t, path) + setv(path, 'fixed', true) + + cleanupStack(stack, path) + } } }) } diff --git a/packages/babel-sugar-inject-h/test/rules/funcional.js b/packages/babel-sugar-inject-h/test/rules/funcional.js new file mode 100644 index 0000000..209e70f --- /dev/null +++ b/packages/babel-sugar-inject-h/test/rules/funcional.js @@ -0,0 +1,242 @@ +const functionalTests = [ + { + name: 'Simple injection in purge function (expressions)', + from: ` +const funcExpr = (ctx) => { + return +}; + +function funcTest(ctx) { + return +} +`, + to: ` +const funcExpr = (h, ctx) => { + return ; +}; + +function funcTest(h, ctx) { + return ; +} +`, + }, + { + name: 'Injection in closure with nested function expressions', + from: ` +const funcExpr = (ctx) => () => () => { + return +} + +function funcTest(ctx) { + return function () { + return function () { + return + } + } +} +`, + to: ` +const funcExpr = (h, ctx) => () => () => { + return ; +}; + +function funcTest(h, ctx) { + return function () { + return function () { + return ; + }; + }; +} +`, + }, + { + name: 'Injection disabled in nested functional JSX expressions (variant jsx types)', + from: ` +const obj = { + method () { + return ( + + }, + foo: function() { + return
+ } + }} { ...{ + scopedSlots: { + default: scope => + } + }} /> + ) + } +} +`, + to: ` +const obj = { + method() { + const h = this.$createElement; + return ; + }, + + foo: function () { + return
; + } + }} {...{ + scopedSlots: { + default: scope => + } + }} />; + } + +}; +`, + }, + { + name: 'Injection disabled when the closure has an explicit parameter named `h`', + from: ` +function funcTestWithH(ctx, h) { + return +} + +const closure1WithH = (ctx, h) => { + return +} + +const closure2WithH = ctx => () => h => { + return +} + +const closure3WithH = ctx => h => () => { + return +} + +const closure4WithH = (h, ctx) => () => () => { + return +} +`, + to: ` +function funcTestWithH(ctx, h) { + return ; +} + +const closure1WithH = (ctx, h) => { + return ; +}; + +const closure2WithH = ctx => () => h => { + return ; +}; + +const closure3WithH = ctx => h => () => { + return ; +}; + +const closure4WithH = (h, ctx) => () => () => { + return ; +}; +`, + }, + { + name: 'Injection in object member with type of function/arrow expressions', + from: ` +const obj = { + foo: function () { + return + }, + baz: () => , + render: () => +} +`, + to: ` +const obj = { + foo: function () { + const h = this.$createElement; + return ; + }, + baz: h => , + render: h => +}; +`, + }, + { + name: 'Injection should be disabled when the functional JSX scope owned by an object', + from: ` +const closureFunc = cfg => { + const obj = { + ...cfg, + render: () => + } + return obj +} +`, + to: ` +const closureFunc = cfg => { + const obj = { ...cfg, + render: h => + }; + return obj; +}; +`, + }, + { + name: 'Injection in object method/function with closure JSX expressions', + from: ` +const obj = { + arrowFn: () => { + const closureFunc = cfg => { + return + } + return closureFunc({}) + }, + func: function () { + const closureFunc = cfg => { + return + } + return closureFunc({}) + }, + renderItem() { + const closureFunc = cfg => { + return + } + return closureFunc({}) + } + +}; +`, + to: ` +const obj = { + arrowFn: h => { + const closureFunc = cfg => { + return ; + }; + + return closureFunc({}); + }, + func: function () { + const h = this.$createElement; + + const closureFunc = cfg => { + return ; + }; + + return closureFunc({}); + }, + + renderItem() { + const h = this.$createElement; + + const closureFunc = cfg => { + return ; + }; + + return closureFunc({}); + } + +}; +`, + }, +] + +module.exports = functionalTests diff --git a/packages/babel-sugar-inject-h/test/test.js b/packages/babel-sugar-inject-h/test/test.js index 3c59e96..ccda417 100644 --- a/packages/babel-sugar-inject-h/test/test.js +++ b/packages/babel-sugar-inject-h/test/test.js @@ -3,6 +3,8 @@ import { transform } from '@babel/core' import plugin from '../dist/plugin.testing' import jsxPlugin from '../../babel-plugin-transform-vue-jsx/dist/plugin.testing' +import functionalTests from './rules/funcional' + const transpile = src => new Promise((resolve, reject) => { transform( @@ -106,23 +108,24 @@ const tests = [ };`, }, { - name: 'Arguments-based injection into render function', + name: 'Arguments-based injection into render method', from: `const obj = { render () { return
test
} }`, to: `const obj = { - render() { - const h = arguments[0]; + render(h) { return
test
; } };`, }, + + ...functionalTests ] -tests.forEach(({ name, from, to }) => test(name, async t => t.is(await transpile(from), to))) +tests.forEach(({ name, from, to }) => test(name, async t => t.is((await transpile(from)).trim(), to.trim()))) test('Should work with JSX plugin enabled', async t => { const from = `const obj = { @@ -131,8 +134,7 @@ test('Should work with JSX plugin enabled', async t => { } }` const to = `const obj = { - render() { - const h = arguments[0]; + render(h) { return h("div", ["test"]); }