From 620450b5ba9e80c10bbc93ab667009dbf2410b0a Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Wed, 26 Nov 2025 16:58:43 +0800 Subject: [PATCH] style: format --- .prettierrc | 4 +- eslint.config.js | 14 +- .../babel-helper-vue-transform-on/index.d.mts | 6 +- .../babel-helper-vue-transform-on/index.mjs | 10 +- packages/babel-plugin-jsx/README-zh_CN.md | 64 ++- packages/babel-plugin-jsx/README.md | 64 ++- packages/babel-plugin-jsx/src/index.ts | 150 +++--- packages/babel-plugin-jsx/src/interface.ts | 32 +- .../babel-plugin-jsx/src/parseDirectives.ts | 159 +++--- packages/babel-plugin-jsx/src/patchFlags.ts | 2 +- packages/babel-plugin-jsx/src/slotFlags.ts | 2 +- .../babel-plugin-jsx/src/sugar-fragment.ts | 32 +- .../babel-plugin-jsx/src/transform-vue-jsx.ts | 355 +++++++------ packages/babel-plugin-jsx/src/utils.ts | 232 ++++----- packages/babel-plugin-jsx/test/index.test.tsx | 478 +++++++++--------- .../test/resolve-type.test.tsx | 18 +- packages/babel-plugin-jsx/test/setup.ts | 2 +- .../babel-plugin-jsx/test/snapshot.test.ts | 82 +-- .../babel-plugin-jsx/test/v-model.test.tsx | 298 +++++------ .../babel-plugin-jsx/test/v-models.test.tsx | 98 ++-- .../babel-plugin-resolve-type/src/index.ts | 217 ++++---- .../test/resolve-type.test.tsx | 140 ++--- packages/jsx-explorer/src/editor.worker.ts | 10 +- packages/jsx-explorer/src/index.ts | 88 ++-- packages/jsx-explorer/src/options.tsx | 24 +- packages/jsx-explorer/vite.config.ts | 6 +- tsdown.config.ts | 4 +- vitest.config.ts | 8 +- 28 files changed, 1297 insertions(+), 1302 deletions(-) diff --git a/.prettierrc b/.prettierrc index c1a6f66..b2095be 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "singleQuote": true, - "trailingComma": "es5" + "semi": false, + "singleQuote": true } diff --git a/eslint.config.js b/eslint.config.js index 16772c2..41c4ac6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,9 @@ // @ts-check -import { builtinModules } from 'node:module'; -import tseslint from 'typescript-eslint'; -import importX from 'eslint-plugin-import-x'; -import eslint from '@eslint/js'; -import eslintConfigPrettier from 'eslint-config-prettier'; +import { builtinModules } from 'node:module' +import tseslint from 'typescript-eslint' +import importX from 'eslint-plugin-import-x' +import eslint from '@eslint/js' +import eslintConfigPrettier from 'eslint-config-prettier' export default tseslint.config( eslint.configs.recommended, @@ -63,5 +63,5 @@ export default tseslint.config( eslintConfigPrettier, { ignores: ['**/dist/', '**/coverage/'], - } -); + }, +) diff --git a/packages/babel-helper-vue-transform-on/index.d.mts b/packages/babel-helper-vue-transform-on/index.d.mts index aac66eb..89a1932 100644 --- a/packages/babel-helper-vue-transform-on/index.d.mts +++ b/packages/babel-helper-vue-transform-on/index.d.mts @@ -1,5 +1,5 @@ declare function transformOn( - obj: Record -): Record<`on${string}`, any>; + obj: Record, +): Record<`on${string}`, any> -export { transformOn as default, transformOn as 'module.exports' }; +export { transformOn as default, transformOn as 'module.exports' } diff --git a/packages/babel-helper-vue-transform-on/index.mjs b/packages/babel-helper-vue-transform-on/index.mjs index b3a9a6b..809b00b 100644 --- a/packages/babel-helper-vue-transform-on/index.mjs +++ b/packages/babel-helper-vue-transform-on/index.mjs @@ -1,9 +1,9 @@ function transformOn(obj) { - const result = {}; + const result = {} Object.keys(obj).forEach((evt) => { - result[`on${evt[0].toUpperCase()}${evt.slice(1)}`] = obj[evt]; - }); - return result; + result[`on${evt[0].toUpperCase()}${evt.slice(1)}`] = obj[evt] + }) + return result } -export { transformOn as default, transformOn as 'module.exports' }; +export { transformOn as default, transformOn as 'module.exports' } diff --git a/packages/babel-plugin-jsx/README-zh_CN.md b/packages/babel-plugin-jsx/README-zh_CN.md index 089a45b..150872b 100644 --- a/packages/babel-plugin-jsx/README-zh_CN.md +++ b/packages/babel-plugin-jsx/README-zh_CN.md @@ -88,7 +88,7 @@ Default: `false` 函数式组件 ```jsx -const App = () =>
; +const App = () =>
``` 在 render 中使用 @@ -96,27 +96,25 @@ const App = () =>
; ```jsx const App = { render() { - return
Vue 3.0
; + return
Vue 3.0
}, -}; +} ``` ```jsx -import { withModifiers, defineComponent } from 'vue'; +import { withModifiers, defineComponent } from 'vue' const App = defineComponent({ setup() { - const count = ref(0); + const count = ref(0) const inc = () => { - count.value++; - }; + count.value++ + } - return () => ( -
{count.value}
- ); + return () =>
{count.value}
}, -}); +}) ``` Fragment @@ -127,20 +125,20 @@ const App = () => ( I'm Fragment -); +) ``` ### Attributes / Props ```jsx -const App = () => ; +const App = () => ``` 动态绑定: ```jsx -const placeholderText = 'email'; -const App = () => ; +const placeholderText = 'email' +const App = () => ``` ### 指令 @@ -150,12 +148,12 @@ const App = () => ; ```jsx const App = { data() { - return { visible: true }; + return { visible: true } }, render() { - return ; + return }, -}; +} ``` #### v-model @@ -191,7 +189,7 @@ h(A, { modifier: true, }, 'onUpdate:argument': ($event) => (val = $event), -}); +}) ``` #### v-models (从 1.1.0 开始不推荐使用) @@ -234,7 +232,7 @@ h(A, { modifier: true, }, 'onUpdate:bar': ($event) => (bar = $event), -}); +}) ``` #### 自定义指令 @@ -245,18 +243,18 @@ h(A, { const App = { directives: { custom: customDirective }, setup() { - return () => ; + return () => }, -}; +} ``` ```jsx const App = { directives: { custom: customDirective }, setup() { - return () => ; + return () => }, -}; +} ``` ### 插槽 @@ -269,20 +267,20 @@ const A = (props, { slots }) => (

{slots.default ? slots.default() : 'foo'}

{slots.bar?.()}

-); +) const App = { setup() { const slots = { bar: () => B, - }; + } return () => (
A
- ); + ) }, -}; +} // or @@ -291,10 +289,10 @@ const App = { const slots = { default: () =>
A
, bar: () => B, - }; - return () => ; + } + return () => }, -}; +} // 或者,当 `enableObjectSlots` 不是 `false` 时,您可以使用对象插槽 const App = { @@ -309,9 +307,9 @@ const App = { {() => 'foo'} - ); + ) }, -}; +} ``` ### 在 TypeScript 中使用 diff --git a/packages/babel-plugin-jsx/README.md b/packages/babel-plugin-jsx/README.md index ee375d9..69e84a0 100644 --- a/packages/babel-plugin-jsx/README.md +++ b/packages/babel-plugin-jsx/README.md @@ -92,7 +92,7 @@ Default: `false` functional component ```jsx -const App = () =>
Vue 3.0
; +const App = () =>
Vue 3.0
``` with render @@ -100,27 +100,25 @@ with render ```jsx const App = { render() { - return
Vue 3.0
; + return
Vue 3.0
}, -}; +} ``` ```jsx -import { withModifiers, defineComponent } from 'vue'; +import { withModifiers, defineComponent } from 'vue' const App = defineComponent({ setup() { - const count = ref(0); + const count = ref(0) const inc = () => { - count.value++; - }; + count.value++ + } - return () => ( -
{count.value}
- ); + return () =>
{count.value}
}, -}); +}) ``` Fragment @@ -131,20 +129,20 @@ const App = () => ( I'm Fragment -); +) ``` ### Attributes / Props ```jsx -const App = () => ; +const App = () => ``` with a dynamic binding: ```jsx -const placeholderText = 'email'; -const App = () => ; +const placeholderText = 'email' +const App = () => ``` ### Directives @@ -154,12 +152,12 @@ const App = () => ; ```jsx const App = { data() { - return { visible: true }; + return { visible: true } }, render() { - return ; + return }, -}; +} ``` #### v-model @@ -195,7 +193,7 @@ h(A, { modifier: true, }, 'onUpdate:argument': ($event) => (val = $event), -}); +}) ``` #### v-models (Not recommended since v1.1.0) @@ -238,7 +236,7 @@ h(A, { modifier: true, }, 'onUpdate:bar': ($event) => (bar = $event), -}); +}) ``` #### custom directive @@ -249,18 +247,18 @@ Recommended when using string arguments const App = { directives: { custom: customDirective }, setup() { - return () => ; + return () => }, -}; +} ``` ```jsx const App = { directives: { custom: customDirective }, setup() { - return () => ; + return () => }, -}; +} ``` ### Slot @@ -273,20 +271,20 @@ const A = (props, { slots }) => (

