From 5b6323f22a6a2278eba4af722b3407f82062b461 Mon Sep 17 00:00:00 2001 From: Amour1688 <31695475+Amour1688@users.noreply.github.com> Date: Mon, 7 Dec 2020 23:05:41 +0800 Subject: [PATCH] feat: should support passing object slots via JSX children (#204) * feat: should support passing object slots via JSX children * feat: add unit test * feat: remove `cloneDeep` of `isSlot` * chore(deps): add `@babel/template` --- packages/babel-plugin-jsx/package.json | 8 +- packages/babel-plugin-jsx/src/index.ts | 21 ++- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 103 +++++++++++--- .../test/__snapshots__/snapshot.test.ts.snap | 48 ++++++- packages/babel-plugin-jsx/test/index.test.tsx | 126 ++++++++++++++---- .../babel-plugin-jsx/test/snapshot.test.ts | 40 +++++- 6 files changed, 296 insertions(+), 50 deletions(-) diff --git a/packages/babel-plugin-jsx/package.json b/packages/babel-plugin-jsx/package.json index cfae005..6838ddc 100644 --- a/packages/babel-plugin-jsx/package.json +++ b/packages/babel-plugin-jsx/package.json @@ -11,11 +11,10 @@ "url": "git+https://github.com/vuejs/jsx-next.git" }, "scripts": { - "dev": "npm run build && webpack-dev-server", "build": "tsc", "lint": "eslint 'src/*.ts'", - "test": "npm run build && jest --coverage", - "prepublishOnly": "npm run build" + "test": "yarn build && jest --coverage", + "prepublishOnly": "yarn build" }, "bugs": { "url": "https://github.com/vuejs/jsx-next/issues" @@ -26,6 +25,7 @@ "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/template": "^7.0.0", "@babel/traverse": "^7.0.0", "@babel/types": "^7.0.0", "@vue/babel-helper-vue-transform-on": "^1.0.0-rc.2", @@ -48,4 +48,4 @@ "typescript": "^4.0.2", "vue": "3.0.0" } -} +} \ No newline at end of file diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index 8c9e9f9..7bd62b2 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -1,5 +1,6 @@ import * as t from '@babel/types'; import * as BabelCore from '@babel/core'; +import template from '@babel/template'; import syntaxJsx from '@babel/plugin-syntax-jsx'; import { addNamed, isModule, addNamespace } from '@babel/helper-module-imports'; import { NodePath } from '@babel/traverse'; @@ -23,7 +24,6 @@ export type ExcludesBoolean = (x: T | false | true) => x is T; const hasJSX = (parentPath: NodePath) => { let fileHasJSX = false; - parentPath.traverse({ JSXElement(path) { // skip ts error fileHasJSX = true; @@ -62,6 +62,7 @@ export default ({ types }: typeof BabelCore) => ({ 'resolveDirective', 'mergeProps', 'createTextVNode', + 'isVNode', ]; if (isModule(path)) { // import { createVNode } from "vue"; @@ -83,6 +84,24 @@ export default ({ types }: typeof BabelCore) => ({ return identifier; }); }); + state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { + if (importMap.runtimeIsSlot) { + return importMap.runtimeIsSlot; + } + const { name: isVNodeName } = state.get('isVNode')(); + const isSlot = path.scope.generateUidIdentifier('isSlot'); + const ast = template.ast` + function ${isSlot.name}(s) { + return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); + } + `; + const lastImport = (path.get('body') as NodePath[]).filter((p) => p.isImportDeclaration()).pop(); + if (lastImport) { + lastImport.insertAfter(ast); + } + importMap.runtimeIsSlot = isSlot; + return isSlot; + }); } else { // var _vue = require('vue'); let sourceName = ''; diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 886137c..9d77af2 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -83,28 +83,99 @@ const transformJSXElement = ( const slotFlag = path.getData('slotFlag') || SlotFlags.STABLE; - const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [ - tag, - props, - (children.length || slots) ? ( - isComponent - ? t.objectExpression([ - !!children.length && t.objectProperty( + let VNodeChild; + + if (children.length > 1 || slots) { + VNodeChild = isComponent ? t.objectExpression([ + !!children.length && t.objectProperty( + t.identifier('default'), + t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))), + ), + ...(slots ? ( + t.isObjectExpression(slots) + ? (slots! as t.ObjectExpression).properties + : [t.spreadElement(slots!)] + ) : []), + optimize && t.objectProperty( + t.identifier('_'), + t.numericLiteral(slotFlag), + ), + ].filter(Boolean as any)) : t.arrayExpression(children); + } else if (children.length === 1) { + const child = children[0]; + if (t.isIdentifier(child)) { + VNodeChild = t.conditionalExpression( + t.callExpression(state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), [child]), + child, + t.objectExpression([ + t.objectProperty( t.identifier('default'), - t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))), + t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [child]))), ), - ...(slots ? ( - t.isObjectExpression(slots) - ? (slots! as t.ObjectExpression).properties - : [t.spreadElement(slots!)] - ) : []), optimize && t.objectProperty( t.identifier('_'), t.numericLiteral(slotFlag), + ) as any, + ].filter(Boolean)), + ); + } else if ( + t.isCallExpression(child) && child.loc && isComponent + ) { // the element was generated and doesn't have location information + const slotId = path.scope.generateUidIdentifier('slot'); + const scope = path.hub.getScope(); + if (scope) { + scope.push({ + id: slotId, + kind: 'let', + }); + } + + VNodeChild = t.conditionalExpression( + t.callExpression( + state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), + [t.assignmentExpression('=', slotId, child)], + ), + slotId, + t.objectExpression([ + t.objectProperty( + t.identifier('default'), + t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [slotId]))), ), - ].filter(Boolean as any)) - : t.arrayExpression(children) - ) : t.nullLiteral(), + optimize && t.objectProperty( + t.identifier('_'), + t.numericLiteral(slotFlag), + ) as any, + ].filter(Boolean)), + ); + } else if (t.isFunctionExpression(child) || t.isArrowFunctionExpression(child)) { + VNodeChild = t.objectExpression([ + t.objectProperty( + t.identifier('default'), + child, + ), + ]); + } else if (t.isObjectExpression(child)) { + VNodeChild = t.objectExpression([ + ...child.properties, + optimize && t.objectProperty( + t.identifier('_'), + t.numericLiteral(slotFlag), + ), + ].filter(Boolean as any)); + } else { + VNodeChild = isComponent ? t.objectExpression([ + t.objectProperty( + t.identifier('default'), + t.arrowFunctionExpression([], t.arrayExpression([child])), + ), + ]) : t.arrayExpression([child]); + } + } + + const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [ + tag, + props, + VNodeChild || t.nullLiteral(), !!patchFlag && optimize && t.numericLiteral(patchFlag), !!dynamicPropNames.size && optimize && t.arrayExpression( diff --git a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap index 3585413..266c58c 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap @@ -127,9 +127,53 @@ exports[`override props single: single 1`] = ` _createVNode(\\"div\\", a, null);" `; -exports[`reassign variable as component: reassign variable as component 1`] = ` +exports[`passing object slots via JSX children multiple expressions: multiple expressions 1`] = ` "import { createVNode as _createVNode } from \\"vue\\"; +import { resolveComponent as _resolveComponent } from \\"vue\\"; + +_createVNode(_resolveComponent(\\"A\\"), null, { + default: () => [foo, bar], + _: 1 +});" +`; + +exports[`passing object slots via JSX children single expression, function expression: single expression, function expression 1`] = ` +"import { createVNode as _createVNode } from \\"vue\\"; +import { resolveComponent as _resolveComponent } from \\"vue\\"; + +_createVNode(_resolveComponent(\\"A\\"), null, { + default: () => \\"foo\\" +});" +`; + +exports[`passing object slots via JSX children single expression, non-literal value: runtime check: single expression, non-literal value: runtime check 1`] = ` +"import { createVNode as _createVNode } from \\"vue\\"; +import { isVNode as _isVNode } from \\"vue\\"; +import { resolveComponent as _resolveComponent } from \\"vue\\"; + +let _slot; + +function _isSlot(s) { + return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s); +} + +const foo = () => 1; + +_createVNode(_resolveComponent(\\"A\\"), null, _isSlot(_slot = foo()) ? _slot : { + default: () => [_slot], + _: 1 +});" +`; + +exports[`reassign variable as component: reassign variable as component 1`] = ` +"import { isVNode as _isVNode } from \\"vue\\"; +import { createVNode as _createVNode } from \\"vue\\"; import { defineComponent } from 'vue'; + +function _isSlot(s) { + return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s); +} + let a = 1; const A = defineComponent({ setup(_, { @@ -146,7 +190,7 @@ const _a = function () { return a; }(); -a = _createVNode(A, null, { +a = _createVNode(A, null, _isSlot(a) ? a : { default: () => [_a], _: 2 });" diff --git a/packages/babel-plugin-jsx/test/index.test.tsx b/packages/babel-plugin-jsx/test/index.test.tsx index ac0f7db..846e09d 100644 --- a/packages/babel-plugin-jsx/test/index.test.tsx +++ b/packages/babel-plugin-jsx/test/index.test.tsx @@ -1,5 +1,9 @@ import { - reactive, ref, defineComponent, CSSProperties, ComponentPublicInstance, + reactive, + ref, + defineComponent, + CSSProperties, + ComponentPublicInstance, } from 'vue'; import { shallowMount, mount, VueWrapper } from '@vue/test-utils'; @@ -66,9 +70,7 @@ describe('Transform JSX', () => { const wrapper = mount({ render() { - return ( - - ); + return ; }, }); expect(wrapper.classes()).toStrictEqual([]); @@ -123,7 +125,7 @@ describe('Transform JSX', () => { const wrapper = shallowMount({ setup() { // @ts-ignore - return () =>
; + return () =>
; }, }); expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort()); @@ -145,10 +147,12 @@ describe('Transform JSX', () => { const wrapper = shallowMount({ setup() { // @ts-ignore - return () =>
; + return () =>
; }, }); - expect(wrapper.html()).toBe('
'); + expect(wrapper.html()).toBe( + '
', + ); }); test('JSXSpreadChild', () => { @@ -259,7 +263,7 @@ describe('directive', () => { calls.push(1); }, }; - const wrapper = shallowMount(({ + const wrapper = shallowMount({ directives: { custom: customDirective }, setup() { return () => ( @@ -272,28 +276,28 @@ describe('directive', () => { /> ); }, - })); + }); const node = wrapper.vm.$.subTree; expect(calls).toEqual(expect.arrayContaining([1])); expect(node.dirs).toHaveLength(1); }); test('vHtml', () => { - const wrapper = shallowMount(({ + const wrapper = shallowMount({ setup() { return () =>

; }, - })); + }); expect(wrapper.html()).toBe('

foo

'); }); test('vText', () => { const text = 'foo'; - const wrapper = shallowMount(({ + const wrapper = shallowMount({ setup() { return () =>
; }, - })); + }); expect(wrapper.html()).toBe('
foo
'); }); }); @@ -315,7 +319,11 @@ describe('slots', () => { const wrapper = mount({ setup() { - return () => val }}>default; + return () => ( + val }}> + default + + ); }, }); @@ -325,11 +333,7 @@ describe('slots', () => { test('without default', () => { const A = defineComponent({ setup(_, { slots }) { - return () => ( -
- {slots.foo?.('foo')} -
- ); + return () =>
{slots.foo?.('foo')}
; }, }); @@ -362,7 +366,11 @@ describe('PatchFlags', () => { const onClick = () => { visible.value = false; }; - return () =>
NEED_PATCH
; + return () => ( +
+ NEED_PATCH +
+ ); }, }); @@ -380,7 +388,9 @@ describe('PatchFlags', () => { }; return () => ( -
full props
+
+ full props +
); }, }); @@ -455,9 +465,7 @@ describe('variables outside slots', () => { const textarea =