Skip to content

Commit 5b6323f

Browse files
authored
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`
1 parent b443f84 commit 5b6323f

6 files changed

Lines changed: 296 additions & 50 deletions

File tree

packages/babel-plugin-jsx/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
"url": "git+https://github.com/vuejs/jsx-next.git"
1212
},
1313
"scripts": {
14-
"dev": "npm run build && webpack-dev-server",
1514
"build": "tsc",
1615
"lint": "eslint 'src/*.ts'",
17-
"test": "npm run build && jest --coverage",
18-
"prepublishOnly": "npm run build"
16+
"test": "yarn build && jest --coverage",
17+
"prepublishOnly": "yarn build"
1918
},
2019
"bugs": {
2120
"url": "https://github.com/vuejs/jsx-next/issues"
@@ -26,6 +25,7 @@
2625
"dependencies": {
2726
"@babel/helper-module-imports": "^7.0.0",
2827
"@babel/plugin-syntax-jsx": "^7.0.0",
28+
"@babel/template": "^7.0.0",
2929
"@babel/traverse": "^7.0.0",
3030
"@babel/types": "^7.0.0",
3131
"@vue/babel-helper-vue-transform-on": "^1.0.0-rc.2",
@@ -48,4 +48,4 @@
4848
"typescript": "^4.0.2",
4949
"vue": "3.0.0"
5050
}
51-
}
51+
}

packages/babel-plugin-jsx/src/index.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as t from '@babel/types';
22
import * as BabelCore from '@babel/core';
3+
import template from '@babel/template';
34
import syntaxJsx from '@babel/plugin-syntax-jsx';
45
import { addNamed, isModule, addNamespace } from '@babel/helper-module-imports';
56
import { NodePath } from '@babel/traverse';
@@ -23,7 +24,6 @@ export type ExcludesBoolean = <T>(x: T | false | true) => x is T;
2324

2425
const hasJSX = (parentPath: NodePath) => {
2526
let fileHasJSX = false;
26-
2727
parentPath.traverse({
2828
JSXElement(path) { // skip ts error
2929
fileHasJSX = true;
@@ -62,6 +62,7 @@ export default ({ types }: typeof BabelCore) => ({
6262
'resolveDirective',
6363
'mergeProps',
6464
'createTextVNode',
65+
'isVNode',
6566
];
6667
if (isModule(path)) {
6768
// import { createVNode } from "vue";
@@ -83,6 +84,24 @@ export default ({ types }: typeof BabelCore) => ({
8384
return identifier;
8485
});
8586
});
87+
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
88+
if (importMap.runtimeIsSlot) {
89+
return importMap.runtimeIsSlot;
90+
}
91+
const { name: isVNodeName } = state.get('isVNode')();
92+
const isSlot = path.scope.generateUidIdentifier('isSlot');
93+
const ast = template.ast`
94+
function ${isSlot.name}(s) {
95+
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s));
96+
}
97+
`;
98+
const lastImport = (path.get('body') as NodePath[]).filter((p) => p.isImportDeclaration()).pop();
99+
if (lastImport) {
100+
lastImport.insertAfter(ast);
101+
}
102+
importMap.runtimeIsSlot = isSlot;
103+
return isSlot;
104+
});
86105
} else {
87106
// var _vue = require('vue');
88107
let sourceName = '';

packages/babel-plugin-jsx/src/transform-vue-jsx.ts

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,28 +83,99 @@ const transformJSXElement = (
8383

8484
const slotFlag = path.getData('slotFlag') || SlotFlags.STABLE;
8585

86-
const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [
87-
tag,
88-
props,
89-
(children.length || slots) ? (
90-
isComponent
91-
? t.objectExpression([
92-
!!children.length && t.objectProperty(
86+
let VNodeChild;
87+
88+
if (children.length > 1 || slots) {
89+
VNodeChild = isComponent ? t.objectExpression([
90+
!!children.length && t.objectProperty(
91+
t.identifier('default'),
92+
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))),
93+
),
94+
...(slots ? (
95+
t.isObjectExpression(slots)
96+
? (slots! as t.ObjectExpression).properties
97+
: [t.spreadElement(slots!)]
98+
) : []),
99+
optimize && t.objectProperty(
100+
t.identifier('_'),
101+
t.numericLiteral(slotFlag),
102+
),
103+
].filter(Boolean as any)) : t.arrayExpression(children);
104+
} else if (children.length === 1) {
105+
const child = children[0];
106+
if (t.isIdentifier(child)) {
107+
VNodeChild = t.conditionalExpression(
108+
t.callExpression(state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), [child]),
109+
child,
110+
t.objectExpression([
111+
t.objectProperty(
93112
t.identifier('default'),
94-
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, children))),
113+
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [child]))),
95114
),
96-
...(slots ? (
97-
t.isObjectExpression(slots)
98-
? (slots! as t.ObjectExpression).properties
99-
: [t.spreadElement(slots!)]
100-
) : []),
101115
optimize && t.objectProperty(
102116
t.identifier('_'),
103117
t.numericLiteral(slotFlag),
118+
) as any,
119+
].filter(Boolean)),
120+
);
121+
} else if (
122+
t.isCallExpression(child) && child.loc && isComponent
123+
) { // the element was generated and doesn't have location information
124+
const slotId = path.scope.generateUidIdentifier('slot');
125+
const scope = path.hub.getScope();
126+
if (scope) {
127+
scope.push({
128+
id: slotId,
129+
kind: 'let',
130+
});
131+
}
132+
133+
VNodeChild = t.conditionalExpression(
134+
t.callExpression(
135+
state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(),
136+
[t.assignmentExpression('=', slotId, child)],
137+
),
138+
slotId,
139+
t.objectExpression([
140+
t.objectProperty(
141+
t.identifier('default'),
142+
t.arrowFunctionExpression([], t.arrayExpression(buildIIFE(path, [slotId]))),
104143
),
105-
].filter(Boolean as any))
106-
: t.arrayExpression(children)
107-
) : t.nullLiteral(),
144+
optimize && t.objectProperty(
145+
t.identifier('_'),
146+
t.numericLiteral(slotFlag),
147+
) as any,
148+
].filter(Boolean)),
149+
);
150+
} else if (t.isFunctionExpression(child) || t.isArrowFunctionExpression(child)) {
151+
VNodeChild = t.objectExpression([
152+
t.objectProperty(
153+
t.identifier('default'),
154+
child,
155+
),
156+
]);
157+
} else if (t.isObjectExpression(child)) {
158+
VNodeChild = t.objectExpression([
159+
...child.properties,
160+
optimize && t.objectProperty(
161+
t.identifier('_'),
162+
t.numericLiteral(slotFlag),
163+
),
164+
].filter(Boolean as any));
165+
} else {
166+
VNodeChild = isComponent ? t.objectExpression([
167+
t.objectProperty(
168+
t.identifier('default'),
169+
t.arrowFunctionExpression([], t.arrayExpression([child])),
170+
),
171+
]) : t.arrayExpression([child]);
172+
}
173+
}
174+
175+
const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [
176+
tag,
177+
props,
178+
VNodeChild || t.nullLiteral(),
108179
!!patchFlag && optimize && t.numericLiteral(patchFlag),
109180
!!dynamicPropNames.size && optimize
110181
&& t.arrayExpression(

packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,53 @@ exports[`override props single: single 1`] = `
127127
_createVNode(\\"div\\", a, null);"
128128
`;
129129

130-
exports[`reassign variable as component: reassign variable as component 1`] = `
130+
exports[`passing object slots via JSX children multiple expressions: multiple expressions 1`] = `
131+
"import { createVNode as _createVNode } from \\"vue\\";
132+
import { resolveComponent as _resolveComponent } from \\"vue\\";
133+
134+
_createVNode(_resolveComponent(\\"A\\"), null, {
135+
default: () => [foo, bar],
136+
_: 1
137+
});"
138+
`;
139+
140+
exports[`passing object slots via JSX children single expression, function expression: single expression, function expression 1`] = `
141+
"import { createVNode as _createVNode } from \\"vue\\";
142+
import { resolveComponent as _resolveComponent } from \\"vue\\";
143+
144+
_createVNode(_resolveComponent(\\"A\\"), null, {
145+
default: () => \\"foo\\"
146+
});"
147+
`;
148+
149+
exports[`passing object slots via JSX children single expression, non-literal value: runtime check: single expression, non-literal value: runtime check 1`] = `
131150
"import { createVNode as _createVNode } from \\"vue\\";
151+
import { isVNode as _isVNode } from \\"vue\\";
152+
import { resolveComponent as _resolveComponent } from \\"vue\\";
153+
154+
let _slot;
155+
156+
function _isSlot(s) {
157+
return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s);
158+
}
159+
160+
const foo = () => 1;
161+
162+
_createVNode(_resolveComponent(\\"A\\"), null, _isSlot(_slot = foo()) ? _slot : {
163+
default: () => [_slot],
164+
_: 1
165+
});"
166+
`;
167+
168+
exports[`reassign variable as component: reassign variable as component 1`] = `
169+
"import { isVNode as _isVNode } from \\"vue\\";
170+
import { createVNode as _createVNode } from \\"vue\\";
132171
import { defineComponent } from 'vue';
172+
173+
function _isSlot(s) {
174+
return typeof s === 'function' || Object.prototype.toString.call(s) === '[object Object]' && !_isVNode(s);
175+
}
176+
133177
let a = 1;
134178
const A = defineComponent({
135179
setup(_, {
@@ -146,7 +190,7 @@ const _a = function () {
146190
return a;
147191
}();
148192
149-
a = _createVNode(A, null, {
193+
a = _createVNode(A, null, _isSlot(a) ? a : {
150194
default: () => [_a],
151195
_: 2
152196
});"

0 commit comments

Comments
 (0)