{slots.default ? slots.default() : 'foo'}

{slots.bar?.()}

-); +) const App = { setup() { const slots = { bar: () => B, - }; + } return () => (
A
- ); + ) }, -}; +} // or @@ -295,10 +293,10 @@ const App = { const slots = { default: () =>
A
, bar: () => B, - }; - return () => ; + } + return () => }, -}; +} // or you can use object slots when `enableObjectSlots` is not false. const App = { @@ -313,9 +311,9 @@ const App = { {() => 'foo'} - ); + ) }, -}; +} ``` ### In TypeScript diff --git a/packages/babel-plugin-jsx/src/index.ts b/packages/babel-plugin-jsx/src/index.ts index f5fa15f..f15fe8d 100644 --- a/packages/babel-plugin-jsx/src/index.ts +++ b/packages/babel-plugin-jsx/src/index.ts @@ -1,58 +1,58 @@ -import t from '@babel/types'; -import type * as BabelCore from '@babel/core'; -import _template from '@babel/template'; +import t from '@babel/types' +import type * as BabelCore from '@babel/core' +import _template from '@babel/template' // @ts-expect-error -import _syntaxJsx from '@babel/plugin-syntax-jsx'; -import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'; -import { type NodePath, type Visitor } from '@babel/traverse'; -import ResolveType from '@vue/babel-plugin-resolve-type'; -import { declare } from '@babel/helper-plugin-utils'; -import transformVueJSX from './transform-vue-jsx'; -import sugarFragment from './sugar-fragment'; -import type { State, VueJSXPluginOptions } from './interface'; +import _syntaxJsx from '@babel/plugin-syntax-jsx' +import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports' +import { type NodePath, type Visitor } from '@babel/traverse' +import ResolveType from '@vue/babel-plugin-resolve-type' +import { declare } from '@babel/helper-plugin-utils' +import transformVueJSX from './transform-vue-jsx' +import sugarFragment from './sugar-fragment' +import type { State, VueJSXPluginOptions } from './interface' -export { VueJSXPluginOptions }; +export { VueJSXPluginOptions } const hasJSX = (parentPath: NodePath) => { - let fileHasJSX = false; + let fileHasJSX = false parentPath.traverse({ JSXElement(path) { // skip ts error - fileHasJSX = true; - path.stop(); + fileHasJSX = true + path.stop() }, JSXFragment(path) { - fileHasJSX = true; - path.stop(); + fileHasJSX = true + path.stop() }, - }); + }) - return fileHasJSX; -}; + return fileHasJSX +} -const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; +const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/ /* #__NO_SIDE_EFFECTS__ */ function interopDefault(m: any) { - return m.default || m; + return m.default || m } -const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx); -const template = /*#__PURE__*/ interopDefault(_template); +const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx) +const template = /*#__PURE__*/ interopDefault(_template) const plugin: ( api: object, options: VueJSXPluginOptions | null | undefined, - dirname: string + dirname: string, ) => BabelCore.PluginObj = declare< VueJSXPluginOptions, BabelCore.PluginObj >((api, opt, dirname) => { - const { types } = api; - let resolveType: BabelCore.PluginObj | undefined; + const { types } = api + let resolveType: BabelCore.PluginObj | undefined if (opt.resolveType) { - if (typeof opt.resolveType === 'boolean') opt.resolveType = {}; - resolveType = ResolveType(api, opt.resolveType, dirname); + if (typeof opt.resolveType === 'boolean') opt.resolveType = {} + resolveType = ResolveType(api, opt.resolveType, dirname) } return { ...(resolveType || {}), @@ -81,117 +81,117 @@ const plugin: ( 'mergeProps', 'createTextVNode', 'isVNode', - ]; + ] if (isModule(path)) { // import { createVNode } from "vue"; const importMap: Record< string, t.MemberExpression | t.Identifier - > = {}; + > = {} importNames.forEach((name) => { state.set(name, () => { if (importMap[name]) { - return types.cloneNode(importMap[name]); + return types.cloneNode(importMap[name]) } const identifier = addNamed(path, name, 'vue', { ensureLiveReference: true, - }); - importMap[name] = identifier; - return identifier; - }); - }); - const { enableObjectSlots = true } = state.opts; + }) + importMap[name] = identifier + return identifier + }) + }) + const { enableObjectSlots = true } = state.opts if (enableObjectSlots) { state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { if (importMap.runtimeIsSlot) { - return importMap.runtimeIsSlot; + return importMap.runtimeIsSlot } const { name: isVNodeName } = state.get( - 'isVNode' - )() as t.Identifier; - const isSlot = path.scope.generateUidIdentifier('isSlot'); + 'isVNode', + )() as t.Identifier + 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(); + .pop() if (lastImport) { - lastImport.insertAfter(ast); + lastImport.insertAfter(ast) } - importMap.runtimeIsSlot = isSlot; - return isSlot; - }); + importMap.runtimeIsSlot = isSlot + return isSlot + }) } } else { // var _vue = require('vue'); - let sourceName: t.Identifier; + let sourceName: t.Identifier importNames.forEach((name) => { state.set(name, () => { if (!sourceName) { sourceName = addNamespace(path, 'vue', { ensureLiveReference: true, - }); + }) } - return t.memberExpression(sourceName, t.identifier(name)); - }); - }); + return t.memberExpression(sourceName, t.identifier(name)) + }) + }) - const helpers: Record = {}; + const helpers: Record = {} - const { enableObjectSlots = true } = state.opts; + const { enableObjectSlots = true } = state.opts if (enableObjectSlots) { state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { if (helpers.runtimeIsSlot) { - return helpers.runtimeIsSlot; + return helpers.runtimeIsSlot } - const isSlot = path.scope.generateUidIdentifier('isSlot'); + const isSlot = path.scope.generateUidIdentifier('isSlot') const { object: objectName } = state.get( - 'isVNode' - )() as t.MemberExpression; + 'isVNode', + )() as t.MemberExpression const ast = template.ast` function ${isSlot.name}(s) { return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ (objectName as t.Identifier).name }.isVNode(s)); } - `; + ` - const nodePaths = path.get('body') as NodePath[]; + const nodePaths = path.get('body') as NodePath[] const lastImport = nodePaths .filter( (p) => p.isVariableDeclaration() && p.node.declarations.some( (d) => - (d.id as t.Identifier)?.name === sourceName.name - ) + (d.id as t.Identifier)?.name === sourceName.name, + ), ) - .pop(); + .pop() if (lastImport) { - lastImport.insertAfter(ast); + lastImport.insertAfter(ast) } - return isSlot; - }); + return isSlot + }) } } const { opts: { pragma = '' }, file, - } = state; + } = state if (pragma) { - state.set('createVNode', () => t.identifier(pragma)); + state.set('createVNode', () => t.identifier(pragma)) } if (file.ast.comments) { for (const comment of file.ast.comments) { - const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value); + const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value) if (jsxMatches) { - state.set('createVNode', () => t.identifier(jsxMatches[1])); + state.set('createVNode', () => t.identifier(jsxMatches[1])) } } } @@ -199,8 +199,8 @@ const plugin: ( }, }, }, - }; -}); + } +}) -export default plugin; -export { plugin as 'module.exports' }; +export default plugin +export { plugin as 'module.exports' } diff --git a/packages/babel-plugin-jsx/src/interface.ts b/packages/babel-plugin-jsx/src/interface.ts index cb0524d..8e83a9a 100644 --- a/packages/babel-plugin-jsx/src/interface.ts +++ b/packages/babel-plugin-jsx/src/interface.ts @@ -1,32 +1,32 @@ -import type t from '@babel/types'; -import type * as BabelCore from '@babel/core'; -import { type Options } from '@vue/babel-plugin-resolve-type'; +import type t from '@babel/types' +import type * as BabelCore from '@babel/core' +import { type Options } from '@vue/babel-plugin-resolve-type' -export type Slots = t.Identifier | t.ObjectExpression | null; +export type Slots = t.Identifier | t.ObjectExpression | null export type State = { - get: (name: string) => any; - set: (name: string, value: any) => any; - opts: VueJSXPluginOptions; - file: BabelCore.BabelFile; -}; + get: (name: string) => any + set: (name: string, value: any) => any + opts: VueJSXPluginOptions + file: BabelCore.BabelFile +} export interface VueJSXPluginOptions { /** transform `on: { click: xx }` to `onClick: xxx` */ - transformOn?: boolean; + transformOn?: boolean /** enable optimization or not. */ - optimize?: boolean; + optimize?: boolean /** merge static and dynamic class / style attributes / onXXX handlers */ - mergeProps?: boolean; + mergeProps?: boolean /** configuring custom elements */ - isCustomElement?: (tag: string) => boolean; + isCustomElement?: (tag: string) => boolean /** enable object slots syntax */ - enableObjectSlots?: boolean; + enableObjectSlots?: boolean /** Replace the function used when compiling JSX expressions */ - pragma?: string; + pragma?: string /** * (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`) * @default false */ - resolveType?: Options | boolean; + resolveType?: Options | boolean } diff --git a/packages/babel-plugin-jsx/src/parseDirectives.ts b/packages/babel-plugin-jsx/src/parseDirectives.ts index 01be703..036eec2 100644 --- a/packages/babel-plugin-jsx/src/parseDirectives.ts +++ b/packages/babel-plugin-jsx/src/parseDirectives.ts @@ -1,13 +1,13 @@ -import t from '@babel/types'; -import { type NodePath } from '@babel/traverse'; -import { createIdentifier } from './utils'; -import type { State } from './interface'; +import t from '@babel/types' +import { type NodePath } from '@babel/traverse' +import { createIdentifier } from './utils' +import type { State } from './interface' export type Tag = | t.Identifier | t.MemberExpression | t.StringLiteral - | t.CallExpression; + | t.CallExpression /** * Get JSX element type @@ -17,111 +17,111 @@ export type Tag = const getType = (path: NodePath) => { const typePath = path.get('attributes').find((attribute) => { if (!attribute.isJSXAttribute()) { - return false; + return false } return ( attribute.get('name').isJSXIdentifier() && (attribute.get('name') as NodePath).node.name === 'type' - ); - }) as NodePath | undefined; + ) + }) as NodePath | undefined - return typePath ? typePath.get('value').node : null; -}; + return typePath ? typePath.get('value').node : null +} const parseModifiers = (value: any): string[] => t.isArrayExpression(value) ? value.elements .map((el) => (t.isStringLiteral(el) ? el.value : '')) .filter(Boolean) - : []; + : [] const parseDirectives = (params: { - name: string; - path: NodePath; - value: t.Expression | null; - state: State; - tag: Tag; - isComponent: boolean; + name: string + path: NodePath + value: t.Expression | null + state: State + tag: Tag + isComponent: boolean }) => { - const { path, value, state, tag, isComponent } = params; - const args: Array = []; - const vals: t.Expression[] = []; - const modifiersSet: Set[] = []; + const { path, value, state, tag, isComponent } = params + const args: Array = [] + const vals: t.Expression[] = [] + const modifiersSet: Set[] = [] - let directiveName; - let directiveArgument; - let directiveModifiers; + let directiveName + let directiveArgument + let directiveModifiers if ('namespace' in path.node.name) { - [directiveName, directiveArgument] = params.name.split(':'); - directiveName = path.node.name.namespace.name; - directiveArgument = path.node.name.name.name; - directiveModifiers = directiveArgument.split('_').slice(1); + ;[directiveName, directiveArgument] = params.name.split(':') + directiveName = path.node.name.namespace.name + directiveArgument = path.node.name.name.name + directiveModifiers = directiveArgument.split('_').slice(1) } else { - const underscoreModifiers = params.name.split('_'); - directiveName = underscoreModifiers.shift() || ''; - directiveModifiers = underscoreModifiers; + const underscoreModifiers = params.name.split('_') + directiveName = underscoreModifiers.shift() || '' + directiveModifiers = underscoreModifiers } directiveName = directiveName .replace(/^v/, '') .replace(/^-/, '') - .replace(/^\S/, (s: string) => s.toLowerCase()); + .replace(/^\S/, (s: string) => s.toLowerCase()) if (directiveArgument) { - args.push(t.stringLiteral(directiveArgument.split('_')[0])); + args.push(t.stringLiteral(directiveArgument.split('_')[0])) } - const isVModels = directiveName === 'models'; - const isVModel = directiveName === 'model'; + const isVModels = directiveName === 'models' + const isVModel = directiveName === 'model' if (isVModel && !path.get('value').isJSXExpressionContainer()) { - throw new Error('You have to use JSX Expression inside your v-model'); + throw new Error('You have to use JSX Expression inside your v-model') } if (isVModels && !isComponent) { - throw new Error('v-models can only use in custom components'); + throw new Error('v-models can only use in custom components') } const shouldResolve = !['html', 'text', 'model', 'slots', 'models'].includes(directiveName) || - (isVModel && !isComponent); + (isVModel && !isComponent) - let modifiers = directiveModifiers; + let modifiers = directiveModifiers if (t.isArrayExpression(value)) { - const elementsList = isVModels ? value.elements! : [value]; + const elementsList = isVModels ? value.elements! : [value] elementsList.forEach((element) => { if (isVModels && !t.isArrayExpression(element)) { - throw new Error('You should pass a Two-dimensional Arrays to v-models'); + throw new Error('You should pass a Two-dimensional Arrays to v-models') } - const { elements } = element as t.ArrayExpression; - const [first, second, third] = elements; + const { elements } = element as t.ArrayExpression + const [first, second, third] = elements if ( second && !t.isArrayExpression(second) && !t.isSpreadElement(second) ) { - args.push(second); - modifiers = parseModifiers(third as t.ArrayExpression); + args.push(second) + modifiers = parseModifiers(third as t.ArrayExpression) } else if (t.isArrayExpression(second)) { if (!shouldResolve) { - args.push(t.nullLiteral()); + args.push(t.nullLiteral()) } - modifiers = parseModifiers(second); + modifiers = parseModifiers(second) } else if (!shouldResolve) { // work as v-model={[value]} or v-models={[[value]]} - args.push(t.nullLiteral()); + args.push(t.nullLiteral()) } - modifiersSet.push(new Set(modifiers)); - vals.push(first as t.Expression); - }); + modifiersSet.push(new Set(modifiers)) + vals.push(first as t.Expression) + }) } else if (isVModel && !shouldResolve) { // work as v-model={value} - args.push(t.nullLiteral()); - modifiersSet.push(new Set(directiveModifiers)); + args.push(t.nullLiteral()) + modifiersSet.push(new Set(directiveModifiers)) } else { - modifiersSet.push(new Set(directiveModifiers)); + modifiersSet.push(new Set(directiveModifiers)) } return { @@ -139,59 +139,62 @@ const parseDirectives = (params: { !!modifiersSet[0]?.size && t.objectExpression( [...modifiersSet[0]].map((modifier) => - t.objectProperty(t.identifier(modifier), t.booleanLiteral(true)) - ) + t.objectProperty( + t.identifier(modifier), + t.booleanLiteral(true), + ), + ), ), ].filter(Boolean) as t.Expression[]) : undefined, - }; -}; + } +} const resolveDirective = ( path: NodePath, state: State, tag: Tag, - directiveName: string + directiveName: string, ) => { if (directiveName === 'show') { - return createIdentifier(state, 'vShow'); + return createIdentifier(state, 'vShow') } if (directiveName === 'model') { - let modelToUse; - const type = getType(path.parentPath as NodePath); + let modelToUse + const type = getType(path.parentPath as NodePath) switch ((tag as t.StringLiteral).value) { case 'select': - modelToUse = createIdentifier(state, 'vModelSelect'); - break; + modelToUse = createIdentifier(state, 'vModelSelect') + break case 'textarea': - modelToUse = createIdentifier(state, 'vModelText'); - break; + modelToUse = createIdentifier(state, 'vModelText') + break default: if (t.isStringLiteral(type) || !type) { switch ((type as t.StringLiteral)?.value) { case 'checkbox': - modelToUse = createIdentifier(state, 'vModelCheckbox'); - break; + modelToUse = createIdentifier(state, 'vModelCheckbox') + break case 'radio': - modelToUse = createIdentifier(state, 'vModelRadio'); - break; + modelToUse = createIdentifier(state, 'vModelRadio') + break default: - modelToUse = createIdentifier(state, 'vModelText'); + modelToUse = createIdentifier(state, 'vModelText') } } else { - modelToUse = createIdentifier(state, 'vModelDynamic'); + modelToUse = createIdentifier(state, 'vModelDynamic') } } - return modelToUse; + return modelToUse } const referenceName = - 'v' + directiveName[0].toUpperCase() + directiveName.slice(1); + 'v' + directiveName[0].toUpperCase() + directiveName.slice(1) if (path.scope.references[referenceName]) { - return t.identifier(referenceName); + return t.identifier(referenceName) } return t.callExpression(createIdentifier(state, 'resolveDirective'), [ t.stringLiteral(directiveName), - ]); -}; + ]) +} -export default parseDirectives; +export default parseDirectives diff --git a/packages/babel-plugin-jsx/src/patchFlags.ts b/packages/babel-plugin-jsx/src/patchFlags.ts index 790c977..897aa05 100644 --- a/packages/babel-plugin-jsx/src/patchFlags.ts +++ b/packages/babel-plugin-jsx/src/patchFlags.ts @@ -31,4 +31,4 @@ export const PatchFlagNames = { [PatchFlags.NEED_PATCH]: 'NEED_PATCH', [PatchFlags.HOISTED]: 'HOISTED', [PatchFlags.BAIL]: 'BAIL', -}; +} diff --git a/packages/babel-plugin-jsx/src/slotFlags.ts b/packages/babel-plugin-jsx/src/slotFlags.ts index 8ba8403..e801681 100644 --- a/packages/babel-plugin-jsx/src/slotFlags.ts +++ b/packages/babel-plugin-jsx/src/slotFlags.ts @@ -21,4 +21,4 @@ const enum SlotFlags { FORWARDED = 3, } -export default SlotFlags; +export default SlotFlags diff --git a/packages/babel-plugin-jsx/src/sugar-fragment.ts b/packages/babel-plugin-jsx/src/sugar-fragment.ts index 99a810b..efac45f 100644 --- a/packages/babel-plugin-jsx/src/sugar-fragment.ts +++ b/packages/babel-plugin-jsx/src/sugar-fragment.ts @@ -1,25 +1,25 @@ -import t from '@babel/types'; -import { type NodePath, type Visitor } from '@babel/traverse'; -import type { State } from './interface'; -import { FRAGMENT, createIdentifier } from './utils'; +import t from '@babel/types' +import { type NodePath, type Visitor } from '@babel/traverse' +import type { State } from './interface' +import { FRAGMENT, createIdentifier } from './utils' const transformFragment = ( path: NodePath, - Fragment: t.JSXIdentifier | t.JSXMemberExpression + Fragment: t.JSXIdentifier | t.JSXMemberExpression, ) => { - const children = path.get('children') || []; + const children = path.get('children') || [] return t.jsxElement( t.jsxOpeningElement(Fragment, []), t.jsxClosingElement(Fragment), children.map(({ node }) => node), - false - ); -}; + false, + ) +} const visitor: Visitor = { JSXFragment: { enter(path, state) { - const fragmentCallee = createIdentifier(state, FRAGMENT); + const fragmentCallee = createIdentifier(state, FRAGMENT) path.replaceWith( transformFragment( path, @@ -27,12 +27,12 @@ const visitor: Visitor = { ? t.jsxIdentifier(fragmentCallee.name) : t.jsxMemberExpression( t.jsxIdentifier((fragmentCallee.object as t.Identifier).name), - t.jsxIdentifier((fragmentCallee.property as t.Identifier).name) - ) - ) - ); + t.jsxIdentifier((fragmentCallee.property as t.Identifier).name), + ), + ), + ) }, }, -}; +} -export default visitor; +export default visitor diff --git a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts index e563b2f..e5a9c7c 100644 --- a/packages/babel-plugin-jsx/src/transform-vue-jsx.ts +++ b/packages/babel-plugin-jsx/src/transform-vue-jsx.ts @@ -1,6 +1,6 @@ -import t from '@babel/types'; -import { type NodePath, type Visitor } from '@babel/traverse'; -import { addDefault } from '@babel/helper-module-imports'; +import t from '@babel/types' +import { type NodePath, type Visitor } from '@babel/traverse' +import { addDefault } from '@babel/helper-module-imports' import { buildIIFE, checkIsComponent, @@ -17,43 +17,43 @@ import { transformJSXText, transformText, walksScope, -} from './utils'; -import SlotFlags from './slotFlags'; -import { PatchFlags } from './patchFlags'; -import parseDirectives from './parseDirectives'; -import type { Slots, State } from './interface'; +} from './utils' +import SlotFlags from './slotFlags' +import { PatchFlags } from './patchFlags' +import parseDirectives from './parseDirectives' +import type { Slots, State } from './interface' -const xlinkRE = /^xlink([A-Z])/; +const xlinkRE = /^xlink([A-Z])/ -type ExcludesBoolean = (x: T | false | true) => x is T; +type ExcludesBoolean = (x: T | false | true) => x is T const getJSXAttributeValue = ( path: NodePath, - state: State + state: State, ): t.StringLiteral | t.Expression | null => { - const valuePath = path.get('value'); + const valuePath = path.get('value') if (valuePath.isJSXElement()) { - return transformJSXElement(valuePath, state); + return transformJSXElement(valuePath, state) } if (valuePath.isStringLiteral()) { - return t.stringLiteral(transformText(valuePath.node.value)); + return t.stringLiteral(transformText(valuePath.node.value)) } if (valuePath.isJSXExpressionContainer()) { - return transformJSXExpressionContainer(valuePath); + return transformJSXExpressionContainer(valuePath) } - return null; -}; + return null +} const buildProps = (path: NodePath, state: State) => { - const tag = getTag(path, state); - const isComponent = checkIsComponent(path.get('openingElement'), state); - const props = path.get('openingElement').get('attributes'); - const directives: t.ArrayExpression[] = []; - const dynamicPropNames = new Set(); + const tag = getTag(path, state) + const isComponent = checkIsComponent(path.get('openingElement'), state) + const props = path.get('openingElement').get('attributes') + const directives: t.ArrayExpression[] = [] + const dynamicPropNames = new Set() - let slots: Slots = null; - let patchFlag = 0; + let slots: Slots = null + let patchFlag = 0 if (props.length === 0) { return { @@ -64,26 +64,25 @@ const buildProps = (path: NodePath, state: State) => { directives, patchFlag, dynamicPropNames, - }; + } } - let properties: t.ObjectProperty[] = []; + let properties: t.ObjectProperty[] = [] // patchFlag analysis - let hasRef = false; - let hasClassBinding = false; - let hasStyleBinding = false; - let hasHydrationEventBinding = false; - let hasDynamicKeys = false; + let hasRef = false + let hasClassBinding = false + let hasStyleBinding = false + let hasHydrationEventBinding = false + let hasDynamicKeys = false - const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = - []; - const { mergeProps = true } = state.opts; + const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = [] + const { mergeProps = true } = state.opts props.forEach((prop) => { if (prop.isJSXAttribute()) { - let name = getJSXAttributeName(prop); + let name = getJSXAttributeName(prop) - const attributeValue = getJSXAttributeValue(prop, state); + const attributeValue = getJSXAttributeValue(prop, state) if (!isConstant(attributeValue) || name === 'ref') { if ( @@ -95,17 +94,17 @@ const buildProps = (path: NodePath, state: State) => { // omit v-model handlers name !== 'onUpdate:modelValue' ) { - hasHydrationEventBinding = true; + hasHydrationEventBinding = true } if (name === 'ref') { - hasRef = true; + hasRef = true } else if (name === 'class' && !isComponent) { - hasClassBinding = true; + hasClassBinding = true } else if (name === 'style' && !isComponent) { - hasStyleBinding = true; + hasStyleBinding = true } else if (name !== 'key' && !isDirective(name) && name !== 'on') { - dynamicPropNames.add(name); + dynamicPropNames.add(name) } } if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) { @@ -114,15 +113,15 @@ const buildProps = (path: NodePath, state: State) => { 'transformOn', addDefault(path, '@vue/babel-helper-vue-transform-on', { nameHint: '_transformOn', - }) - ); + }), + ) } mergeArgs.push( t.callExpression(state.get('transformOn'), [ attributeValue || t.booleanLiteral(true), - ]) - ); - return; + ]), + ) + return } if (isDirective(name)) { const { directive, modifiers, values, args, directiveName } = @@ -133,34 +132,34 @@ const buildProps = (path: NodePath, state: State) => { path: prop, state, value: attributeValue, - }); + }) if (directiveName === 'slots') { - slots = attributeValue as Slots; - return; + slots = attributeValue as Slots + return } if (directive) { - directives.push(t.arrayExpression(directive)); + directives.push(t.arrayExpression(directive)) } else if (directiveName === 'html') { properties.push( - t.objectProperty(t.stringLiteral('innerHTML'), values[0] as any) - ); - dynamicPropNames.add('innerHTML'); + t.objectProperty(t.stringLiteral('innerHTML'), values[0] as any), + ) + dynamicPropNames.add('innerHTML') } else if (directiveName === 'text') { properties.push( - t.objectProperty(t.stringLiteral('textContent'), values[0] as any) - ); - dynamicPropNames.add('textContent'); + t.objectProperty(t.stringLiteral('textContent'), values[0] as any), + ) + dynamicPropNames.add('textContent') } if (['models', 'model'].includes(directiveName)) { values.forEach((value, index) => { - const propName = args[index]; + const propName = args[index] // v-model target with variable const isDynamic = propName && !t.isStringLiteral(propName) && - !t.isNullLiteral(propName); + !t.isNullLiteral(propName) // must be v-model or v-models and is a component if (!directive) { @@ -170,13 +169,13 @@ const buildProps = (path: NodePath, state: State) => { ? t.stringLiteral('modelValue') : propName, value as any, - isDynamic - ) - ); + isDynamic, + ), + ) if (!isDynamic) { dynamicPropNames.add( - (propName as t.StringLiteral)?.value || 'modelValue' - ); + (propName as t.StringLiteral)?.value || 'modelValue', + ) } if (modifiers[index]?.size) { @@ -186,24 +185,24 @@ const buildProps = (path: NodePath, state: State) => { ? t.binaryExpression( '+', propName, - t.stringLiteral('Modifiers') + t.stringLiteral('Modifiers'), ) : t.stringLiteral( `${ (propName as t.StringLiteral)?.value || 'model' - }Modifiers` + }Modifiers`, ), t.objectExpression( [...modifiers[index]].map((modifier) => t.objectProperty( t.stringLiteral(modifier), - t.booleanLiteral(true) - ) - ) + t.booleanLiteral(true), + ), + ), ), - isDynamic - ) - ); + isDynamic, + ), + ) } } @@ -212,8 +211,8 @@ const buildProps = (path: NodePath, state: State) => { : t.stringLiteral( `onUpdate:${ (propName as t.StringLiteral)?.value || 'modelValue' - }` - ); + }`, + ) properties.push( t.objectProperty( @@ -223,68 +222,68 @@ const buildProps = (path: NodePath, state: State) => { t.assignmentExpression( '=', value as any, - t.identifier('$event') - ) + t.identifier('$event'), + ), ), - isDynamic - ) - ); + isDynamic, + ), + ) if (!isDynamic) { - dynamicPropNames.add((updateName as t.StringLiteral).value); + dynamicPropNames.add((updateName as t.StringLiteral).value) } else { - hasDynamicKeys = true; + hasDynamicKeys = true } - }); + }) } } else { if (name.match(xlinkRE)) { name = name.replace( xlinkRE, - (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}` - ); + (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`, + ) } properties.push( t.objectProperty( t.stringLiteral(name), - attributeValue || t.booleanLiteral(true) - ) - ); + attributeValue || t.booleanLiteral(true), + ), + ) } } else { if (properties.length && mergeProps) { mergeArgs.push( - t.objectExpression(dedupeProperties(properties, mergeProps)) - ); - properties = []; + t.objectExpression(dedupeProperties(properties, mergeProps)), + ) + properties = [] } // JSXSpreadAttribute - hasDynamicKeys = true; + hasDynamicKeys = true transformJSXSpreadAttribute( path as NodePath, prop as NodePath, mergeProps, - mergeProps ? mergeArgs : properties - ); + mergeProps ? mergeArgs : properties, + ) } - }); + }) // patchFlag analysis if (hasDynamicKeys) { - patchFlag |= PatchFlags.FULL_PROPS; + patchFlag |= PatchFlags.FULL_PROPS } else { if (hasClassBinding) { - patchFlag |= PatchFlags.CLASS; + patchFlag |= PatchFlags.CLASS } if (hasStyleBinding) { - patchFlag |= PatchFlags.STYLE; + patchFlag |= PatchFlags.STYLE } if (dynamicPropNames.size) { - patchFlag |= PatchFlags.PROPS; + patchFlag |= PatchFlags.PROPS } if (hasHydrationEventBinding) { - patchFlag |= PatchFlags.HYDRATE_EVENTS; + patchFlag |= PatchFlags.HYDRATE_EVENTS } } @@ -292,34 +291,34 @@ const buildProps = (path: NodePath, state: State) => { (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (hasRef || directives.length > 0) ) { - patchFlag |= PatchFlags.NEED_PATCH; + patchFlag |= PatchFlags.NEED_PATCH } let propsExpression: t.Expression | t.ObjectProperty | t.Literal = - t.nullLiteral(); + t.nullLiteral() if (mergeArgs.length) { if (properties.length) { mergeArgs.push( - t.objectExpression(dedupeProperties(properties, mergeProps)) - ); + t.objectExpression(dedupeProperties(properties, mergeProps)), + ) } if (mergeArgs.length > 1) { propsExpression = t.callExpression( createIdentifier(state, 'mergeProps'), - mergeArgs - ); + mergeArgs, + ) } else { // single no need for a mergeProps call - propsExpression = mergeArgs[0]; + propsExpression = mergeArgs[0] } } else if (properties.length) { // single no need for spread if (properties.length === 1 && t.isSpreadElement(properties[0])) { - propsExpression = (properties[0] as unknown as t.SpreadElement).argument; + propsExpression = (properties[0] as unknown as t.SpreadElement).argument } else { propsExpression = t.objectExpression( - dedupeProperties(properties, mergeProps) - ); + dedupeProperties(properties, mergeProps), + ) } } @@ -331,8 +330,8 @@ const buildProps = (path: NodePath, state: State) => { directives, patchFlag, dynamicPropNames, - }; -}; + } +} /** * Get children from Array of JSX children @@ -347,52 +346,52 @@ const getChildren = ( | t.JSXElement | t.JSXFragment >[], - state: State + state: State, ): t.Expression[] => paths .map((path) => { if (path.isJSXText()) { - const transformedText = transformJSXText(path); + const transformedText = transformJSXText(path) if (transformedText) { return t.callExpression(createIdentifier(state, 'createTextVNode'), [ transformedText, - ]); + ]) } - return transformedText; + return transformedText } if (path.isJSXExpressionContainer()) { - const expression = transformJSXExpressionContainer(path); + const expression = transformJSXExpressionContainer(path) if (t.isIdentifier(expression)) { - const { name } = expression; - const { referencePaths = [] } = path.scope.getBinding(name) || {}; + const { name } = expression + const { referencePaths = [] } = path.scope.getBinding(name) || {} referencePaths.forEach((referencePath) => { - walksScope(referencePath, name, SlotFlags.DYNAMIC); - }); + walksScope(referencePath, name, SlotFlags.DYNAMIC) + }) } - return expression; + return expression } if (path.isJSXSpreadChild()) { - return transformJSXSpreadChild(path); + return transformJSXSpreadChild(path) } if (path.isCallExpression()) { - return (path as NodePath).node; + return (path as NodePath).node } if (path.isJSXElement()) { - return transformJSXElement(path, state); + return transformJSXElement(path, state) } - throw new Error(`getChildren: ${path.type} is not supported`); + throw new Error(`getChildren: ${path.type} is not supported`) }) .filter( - ((value: any) => value != null && !t.isJSXEmptyExpression(value)) as any - ); + ((value: any) => value != null && !t.isJSXEmptyExpression(value)) as any, + ) const transformJSXElement = ( path: NodePath, - state: State + state: State, ): t.CallExpression => { - const children = getChildren(path.get('children'), state); + const children = getChildren(path.get('children'), state) const { tag, props, @@ -401,9 +400,9 @@ const transformJSXElement = ( patchFlag, dynamicPropNames, slots, - } = buildProps(path, state); + } = buildProps(path, state) - const { optimize = false } = state.opts; + const { optimize = false } = state.opts // #541 - directives can't be resolved in optimized slots // all parents should be deoptimized @@ -413,19 +412,19 @@ const transformJSXElement = ( (d) => d.elements?.[0]?.type === 'CallExpression' && d.elements[0].callee.type === 'Identifier' && - d.elements[0].callee.name === '_resolveDirective' + d.elements[0].callee.name === '_resolveDirective', ) ) { - let currentPath = path; + let currentPath = path while (currentPath.parentPath?.isJSXElement()) { - currentPath = currentPath.parentPath; - currentPath.setData('slotFlag', 0); + currentPath = currentPath.parentPath + currentPath.setData('slotFlag', 0) } } - const slotFlag = path.getData('slotFlag') ?? SlotFlags.STABLE; - const optimizeSlots = optimize && slotFlag !== 0; - let VNodeChild; + const slotFlag = path.getData('slotFlag') ?? SlotFlags.STABLE + const optimizeSlots = optimize && slotFlag !== 0 + let VNodeChild if (children.length > 1 || slots) { /* @@ -442,8 +441,8 @@ const transformJSXElement = ( t.identifier('default'), t.arrowFunctionExpression( [], - t.arrayExpression(buildIIFE(path, children)) - ) + t.arrayExpression(buildIIFE(path, children)), + ), ), ...(slots ? t.isObjectExpression(slots) @@ -452,53 +451,53 @@ const transformJSXElement = ( : []), optimizeSlots && t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), - ].filter(Boolean as any) + ].filter(Boolean as any), ) : slots - : t.arrayExpression(children); + : t.arrayExpression(children) } else if (children.length === 1) { /* {a} or {() => a} */ - const { enableObjectSlots = true } = state.opts; - const child = children[0]; + const { enableObjectSlots = true } = state.opts + const child = children[0] const objectExpression = t.objectExpression( [ t.objectProperty( t.identifier('default'), t.arrowFunctionExpression( [], - t.arrayExpression(buildIIFE(path, [child])) - ) + t.arrayExpression(buildIIFE(path, [child])), + ), ), optimizeSlots && (t.objectProperty( t.identifier('_'), - t.numericLiteral(slotFlag) + t.numericLiteral(slotFlag), ) as any), - ].filter(Boolean) - ); + ].filter(Boolean), + ) if (t.isIdentifier(child) && isComponent) { VNodeChild = enableObjectSlots ? t.conditionalExpression( t.callExpression( state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), - [child] + [child], ), child, - objectExpression + objectExpression, ) - : objectExpression; + : objectExpression } else if (t.isCallExpression(child) && child.loc && isComponent) { // the element was generated and doesn't have location information if (enableObjectSlots) { - const { scope } = path; - const slotId = scope.generateUidIdentifier('slot'); + const { scope } = path + const slotId = scope.generateUidIdentifier('slot') if (scope) { scope.push({ id: slotId, kind: 'let', - }); + }) } const alternate = t.objectExpression( [ @@ -506,24 +505,24 @@ const transformJSXElement = ( t.identifier('default'), t.arrowFunctionExpression( [], - t.arrayExpression(buildIIFE(path, [slotId])) - ) + t.arrayExpression(buildIIFE(path, [slotId])), + ), ), optimizeSlots && (t.objectProperty( t.identifier('_'), - t.numericLiteral(slotFlag) + t.numericLiteral(slotFlag), ) as any), - ].filter(Boolean) - ); - const assignment = t.assignmentExpression('=', slotId, child); + ].filter(Boolean), + ) + const assignment = t.assignmentExpression('=', slotId, child) const condition = t.callExpression( state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), - [assignment] - ); - VNodeChild = t.conditionalExpression(condition, slotId, alternate); + [assignment], + ) + VNodeChild = t.conditionalExpression(condition, slotId, alternate) } else { - VNodeChild = objectExpression; + VNodeChild = objectExpression } } else if ( t.isFunctionExpression(child) || @@ -531,24 +530,24 @@ const transformJSXElement = ( ) { VNodeChild = t.objectExpression([ t.objectProperty(t.identifier('default'), child), - ]); + ]) } else if (t.isObjectExpression(child)) { VNodeChild = t.objectExpression( [ ...child.properties, optimizeSlots && t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), - ].filter(Boolean as any) - ); + ].filter(Boolean as any), + ) } else { VNodeChild = isComponent ? t.objectExpression([ t.objectProperty( t.identifier('default'), - t.arrowFunctionExpression([], t.arrayExpression([child])) + t.arrowFunctionExpression([], t.arrayExpression([child])), ), ]) - : t.arrayExpression([child]); + : t.arrayExpression([child]) } } @@ -562,27 +561,27 @@ const transformJSXElement = ( !!dynamicPropNames.size && optimize && t.arrayExpression( - [...dynamicPropNames.keys()].map((name) => t.stringLiteral(name)) + [...dynamicPropNames.keys()].map((name) => t.stringLiteral(name)), ), - ].filter(Boolean as unknown as ExcludesBoolean) - ); + ].filter(Boolean as unknown as ExcludesBoolean), + ) if (!directives.length) { - return createVNode; + return createVNode } return t.callExpression(createIdentifier(state, 'withDirectives'), [ createVNode, t.arrayExpression(directives), - ]); -}; + ]) +} const visitor: Visitor = { JSXElement: { exit(path, state) { - path.replaceWith(transformJSXElement(path, state)); + path.replaceWith(transformJSXElement(path, state)) }, }, -}; +} -export default visitor; +export default visitor diff --git a/packages/babel-plugin-jsx/src/utils.ts b/packages/babel-plugin-jsx/src/utils.ts index 520fd97..73b83a8 100644 --- a/packages/babel-plugin-jsx/src/utils.ts +++ b/packages/babel-plugin-jsx/src/utils.ts @@ -1,11 +1,11 @@ -import t from '@babel/types'; -import { type NodePath } from '@babel/traverse'; -import { isHTMLTag, isSVGTag } from '@vue/shared'; -import type { State } from './interface'; -import SlotFlags from './slotFlags'; -export const JSX_HELPER_KEY = 'JSX_HELPER_KEY'; -export const FRAGMENT = 'Fragment'; -export const KEEP_ALIVE = 'KeepAlive'; +import t from '@babel/types' +import { type NodePath } from '@babel/traverse' +import { isHTMLTag, isSVGTag } from '@vue/shared' +import type { State } from './interface' +import SlotFlags from './slotFlags' +export const JSX_HELPER_KEY = 'JSX_HELPER_KEY' +export const FRAGMENT = 'Fragment' +export const KEEP_ALIVE = 'KeepAlive' /** * create Identifier @@ -16,8 +16,8 @@ export const KEEP_ALIVE = 'KeepAlive'; */ export const createIdentifier = ( state: State, - name: string -): t.Identifier | t.MemberExpression => state.get(name)(); + name: string, +): t.Identifier | t.MemberExpression => state.get(name)() /** * Checks if string is describing a directive @@ -25,7 +25,7 @@ export const createIdentifier = ( */ export const isDirective = (src: string): boolean => src.startsWith('v-') || - (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z'); + (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z') /** * Should transformed to slots @@ -34,7 +34,7 @@ export const isDirective = (src: string): boolean => */ // if _Fragment is already imported, it will end with number export const shouldTransformedToSlots = (tag: string) => - !(tag.match(RegExp(`^_?${FRAGMENT}\\d*$`)) || tag === KEEP_ALIVE); + !(tag.match(RegExp(`^_?${FRAGMENT}\\d*$`)) || tag === KEEP_ALIVE) /** * Check if a Node is a component @@ -45,23 +45,23 @@ export const shouldTransformedToSlots = (tag: string) => */ export const checkIsComponent = ( path: NodePath, - state: State + state: State, ): boolean => { - const namePath = path.get('name'); + const namePath = path.get('name') if (namePath.isJSXMemberExpression()) { - return shouldTransformedToSlots(namePath.node.property.name); // For withCtx + return shouldTransformedToSlots(namePath.node.property.name) // For withCtx } - const tag = (namePath as NodePath).node.name; + const tag = (namePath as NodePath).node.name return ( !state.opts.isCustomElement?.(tag) && shouldTransformedToSlots(tag) && !isHTMLTag(tag) && !isSVGTag(tag) - ); -}; + ) +} /** * Transform JSXMemberExpression to MemberExpression @@ -69,20 +69,20 @@ export const checkIsComponent = ( * @returns MemberExpression */ export const transformJSXMemberExpression = ( - path: NodePath + path: NodePath, ): t.MemberExpression => { - const objectPath = path.node.object; - const propertyPath = path.node.property; + const objectPath = path.node.object + const propertyPath = path.node.property const transformedObject = t.isJSXMemberExpression(objectPath) ? transformJSXMemberExpression( - path.get('object') as NodePath + path.get('object') as NodePath, ) : t.isJSXIdentifier(objectPath) ? t.identifier(objectPath.name) - : t.nullLiteral(); - const transformedProperty = t.identifier(propertyPath.name); - return t.memberExpression(transformedObject, transformedProperty); -}; + : t.nullLiteral() + const transformedProperty = t.identifier(propertyPath.name) + return t.memberExpression(transformedObject, transformedProperty) +} /** * Get tag (first attribute for h) from JSXOpeningElement @@ -92,11 +92,11 @@ export const transformJSXMemberExpression = ( */ export const getTag = ( path: NodePath, - state: State + state: State, ): t.Identifier | t.CallExpression | t.StringLiteral | t.MemberExpression => { - const namePath = path.get('openingElement').get('name'); + const namePath = path.get('openingElement').get('name') if (namePath.isJSXIdentifier()) { - const { name } = namePath.node; + const { name } = namePath.node if (!isHTMLTag(name) && !isSVGTag(name)) { return name === FRAGMENT ? createIdentifier(state, FRAGMENT) @@ -106,26 +106,26 @@ export const getTag = ( ? t.stringLiteral(name) : t.callExpression(createIdentifier(state, 'resolveComponent'), [ t.stringLiteral(name), - ]); + ]) } - return t.stringLiteral(name); + return t.stringLiteral(name) } if (namePath.isJSXMemberExpression()) { - return transformJSXMemberExpression(namePath); + return transformJSXMemberExpression(namePath) } - throw new Error(`getTag: ${namePath.type} is not supported`); -}; + throw new Error(`getTag: ${namePath.type} is not supported`) +} export const getJSXAttributeName = (path: NodePath): string => { - const nameNode = path.node.name; + const nameNode = path.node.name if (t.isJSXIdentifier(nameNode)) { - return nameNode.name; + return nameNode.name } - return `${nameNode.namespace.name}:${nameNode.name.name}`; -}; + return `${nameNode.namespace.name}:${nameNode.name.name}` +} /** * Transform JSXText to StringLiteral @@ -133,56 +133,56 @@ export const getJSXAttributeName = (path: NodePath): string => { * @returns StringLiteral | null */ export const transformJSXText = ( - path: NodePath + path: NodePath, ): t.StringLiteral | null => { - const str = transformText(path.node.value); - return str !== '' ? t.stringLiteral(str) : null; -}; + const str = transformText(path.node.value) + return str !== '' ? t.stringLiteral(str) : null +} export const transformText = (text: string) => { - const lines = text.split(/\r\n|\n|\r/); + const lines = text.split(/\r\n|\n|\r/) - let lastNonEmptyLine = 0; + let lastNonEmptyLine = 0 for (let i = 0; i < lines.length; i++) { if (lines[i].match(/[^ \t]/)) { - lastNonEmptyLine = i; + lastNonEmptyLine = i } } - let str = ''; + let str = '' for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + const line = lines[i] - const isFirstLine = i === 0; - const isLastLine = i === lines.length - 1; - const isLastNonEmptyLine = i === lastNonEmptyLine; + const isFirstLine = i === 0 + const isLastLine = i === lines.length - 1 + const isLastNonEmptyLine = i === lastNonEmptyLine // replace rendered whitespace tabs with spaces - let trimmedLine = line.replace(/\t/g, ' '); + let trimmedLine = line.replace(/\t/g, ' ') // trim whitespace touching a newline if (!isFirstLine) { - trimmedLine = trimmedLine.replace(/^[ ]+/, ''); + trimmedLine = trimmedLine.replace(/^[ ]+/, '') } // trim whitespace touching an endline if (!isLastLine) { - trimmedLine = trimmedLine.replace(/[ ]+$/, ''); + trimmedLine = trimmedLine.replace(/[ ]+$/, '') } if (trimmedLine) { if (!isLastNonEmptyLine) { - trimmedLine += ' '; + trimmedLine += ' ' } - str += trimmedLine; + str += trimmedLine } } - return str; -}; + return str +} /** * Transform JSXExpressionContainer to Expression @@ -190,8 +190,8 @@ export const transformText = (text: string) => { * @returns Expression */ export const transformJSXExpressionContainer = ( - path: NodePath -): t.Expression => path.get('expression').node as t.Expression; + path: NodePath, +): t.Expression => path.get('expression').node as t.Expression /** * Transform JSXSpreadChild @@ -199,33 +199,33 @@ export const transformJSXExpressionContainer = ( * @returns SpreadElement */ export const transformJSXSpreadChild = ( - path: NodePath -): t.SpreadElement => t.spreadElement(path.get('expression').node); + path: NodePath, +): t.SpreadElement => t.spreadElement(path.get('expression').node) export const walksScope = ( path: NodePath, name: string, - slotFlag: SlotFlags + slotFlag: SlotFlags, ): void => { if (path.scope.hasBinding(name) && path.parentPath) { if (t.isJSXElement(path.parentPath.node)) { - path.parentPath.setData('slotFlag', slotFlag); + path.parentPath.setData('slotFlag', slotFlag) } - walksScope(path.parentPath, name, slotFlag); + walksScope(path.parentPath, name, slotFlag) } -}; +} export const buildIIFE = ( path: NodePath, - children: t.Expression[] + children: t.Expression[], ) => { - const { parentPath } = path; + const { parentPath } = path if (parentPath.isAssignmentExpression()) { - const { left } = parentPath.node as t.AssignmentExpression; + const { left } = parentPath.node as t.AssignmentExpression if (t.isIdentifier(left)) { return children.map((child) => { if (t.isIdentifier(child) && child.name === left.name) { - const insertName = path.scope.generateUidIdentifier(child.name); + const insertName = path.scope.generateUidIdentifier(child.name) parentPath.insertBefore( t.variableDeclaration('const', [ t.variableDeclarator( @@ -234,69 +234,69 @@ export const buildIIFE = ( t.functionExpression( null, [], - t.blockStatement([t.returnStatement(child)]) + t.blockStatement([t.returnStatement(child)]), ), - [] - ) + [], + ), ), - ]) - ); - return insertName; + ]), + ) + return insertName } - return child; - }); + return child + }) } } - return children; -}; + return children +} -const onRE = /^on[^a-z]/; +const onRE = /^on[^a-z]/ -export const isOn = (key: string) => onRE.test(key); +export const isOn = (key: string) => onRE.test(key) const mergeAsArray = ( existing: t.ObjectProperty, - incoming: t.ObjectProperty + incoming: t.ObjectProperty, ) => { if (t.isArrayExpression(existing.value)) { - existing.value.elements.push(incoming.value as t.Expression); + existing.value.elements.push(incoming.value as t.Expression) } else { existing.value = t.arrayExpression([ existing.value as t.Expression, incoming.value as t.Expression, - ]); + ]) } -}; +} export const dedupeProperties = ( properties: t.ObjectProperty[] = [], - mergeProps?: boolean + mergeProps?: boolean, ) => { if (!mergeProps) { - return properties; + return properties } - const knownProps = new Map(); - const deduped: t.ObjectProperty[] = []; + const knownProps = new Map() + const deduped: t.ObjectProperty[] = [] properties.forEach((prop) => { if (t.isStringLiteral(prop.key)) { - const { value: name } = prop.key; - const existing = knownProps.get(name); + const { value: name } = prop.key + const existing = knownProps.get(name) if (existing) { if (name === 'style' || name === 'class' || name.startsWith('on')) { - mergeAsArray(existing, prop); + mergeAsArray(existing, prop) } } else { - knownProps.set(name, prop); - deduped.push(prop); + knownProps.set(name, prop) + deduped.push(prop) } } else { // v-model target with variable - deduped.push(prop); + deduped.push(prop) } - }); + }) - return deduped; -}; + return deduped +} /** * Check if an attribute value is constant @@ -304,52 +304,52 @@ export const dedupeProperties = ( * @returns boolean */ export const isConstant = ( - node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null + node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null, ): boolean => { if (t.isIdentifier(node)) { - return node.name === 'undefined'; + return node.name === 'undefined' } if (t.isArrayExpression(node)) { - const { elements } = node; - return elements.every((element) => element && isConstant(element)); + const { elements } = node + return elements.every((element) => element && isConstant(element)) } if (t.isObjectExpression(node)) { return node.properties.every((property) => - isConstant((property as any).value) - ); + isConstant((property as any).value), + ) } if ( t.isTemplateLiteral(node) ? !node.expressions.length : t.isLiteral(node) ) { - return true; + return true } - return false; -}; + return false +} export const transformJSXSpreadAttribute = ( nodePath: NodePath, path: NodePath, mergeProps: boolean, - args: (t.ObjectProperty | t.Expression | t.SpreadElement)[] + args: (t.ObjectProperty | t.Expression | t.SpreadElement)[], ) => { const argument = path.get('argument') as NodePath< t.ObjectExpression | t.Identifier - >; + > const properties = t.isObjectExpression(argument.node) ? argument.node.properties - : undefined; + : undefined if (!properties) { if (argument.isIdentifier()) { walksScope( nodePath, (argument.node as t.Identifier).name, - SlotFlags.DYNAMIC - ); + SlotFlags.DYNAMIC, + ) } - args.push(mergeProps ? argument.node : t.spreadElement(argument.node)); + args.push(mergeProps ? argument.node : t.spreadElement(argument.node)) } else if (mergeProps) { - args.push(t.objectExpression(properties)); + args.push(t.objectExpression(properties)) } else { - args.push(...(properties as t.ObjectProperty[])); + args.push(...(properties as t.ObjectProperty[])) } -}; +} diff --git a/packages/babel-plugin-jsx/test/index.test.tsx b/packages/babel-plugin-jsx/test/index.test.tsx index efd4745..ef71e20 100644 --- a/packages/babel-plugin-jsx/test/index.test.tsx +++ b/packages/babel-plugin-jsx/test/index.test.tsx @@ -5,57 +5,57 @@ import { defineComponent, reactive, ref, -} from 'vue'; -import { type VueWrapper, mount, shallowMount } from '@vue/test-utils'; +} from 'vue' +import { type VueWrapper, mount, shallowMount } from '@vue/test-utils' const patchFlagExpect = ( wrapper: VueWrapper, flag: number, - dynamic: string[] | null + dynamic: string[] | null, ) => { - const { patchFlag, dynamicProps } = wrapper.vm.$.subTree as any; + const { patchFlag, dynamicProps } = wrapper.vm.$.subTree as any - expect(patchFlag).toBe(flag); - expect(dynamicProps).toEqual(dynamic); -}; + expect(patchFlag).toBe(flag) + expect(dynamicProps).toEqual(dynamic) +} describe('Transform JSX', () => { test('should render with render function', () => { const wrapper = shallowMount({ render() { - return
123
; + return
123
}, - }); - expect(wrapper.text()).toBe('123'); - }); + }) + expect(wrapper.text()).toBe('123') + }) test('should render with setup', () => { const wrapper = shallowMount({ setup() { - return () =>
123
; + return () =>
123
}, - }); - expect(wrapper.text()).toBe('123'); - }); + }) + expect(wrapper.text()).toBe('123') + }) test('Extracts attrs', () => { const wrapper = shallowMount({ setup() { - return () =>
; + return () =>
}, - }); - expect(wrapper.element.id).toBe('hi'); - }); + }) + expect(wrapper.element.id).toBe('hi') + }) test('Binds attrs', () => { - const id = 'foo'; + const id = 'foo' const wrapper = shallowMount({ setup() { - return () =>
{id}
; + return () =>
{id}
}, - }); - expect(wrapper.text()).toBe('foo'); - }); + }) + expect(wrapper.text()).toBe('foo') + }) test('should not fallthrough with inheritAttrs: false', () => { const Child = defineComponent({ @@ -63,25 +63,25 @@ describe('Transform JSX', () => { foo: Number, }, setup(props) { - return () =>
{props.foo}
; + return () =>
{props.foo}
}, - }); + }) - Child.inheritAttrs = false; + Child.inheritAttrs = false const wrapper = mount({ render() { - return ; + return }, - }); - expect(wrapper.classes()).toStrictEqual([]); - expect(wrapper.text()).toBe('1'); - }); + }) + expect(wrapper.classes()).toStrictEqual([]) + expect(wrapper.text()).toBe('1') + }) test('Fragment', () => { - const Child = () =>
123
; + const Child = () =>
123
- Child.inheritAttrs = false; + Child.inheritAttrs = false const wrapper = mount({ setup() { @@ -90,146 +90,146 @@ describe('Transform JSX', () => {
456
- ); + ) }, - }); + }) - expect(wrapper.html()).toBe('
123
\n
456
'); - }); + expect(wrapper.html()).toBe('
123
\n
456
') + }) test('nested component', () => { const A = { B: defineComponent({ setup() { - return () =>
123
; + return () =>
123
}, }), - }; + } - A.B.inheritAttrs = false; + A.B.inheritAttrs = false - const wrapper = mount(() => ); + const wrapper = mount(() => ) - expect(wrapper.html()).toBe('
123
'); - }); + expect(wrapper.html()).toBe('
123
') + }) test('xlink:href', () => { const wrapper = shallowMount({ setup() { - return () => ; + return () => }, - }); - expect(wrapper.attributes()['xlink:href']).toBe('#name'); - }); + }) + expect(wrapper.attributes()['xlink:href']).toBe('#name') + }) test('Merge class', () => { const wrapper = shallowMount({ setup() { // @ts-expect-error - return () =>
; + return () =>
}, - }); - expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort()); - }); + }) + expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort()) + }) test('Merge style', () => { const propsA = { style: { color: 'red', } as CSSProperties, - }; + } const propsB = { style: { color: 'blue', width: '300px', height: '300px', } as CSSProperties, - }; + } const wrapper = shallowMount({ setup() { // @ts-ignore - return () =>
; + return () =>
}, - }); + }) expect(wrapper.html()).toBe( - '
' - ); - }); + '
', + ) + }) test('JSXSpreadChild', () => { - const a = ['1', '2']; + const a = ['1', '2'] const wrapper = shallowMount({ setup() { - return () =>
{[...a]}
; + return () =>
{[...a]}
}, - }); - expect(wrapper.text()).toBe('12'); - }); + }) + expect(wrapper.text()).toBe('12') + }) test('domProps input[value]', () => { - const val = 'foo'; + const val = 'foo' const wrapper = shallowMount({ setup() { - return () => ; + return () => }, - }); - expect(wrapper.html()).toBe(''); - }); + }) + expect(wrapper.html()).toBe('') + }) test('domProps input[checked]', () => { - const val = true; + const val = true const wrapper = shallowMount({ setup() { - return () => ; + return () => }, - }); + }) - expect(wrapper.vm.$.subTree?.props?.checked).toBe(val); - }); + expect(wrapper.vm.$.subTree?.props?.checked).toBe(val) + }) test('domProps option[selected]', () => { - const val = true; + const val = true const wrapper = shallowMount({ render() { - return
- ); + ) }, - }); + }) - A.inheritAttrs = false; + A.inheritAttrs = false const wrapper = mount({ setup() { @@ -308,98 +308,98 @@ describe('slots', () => { val }}> default - ); + ) }, - }); + }) - expect(wrapper.html()).toBe('
defaultval
'); - }); + expect(wrapper.html()).toBe('
defaultval
') + }) test('without default', () => { const A = defineComponent({ setup(_, { slots }) { - return () =>
{slots.foo?.('foo')}
; + return () =>
{slots.foo?.('foo')}
}, - }); + }) - A.inheritAttrs = false; + A.inheritAttrs = false const wrapper = mount({ setup() { - return () => val }} />; + return () => val }} /> }, - }); + }) - expect(wrapper.html()).toBe('
foo
'); - }); -}); + expect(wrapper.html()).toBe('
foo
') + }) +}) describe('PatchFlags', () => { test('static', () => { const wrapper = shallowMount({ setup() { - return () =>
static
; + return () =>
static
}, - }); - patchFlagExpect(wrapper, 0, null); - }); + }) + patchFlagExpect(wrapper, 0, null) + }) test('props', async () => { const wrapper = mount({ setup() { - const visible = ref(true); + const visible = ref(true) const onClick = () => { - visible.value = false; - }; + visible.value = false + } return () => (
NEED_PATCH
- ); + ) }, - }); + }) - patchFlagExpect(wrapper, 8, ['onClick']); - await wrapper.trigger('click'); - expect(wrapper.html()).toBe('
NEED_PATCH
'); - }); + patchFlagExpect(wrapper, 8, ['onClick']) + await wrapper.trigger('click') + expect(wrapper.html()).toBe('
NEED_PATCH
') + }) test('#728: template literals with expressions should be treated as dynamic', async () => { const wrapper = mount({ setup() { - const foo = ref(0); + const foo = ref(0) return () => ( - ); + ) }, - }); - patchFlagExpect(wrapper, 8, ['value', 'onClick']); - await wrapper.trigger('click'); - expect(wrapper.html()).toBe(''); - }); + }) + patchFlagExpect(wrapper, 8, ['value', 'onClick']) + await wrapper.trigger('click') + expect(wrapper.html()).toBe('') + }) test('full props', async () => { const wrapper = mount({ setup() { - const bindProps = reactive({ class: 'a', style: { marginTop: 10 } }); + const bindProps = reactive({ class: 'a', style: { marginTop: 10 } }) const onClick = () => { - bindProps.class = 'b'; - }; + bindProps.class = 'b' + } return () => (
full props
- ); + ) }, - }); - patchFlagExpect(wrapper, 16, ['onClick']); + }) + patchFlagExpect(wrapper, 16, ['onClick']) - await wrapper.trigger('click'); + await wrapper.trigger('click') - expect(wrapper.classes().sort()).toEqual(['b', 'static'].sort()); - }); -}); + expect(wrapper.classes().sort()).toEqual(['b', 'static'].sort()) + }) +}) describe('variables outside slots', () => { const A = defineComponent({ @@ -407,11 +407,11 @@ describe('variables outside slots', () => { inc: Function, }, render() { - return this.$slots.default?.(); + return this.$slots.default?.() }, - }); + }) - A.inheritAttrs = false; + A.inheritAttrs = false test('internal', async () => { const wrapper = mount( @@ -419,17 +419,17 @@ describe('variables outside slots', () => { data() { return { val: 0, - }; + } }, methods: { inc() { - this.val += 1; + this.val += 1 }, }, render() { const attrs = { innerHTML: `${this.val}`, - }; + } return (
@@ -441,15 +441,15 @@ describe('variables outside slots', () => { +1 - ); + ) }, - }) - ); + }), + ) - expect(wrapper.get('#textarea').element.innerHTML).toBe('0'); - await wrapper.get('#button').trigger('click'); - expect(wrapper.get('#textarea').element.innerHTML).toBe('1'); - }); + expect(wrapper.get('#textarea').element.innerHTML).toBe('0') + await wrapper.get('#button').trigger('click') + expect(wrapper.get('#textarea').element.innerHTML).toBe('1') + }) test('forwarded', async () => { const wrapper = mount( @@ -457,18 +457,18 @@ describe('variables outside slots', () => { data() { return { val: 0, - }; + } }, methods: { inc() { - this.val += 1; + this.val += 1 }, }, render() { const attrs = { innerHTML: `${this.val}`, - }; - const textarea =