From cb21ed9f3938b87fc4d333468ef9a0541b49b82c Mon Sep 17 00:00:00 2001 From: Kael Date: Sun, 5 Sep 2021 22:38:46 +1000 Subject: [PATCH 1/6] perf: use compiler hints vuejs/vue-next#3334 --- packages/babel-plugin-jsx/src/index.ts | 25 +++-- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 37 +++++-- .../test/__snapshots__/snapshot.test.ts.snap | 100 +++++++++--------- 3 files changed, 94 insertions(+), 68 deletions(-) diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index 2c871d96..21a8e053 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -39,21 +39,26 @@ export default ({ types }: typeof BabelCore) => ({ enter(path: NodePath, state: State) { if (hasJSX(path)) { const importNames = [ - 'createVNode', 'Fragment', + 'createElementVNode', + 'createTextVNode', + 'createVNode', + 'guardReactiveProps', + 'isVNode', + 'mergeProps', + 'normalizeClass', + 'normalizeProps', + 'normalizeStyle', 'resolveComponent', - 'withDirectives', - 'vShow', - 'vModelSelect', - 'vModelText', + 'resolveDirective', 'vModelCheckbox', + 'vModelDynamic', 'vModelRadio', + 'vModelSelect', 'vModelText', - 'vModelDynamic', - 'resolveDirective', - 'mergeProps', - 'createTextVNode', - 'isVNode', + 'vModelText', + 'vShow', + 'withDirectives', ]; if (isModule(path)) { // import { createVNode } from "vue"; diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 6b02ac94..919a54ee 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -78,7 +78,7 @@ const buildProps = (path: NodePath, state: State) => { let hasDynamicKeys = false; const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = []; - const { mergeProps = true } = state.opts; + const { mergeProps = true, optimize = false } = state.opts; props .forEach((prop) => { if (prop.isJSXAttribute()) { @@ -286,6 +286,22 @@ const buildProps = (path: NodePath, state: State) => { propsExpression = (properties[0] as unknown as t.SpreadElement).argument; } else { propsExpression = t.objectExpression(dedupeProperties(properties, mergeProps)); + if (optimize) { + if (hasClassBinding) { + const klass = (propsExpression.properties as t.ObjectProperty[]) + .find((prop) => 'value' in prop.key && prop.key.value === 'class'); + if (klass?.type === 'ObjectProperty') { + klass.value = t.callExpression(createIdentifier(state, 'normalizeClass'), [klass.value as any]); + } + } + if (hasStyleBinding) { + const style = (propsExpression.properties as t.ObjectProperty[]) + .find((prop) => 'value' in prop.key && prop.key.value === 'style'); + if (style?.type === 'ObjectProperty') { + style.value = t.callExpression(createIdentifier(state, 'normalizeStyle'), [style.value as any]); + } + } + } } } @@ -478,16 +494,21 @@ const transformJSXElement = ( } } - const createVNode = t.callExpression(createIdentifier(state, 'createVNode'), [ - tag, - props, - VNodeChild || t.nullLiteral(), - !!patchFlag && optimize && t.numericLiteral(patchFlag), - !!dynamicPropNames.size && optimize + const createVNode = t.callExpression( + optimize + ? createIdentifier(state, isComponent ? 'createVNode' : 'createElementVNode') + : createIdentifier(state, 'createVNode'), + [ + tag, + props, + VNodeChild || t.nullLiteral(), + !!patchFlag && optimize && t.numericLiteral(patchFlag), + !!dynamicPropNames.size && optimize && t.arrayExpression( [...dynamicPropNames.keys()].map((name) => t.stringLiteral(name)), ), - ].filter(Boolean as unknown as ExcludesBoolean)); + ].filter(Boolean as unknown as ExcludesBoolean), + ); if (!directives.length) { return createVNode; 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 bf1b6656..8d66610e 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MereProps Order: MereProps Order 1`] = ` -"import { createVNode as _createVNode, mergeProps as _mergeProps, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, mergeProps as _mergeProps, createTextVNode as _createTextVNode } from \\"vue\\"; -_createVNode(\\"button\\", _mergeProps({ +_createElementVNode(\\"button\\", _mergeProps({ \\"loading\\": true }, x, { \\"type\\": \\"submit\\" @@ -11,11 +11,11 @@ _createVNode(\\"button\\", _mergeProps({ `; exports[`Merge class/ style attributes into array: Merge class/ style attributes into array 1`] = ` -"import { createVNode as _createVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, normalizeStyle as _normalizeStyle, normalizeClass as _normalizeClass } from \\"vue\\"; -_createVNode(\\"div\\", { - \\"class\\": [\\"a\\", b], - \\"style\\": [\\"color: red\\", s] +_createElementVNode(\\"div\\", { + \\"class\\": _normalizeClass([\\"a\\", b]), + \\"style\\": _normalizeStyle([\\"color: red\\", s]) }, null, 6);" `; @@ -25,9 +25,9 @@ createVNode('div', null, ['Without JSX should work']);" `; exports[`Without props: Without props 1`] = ` -"import { createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; -_createVNode(\\"a\\", null, [_createTextVNode(\\"a\\")]);" +_createElementVNode(\\"a\\", null, [_createTextVNode(\\"a\\")]);" `; exports[`custom directive: custom directive 1`] = ` @@ -37,9 +37,9 @@ _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_re `; exports[`custom directive: custom directive 2`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, resolveDirective as _resolveDirective, resolveComponent as _resolveComponent, Fragment as _Fragment } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, withDirectives as _withDirectives, createVNode as _createVNode, resolveDirective as _resolveDirective, resolveComponent as _resolveComponent, Fragment as _Fragment } from \\"vue\\"; -_createVNode(_Fragment, null, [_withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x]]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x]]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x, 'y']]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x, 'y', { +_createElementVNode(_Fragment, null, [_withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x]]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x]]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x, 'y']]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x, 'y', { a: true, b: true }]]), _withDirectives(_createVNode(_resolveComponent(\\"A\\"), null, null, 512), [[_resolveDirective(\\"xxx\\"), x, void 0, { @@ -64,32 +64,32 @@ _createVNode(_resolveComponent(\\"Badge\\"), null, { `; exports[`dynamic type in input: dynamic type in input 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelDynamic as _vModelDynamic } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelDynamic as _vModelDynamic } from \\"vue\\"; -_withDirectives(_createVNode(\\"input\\", { +_withDirectives(_createElementVNode(\\"input\\", { \\"type\\": type, \\"onUpdate:modelValue\\": $event => test = $event }, null, 8, [\\"type\\", \\"onUpdate:modelValue\\"]), [[_vModelDynamic, test]]);" `; exports[`input[type="checkbox"]: input[type="checkbox"] 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelCheckbox as _vModelCheckbox } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelCheckbox as _vModelCheckbox } from \\"vue\\"; -_withDirectives(_createVNode(\\"input\\", { +_withDirectives(_createElementVNode(\\"input\\", { \\"type\\": \\"checkbox\\", \\"onUpdate:modelValue\\": $event => test = $event }, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelCheckbox, test]]);" `; exports[`input[type="radio"]: input[type="radio"] 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelRadio as _vModelRadio, Fragment as _Fragment } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelRadio as _vModelRadio, Fragment as _Fragment } from \\"vue\\"; -_createVNode(_Fragment, null, [_withDirectives(_createVNode(\\"input\\", { +_createElementVNode(_Fragment, null, [_withDirectives(_createElementVNode(\\"input\\", { \\"type\\": \\"radio\\", \\"value\\": \\"1\\", \\"onUpdate:modelValue\\": $event => test = $event, \\"name\\": \\"test\\" -}, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelRadio, test]]), _withDirectives(_createVNode(\\"input\\", { +}, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelRadio, test]]), _withDirectives(_createElementVNode(\\"input\\", { \\"type\\": \\"radio\\", \\"value\\": \\"2\\", \\"onUpdate:modelValue\\": $event => test = $event, @@ -98,9 +98,9 @@ _createVNode(_Fragment, null, [_withDirectives(_createVNode(\\"input\\", { `; exports[`input[type="text"] .lazy modifier: input[type="text"] .lazy modifier 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelText as _vModelText } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelText as _vModelText } from \\"vue\\"; -_withDirectives(_createVNode(\\"input\\", { +_withDirectives(_createElementVNode(\\"input\\", { \\"onUpdate:modelValue\\": $event => test = $event }, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelText, test, void 0, { lazy: true @@ -108,9 +108,9 @@ _withDirectives(_createVNode(\\"input\\", { `; exports[`input[type="text"]: input[type="text"] 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelText as _vModelText } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelText as _vModelText } from \\"vue\\"; -_withDirectives(_createVNode(\\"input\\", { +_withDirectives(_createElementVNode(\\"input\\", { \\"onUpdate:modelValue\\": $event => test = $event }, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelText, test]]);" `; @@ -122,17 +122,17 @@ _createVNode(\\"foo\\", null, [_createVNode(\\"span\\", null, [_createTextVNode( `; exports[`named import specifier \`Keep Alive\`: named import specifier \`Keep Alive\` 1`] = ` -"import { createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; import { KeepAlive } from 'vue'; -_createVNode(KeepAlive, null, [_createTextVNode(\\"123\\")]);" +_createElementVNode(KeepAlive, null, [_createTextVNode(\\"123\\")]);" `; exports[`namespace specifier \`Keep Alive\`: namespace specifier \`Keep Alive\` 1`] = ` -"import { createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; import * as Vue from 'vue'; -_createVNode(Vue.KeepAlive, null, [_createTextVNode(\\"123\\")]);" +_createElementVNode(Vue.KeepAlive, null, [_createTextVNode(\\"123\\")]);" `; exports[`override props multiple: multiple 1`] = ` @@ -191,7 +191,7 @@ _createVNode(_resolveComponent(\\"A\\"), null, _isSlot(_slot = foo()) ? _slot : `; exports[`reassign variable as component: reassign variable as component 1`] = ` -"import { isVNode as _isVNode, createVNode as _createVNode } from \\"vue\\"; +"import { createVNode as _createVNode, isVNode as _isVNode, createElementVNode as _createElementVNode } from \\"vue\\"; import { defineComponent } from 'vue'; function _isSlot(s) { @@ -203,7 +203,7 @@ const A = defineComponent({ setup(_, { slots }) { - return () => _createVNode(\\"span\\", null, [slots.default()]); + return () => _createElementVNode(\\"span\\", null, [slots.default()]); } }); @@ -221,15 +221,15 @@ a = _createVNode(A, null, _isSlot(a) ? a : { `; exports[`select: select 1`] = ` -"import { withDirectives as _withDirectives, vModelSelect as _vModelSelect, createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { withDirectives as _withDirectives, vModelSelect as _vModelSelect, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; -_withDirectives(_createVNode(\\"select\\", { +_withDirectives(_createElementVNode(\\"select\\", { \\"onUpdate:modelValue\\": $event => test = $event -}, [_createVNode(\\"option\\", { +}, [_createElementVNode(\\"option\\", { \\"value\\": \\"1\\" -}, [_createTextVNode(\\"a\\")]), _createVNode(\\"option\\", { +}, [_createTextVNode(\\"a\\")]), _createElementVNode(\\"option\\", { \\"value\\": 2 -}, [_createTextVNode(\\"b\\")]), _createVNode(\\"option\\", { +}, [_createTextVNode(\\"b\\")]), _createElementVNode(\\"option\\", { \\"value\\": 3 }, [_createTextVNode(\\"c\\")])], 8, [\\"onUpdate:modelValue\\"]), [[_vModelSelect, test]]);" `; @@ -240,39 +240,39 @@ custom(\\"div\\", null, [_createTextVNode(\\"pragma\\")]);" `; exports[`should keep \`import * as Vue from "vue"\`: should keep \`import * as Vue from "vue"\` 1`] = ` -"import { createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; import * as Vue from 'vue'; -_createVNode(\\"div\\", null, [_createTextVNode(\\"Vue\\")]);" +_createElementVNode(\\"div\\", null, [_createTextVNode(\\"Vue\\")]);" `; exports[`single no need for a mergeProps call: single no need for a mergeProps call 1`] = ` -"import { createVNode as _createVNode, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; -_createVNode(\\"div\\", x, [_createTextVNode(\\"single\\")], 16);" +_createElementVNode(\\"div\\", x, [_createTextVNode(\\"single\\")], 16);" `; exports[`specifiers should be merged into a single importDeclaration: specifiers should be merged into a single importDeclaration 1`] = ` -"import { createVNode as _createVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode } from \\"vue\\"; import { createVNode, Fragment as _Fragment } from 'vue'; import { vShow } from 'vue'; -_createVNode(_Fragment, null, null);" +_createElementVNode(_Fragment, null, null);" `; exports[`textarea: textarea 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vModelText as _vModelText } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vModelText as _vModelText } from \\"vue\\"; -_withDirectives(_createVNode(\\"textarea\\", { +_withDirectives(_createElementVNode(\\"textarea\\", { \\"onUpdate:modelValue\\": $event => test = $event }, null, 8, [\\"onUpdate:modelValue\\"]), [[_vModelText, test]]);" `; exports[`use "@jsx" comment specify pragma: use "@jsx" comment specify pragma 1`] = ` -"import { createTextVNode as _createTextVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode } from \\"vue\\"; /* @jsx custom */ -custom(\\"div\\", { +_createElementVNode(\\"div\\", { \\"id\\": \\"custom\\" }, [_createTextVNode(\\"Hello\\")]);" `; @@ -293,7 +293,7 @@ _createVNode(_resolveComponent(\\"A\\"), null, slots);" `; exports[`v-model target value support variable: v-model target value support variable 1`] = ` -"import { createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment } from \\"vue\\"; +"import { createElementVNode as _createElementVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, Fragment as _Fragment } from \\"vue\\"; const foo = 'foo'; const a = () => 'a'; @@ -302,7 +302,7 @@ const b = { c: 'c' }; -_createVNode(_Fragment, null, [_createVNode(_resolveComponent(\\"A\\"), { +_createElementVNode(_Fragment, null, [_createVNode(_resolveComponent(\\"A\\"), { [foo]: xx, [\\"onUpdate\\" + foo]: $event => xx = $event }, null, 16), _createVNode(_resolveComponent(\\"B\\"), { @@ -339,15 +339,15 @@ _createVNode(_Fragment, null, [_createVNode(_resolveComponent(\\"A\\"), { `; exports[`v-show: v-show 1`] = ` -"import { withDirectives as _withDirectives, createVNode as _createVNode, vShow as _vShow, createTextVNode as _createTextVNode } from \\"vue\\"; +"import { withDirectives as _withDirectives, createElementVNode as _createElementVNode, vShow as _vShow, createTextVNode as _createTextVNode } from \\"vue\\"; -_withDirectives(_createVNode(\\"div\\", null, [_createTextVNode(\\"vShow\\")], 512), [[_vShow, x]]);" +_withDirectives(_createElementVNode(\\"div\\", null, [_createTextVNode(\\"vShow\\")], 512), [[_vShow, x]]);" `; exports[`vHtml: vHtml 1`] = ` -"import { createVNode as _createVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode } from \\"vue\\"; -_createVNode(\\"h1\\", { +_createElementVNode(\\"h1\\", { \\"innerHTML\\": \\"
foo
\\" }, null, 8, [\\"innerHTML\\"]);" `; @@ -371,9 +371,9 @@ _createVNode(_resolveComponent(\\"C\\"), { `; exports[`vText: vText 1`] = ` -"import { createVNode as _createVNode } from \\"vue\\"; +"import { createElementVNode as _createElementVNode } from \\"vue\\"; -_createVNode(\\"div\\", { +_createElementVNode(\\"div\\", { \\"textContent\\": text }, null, 8, [\\"textContent\\"]);" `; From 6e91245a56d0aef2eeb287b0dbce914abc3fa666 Mon Sep 17 00:00:00 2001 From: Kael Date: Sun, 5 Sep 2021 23:03:00 +1000 Subject: [PATCH 2/6] refactor: use proper type guards --- packages/babel-plugin-jsx/src/transform-vue-jsx.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 919a54ee..ce7588bf 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -288,16 +288,16 @@ const buildProps = (path: NodePath, state: State) => { propsExpression = t.objectExpression(dedupeProperties(properties, mergeProps)); if (optimize) { if (hasClassBinding) { - const klass = (propsExpression.properties as t.ObjectProperty[]) - .find((prop) => 'value' in prop.key && prop.key.value === 'class'); - if (klass?.type === 'ObjectProperty') { + const klass = propsExpression.properties + .find((prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === 'class'); + if (t.isObjectProperty(klass)) { klass.value = t.callExpression(createIdentifier(state, 'normalizeClass'), [klass.value as any]); } } if (hasStyleBinding) { - const style = (propsExpression.properties as t.ObjectProperty[]) - .find((prop) => 'value' in prop.key && prop.key.value === 'style'); - if (style?.type === 'ObjectProperty') { + const style = propsExpression.properties + .find((prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === 'style'); + if (t.isObjectProperty(style)) { style.value = t.callExpression(createIdentifier(state, 'normalizeStyle'), [style.value as any]); } } From f38856d293dfd492836ab34832031f8586414d62 Mon Sep 17 00:00:00 2001 From: Kael Date: Fri, 23 May 2025 22:01:40 +1000 Subject: [PATCH 3/6] run prettier --- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 878d5f26..92f98251 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -322,17 +322,31 @@ const buildProps = (path: NodePath, state: State) => { ); if (optimize) { if (hasClassBinding) { - const klass = propsExpression.properties - .find((prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === 'class'); + const klass = propsExpression.properties.find( + (prop) => + t.isObjectProperty(prop) && + t.isStringLiteral(prop.key) && + prop.key.value === 'class' + ); if (t.isObjectProperty(klass)) { - klass.value = t.callExpression(createIdentifier(state, 'normalizeClass'), [klass.value as any]); + klass.value = t.callExpression( + createIdentifier(state, 'normalizeClass'), + [klass.value as any] + ); } } if (hasStyleBinding) { - const style = propsExpression.properties - .find((prop) => t.isObjectProperty(prop) && t.isStringLiteral(prop.key) && prop.key.value === 'style'); + const style = propsExpression.properties.find( + (prop) => + t.isObjectProperty(prop) && + t.isStringLiteral(prop.key) && + prop.key.value === 'style' + ); if (t.isObjectProperty(style)) { - style.value = t.callExpression(createIdentifier(state, 'normalizeStyle'), [style.value as any]); + style.value = t.callExpression( + createIdentifier(state, 'normalizeStyle'), + [style.value as any] + ); } } } @@ -570,18 +584,22 @@ const transformJSXElement = ( const createVNode = t.callExpression( optimize - ? createIdentifier(state, isComponent ? 'createVNode' : 'createElementVNode') + ? createIdentifier( + state, + isComponent ? 'createVNode' : 'createElementVNode' + ) : createIdentifier(state, 'createVNode'), [ tag, props, VNodeChild || t.nullLiteral(), !!patchFlag && optimize && t.numericLiteral(patchFlag), - !!dynamicPropNames.size && optimize&& + !!dynamicPropNames.size && + optimize && t.arrayExpression( [...dynamicPropNames.keys()].map((name) => t.stringLiteral(name)) - ), - ].filter(Boolean as unknown as ExcludesBoolean), + ), + ].filter(Boolean as unknown as ExcludesBoolean) ); if (!directives.length) { From e025421a668f52b09cc1ae8badf9b3626b9dec8b Mon Sep 17 00:00:00 2001 From: Kael Date: Fri, 23 May 2025 22:12:26 +1000 Subject: [PATCH 4/6] restore original importNames order --- packages/babel-plugin-jsx/src/index.ts | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index b1135e6e..e54b37ae 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -60,26 +60,26 @@ export default declare>( enter(path, state) { if (hasJSX(path)) { const importNames = [ - 'Fragment', - 'createElementVNode', - 'createTextVNode', 'createVNode', - 'guardReactiveProps', - 'isVNode', - 'mergeProps', - 'normalizeClass', - 'normalizeProps', - 'normalizeStyle', + 'createElementVNode', + 'Fragment', 'resolveComponent', - 'resolveDirective', - 'vModelCheckbox', - 'vModelDynamic', - 'vModelRadio', + 'withDirectives', + 'vShow', 'vModelSelect', 'vModelText', + 'vModelCheckbox', + 'vModelRadio', 'vModelText', - 'vShow', - 'withDirectives', + 'vModelDynamic', + 'resolveDirective', + 'mergeProps', + 'normalizeProps', + 'normalizeClass', + 'normalizeStyle', + 'guardReactiveProps', + 'createTextVNode', + 'isVNode', ]; if (isModule(path)) { // import { createVNode } from "vue"; From ba0583c1516d338d154c43d92d9811ce8349f6f4 Mon Sep 17 00:00:00 2001 From: Kael Date: Fri, 23 May 2025 23:49:11 +1000 Subject: [PATCH 5/6] don't gate behind optimize --- packages/babel-plugin-jsx/src/index.ts | 1 + .../babel-plugin-jsx/src/transform-vue-jsx.ts | 66 +++++++++---------- .../__snapshots__/resolve-type.test.tsx.snap | 4 +- .../test/__snapshots__/snapshot.test.ts.snap | 22 +++---- 4 files changed, 46 insertions(+), 47 deletions(-) diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index e54b37ae..15a2501a 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -184,6 +184,7 @@ export default declare>( if (pragma) { state.set('createVNode', () => t.identifier(pragma)); + state.set('createElementVNode', () => t.identifier(pragma)); } if (file.ast.comments) { diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 92f98251..0dd26ec5 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -78,7 +78,7 @@ const buildProps = (path: NodePath, state: State) => { const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = []; - const { mergeProps = true, optimize = false } = state.opts; + const { mergeProps = true } = state.opts; props.forEach((prop) => { if (prop.isJSXAttribute()) { let name = getJSXAttributeName(prop); @@ -310,7 +310,19 @@ const buildProps = (path: NodePath, state: State) => { ); } else { // single no need for a mergeProps call - propsExpression = mergeArgs[0]; + if (isComponent) { + // createVNode already normalizes props + propsExpression = mergeArgs[0]; + } else { + propsExpression = t.callExpression( + createIdentifier(state, 'normalizeProps'), + [ + t.callExpression(createIdentifier(state, 'guardReactiveProps'), [ + mergeArgs[0], + ]), + ] + ); + } } } else if (properties.length) { // single no need for spread @@ -320,34 +332,25 @@ const buildProps = (path: NodePath, state: State) => { propsExpression = t.objectExpression( dedupeProperties(properties, mergeProps) ); - if (optimize) { - if (hasClassBinding) { - const klass = propsExpression.properties.find( - (prop) => - t.isObjectProperty(prop) && - t.isStringLiteral(prop.key) && - prop.key.value === 'class' + for (let i = 0; i < propsExpression.properties.length; i++) { + const property = propsExpression.properties[i]; + if ( + !t.isObjectProperty(property) || + !t.isStringLiteral(property.key) || + !t.isExpression(property.value) || + isConstant(property.value) + ) + continue; + if (property.key.value === 'class') { + property.value = t.callExpression( + createIdentifier(state, 'normalizeClass'), + [property.value] ); - if (t.isObjectProperty(klass)) { - klass.value = t.callExpression( - createIdentifier(state, 'normalizeClass'), - [klass.value as any] - ); - } - } - if (hasStyleBinding) { - const style = propsExpression.properties.find( - (prop) => - t.isObjectProperty(prop) && - t.isStringLiteral(prop.key) && - prop.key.value === 'style' + } else if (property.key.value === 'style') { + property.value = t.callExpression( + createIdentifier(state, 'normalizeStyle'), + [property.value] ); - if (t.isObjectProperty(style)) { - style.value = t.callExpression( - createIdentifier(state, 'normalizeStyle'), - [style.value as any] - ); - } } } } @@ -583,12 +586,7 @@ const transformJSXElement = ( } const createVNode = t.callExpression( - optimize - ? createIdentifier( - state, - isComponent ? 'createVNode' : 'createElementVNode' - ) - : createIdentifier(state, 'createVNode'), + createIdentifier(state, isComponent ? 'createVNode' : 'createElementVNode'), [ tag, props, diff --git a/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap b/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap index 4755eb46..fbcbfea6 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/resolve-type.test.tsx.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`resolve type > runtime props > basic 1`] = ` -"import { createVNode as _createVNode } from "vue"; +"import { createElementVNode as _createElementVNode } from "vue"; interface Props { foo?: string; } -const App = defineComponent((props: Props) => _createVNode("div", null, null), { +const App = defineComponent((props: Props) => _createElementVNode("div", null, null), { props: { foo: { type: String, 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 275c4bbd..1d6f8532 100644 --- a/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap +++ b/packages/babel-plugin-jsx/test/__snapshots__/snapshot.test.ts.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`_Fragment already imported > _Fragment already imported 1`] = ` -"import { Fragment as _Fragment, Fragment as _Fragment2, createTextVNode as _createTextVNode, createVNode as _createVNode } from 'vue'; -const Root1 = () => _createVNode(_Fragment2, null, [_createTextVNode("root1")]); -const Root2 = () => _createVNode(_Fragment, null, [_createTextVNode("root2")]);" +"import { Fragment as _Fragment, Fragment as _Fragment2, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode } from 'vue'; +const Root1 = () => _createElementVNode(_Fragment2, null, [_createTextVNode("root1")]); +const Root2 = () => _createElementVNode(_Fragment, null, [_createTextVNode("root2")]);" `; exports[`MereProps Order > MereProps Order 1`] = ` @@ -125,8 +125,8 @@ _withDirectives(_createElementVNode("input", { `; exports[`isCustomElement > isCustomElement 1`] = ` -"import { createTextVNode as _createTextVNode, createVNode as _createVNode } from "vue"; -_createVNode("foo", null, [_createVNode("span", null, [_createTextVNode("foo")])]);" +"import { createTextVNode as _createTextVNode, createElementVNode as _createElementVNode } from "vue"; +_createElementVNode("foo", null, [_createElementVNode("span", null, [_createTextVNode("foo")])]);" `; exports[`named import specifier \`Keep Alive\` > named import specifier \`Keep Alive\` 1`] = ` @@ -141,7 +141,7 @@ _createElementVNode(Vue.KeepAlive, null, [_createTextVNode("123")]);" `; exports[`override props multiple > multiple 1`] = ` -"import { resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue"; +"import { resolveComponent as _resolveComponent, normalizeStyle as _normalizeStyle, createVNode as _createVNode } from "vue"; _createVNode(_resolveComponent("A"), { "loading": true, ...a, @@ -150,13 +150,13 @@ _createVNode(_resolveComponent("A"), { d: 2 }, "class": "x", - "style": x + "style": _normalizeStyle(x) }, null);" `; exports[`override props single > single 1`] = ` -"import { createVNode as _createVNode } from "vue"; -_createVNode("div", a, null);" +"import { createElementVNode as _createElementVNode } from "vue"; +_createElementVNode("div", a, null);" `; exports[`passing object slots via JSX children directive in slot > directive in slot 1`] = ` @@ -274,8 +274,8 @@ _createElementVNode("div", null, [_createTextVNode("Vue")]);" `; exports[`single no need for a mergeProps call > single no need for a mergeProps call 1`] = ` -"import { createTextVNode as _createTextVNode, createElementVNode as _createElementVNode } from "vue"; -_createElementVNode("div", x, [_createTextVNode("single")], 16);" +"import { createTextVNode as _createTextVNode, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, createElementVNode as _createElementVNode } from "vue"; +_createElementVNode("div", _normalizeProps(_guardReactiveProps(x)), [_createTextVNode("single")], 16);" `; exports[`specifiers should be merged into a single importDeclaration > specifiers should be merged into a single importDeclaration 1`] = ` From df3051c87f7292a42c4e10b5b124f9b9ceb23bf4 Mon Sep 17 00:00:00 2001 From: Kael Date: Mon, 2 Jun 2025 19:03:13 +1000 Subject: [PATCH 6/6] fix: normalize constant class and style --- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index 0dd26ec5..bab6de18 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -337,20 +337,28 @@ const buildProps = (path: NodePath, state: State) => { if ( !t.isObjectProperty(property) || !t.isStringLiteral(property.key) || - !t.isExpression(property.value) || - isConstant(property.value) - ) + !t.isExpression(property.value) + ) { continue; + } + // TODO: if isConstant, pre-normalize class and style during build if (property.key.value === 'class') { - property.value = t.callExpression( - createIdentifier(state, 'normalizeClass'), - [property.value] - ); + if (!t.isStringLiteral(property.value)) { + property.value = t.callExpression( + createIdentifier(state, 'normalizeClass'), + [property.value] + ); + } } else if (property.key.value === 'style') { - property.value = t.callExpression( - createIdentifier(state, 'normalizeStyle'), - [property.value] - ); + if ( + !t.isStringLiteral(property.value) && + !t.isObjectExpression(property.value) + ) { + property.value = t.callExpression( + createIdentifier(state, 'normalizeStyle'), + [property.value] + ); + } } } }