From 7277fe4bc45d6225a693ced3f4a70c735dcd75c2 Mon Sep 17 00:00:00 2001 From: Kael Date: Mon, 3 Mar 2025 21:18:24 +1100 Subject: [PATCH] fix: deoptimize slots when a directive is used (#638) fixes #541 * fix: deoptimize slots when a directive is used * add test * feat: don't resolve directives in scope --- .../babel-plugin-jsx/src/parseDirectives.ts | 5 +++ .../babel-plugin-jsx/src/transform-vue-jsx.ts | 29 +++++++++--- .../test/__snapshots__/snapshot.test.ts.snap | 44 +++++++++++++++++++ .../babel-plugin-jsx/test/snapshot.test.ts | 41 +++++++++++++++++ 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/packages/babel-plugin-jsx/src/parseDirectives.ts b/packages/babel-plugin-jsx/src/parseDirectives.ts index 90ca1409..ba23fb09 100644 --- a/packages/babel-plugin-jsx/src/parseDirectives.ts +++ b/packages/babel-plugin-jsx/src/parseDirectives.ts @@ -184,6 +184,11 @@ const resolveDirective = ( } return modelToUse; } + const referenceName = + 'v' + directiveName[0].toUpperCase() + directiveName.slice(1); + if (path.scope.references[referenceName]) { + return t.identifier(referenceName); + } return t.callExpression(createIdentifier(state, 'resolveDirective'), [ t.stringLiteral(directiveName), ]); diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 0ebefa51..8b32e44c 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -405,7 +405,26 @@ const transformJSXElement = ( const { optimize = false } = state.opts; - const slotFlag = path.getData('slotFlag') || SlotFlags.STABLE; + // #541 - directives can't be resolved in optimized slots + // all parents should be deoptimized + if ( + directives.length && + directives.some( + (d) => + d.elements?.[0]?.type === 'CallExpression' && + d.elements[0].callee.type === 'Identifier' && + d.elements[0].callee.name === '_resolveDirective' + ) + ) { + let currentPath = path; + while (currentPath.parentPath?.isJSXElement()) { + currentPath = currentPath.parentPath; + currentPath.setData('slotFlag', 0); + } + } + + const slotFlag = path.getData('slotFlag') ?? SlotFlags.STABLE; + const optimizeSlots = optimize && slotFlag !== 0; let VNodeChild; if (children.length > 1 || slots) { @@ -431,7 +450,7 @@ const transformJSXElement = ( ? (slots! as t.ObjectExpression).properties : [t.spreadElement(slots!)] : []), - optimize && + optimizeSlots && t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), ].filter(Boolean as any) ) @@ -452,7 +471,7 @@ const transformJSXElement = ( t.arrayExpression(buildIIFE(path, [child])) ) ), - optimize && + optimizeSlots && (t.objectProperty( t.identifier('_'), t.numericLiteral(slotFlag) @@ -490,7 +509,7 @@ const transformJSXElement = ( t.arrayExpression(buildIIFE(path, [slotId])) ) ), - optimize && + optimizeSlots && (t.objectProperty( t.identifier('_'), t.numericLiteral(slotFlag) @@ -517,7 +536,7 @@ const transformJSXElement = ( VNodeChild = t.objectExpression( [ ...child.properties, - optimize && + optimizeSlots && t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), ].filter(Boolean as any) ); 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 fe97df98..c4c2f986 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap @@ -63,6 +63,12 @@ _createVNode(_Fragment, null, [_withDirectives(_createVNode(_resolveComponent("A }]])]);" `; +exports[`directive in scope > directive in scope 1`] = ` +"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withDirectives as _withDirectives } from "vue"; +const vXxx = {}; +_withDirectives(_createVNode(_resolveComponent("A"), null, null, 512), [[vXxx]]);" +`; + exports[`disable object slot syntax with defaultSlot > defaultSlot 1`] = ` "import { resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue"; _createVNode(_resolveComponent("Badge"), null, { @@ -153,6 +159,31 @@ exports[`override props single > single 1`] = ` _createVNode("div", a, null);" `; +exports[`passing object slots via JSX children directive in slot > directive in slot 1`] = ` +"import { Fragment as _Fragment, resolveDirective as _resolveDirective, createVNode as _createVNode, withDirectives as _withDirectives, resolveComponent as _resolveComponent } from "vue"; +_createVNode(_Fragment, null, [_createVNode(_resolveComponent("A"), null, { + default: () => [_withDirectives(_createVNode("div", null, null, 512), [[_resolveDirective("xxx")]]), foo] +}), _createVNode(_resolveComponent("A"), null, { + default: () => [_createVNode(_resolveComponent("B"), null, { + default: () => [_withDirectives(_createVNode("div", null, null, 512), [[_resolveDirective("xxx")]]), foo] + })] +})]);" +`; + +exports[`passing object slots via JSX children directive in slot, in scope > directive in slot, in scope 1`] = ` +"import { Fragment as _Fragment, createVNode as _createVNode, withDirectives as _withDirectives, resolveComponent as _resolveComponent } from "vue"; +const vXxx = {}; +_createVNode(_Fragment, null, [_createVNode(_resolveComponent("A"), null, { + default: () => [_withDirectives(_createVNode("div", null, null, 512), [[vXxx]]), foo], + _: 1 +}), _createVNode(_resolveComponent("A"), null, { + default: () => [_createVNode(_resolveComponent("B"), null, { + default: () => [_withDirectives(_createVNode("div", null, null, 512), [[vXxx]]), foo], + _: 1 + })] +})]);" +`; + exports[`passing object slots via JSX children multiple expressions > multiple expressions 1`] = ` "import { resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue"; _createVNode(_resolveComponent("A"), null, { @@ -161,6 +192,19 @@ _createVNode(_resolveComponent("A"), null, { });" `; +exports[`passing object slots via JSX children no directive in slot > no directive in slot 1`] = ` +"import { Fragment as _Fragment, createVNode as _createVNode, resolveComponent as _resolveComponent } from "vue"; +_createVNode(_Fragment, null, [_createVNode(_resolveComponent("A"), null, { + default: () => [_createVNode("div", null, null), foo], + _: 1 +}), _createVNode(_resolveComponent("A"), null, { + default: () => [_createVNode(_resolveComponent("B"), null, { + default: () => [_createVNode("div", null, null), foo], + _: 1 + })] +})]);" +`; + exports[`passing object slots via JSX children single expression, function expression > single expression, function expression 1`] = ` "import { resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue"; _createVNode(_resolveComponent("A"), null, { diff --git a/packages/babel-plugin-jsx/test/snapshot.test.ts b/packages/babel-plugin-jsx/test/snapshot.test.ts index e1488fd2..00e92c9c 100644 --- a/packages/babel-plugin-jsx/test/snapshot.test.ts +++ b/packages/babel-plugin-jsx/test/snapshot.test.ts @@ -155,6 +155,13 @@ const transpile = (source: string, options: VueJSXPluginOptions = {}) => `, }, + { + name: 'directive in scope', + from: ` + const vXxx = {}; + + `, + }, { name: 'vModels', from: '', @@ -255,6 +262,40 @@ const slotsTests: Test[] = [ {foo()}; `, }, + { + name: 'no directive in slot', + from: ` + <> +
{foo} + +
{foo} + + + `, + }, + { + name: 'directive in slot', + from: ` + <> +
{foo} + +
{foo} + + + `, + }, + { + name: 'directive in slot, in scope', + from: ` + const vXxx = {}; + <> +
{foo} + +
{foo} + + + `, + }, ]; slotsTests.forEach(({ name, from }) => {