style: format

This commit is contained in:
Kevin Deng
2025-11-26 16:58:43 +08:00
parent 85d98b5b6f
commit 620450b5ba
28 changed files with 1297 additions and 1302 deletions

View File

@@ -1,4 +1,4 @@
{ {
"singleQuote": true, "semi": false,
"trailingComma": "es5" "singleQuote": true
} }

View File

@@ -1,9 +1,9 @@
// @ts-check // @ts-check
import { builtinModules } from 'node:module'; import { builtinModules } from 'node:module'
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint'
import importX from 'eslint-plugin-import-x'; import importX from 'eslint-plugin-import-x'
import eslint from '@eslint/js'; import eslint from '@eslint/js'
import eslintConfigPrettier from 'eslint-config-prettier'; import eslintConfigPrettier from 'eslint-config-prettier'
export default tseslint.config( export default tseslint.config(
eslint.configs.recommended, eslint.configs.recommended,
@@ -63,5 +63,5 @@ export default tseslint.config(
eslintConfigPrettier, eslintConfigPrettier,
{ {
ignores: ['**/dist/', '**/coverage/'], ignores: ['**/dist/', '**/coverage/'],
} },
); )

View File

@@ -1,5 +1,5 @@
declare function transformOn( declare function transformOn(
obj: Record<string, any> obj: Record<string, any>,
): Record<`on${string}`, any>; ): Record<`on${string}`, any>
export { transformOn as default, transformOn as 'module.exports' }; export { transformOn as default, transformOn as 'module.exports' }

View File

@@ -1,9 +1,9 @@
function transformOn(obj) { function transformOn(obj) {
const result = {}; const result = {}
Object.keys(obj).forEach((evt) => { Object.keys(obj).forEach((evt) => {
result[`on${evt[0].toUpperCase()}${evt.slice(1)}`] = obj[evt]; result[`on${evt[0].toUpperCase()}${evt.slice(1)}`] = obj[evt]
}); })
return result; return result
} }
export { transformOn as default, transformOn as 'module.exports' }; export { transformOn as default, transformOn as 'module.exports' }

View File

@@ -88,7 +88,7 @@ Default: `false`
函数式组件 函数式组件
```jsx ```jsx
const App = () => <div></div>; const App = () => <div></div>
``` ```
在 render 中使用 在 render 中使用
@@ -96,27 +96,25 @@ const App = () => <div></div>;
```jsx ```jsx
const App = { const App = {
render() { render() {
return <div>Vue 3.0</div>; return <div>Vue 3.0</div>
}, },
}; }
``` ```
```jsx ```jsx
import { withModifiers, defineComponent } from 'vue'; import { withModifiers, defineComponent } from 'vue'
const App = defineComponent({ const App = defineComponent({
setup() { setup() {
const count = ref(0); const count = ref(0)
const inc = () => { const inc = () => {
count.value++; count.value++
}; }
return () => ( return () => <div onClick={withModifiers(inc, ['self'])}>{count.value}</div>
<div onClick={withModifiers(inc, ['self'])}>{count.value}</div>
);
}, },
}); })
``` ```
Fragment Fragment
@@ -127,20 +125,20 @@ const App = () => (
<span>I'm</span> <span>I'm</span>
<span>Fragment</span> <span>Fragment</span>
</> </>
); )
``` ```
### Attributes / Props ### Attributes / Props
```jsx ```jsx
const App = () => <input type="email" />; const App = () => <input type="email" />
``` ```
动态绑定: 动态绑定:
```jsx ```jsx
const placeholderText = 'email'; const placeholderText = 'email'
const App = () => <input type="email" placeholder={placeholderText} />; const App = () => <input type="email" placeholder={placeholderText} />
``` ```
### 指令 ### 指令
@@ -150,12 +148,12 @@ const App = () => <input type="email" placeholder={placeholderText} />;
```jsx ```jsx
const App = { const App = {
data() { data() {
return { visible: true }; return { visible: true }
}, },
render() { render() {
return <input v-show={this.visible} />; return <input v-show={this.visible} />
}, },
}; }
``` ```
#### v-model #### v-model
@@ -191,7 +189,7 @@ h(A, {
modifier: true, modifier: true,
}, },
'onUpdate:argument': ($event) => (val = $event), 'onUpdate:argument': ($event) => (val = $event),
}); })
``` ```
#### v-models (从 1.1.0 开始不推荐使用) #### v-models (从 1.1.0 开始不推荐使用)
@@ -234,7 +232,7 @@ h(A, {
modifier: true, modifier: true,
}, },
'onUpdate:bar': ($event) => (bar = $event), 'onUpdate:bar': ($event) => (bar = $event),
}); })
``` ```
#### 自定义指令 #### 自定义指令
@@ -245,18 +243,18 @@ h(A, {
const App = { const App = {
directives: { custom: customDirective }, directives: { custom: customDirective },
setup() { setup() {
return () => <a v-custom:arg={val} />; return () => <a v-custom:arg={val} />
}, },
}; }
``` ```
```jsx ```jsx
const App = { const App = {
directives: { custom: customDirective }, directives: { custom: customDirective },
setup() { setup() {
return () => <a v-custom={[val, 'arg', ['a', 'b']]} />; return () => <a v-custom={[val, 'arg', ['a', 'b']]} />
}, },
}; }
``` ```
### 插槽 ### 插槽
@@ -269,20 +267,20 @@ const A = (props, { slots }) => (
<h1>{slots.default ? slots.default() : 'foo'}</h1> <h1>{slots.default ? slots.default() : 'foo'}</h1>
<h2>{slots.bar?.()}</h2> <h2>{slots.bar?.()}</h2>
</> </>
); )
const App = { const App = {
setup() { setup() {
const slots = { const slots = {
bar: () => <span>B</span>, bar: () => <span>B</span>,
}; }
return () => ( return () => (
<A v-slots={slots}> <A v-slots={slots}>
<div>A</div> <div>A</div>
</A> </A>
); )
}, },
}; }
// or // or
@@ -291,10 +289,10 @@ const App = {
const slots = { const slots = {
default: () => <div>A</div>, default: () => <div>A</div>,
bar: () => <span>B</span>, bar: () => <span>B</span>,
}; }
return () => <A v-slots={slots} />; return () => <A v-slots={slots} />
}, },
}; }
// 或者,当 `enableObjectSlots` 不是 `false` 时,您可以使用对象插槽 // 或者,当 `enableObjectSlots` 不是 `false` 时,您可以使用对象插槽
const App = { const App = {
@@ -309,9 +307,9 @@ const App = {
</A> </A>
<B>{() => 'foo'}</B> <B>{() => 'foo'}</B>
</> </>
); )
}, },
}; }
``` ```
### 在 TypeScript 中使用 ### 在 TypeScript 中使用

View File

@@ -92,7 +92,7 @@ Default: `false`
functional component functional component
```jsx ```jsx
const App = () => <div>Vue 3.0</div>; const App = () => <div>Vue 3.0</div>
``` ```
with render with render
@@ -100,27 +100,25 @@ with render
```jsx ```jsx
const App = { const App = {
render() { render() {
return <div>Vue 3.0</div>; return <div>Vue 3.0</div>
}, },
}; }
``` ```
```jsx ```jsx
import { withModifiers, defineComponent } from 'vue'; import { withModifiers, defineComponent } from 'vue'
const App = defineComponent({ const App = defineComponent({
setup() { setup() {
const count = ref(0); const count = ref(0)
const inc = () => { const inc = () => {
count.value++; count.value++
}; }
return () => ( return () => <div onClick={withModifiers(inc, ['self'])}>{count.value}</div>
<div onClick={withModifiers(inc, ['self'])}>{count.value}</div>
);
}, },
}); })
``` ```
Fragment Fragment
@@ -131,20 +129,20 @@ const App = () => (
<span>I'm</span> <span>I'm</span>
<span>Fragment</span> <span>Fragment</span>
</> </>
); )
``` ```
### Attributes / Props ### Attributes / Props
```jsx ```jsx
const App = () => <input type="email" />; const App = () => <input type="email" />
``` ```
with a dynamic binding: with a dynamic binding:
```jsx ```jsx
const placeholderText = 'email'; const placeholderText = 'email'
const App = () => <input type="email" placeholder={placeholderText} />; const App = () => <input type="email" placeholder={placeholderText} />
``` ```
### Directives ### Directives
@@ -154,12 +152,12 @@ const App = () => <input type="email" placeholder={placeholderText} />;
```jsx ```jsx
const App = { const App = {
data() { data() {
return { visible: true }; return { visible: true }
}, },
render() { render() {
return <input v-show={this.visible} />; return <input v-show={this.visible} />
}, },
}; }
``` ```
#### v-model #### v-model
@@ -195,7 +193,7 @@ h(A, {
modifier: true, modifier: true,
}, },
'onUpdate:argument': ($event) => (val = $event), 'onUpdate:argument': ($event) => (val = $event),
}); })
``` ```
#### v-models (Not recommended since v1.1.0) #### v-models (Not recommended since v1.1.0)
@@ -238,7 +236,7 @@ h(A, {
modifier: true, modifier: true,
}, },
'onUpdate:bar': ($event) => (bar = $event), 'onUpdate:bar': ($event) => (bar = $event),
}); })
``` ```
#### custom directive #### custom directive
@@ -249,18 +247,18 @@ Recommended when using string arguments
const App = { const App = {
directives: { custom: customDirective }, directives: { custom: customDirective },
setup() { setup() {
return () => <a v-custom:arg={val} />; return () => <a v-custom:arg={val} />
}, },
}; }
``` ```
```jsx ```jsx
const App = { const App = {
directives: { custom: customDirective }, directives: { custom: customDirective },
setup() { setup() {
return () => <a v-custom={[val, 'arg', ['a', 'b']]} />; return () => <a v-custom={[val, 'arg', ['a', 'b']]} />
}, },
}; }
``` ```
### Slot ### Slot
@@ -273,20 +271,20 @@ const A = (props, { slots }) => (
<h1>{slots.default ? slots.default() : 'foo'}</h1> <h1>{slots.default ? slots.default() : 'foo'}</h1>
<h2>{slots.bar?.()}</h2> <h2>{slots.bar?.()}</h2>
</> </>
); )
const App = { const App = {
setup() { setup() {
const slots = { const slots = {
bar: () => <span>B</span>, bar: () => <span>B</span>,
}; }
return () => ( return () => (
<A v-slots={slots}> <A v-slots={slots}>
<div>A</div> <div>A</div>
</A> </A>
); )
}, },
}; }
// or // or
@@ -295,10 +293,10 @@ const App = {
const slots = { const slots = {
default: () => <div>A</div>, default: () => <div>A</div>,
bar: () => <span>B</span>, bar: () => <span>B</span>,
}; }
return () => <A v-slots={slots} />; return () => <A v-slots={slots} />
}, },
}; }
// or you can use object slots when `enableObjectSlots` is not false. // or you can use object slots when `enableObjectSlots` is not false.
const App = { const App = {
@@ -313,9 +311,9 @@ const App = {
</A> </A>
<B>{() => 'foo'}</B> <B>{() => 'foo'}</B>
</> </>
); )
}, },
}; }
``` ```
### In TypeScript ### In TypeScript

View File

@@ -1,58 +1,58 @@
import t from '@babel/types'; import t from '@babel/types'
import type * as BabelCore from '@babel/core'; import type * as BabelCore from '@babel/core'
import _template from '@babel/template'; import _template from '@babel/template'
// @ts-expect-error // @ts-expect-error
import _syntaxJsx from '@babel/plugin-syntax-jsx'; import _syntaxJsx from '@babel/plugin-syntax-jsx'
import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'; import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'
import { type NodePath, type Visitor } from '@babel/traverse'; import { type NodePath, type Visitor } from '@babel/traverse'
import ResolveType from '@vue/babel-plugin-resolve-type'; import ResolveType from '@vue/babel-plugin-resolve-type'
import { declare } from '@babel/helper-plugin-utils'; import { declare } from '@babel/helper-plugin-utils'
import transformVueJSX from './transform-vue-jsx'; import transformVueJSX from './transform-vue-jsx'
import sugarFragment from './sugar-fragment'; import sugarFragment from './sugar-fragment'
import type { State, VueJSXPluginOptions } from './interface'; import type { State, VueJSXPluginOptions } from './interface'
export { VueJSXPluginOptions }; export { VueJSXPluginOptions }
const hasJSX = (parentPath: NodePath<t.Program>) => { const hasJSX = (parentPath: NodePath<t.Program>) => {
let fileHasJSX = false; let fileHasJSX = false
parentPath.traverse({ parentPath.traverse({
JSXElement(path) { JSXElement(path) {
// skip ts error // skip ts error
fileHasJSX = true; fileHasJSX = true
path.stop(); path.stop()
}, },
JSXFragment(path) { JSXFragment(path) {
fileHasJSX = true; fileHasJSX = true
path.stop(); 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__ */ /* #__NO_SIDE_EFFECTS__ */
function interopDefault(m: any) { function interopDefault(m: any) {
return m.default || m; return m.default || m
} }
const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx); const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx)
const template = /*#__PURE__*/ interopDefault(_template); const template = /*#__PURE__*/ interopDefault(_template)
const plugin: ( const plugin: (
api: object, api: object,
options: VueJSXPluginOptions | null | undefined, options: VueJSXPluginOptions | null | undefined,
dirname: string dirname: string,
) => BabelCore.PluginObj<State> = declare< ) => BabelCore.PluginObj<State> = declare<
VueJSXPluginOptions, VueJSXPluginOptions,
BabelCore.PluginObj<State> BabelCore.PluginObj<State>
>((api, opt, dirname) => { >((api, opt, dirname) => {
const { types } = api; const { types } = api
let resolveType: BabelCore.PluginObj<BabelCore.PluginPass> | undefined; let resolveType: BabelCore.PluginObj<BabelCore.PluginPass> | undefined
if (opt.resolveType) { if (opt.resolveType) {
if (typeof opt.resolveType === 'boolean') opt.resolveType = {}; if (typeof opt.resolveType === 'boolean') opt.resolveType = {}
resolveType = ResolveType(api, opt.resolveType, dirname); resolveType = ResolveType(api, opt.resolveType, dirname)
} }
return { return {
...(resolveType || {}), ...(resolveType || {}),
@@ -81,117 +81,117 @@ const plugin: (
'mergeProps', 'mergeProps',
'createTextVNode', 'createTextVNode',
'isVNode', 'isVNode',
]; ]
if (isModule(path)) { if (isModule(path)) {
// import { createVNode } from "vue"; // import { createVNode } from "vue";
const importMap: Record< const importMap: Record<
string, string,
t.MemberExpression | t.Identifier t.MemberExpression | t.Identifier
> = {}; > = {}
importNames.forEach((name) => { importNames.forEach((name) => {
state.set(name, () => { state.set(name, () => {
if (importMap[name]) { if (importMap[name]) {
return types.cloneNode(importMap[name]); return types.cloneNode(importMap[name])
} }
const identifier = addNamed(path, name, 'vue', { const identifier = addNamed(path, name, 'vue', {
ensureLiveReference: true, ensureLiveReference: true,
}); })
importMap[name] = identifier; importMap[name] = identifier
return identifier; return identifier
}); })
}); })
const { enableObjectSlots = true } = state.opts; const { enableObjectSlots = true } = state.opts
if (enableObjectSlots) { if (enableObjectSlots) {
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
if (importMap.runtimeIsSlot) { if (importMap.runtimeIsSlot) {
return importMap.runtimeIsSlot; return importMap.runtimeIsSlot
} }
const { name: isVNodeName } = state.get( const { name: isVNodeName } = state.get(
'isVNode' 'isVNode',
)() as t.Identifier; )() as t.Identifier
const isSlot = path.scope.generateUidIdentifier('isSlot'); const isSlot = path.scope.generateUidIdentifier('isSlot')
const ast = template.ast` const ast = template.ast`
function ${isSlot.name}(s) { function ${isSlot.name}(s) {
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s)); return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s));
} }
`; `
const lastImport = (path.get('body') as NodePath[]) const lastImport = (path.get('body') as NodePath[])
.filter((p) => p.isImportDeclaration()) .filter((p) => p.isImportDeclaration())
.pop(); .pop()
if (lastImport) { if (lastImport) {
lastImport.insertAfter(ast); lastImport.insertAfter(ast)
} }
importMap.runtimeIsSlot = isSlot; importMap.runtimeIsSlot = isSlot
return isSlot; return isSlot
}); })
} }
} else { } else {
// var _vue = require('vue'); // var _vue = require('vue');
let sourceName: t.Identifier; let sourceName: t.Identifier
importNames.forEach((name) => { importNames.forEach((name) => {
state.set(name, () => { state.set(name, () => {
if (!sourceName) { if (!sourceName) {
sourceName = addNamespace(path, 'vue', { sourceName = addNamespace(path, 'vue', {
ensureLiveReference: true, ensureLiveReference: true,
}); })
} }
return t.memberExpression(sourceName, t.identifier(name)); return t.memberExpression(sourceName, t.identifier(name))
}); })
}); })
const helpers: Record<string, t.Identifier> = {}; const helpers: Record<string, t.Identifier> = {}
const { enableObjectSlots = true } = state.opts; const { enableObjectSlots = true } = state.opts
if (enableObjectSlots) { if (enableObjectSlots) {
state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => { state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
if (helpers.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( const { object: objectName } = state.get(
'isVNode' 'isVNode',
)() as t.MemberExpression; )() as t.MemberExpression
const ast = template.ast` const ast = template.ast`
function ${isSlot.name}(s) { function ${isSlot.name}(s) {
return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${ return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${
(objectName as t.Identifier).name (objectName as t.Identifier).name
}.isVNode(s)); }.isVNode(s));
} }
`; `
const nodePaths = path.get('body') as NodePath[]; const nodePaths = path.get('body') as NodePath[]
const lastImport = nodePaths const lastImport = nodePaths
.filter( .filter(
(p) => (p) =>
p.isVariableDeclaration() && p.isVariableDeclaration() &&
p.node.declarations.some( p.node.declarations.some(
(d) => (d) =>
(d.id as t.Identifier)?.name === sourceName.name (d.id as t.Identifier)?.name === sourceName.name,
) ),
) )
.pop(); .pop()
if (lastImport) { if (lastImport) {
lastImport.insertAfter(ast); lastImport.insertAfter(ast)
} }
return isSlot; return isSlot
}); })
} }
} }
const { const {
opts: { pragma = '' }, opts: { pragma = '' },
file, file,
} = state; } = state
if (pragma) { if (pragma) {
state.set('createVNode', () => t.identifier(pragma)); state.set('createVNode', () => t.identifier(pragma))
} }
if (file.ast.comments) { if (file.ast.comments) {
for (const comment of 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) { 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 default plugin
export { plugin as 'module.exports' }; export { plugin as 'module.exports' }

View File

@@ -1,32 +1,32 @@
import type t from '@babel/types'; import type t from '@babel/types'
import type * as BabelCore from '@babel/core'; import type * as BabelCore from '@babel/core'
import { type Options } from '@vue/babel-plugin-resolve-type'; 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 = { export type State = {
get: (name: string) => any; get: (name: string) => any
set: (name: string, value: any) => any; set: (name: string, value: any) => any
opts: VueJSXPluginOptions; opts: VueJSXPluginOptions
file: BabelCore.BabelFile; file: BabelCore.BabelFile
}; }
export interface VueJSXPluginOptions { export interface VueJSXPluginOptions {
/** transform `on: { click: xx }` to `onClick: xxx` */ /** transform `on: { click: xx }` to `onClick: xxx` */
transformOn?: boolean; transformOn?: boolean
/** enable optimization or not. */ /** enable optimization or not. */
optimize?: boolean; optimize?: boolean
/** merge static and dynamic class / style attributes / onXXX handlers */ /** merge static and dynamic class / style attributes / onXXX handlers */
mergeProps?: boolean; mergeProps?: boolean
/** configuring custom elements */ /** configuring custom elements */
isCustomElement?: (tag: string) => boolean; isCustomElement?: (tag: string) => boolean
/** enable object slots syntax */ /** enable object slots syntax */
enableObjectSlots?: boolean; enableObjectSlots?: boolean
/** Replace the function used when compiling JSX expressions */ /** Replace the function used when compiling JSX expressions */
pragma?: string; pragma?: string
/** /**
* (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`) * (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`)
* @default false * @default false
*/ */
resolveType?: Options | boolean; resolveType?: Options | boolean
} }

View File

@@ -1,13 +1,13 @@
import t from '@babel/types'; import t from '@babel/types'
import { type NodePath } from '@babel/traverse'; import { type NodePath } from '@babel/traverse'
import { createIdentifier } from './utils'; import { createIdentifier } from './utils'
import type { State } from './interface'; import type { State } from './interface'
export type Tag = export type Tag =
| t.Identifier | t.Identifier
| t.MemberExpression | t.MemberExpression
| t.StringLiteral | t.StringLiteral
| t.CallExpression; | t.CallExpression
/** /**
* Get JSX element type * Get JSX element type
@@ -17,111 +17,111 @@ export type Tag =
const getType = (path: NodePath<t.JSXOpeningElement>) => { const getType = (path: NodePath<t.JSXOpeningElement>) => {
const typePath = path.get('attributes').find((attribute) => { const typePath = path.get('attributes').find((attribute) => {
if (!attribute.isJSXAttribute()) { if (!attribute.isJSXAttribute()) {
return false; return false
} }
return ( return (
attribute.get('name').isJSXIdentifier() && attribute.get('name').isJSXIdentifier() &&
(attribute.get('name') as NodePath<t.JSXIdentifier>).node.name === 'type' (attribute.get('name') as NodePath<t.JSXIdentifier>).node.name === 'type'
); )
}) as NodePath<t.JSXAttribute> | undefined; }) as NodePath<t.JSXAttribute> | undefined
return typePath ? typePath.get('value').node : null; return typePath ? typePath.get('value').node : null
}; }
const parseModifiers = (value: any): string[] => const parseModifiers = (value: any): string[] =>
t.isArrayExpression(value) t.isArrayExpression(value)
? value.elements ? value.elements
.map((el) => (t.isStringLiteral(el) ? el.value : '')) .map((el) => (t.isStringLiteral(el) ? el.value : ''))
.filter(Boolean) .filter(Boolean)
: []; : []
const parseDirectives = (params: { const parseDirectives = (params: {
name: string; name: string
path: NodePath<t.JSXAttribute>; path: NodePath<t.JSXAttribute>
value: t.Expression | null; value: t.Expression | null
state: State; state: State
tag: Tag; tag: Tag
isComponent: boolean; isComponent: boolean
}) => { }) => {
const { path, value, state, tag, isComponent } = params; const { path, value, state, tag, isComponent } = params
const args: Array<t.Expression | t.NullLiteral> = []; const args: Array<t.Expression | t.NullLiteral> = []
const vals: t.Expression[] = []; const vals: t.Expression[] = []
const modifiersSet: Set<string>[] = []; const modifiersSet: Set<string>[] = []
let directiveName; let directiveName
let directiveArgument; let directiveArgument
let directiveModifiers; let directiveModifiers
if ('namespace' in path.node.name) { if ('namespace' in path.node.name) {
[directiveName, directiveArgument] = params.name.split(':'); ;[directiveName, directiveArgument] = params.name.split(':')
directiveName = path.node.name.namespace.name; directiveName = path.node.name.namespace.name
directiveArgument = path.node.name.name.name; directiveArgument = path.node.name.name.name
directiveModifiers = directiveArgument.split('_').slice(1); directiveModifiers = directiveArgument.split('_').slice(1)
} else { } else {
const underscoreModifiers = params.name.split('_'); const underscoreModifiers = params.name.split('_')
directiveName = underscoreModifiers.shift() || ''; directiveName = underscoreModifiers.shift() || ''
directiveModifiers = underscoreModifiers; directiveModifiers = underscoreModifiers
} }
directiveName = directiveName directiveName = directiveName
.replace(/^v/, '') .replace(/^v/, '')
.replace(/^-/, '') .replace(/^-/, '')
.replace(/^\S/, (s: string) => s.toLowerCase()); .replace(/^\S/, (s: string) => s.toLowerCase())
if (directiveArgument) { if (directiveArgument) {
args.push(t.stringLiteral(directiveArgument.split('_')[0])); args.push(t.stringLiteral(directiveArgument.split('_')[0]))
} }
const isVModels = directiveName === 'models'; const isVModels = directiveName === 'models'
const isVModel = directiveName === 'model'; const isVModel = directiveName === 'model'
if (isVModel && !path.get('value').isJSXExpressionContainer()) { 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) { 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 = const shouldResolve =
!['html', 'text', 'model', 'slots', 'models'].includes(directiveName) || !['html', 'text', 'model', 'slots', 'models'].includes(directiveName) ||
(isVModel && !isComponent); (isVModel && !isComponent)
let modifiers = directiveModifiers; let modifiers = directiveModifiers
if (t.isArrayExpression(value)) { if (t.isArrayExpression(value)) {
const elementsList = isVModels ? value.elements! : [value]; const elementsList = isVModels ? value.elements! : [value]
elementsList.forEach((element) => { elementsList.forEach((element) => {
if (isVModels && !t.isArrayExpression(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 { elements } = element as t.ArrayExpression
const [first, second, third] = elements; const [first, second, third] = elements
if ( if (
second && second &&
!t.isArrayExpression(second) && !t.isArrayExpression(second) &&
!t.isSpreadElement(second) !t.isSpreadElement(second)
) { ) {
args.push(second); args.push(second)
modifiers = parseModifiers(third as t.ArrayExpression); modifiers = parseModifiers(third as t.ArrayExpression)
} else if (t.isArrayExpression(second)) { } else if (t.isArrayExpression(second)) {
if (!shouldResolve) { if (!shouldResolve) {
args.push(t.nullLiteral()); args.push(t.nullLiteral())
} }
modifiers = parseModifiers(second); modifiers = parseModifiers(second)
} else if (!shouldResolve) { } else if (!shouldResolve) {
// work as v-model={[value]} or v-models={[[value]]} // work as v-model={[value]} or v-models={[[value]]}
args.push(t.nullLiteral()); args.push(t.nullLiteral())
} }
modifiersSet.push(new Set(modifiers)); modifiersSet.push(new Set(modifiers))
vals.push(first as t.Expression); vals.push(first as t.Expression)
}); })
} else if (isVModel && !shouldResolve) { } else if (isVModel && !shouldResolve) {
// work as v-model={value} // work as v-model={value}
args.push(t.nullLiteral()); args.push(t.nullLiteral())
modifiersSet.push(new Set(directiveModifiers)); modifiersSet.push(new Set(directiveModifiers))
} else { } else {
modifiersSet.push(new Set(directiveModifiers)); modifiersSet.push(new Set(directiveModifiers))
} }
return { return {
@@ -139,59 +139,62 @@ const parseDirectives = (params: {
!!modifiersSet[0]?.size && !!modifiersSet[0]?.size &&
t.objectExpression( t.objectExpression(
[...modifiersSet[0]].map((modifier) => [...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[]) ].filter(Boolean) as t.Expression[])
: undefined, : undefined,
}; }
}; }
const resolveDirective = ( const resolveDirective = (
path: NodePath<t.JSXAttribute>, path: NodePath<t.JSXAttribute>,
state: State, state: State,
tag: Tag, tag: Tag,
directiveName: string directiveName: string,
) => { ) => {
if (directiveName === 'show') { if (directiveName === 'show') {
return createIdentifier(state, 'vShow'); return createIdentifier(state, 'vShow')
} }
if (directiveName === 'model') { if (directiveName === 'model') {
let modelToUse; let modelToUse
const type = getType(path.parentPath as NodePath<t.JSXOpeningElement>); const type = getType(path.parentPath as NodePath<t.JSXOpeningElement>)
switch ((tag as t.StringLiteral).value) { switch ((tag as t.StringLiteral).value) {
case 'select': case 'select':
modelToUse = createIdentifier(state, 'vModelSelect'); modelToUse = createIdentifier(state, 'vModelSelect')
break; break
case 'textarea': case 'textarea':
modelToUse = createIdentifier(state, 'vModelText'); modelToUse = createIdentifier(state, 'vModelText')
break; break
default: default:
if (t.isStringLiteral(type) || !type) { if (t.isStringLiteral(type) || !type) {
switch ((type as t.StringLiteral)?.value) { switch ((type as t.StringLiteral)?.value) {
case 'checkbox': case 'checkbox':
modelToUse = createIdentifier(state, 'vModelCheckbox'); modelToUse = createIdentifier(state, 'vModelCheckbox')
break; break
case 'radio': case 'radio':
modelToUse = createIdentifier(state, 'vModelRadio'); modelToUse = createIdentifier(state, 'vModelRadio')
break; break
default: default:
modelToUse = createIdentifier(state, 'vModelText'); modelToUse = createIdentifier(state, 'vModelText')
} }
} else { } else {
modelToUse = createIdentifier(state, 'vModelDynamic'); modelToUse = createIdentifier(state, 'vModelDynamic')
} }
} }
return modelToUse; return modelToUse
} }
const referenceName = const referenceName =
'v' + directiveName[0].toUpperCase() + directiveName.slice(1); 'v' + directiveName[0].toUpperCase() + directiveName.slice(1)
if (path.scope.references[referenceName]) { if (path.scope.references[referenceName]) {
return t.identifier(referenceName); return t.identifier(referenceName)
} }
return t.callExpression(createIdentifier(state, 'resolveDirective'), [ return t.callExpression(createIdentifier(state, 'resolveDirective'), [
t.stringLiteral(directiveName), t.stringLiteral(directiveName),
]); ])
}; }
export default parseDirectives; export default parseDirectives

View File

@@ -31,4 +31,4 @@ export const PatchFlagNames = {
[PatchFlags.NEED_PATCH]: 'NEED_PATCH', [PatchFlags.NEED_PATCH]: 'NEED_PATCH',
[PatchFlags.HOISTED]: 'HOISTED', [PatchFlags.HOISTED]: 'HOISTED',
[PatchFlags.BAIL]: 'BAIL', [PatchFlags.BAIL]: 'BAIL',
}; }

View File

@@ -21,4 +21,4 @@ const enum SlotFlags {
FORWARDED = 3, FORWARDED = 3,
} }
export default SlotFlags; export default SlotFlags

View File

@@ -1,25 +1,25 @@
import t from '@babel/types'; import t from '@babel/types'
import { type NodePath, type Visitor } from '@babel/traverse'; import { type NodePath, type Visitor } from '@babel/traverse'
import type { State } from './interface'; import type { State } from './interface'
import { FRAGMENT, createIdentifier } from './utils'; import { FRAGMENT, createIdentifier } from './utils'
const transformFragment = ( const transformFragment = (
path: NodePath<t.JSXFragment>, path: NodePath<t.JSXFragment>,
Fragment: t.JSXIdentifier | t.JSXMemberExpression Fragment: t.JSXIdentifier | t.JSXMemberExpression,
) => { ) => {
const children = path.get('children') || []; const children = path.get('children') || []
return t.jsxElement( return t.jsxElement(
t.jsxOpeningElement(Fragment, []), t.jsxOpeningElement(Fragment, []),
t.jsxClosingElement(Fragment), t.jsxClosingElement(Fragment),
children.map(({ node }) => node), children.map(({ node }) => node),
false false,
); )
}; }
const visitor: Visitor<State> = { const visitor: Visitor<State> = {
JSXFragment: { JSXFragment: {
enter(path, state) { enter(path, state) {
const fragmentCallee = createIdentifier(state, FRAGMENT); const fragmentCallee = createIdentifier(state, FRAGMENT)
path.replaceWith( path.replaceWith(
transformFragment( transformFragment(
path, path,
@@ -27,12 +27,12 @@ const visitor: Visitor<State> = {
? t.jsxIdentifier(fragmentCallee.name) ? t.jsxIdentifier(fragmentCallee.name)
: t.jsxMemberExpression( : t.jsxMemberExpression(
t.jsxIdentifier((fragmentCallee.object as t.Identifier).name), 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

View File

@@ -1,6 +1,6 @@
import t from '@babel/types'; import t from '@babel/types'
import { type NodePath, type Visitor } from '@babel/traverse'; import { type NodePath, type Visitor } from '@babel/traverse'
import { addDefault } from '@babel/helper-module-imports'; import { addDefault } from '@babel/helper-module-imports'
import { import {
buildIIFE, buildIIFE,
checkIsComponent, checkIsComponent,
@@ -17,43 +17,43 @@ import {
transformJSXText, transformJSXText,
transformText, transformText,
walksScope, walksScope,
} from './utils'; } from './utils'
import SlotFlags from './slotFlags'; import SlotFlags from './slotFlags'
import { PatchFlags } from './patchFlags'; import { PatchFlags } from './patchFlags'
import parseDirectives from './parseDirectives'; import parseDirectives from './parseDirectives'
import type { Slots, State } from './interface'; import type { Slots, State } from './interface'
const xlinkRE = /^xlink([A-Z])/; const xlinkRE = /^xlink([A-Z])/
type ExcludesBoolean = <T>(x: T | false | true) => x is T; type ExcludesBoolean = <T>(x: T | false | true) => x is T
const getJSXAttributeValue = ( const getJSXAttributeValue = (
path: NodePath<t.JSXAttribute>, path: NodePath<t.JSXAttribute>,
state: State state: State,
): t.StringLiteral | t.Expression | null => { ): t.StringLiteral | t.Expression | null => {
const valuePath = path.get('value'); const valuePath = path.get('value')
if (valuePath.isJSXElement()) { if (valuePath.isJSXElement()) {
return transformJSXElement(valuePath, state); return transformJSXElement(valuePath, state)
} }
if (valuePath.isStringLiteral()) { if (valuePath.isStringLiteral()) {
return t.stringLiteral(transformText(valuePath.node.value)); return t.stringLiteral(transformText(valuePath.node.value))
} }
if (valuePath.isJSXExpressionContainer()) { if (valuePath.isJSXExpressionContainer()) {
return transformJSXExpressionContainer(valuePath); return transformJSXExpressionContainer(valuePath)
} }
return null; return null
}; }
const buildProps = (path: NodePath<t.JSXElement>, state: State) => { const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
const tag = getTag(path, state); const tag = getTag(path, state)
const isComponent = checkIsComponent(path.get('openingElement'), state); const isComponent = checkIsComponent(path.get('openingElement'), state)
const props = path.get('openingElement').get('attributes'); const props = path.get('openingElement').get('attributes')
const directives: t.ArrayExpression[] = []; const directives: t.ArrayExpression[] = []
const dynamicPropNames = new Set<string>(); const dynamicPropNames = new Set<string>()
let slots: Slots = null; let slots: Slots = null
let patchFlag = 0; let patchFlag = 0
if (props.length === 0) { if (props.length === 0) {
return { return {
@@ -64,26 +64,25 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
directives, directives,
patchFlag, patchFlag,
dynamicPropNames, dynamicPropNames,
}; }
} }
let properties: t.ObjectProperty[] = []; let properties: t.ObjectProperty[] = []
// patchFlag analysis // patchFlag analysis
let hasRef = false; let hasRef = false
let hasClassBinding = false; let hasClassBinding = false
let hasStyleBinding = false; let hasStyleBinding = false
let hasHydrationEventBinding = false; let hasHydrationEventBinding = false
let hasDynamicKeys = false; let hasDynamicKeys = false
const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = const mergeArgs: (t.CallExpression | t.ObjectExpression | t.Identifier)[] = []
[]; const { mergeProps = true } = state.opts
const { mergeProps = true } = state.opts;
props.forEach((prop) => { props.forEach((prop) => {
if (prop.isJSXAttribute()) { 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 (!isConstant(attributeValue) || name === 'ref') {
if ( if (
@@ -95,17 +94,17 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
// omit v-model handlers // omit v-model handlers
name !== 'onUpdate:modelValue' name !== 'onUpdate:modelValue'
) { ) {
hasHydrationEventBinding = true; hasHydrationEventBinding = true
} }
if (name === 'ref') { if (name === 'ref') {
hasRef = true; hasRef = true
} else if (name === 'class' && !isComponent) { } else if (name === 'class' && !isComponent) {
hasClassBinding = true; hasClassBinding = true
} else if (name === 'style' && !isComponent) { } else if (name === 'style' && !isComponent) {
hasStyleBinding = true; hasStyleBinding = true
} else if (name !== 'key' && !isDirective(name) && name !== 'on') { } else if (name !== 'key' && !isDirective(name) && name !== 'on') {
dynamicPropNames.add(name); dynamicPropNames.add(name)
} }
} }
if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) { if (state.opts.transformOn && (name === 'on' || name === 'nativeOn')) {
@@ -114,15 +113,15 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
'transformOn', 'transformOn',
addDefault(path, '@vue/babel-helper-vue-transform-on', { addDefault(path, '@vue/babel-helper-vue-transform-on', {
nameHint: '_transformOn', nameHint: '_transformOn',
}) }),
); )
} }
mergeArgs.push( mergeArgs.push(
t.callExpression(state.get('transformOn'), [ t.callExpression(state.get('transformOn'), [
attributeValue || t.booleanLiteral(true), attributeValue || t.booleanLiteral(true),
]) ]),
); )
return; return
} }
if (isDirective(name)) { if (isDirective(name)) {
const { directive, modifiers, values, args, directiveName } = const { directive, modifiers, values, args, directiveName } =
@@ -133,34 +132,34 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
path: prop, path: prop,
state, state,
value: attributeValue, value: attributeValue,
}); })
if (directiveName === 'slots') { if (directiveName === 'slots') {
slots = attributeValue as Slots; slots = attributeValue as Slots
return; return
} }
if (directive) { if (directive) {
directives.push(t.arrayExpression(directive)); directives.push(t.arrayExpression(directive))
} else if (directiveName === 'html') { } else if (directiveName === 'html') {
properties.push( properties.push(
t.objectProperty(t.stringLiteral('innerHTML'), values[0] as any) t.objectProperty(t.stringLiteral('innerHTML'), values[0] as any),
); )
dynamicPropNames.add('innerHTML'); dynamicPropNames.add('innerHTML')
} else if (directiveName === 'text') { } else if (directiveName === 'text') {
properties.push( properties.push(
t.objectProperty(t.stringLiteral('textContent'), values[0] as any) t.objectProperty(t.stringLiteral('textContent'), values[0] as any),
); )
dynamicPropNames.add('textContent'); dynamicPropNames.add('textContent')
} }
if (['models', 'model'].includes(directiveName)) { if (['models', 'model'].includes(directiveName)) {
values.forEach((value, index) => { values.forEach((value, index) => {
const propName = args[index]; const propName = args[index]
// v-model target with variable // v-model target with variable
const isDynamic = const isDynamic =
propName && propName &&
!t.isStringLiteral(propName) && !t.isStringLiteral(propName) &&
!t.isNullLiteral(propName); !t.isNullLiteral(propName)
// must be v-model or v-models and is a component // must be v-model or v-models and is a component
if (!directive) { if (!directive) {
@@ -170,13 +169,13 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
? t.stringLiteral('modelValue') ? t.stringLiteral('modelValue')
: propName, : propName,
value as any, value as any,
isDynamic isDynamic,
) ),
); )
if (!isDynamic) { if (!isDynamic) {
dynamicPropNames.add( dynamicPropNames.add(
(propName as t.StringLiteral)?.value || 'modelValue' (propName as t.StringLiteral)?.value || 'modelValue',
); )
} }
if (modifiers[index]?.size) { if (modifiers[index]?.size) {
@@ -186,24 +185,24 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
? t.binaryExpression( ? t.binaryExpression(
'+', '+',
propName, propName,
t.stringLiteral('Modifiers') t.stringLiteral('Modifiers'),
) )
: t.stringLiteral( : t.stringLiteral(
`${ `${
(propName as t.StringLiteral)?.value || 'model' (propName as t.StringLiteral)?.value || 'model'
}Modifiers` }Modifiers`,
), ),
t.objectExpression( t.objectExpression(
[...modifiers[index]].map((modifier) => [...modifiers[index]].map((modifier) =>
t.objectProperty( t.objectProperty(
t.stringLiteral(modifier), t.stringLiteral(modifier),
t.booleanLiteral(true) t.booleanLiteral(true),
) ),
) ),
), ),
isDynamic isDynamic,
) ),
); )
} }
} }
@@ -212,8 +211,8 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
: t.stringLiteral( : t.stringLiteral(
`onUpdate:${ `onUpdate:${
(propName as t.StringLiteral)?.value || 'modelValue' (propName as t.StringLiteral)?.value || 'modelValue'
}` }`,
); )
properties.push( properties.push(
t.objectProperty( t.objectProperty(
@@ -223,68 +222,68 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
t.assignmentExpression( t.assignmentExpression(
'=', '=',
value as any, value as any,
t.identifier('$event') t.identifier('$event'),
) ),
), ),
isDynamic isDynamic,
) ),
); )
if (!isDynamic) { if (!isDynamic) {
dynamicPropNames.add((updateName as t.StringLiteral).value); dynamicPropNames.add((updateName as t.StringLiteral).value)
} else { } else {
hasDynamicKeys = true; hasDynamicKeys = true
} }
}); })
} }
} else { } else {
if (name.match(xlinkRE)) { if (name.match(xlinkRE)) {
name = name.replace( name = name.replace(
xlinkRE, xlinkRE,
(_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}` (_, firstCharacter) => `xlink:${firstCharacter.toLowerCase()}`,
); )
} }
properties.push( properties.push(
t.objectProperty( t.objectProperty(
t.stringLiteral(name), t.stringLiteral(name),
attributeValue || t.booleanLiteral(true) attributeValue || t.booleanLiteral(true),
) ),
); )
} }
} else { } else {
if (properties.length && mergeProps) { if (properties.length && mergeProps) {
mergeArgs.push( mergeArgs.push(
t.objectExpression(dedupeProperties(properties, mergeProps)) t.objectExpression(dedupeProperties(properties, mergeProps)),
); )
properties = []; properties = []
} }
// JSXSpreadAttribute // JSXSpreadAttribute
hasDynamicKeys = true; hasDynamicKeys = true
transformJSXSpreadAttribute( transformJSXSpreadAttribute(
path as NodePath, path as NodePath,
prop as NodePath<t.JSXSpreadAttribute>, prop as NodePath<t.JSXSpreadAttribute>,
mergeProps, mergeProps,
mergeProps ? mergeArgs : properties mergeProps ? mergeArgs : properties,
); )
} }
}); })
// patchFlag analysis // patchFlag analysis
if (hasDynamicKeys) { if (hasDynamicKeys) {
patchFlag |= PatchFlags.FULL_PROPS; patchFlag |= PatchFlags.FULL_PROPS
} else { } else {
if (hasClassBinding) { if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS; patchFlag |= PatchFlags.CLASS
} }
if (hasStyleBinding) { if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE; patchFlag |= PatchFlags.STYLE
} }
if (dynamicPropNames.size) { if (dynamicPropNames.size) {
patchFlag |= PatchFlags.PROPS; patchFlag |= PatchFlags.PROPS
} }
if (hasHydrationEventBinding) { if (hasHydrationEventBinding) {
patchFlag |= PatchFlags.HYDRATE_EVENTS; patchFlag |= PatchFlags.HYDRATE_EVENTS
} }
} }
@@ -292,34 +291,34 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
(patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) && (patchFlag === 0 || patchFlag === PatchFlags.HYDRATE_EVENTS) &&
(hasRef || directives.length > 0) (hasRef || directives.length > 0)
) { ) {
patchFlag |= PatchFlags.NEED_PATCH; patchFlag |= PatchFlags.NEED_PATCH
} }
let propsExpression: t.Expression | t.ObjectProperty | t.Literal = let propsExpression: t.Expression | t.ObjectProperty | t.Literal =
t.nullLiteral(); t.nullLiteral()
if (mergeArgs.length) { if (mergeArgs.length) {
if (properties.length) { if (properties.length) {
mergeArgs.push( mergeArgs.push(
t.objectExpression(dedupeProperties(properties, mergeProps)) t.objectExpression(dedupeProperties(properties, mergeProps)),
); )
} }
if (mergeArgs.length > 1) { if (mergeArgs.length > 1) {
propsExpression = t.callExpression( propsExpression = t.callExpression(
createIdentifier(state, 'mergeProps'), createIdentifier(state, 'mergeProps'),
mergeArgs mergeArgs,
); )
} else { } else {
// single no need for a mergeProps call // single no need for a mergeProps call
propsExpression = mergeArgs[0]; propsExpression = mergeArgs[0]
} }
} else if (properties.length) { } else if (properties.length) {
// single no need for spread // single no need for spread
if (properties.length === 1 && t.isSpreadElement(properties[0])) { 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 { } else {
propsExpression = t.objectExpression( propsExpression = t.objectExpression(
dedupeProperties(properties, mergeProps) dedupeProperties(properties, mergeProps),
); )
} }
} }
@@ -331,8 +330,8 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
directives, directives,
patchFlag, patchFlag,
dynamicPropNames, dynamicPropNames,
}; }
}; }
/** /**
* Get children from Array of JSX children * Get children from Array of JSX children
@@ -347,52 +346,52 @@ const getChildren = (
| t.JSXElement | t.JSXElement
| t.JSXFragment | t.JSXFragment
>[], >[],
state: State state: State,
): t.Expression[] => ): t.Expression[] =>
paths paths
.map((path) => { .map((path) => {
if (path.isJSXText()) { if (path.isJSXText()) {
const transformedText = transformJSXText(path); const transformedText = transformJSXText(path)
if (transformedText) { if (transformedText) {
return t.callExpression(createIdentifier(state, 'createTextVNode'), [ return t.callExpression(createIdentifier(state, 'createTextVNode'), [
transformedText, transformedText,
]); ])
} }
return transformedText; return transformedText
} }
if (path.isJSXExpressionContainer()) { if (path.isJSXExpressionContainer()) {
const expression = transformJSXExpressionContainer(path); const expression = transformJSXExpressionContainer(path)
if (t.isIdentifier(expression)) { if (t.isIdentifier(expression)) {
const { name } = expression; const { name } = expression
const { referencePaths = [] } = path.scope.getBinding(name) || {}; const { referencePaths = [] } = path.scope.getBinding(name) || {}
referencePaths.forEach((referencePath) => { referencePaths.forEach((referencePath) => {
walksScope(referencePath, name, SlotFlags.DYNAMIC); walksScope(referencePath, name, SlotFlags.DYNAMIC)
}); })
} }
return expression; return expression
} }
if (path.isJSXSpreadChild()) { if (path.isJSXSpreadChild()) {
return transformJSXSpreadChild(path); return transformJSXSpreadChild(path)
} }
if (path.isCallExpression()) { if (path.isCallExpression()) {
return (path as NodePath<t.CallExpression>).node; return (path as NodePath<t.CallExpression>).node
} }
if (path.isJSXElement()) { 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( .filter(
((value: any) => value != null && !t.isJSXEmptyExpression(value)) as any ((value: any) => value != null && !t.isJSXEmptyExpression(value)) as any,
); )
const transformJSXElement = ( const transformJSXElement = (
path: NodePath<t.JSXElement>, path: NodePath<t.JSXElement>,
state: State state: State,
): t.CallExpression => { ): t.CallExpression => {
const children = getChildren(path.get('children'), state); const children = getChildren(path.get('children'), state)
const { const {
tag, tag,
props, props,
@@ -401,9 +400,9 @@ const transformJSXElement = (
patchFlag, patchFlag,
dynamicPropNames, dynamicPropNames,
slots, 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 // #541 - directives can't be resolved in optimized slots
// all parents should be deoptimized // all parents should be deoptimized
@@ -413,19 +412,19 @@ const transformJSXElement = (
(d) => (d) =>
d.elements?.[0]?.type === 'CallExpression' && d.elements?.[0]?.type === 'CallExpression' &&
d.elements[0].callee.type === 'Identifier' && 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()) { while (currentPath.parentPath?.isJSXElement()) {
currentPath = currentPath.parentPath; currentPath = currentPath.parentPath
currentPath.setData('slotFlag', 0); currentPath.setData('slotFlag', 0)
} }
} }
const slotFlag = path.getData('slotFlag') ?? SlotFlags.STABLE; const slotFlag = path.getData('slotFlag') ?? SlotFlags.STABLE
const optimizeSlots = optimize && slotFlag !== 0; const optimizeSlots = optimize && slotFlag !== 0
let VNodeChild; let VNodeChild
if (children.length > 1 || slots) { if (children.length > 1 || slots) {
/* /*
@@ -442,8 +441,8 @@ const transformJSXElement = (
t.identifier('default'), t.identifier('default'),
t.arrowFunctionExpression( t.arrowFunctionExpression(
[], [],
t.arrayExpression(buildIIFE(path, children)) t.arrayExpression(buildIIFE(path, children)),
) ),
), ),
...(slots ...(slots
? t.isObjectExpression(slots) ? t.isObjectExpression(slots)
@@ -452,53 +451,53 @@ const transformJSXElement = (
: []), : []),
optimizeSlots && optimizeSlots &&
t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)),
].filter(Boolean as any) ].filter(Boolean as any),
) )
: slots : slots
: t.arrayExpression(children); : t.arrayExpression(children)
} else if (children.length === 1) { } else if (children.length === 1) {
/* /*
<A>{a}</A> or <A>{() => a}</A> <A>{a}</A> or <A>{() => a}</A>
*/ */
const { enableObjectSlots = true } = state.opts; const { enableObjectSlots = true } = state.opts
const child = children[0]; const child = children[0]
const objectExpression = t.objectExpression( const objectExpression = t.objectExpression(
[ [
t.objectProperty( t.objectProperty(
t.identifier('default'), t.identifier('default'),
t.arrowFunctionExpression( t.arrowFunctionExpression(
[], [],
t.arrayExpression(buildIIFE(path, [child])) t.arrayExpression(buildIIFE(path, [child])),
) ),
), ),
optimizeSlots && optimizeSlots &&
(t.objectProperty( (t.objectProperty(
t.identifier('_'), t.identifier('_'),
t.numericLiteral(slotFlag) t.numericLiteral(slotFlag),
) as any), ) as any),
].filter(Boolean) ].filter(Boolean),
); )
if (t.isIdentifier(child) && isComponent) { if (t.isIdentifier(child) && isComponent) {
VNodeChild = enableObjectSlots VNodeChild = enableObjectSlots
? t.conditionalExpression( ? t.conditionalExpression(
t.callExpression( t.callExpression(
state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(),
[child] [child],
), ),
child, child,
objectExpression objectExpression,
) )
: objectExpression; : objectExpression
} else if (t.isCallExpression(child) && child.loc && isComponent) { } else if (t.isCallExpression(child) && child.loc && isComponent) {
// the element was generated and doesn't have location information // the element was generated and doesn't have location information
if (enableObjectSlots) { if (enableObjectSlots) {
const { scope } = path; const { scope } = path
const slotId = scope.generateUidIdentifier('slot'); const slotId = scope.generateUidIdentifier('slot')
if (scope) { if (scope) {
scope.push({ scope.push({
id: slotId, id: slotId,
kind: 'let', kind: 'let',
}); })
} }
const alternate = t.objectExpression( const alternate = t.objectExpression(
[ [
@@ -506,24 +505,24 @@ const transformJSXElement = (
t.identifier('default'), t.identifier('default'),
t.arrowFunctionExpression( t.arrowFunctionExpression(
[], [],
t.arrayExpression(buildIIFE(path, [slotId])) t.arrayExpression(buildIIFE(path, [slotId])),
) ),
), ),
optimizeSlots && optimizeSlots &&
(t.objectProperty( (t.objectProperty(
t.identifier('_'), t.identifier('_'),
t.numericLiteral(slotFlag) t.numericLiteral(slotFlag),
) as any), ) as any),
].filter(Boolean) ].filter(Boolean),
); )
const assignment = t.assignmentExpression('=', slotId, child); const assignment = t.assignmentExpression('=', slotId, child)
const condition = t.callExpression( const condition = t.callExpression(
state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(), state.get('@vue/babel-plugin-jsx/runtimeIsSlot')(),
[assignment] [assignment],
); )
VNodeChild = t.conditionalExpression(condition, slotId, alternate); VNodeChild = t.conditionalExpression(condition, slotId, alternate)
} else { } else {
VNodeChild = objectExpression; VNodeChild = objectExpression
} }
} else if ( } else if (
t.isFunctionExpression(child) || t.isFunctionExpression(child) ||
@@ -531,24 +530,24 @@ const transformJSXElement = (
) { ) {
VNodeChild = t.objectExpression([ VNodeChild = t.objectExpression([
t.objectProperty(t.identifier('default'), child), t.objectProperty(t.identifier('default'), child),
]); ])
} else if (t.isObjectExpression(child)) { } else if (t.isObjectExpression(child)) {
VNodeChild = t.objectExpression( VNodeChild = t.objectExpression(
[ [
...child.properties, ...child.properties,
optimizeSlots && optimizeSlots &&
t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)), t.objectProperty(t.identifier('_'), t.numericLiteral(slotFlag)),
].filter(Boolean as any) ].filter(Boolean as any),
); )
} else { } else {
VNodeChild = isComponent VNodeChild = isComponent
? t.objectExpression([ ? t.objectExpression([
t.objectProperty( t.objectProperty(
t.identifier('default'), 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 && !!dynamicPropNames.size &&
optimize && optimize &&
t.arrayExpression( 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) { if (!directives.length) {
return createVNode; return createVNode
} }
return t.callExpression(createIdentifier(state, 'withDirectives'), [ return t.callExpression(createIdentifier(state, 'withDirectives'), [
createVNode, createVNode,
t.arrayExpression(directives), t.arrayExpression(directives),
]); ])
}; }
const visitor: Visitor<State> = { const visitor: Visitor<State> = {
JSXElement: { JSXElement: {
exit(path, state) { exit(path, state) {
path.replaceWith(transformJSXElement(path, state)); path.replaceWith(transformJSXElement(path, state))
}, },
}, },
}; }
export default visitor; export default visitor

View File

@@ -1,11 +1,11 @@
import t from '@babel/types'; import t from '@babel/types'
import { type NodePath } from '@babel/traverse'; import { type NodePath } from '@babel/traverse'
import { isHTMLTag, isSVGTag } from '@vue/shared'; import { isHTMLTag, isSVGTag } from '@vue/shared'
import type { State } from './interface'; import type { State } from './interface'
import SlotFlags from './slotFlags'; import SlotFlags from './slotFlags'
export const JSX_HELPER_KEY = 'JSX_HELPER_KEY'; export const JSX_HELPER_KEY = 'JSX_HELPER_KEY'
export const FRAGMENT = 'Fragment'; export const FRAGMENT = 'Fragment'
export const KEEP_ALIVE = 'KeepAlive'; export const KEEP_ALIVE = 'KeepAlive'
/** /**
* create Identifier * create Identifier
@@ -16,8 +16,8 @@ export const KEEP_ALIVE = 'KeepAlive';
*/ */
export const createIdentifier = ( export const createIdentifier = (
state: State, state: State,
name: string name: string,
): t.Identifier | t.MemberExpression => state.get(name)(); ): t.Identifier | t.MemberExpression => state.get(name)()
/** /**
* Checks if string is describing a directive * Checks if string is describing a directive
@@ -25,7 +25,7 @@ export const createIdentifier = (
*/ */
export const isDirective = (src: string): boolean => export const isDirective = (src: string): boolean =>
src.startsWith('v-') || 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 * Should transformed to slots
@@ -34,7 +34,7 @@ export const isDirective = (src: string): boolean =>
*/ */
// if _Fragment is already imported, it will end with number // if _Fragment is already imported, it will end with number
export const shouldTransformedToSlots = (tag: string) => 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 * Check if a Node is a component
@@ -45,23 +45,23 @@ export const shouldTransformedToSlots = (tag: string) =>
*/ */
export const checkIsComponent = ( export const checkIsComponent = (
path: NodePath<t.JSXOpeningElement>, path: NodePath<t.JSXOpeningElement>,
state: State state: State,
): boolean => { ): boolean => {
const namePath = path.get('name'); const namePath = path.get('name')
if (namePath.isJSXMemberExpression()) { if (namePath.isJSXMemberExpression()) {
return shouldTransformedToSlots(namePath.node.property.name); // For withCtx return shouldTransformedToSlots(namePath.node.property.name) // For withCtx
} }
const tag = (namePath as NodePath<t.JSXIdentifier>).node.name; const tag = (namePath as NodePath<t.JSXIdentifier>).node.name
return ( return (
!state.opts.isCustomElement?.(tag) && !state.opts.isCustomElement?.(tag) &&
shouldTransformedToSlots(tag) && shouldTransformedToSlots(tag) &&
!isHTMLTag(tag) && !isHTMLTag(tag) &&
!isSVGTag(tag) !isSVGTag(tag)
); )
}; }
/** /**
* Transform JSXMemberExpression to MemberExpression * Transform JSXMemberExpression to MemberExpression
@@ -69,20 +69,20 @@ export const checkIsComponent = (
* @returns MemberExpression * @returns MemberExpression
*/ */
export const transformJSXMemberExpression = ( export const transformJSXMemberExpression = (
path: NodePath<t.JSXMemberExpression> path: NodePath<t.JSXMemberExpression>,
): t.MemberExpression => { ): t.MemberExpression => {
const objectPath = path.node.object; const objectPath = path.node.object
const propertyPath = path.node.property; const propertyPath = path.node.property
const transformedObject = t.isJSXMemberExpression(objectPath) const transformedObject = t.isJSXMemberExpression(objectPath)
? transformJSXMemberExpression( ? transformJSXMemberExpression(
path.get('object') as NodePath<t.JSXMemberExpression> path.get('object') as NodePath<t.JSXMemberExpression>,
) )
: t.isJSXIdentifier(objectPath) : t.isJSXIdentifier(objectPath)
? t.identifier(objectPath.name) ? t.identifier(objectPath.name)
: t.nullLiteral(); : t.nullLiteral()
const transformedProperty = t.identifier(propertyPath.name); const transformedProperty = t.identifier(propertyPath.name)
return t.memberExpression(transformedObject, transformedProperty); return t.memberExpression(transformedObject, transformedProperty)
}; }
/** /**
* Get tag (first attribute for h) from JSXOpeningElement * Get tag (first attribute for h) from JSXOpeningElement
@@ -92,11 +92,11 @@ export const transformJSXMemberExpression = (
*/ */
export const getTag = ( export const getTag = (
path: NodePath<t.JSXElement>, path: NodePath<t.JSXElement>,
state: State state: State,
): t.Identifier | t.CallExpression | t.StringLiteral | t.MemberExpression => { ): 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()) { if (namePath.isJSXIdentifier()) {
const { name } = namePath.node; const { name } = namePath.node
if (!isHTMLTag(name) && !isSVGTag(name)) { if (!isHTMLTag(name) && !isSVGTag(name)) {
return name === FRAGMENT return name === FRAGMENT
? createIdentifier(state, FRAGMENT) ? createIdentifier(state, FRAGMENT)
@@ -106,26 +106,26 @@ export const getTag = (
? t.stringLiteral(name) ? t.stringLiteral(name)
: t.callExpression(createIdentifier(state, 'resolveComponent'), [ : t.callExpression(createIdentifier(state, 'resolveComponent'), [
t.stringLiteral(name), t.stringLiteral(name),
]); ])
} }
return t.stringLiteral(name); return t.stringLiteral(name)
} }
if (namePath.isJSXMemberExpression()) { 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<t.JSXAttribute>): string => { export const getJSXAttributeName = (path: NodePath<t.JSXAttribute>): string => {
const nameNode = path.node.name; const nameNode = path.node.name
if (t.isJSXIdentifier(nameNode)) { 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 * Transform JSXText to StringLiteral
@@ -133,56 +133,56 @@ export const getJSXAttributeName = (path: NodePath<t.JSXAttribute>): string => {
* @returns StringLiteral | null * @returns StringLiteral | null
*/ */
export const transformJSXText = ( export const transformJSXText = (
path: NodePath<t.JSXText | t.StringLiteral> path: NodePath<t.JSXText | t.StringLiteral>,
): t.StringLiteral | null => { ): t.StringLiteral | null => {
const str = transformText(path.node.value); const str = transformText(path.node.value)
return str !== '' ? t.stringLiteral(str) : null; return str !== '' ? t.stringLiteral(str) : null
}; }
export const transformText = (text: string) => { 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++) { for (let i = 0; i < lines.length; i++) {
if (lines[i].match(/[^ \t]/)) { if (lines[i].match(/[^ \t]/)) {
lastNonEmptyLine = i; lastNonEmptyLine = i
} }
} }
let str = ''; let str = ''
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i]
const isFirstLine = i === 0; const isFirstLine = i === 0
const isLastLine = i === lines.length - 1; const isLastLine = i === lines.length - 1
const isLastNonEmptyLine = i === lastNonEmptyLine; const isLastNonEmptyLine = i === lastNonEmptyLine
// replace rendered whitespace tabs with spaces // replace rendered whitespace tabs with spaces
let trimmedLine = line.replace(/\t/g, ' '); let trimmedLine = line.replace(/\t/g, ' ')
// trim whitespace touching a newline // trim whitespace touching a newline
if (!isFirstLine) { if (!isFirstLine) {
trimmedLine = trimmedLine.replace(/^[ ]+/, ''); trimmedLine = trimmedLine.replace(/^[ ]+/, '')
} }
// trim whitespace touching an endline // trim whitespace touching an endline
if (!isLastLine) { if (!isLastLine) {
trimmedLine = trimmedLine.replace(/[ ]+$/, ''); trimmedLine = trimmedLine.replace(/[ ]+$/, '')
} }
if (trimmedLine) { if (trimmedLine) {
if (!isLastNonEmptyLine) { if (!isLastNonEmptyLine) {
trimmedLine += ' '; trimmedLine += ' '
} }
str += trimmedLine; str += trimmedLine
} }
} }
return str; return str
}; }
/** /**
* Transform JSXExpressionContainer to Expression * Transform JSXExpressionContainer to Expression
@@ -190,8 +190,8 @@ export const transformText = (text: string) => {
* @returns Expression * @returns Expression
*/ */
export const transformJSXExpressionContainer = ( export const transformJSXExpressionContainer = (
path: NodePath<t.JSXExpressionContainer> path: NodePath<t.JSXExpressionContainer>,
): t.Expression => path.get('expression').node as t.Expression; ): t.Expression => path.get('expression').node as t.Expression
/** /**
* Transform JSXSpreadChild * Transform JSXSpreadChild
@@ -199,33 +199,33 @@ export const transformJSXExpressionContainer = (
* @returns SpreadElement * @returns SpreadElement
*/ */
export const transformJSXSpreadChild = ( export const transformJSXSpreadChild = (
path: NodePath<t.JSXSpreadChild> path: NodePath<t.JSXSpreadChild>,
): t.SpreadElement => t.spreadElement(path.get('expression').node); ): t.SpreadElement => t.spreadElement(path.get('expression').node)
export const walksScope = ( export const walksScope = (
path: NodePath, path: NodePath,
name: string, name: string,
slotFlag: SlotFlags slotFlag: SlotFlags,
): void => { ): void => {
if (path.scope.hasBinding(name) && path.parentPath) { if (path.scope.hasBinding(name) && path.parentPath) {
if (t.isJSXElement(path.parentPath.node)) { 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 = ( export const buildIIFE = (
path: NodePath<t.JSXElement>, path: NodePath<t.JSXElement>,
children: t.Expression[] children: t.Expression[],
) => { ) => {
const { parentPath } = path; const { parentPath } = path
if (parentPath.isAssignmentExpression()) { if (parentPath.isAssignmentExpression()) {
const { left } = parentPath.node as t.AssignmentExpression; const { left } = parentPath.node as t.AssignmentExpression
if (t.isIdentifier(left)) { if (t.isIdentifier(left)) {
return children.map((child) => { return children.map((child) => {
if (t.isIdentifier(child) && child.name === left.name) { if (t.isIdentifier(child) && child.name === left.name) {
const insertName = path.scope.generateUidIdentifier(child.name); const insertName = path.scope.generateUidIdentifier(child.name)
parentPath.insertBefore( parentPath.insertBefore(
t.variableDeclaration('const', [ t.variableDeclaration('const', [
t.variableDeclarator( t.variableDeclarator(
@@ -234,69 +234,69 @@ export const buildIIFE = (
t.functionExpression( t.functionExpression(
null, 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 = ( const mergeAsArray = (
existing: t.ObjectProperty, existing: t.ObjectProperty,
incoming: t.ObjectProperty incoming: t.ObjectProperty,
) => { ) => {
if (t.isArrayExpression(existing.value)) { if (t.isArrayExpression(existing.value)) {
existing.value.elements.push(incoming.value as t.Expression); existing.value.elements.push(incoming.value as t.Expression)
} else { } else {
existing.value = t.arrayExpression([ existing.value = t.arrayExpression([
existing.value as t.Expression, existing.value as t.Expression,
incoming.value as t.Expression, incoming.value as t.Expression,
]); ])
} }
}; }
export const dedupeProperties = ( export const dedupeProperties = (
properties: t.ObjectProperty[] = [], properties: t.ObjectProperty[] = [],
mergeProps?: boolean mergeProps?: boolean,
) => { ) => {
if (!mergeProps) { if (!mergeProps) {
return properties; return properties
} }
const knownProps = new Map<string, t.ObjectProperty>(); const knownProps = new Map<string, t.ObjectProperty>()
const deduped: t.ObjectProperty[] = []; const deduped: t.ObjectProperty[] = []
properties.forEach((prop) => { properties.forEach((prop) => {
if (t.isStringLiteral(prop.key)) { if (t.isStringLiteral(prop.key)) {
const { value: name } = prop.key; const { value: name } = prop.key
const existing = knownProps.get(name); const existing = knownProps.get(name)
if (existing) { if (existing) {
if (name === 'style' || name === 'class' || name.startsWith('on')) { if (name === 'style' || name === 'class' || name.startsWith('on')) {
mergeAsArray(existing, prop); mergeAsArray(existing, prop)
} }
} else { } else {
knownProps.set(name, prop); knownProps.set(name, prop)
deduped.push(prop); deduped.push(prop)
} }
} else { } else {
// v-model target with variable // v-model target with variable
deduped.push(prop); deduped.push(prop)
} }
}); })
return deduped; return deduped
}; }
/** /**
* Check if an attribute value is constant * Check if an attribute value is constant
@@ -304,52 +304,52 @@ export const dedupeProperties = (
* @returns boolean * @returns boolean
*/ */
export const isConstant = ( export const isConstant = (
node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null,
): boolean => { ): boolean => {
if (t.isIdentifier(node)) { if (t.isIdentifier(node)) {
return node.name === 'undefined'; return node.name === 'undefined'
} }
if (t.isArrayExpression(node)) { if (t.isArrayExpression(node)) {
const { elements } = node; const { elements } = node
return elements.every((element) => element && isConstant(element)); return elements.every((element) => element && isConstant(element))
} }
if (t.isObjectExpression(node)) { if (t.isObjectExpression(node)) {
return node.properties.every((property) => return node.properties.every((property) =>
isConstant((property as any).value) isConstant((property as any).value),
); )
} }
if ( if (
t.isTemplateLiteral(node) ? !node.expressions.length : t.isLiteral(node) t.isTemplateLiteral(node) ? !node.expressions.length : t.isLiteral(node)
) { ) {
return true; return true
} }
return false; return false
}; }
export const transformJSXSpreadAttribute = ( export const transformJSXSpreadAttribute = (
nodePath: NodePath, nodePath: NodePath,
path: NodePath<t.JSXSpreadAttribute>, path: NodePath<t.JSXSpreadAttribute>,
mergeProps: boolean, mergeProps: boolean,
args: (t.ObjectProperty | t.Expression | t.SpreadElement)[] args: (t.ObjectProperty | t.Expression | t.SpreadElement)[],
) => { ) => {
const argument = path.get('argument') as NodePath< const argument = path.get('argument') as NodePath<
t.ObjectExpression | t.Identifier t.ObjectExpression | t.Identifier
>; >
const properties = t.isObjectExpression(argument.node) const properties = t.isObjectExpression(argument.node)
? argument.node.properties ? argument.node.properties
: undefined; : undefined
if (!properties) { if (!properties) {
if (argument.isIdentifier()) { if (argument.isIdentifier()) {
walksScope( walksScope(
nodePath, nodePath,
(argument.node as t.Identifier).name, (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) { } else if (mergeProps) {
args.push(t.objectExpression(properties)); args.push(t.objectExpression(properties))
} else { } else {
args.push(...(properties as t.ObjectProperty[])); args.push(...(properties as t.ObjectProperty[]))
} }
}; }

View File

@@ -5,57 +5,57 @@ import {
defineComponent, defineComponent,
reactive, reactive,
ref, ref,
} from 'vue'; } from 'vue'
import { type VueWrapper, mount, shallowMount } from '@vue/test-utils'; import { type VueWrapper, mount, shallowMount } from '@vue/test-utils'
const patchFlagExpect = ( const patchFlagExpect = (
wrapper: VueWrapper<ComponentPublicInstance>, wrapper: VueWrapper<ComponentPublicInstance>,
flag: number, 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(patchFlag).toBe(flag)
expect(dynamicProps).toEqual(dynamic); expect(dynamicProps).toEqual(dynamic)
}; }
describe('Transform JSX', () => { describe('Transform JSX', () => {
test('should render with render function', () => { test('should render with render function', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <div>123</div>; return <div>123</div>
}, },
}); })
expect(wrapper.text()).toBe('123'); expect(wrapper.text()).toBe('123')
}); })
test('should render with setup', () => { test('should render with setup', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div>123</div>; return () => <div>123</div>
}, },
}); })
expect(wrapper.text()).toBe('123'); expect(wrapper.text()).toBe('123')
}); })
test('Extracts attrs', () => { test('Extracts attrs', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div id="hi" />; return () => <div id="hi" />
}, },
}); })
expect(wrapper.element.id).toBe('hi'); expect(wrapper.element.id).toBe('hi')
}); })
test('Binds attrs', () => { test('Binds attrs', () => {
const id = 'foo'; const id = 'foo'
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div>{id}</div>; return () => <div>{id}</div>
}, },
}); })
expect(wrapper.text()).toBe('foo'); expect(wrapper.text()).toBe('foo')
}); })
test('should not fallthrough with inheritAttrs: false', () => { test('should not fallthrough with inheritAttrs: false', () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -63,25 +63,25 @@ describe('Transform JSX', () => {
foo: Number, foo: Number,
}, },
setup(props) { setup(props) {
return () => <div>{props.foo}</div>; return () => <div>{props.foo}</div>
}, },
}); })
Child.inheritAttrs = false; Child.inheritAttrs = false
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <Child class="parent" foo={1} />; return <Child class="parent" foo={1} />
}, },
}); })
expect(wrapper.classes()).toStrictEqual([]); expect(wrapper.classes()).toStrictEqual([])
expect(wrapper.text()).toBe('1'); expect(wrapper.text()).toBe('1')
}); })
test('Fragment', () => { test('Fragment', () => {
const Child = () => <div>123</div>; const Child = () => <div>123</div>
Child.inheritAttrs = false; Child.inheritAttrs = false
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
@@ -90,146 +90,146 @@ describe('Transform JSX', () => {
<Child /> <Child />
<div>456</div> <div>456</div>
</> </>
); )
}, },
}); })
expect(wrapper.html()).toBe('<div>123</div>\n<div>456</div>'); expect(wrapper.html()).toBe('<div>123</div>\n<div>456</div>')
}); })
test('nested component', () => { test('nested component', () => {
const A = { const A = {
B: defineComponent({ B: defineComponent({
setup() { setup() {
return () => <div>123</div>; return () => <div>123</div>
}, },
}), }),
}; }
A.B.inheritAttrs = false; A.B.inheritAttrs = false
const wrapper = mount(() => <A.B />); const wrapper = mount(() => <A.B />)
expect(wrapper.html()).toBe('<div>123</div>'); expect(wrapper.html()).toBe('<div>123</div>')
}); })
test('xlink:href', () => { test('xlink:href', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <use xlinkHref={'#name'}></use>; return () => <use xlinkHref={'#name'}></use>
}, },
}); })
expect(wrapper.attributes()['xlink:href']).toBe('#name'); expect(wrapper.attributes()['xlink:href']).toBe('#name')
}); })
test('Merge class', () => { test('Merge class', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
// @ts-expect-error // @ts-expect-error
return () => <div class="a" {...{ class: 'b' }} />; return () => <div class="a" {...{ class: 'b' }} />
}, },
}); })
expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort()); expect(wrapper.classes().sort()).toEqual(['a', 'b'].sort())
}); })
test('Merge style', () => { test('Merge style', () => {
const propsA = { const propsA = {
style: { style: {
color: 'red', color: 'red',
} as CSSProperties, } as CSSProperties,
}; }
const propsB = { const propsB = {
style: { style: {
color: 'blue', color: 'blue',
width: '300px', width: '300px',
height: '300px', height: '300px',
} as CSSProperties, } as CSSProperties,
}; }
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
// @ts-ignore // @ts-ignore
return () => <div {...propsA} {...propsB} />; return () => <div {...propsA} {...propsB} />
}, },
}); })
expect(wrapper.html()).toBe( expect(wrapper.html()).toBe(
'<div style="color: blue; width: 300px; height: 300px;"></div>' '<div style="color: blue; width: 300px; height: 300px;"></div>',
); )
}); })
test('JSXSpreadChild', () => { test('JSXSpreadChild', () => {
const a = ['1', '2']; const a = ['1', '2']
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div>{[...a]}</div>; return () => <div>{[...a]}</div>
}, },
}); })
expect(wrapper.text()).toBe('12'); expect(wrapper.text()).toBe('12')
}); })
test('domProps input[value]', () => { test('domProps input[value]', () => {
const val = 'foo'; const val = 'foo'
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <input type="text" value={val} />; return () => <input type="text" value={val} />
}, },
}); })
expect(wrapper.html()).toBe('<input type="text" value="foo">'); expect(wrapper.html()).toBe('<input type="text" value="foo">')
}); })
test('domProps input[checked]', () => { test('domProps input[checked]', () => {
const val = true; const val = true
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <input checked={val} />; return () => <input checked={val} />
}, },
}); })
expect(wrapper.vm.$.subTree?.props?.checked).toBe(val); expect(wrapper.vm.$.subTree?.props?.checked).toBe(val)
}); })
test('domProps option[selected]', () => { test('domProps option[selected]', () => {
const val = true; const val = true
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <option selected={val} />; return <option selected={val} />
}, },
}); })
expect(wrapper.vm.$.subTree?.props?.selected).toBe(val); expect(wrapper.vm.$.subTree?.props?.selected).toBe(val)
}); })
test('domProps video[muted]', () => { test('domProps video[muted]', () => {
const val = true; const val = true
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <video muted={val} />; return <video muted={val} />
}, },
}); })
expect(wrapper.vm.$.subTree?.props?.muted).toBe(val); expect(wrapper.vm.$.subTree?.props?.muted).toBe(val)
}); })
test('Spread (single object expression)', () => { test('Spread (single object expression)', () => {
const props = { const props = {
id: '1', id: '1',
}; }
const wrapper = shallowMount({ const wrapper = shallowMount({
render() { render() {
return <div {...props}>123</div>; return <div {...props}>123</div>
}, },
}); })
expect(wrapper.html()).toBe('<div id="1">123</div>'); expect(wrapper.html()).toBe('<div id="1">123</div>')
}); })
test('Spread (mixed)', async () => { test('Spread (mixed)', async () => {
const calls: number[] = []; const calls: number[] = []
const data = { const data = {
id: 'hehe', id: 'hehe',
onClick() { onClick() {
calls.push(3); calls.push(3)
}, },
innerHTML: '2', innerHTML: '2',
class: ['a', 'b'], class: ['a', 'b'],
}; }
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
@@ -241,51 +241,51 @@ describe('Transform JSX', () => {
onClick={() => calls.push(4)} onClick={() => calls.push(4)}
hook-insert={() => calls.push(2)} hook-insert={() => calls.push(2)}
/> />
); )
}, },
}); })
expect(wrapper.attributes('id')).toBe('hehe'); expect(wrapper.attributes('id')).toBe('hehe')
expect(wrapper.attributes('type')).toBe('button'); expect(wrapper.attributes('type')).toBe('button')
expect(wrapper.text()).toBe('2'); expect(wrapper.text()).toBe('2')
expect(wrapper.classes()).toEqual(expect.arrayContaining(['a', 'b', 'c'])); expect(wrapper.classes()).toEqual(expect.arrayContaining(['a', 'b', 'c']))
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(calls).toEqual(expect.arrayContaining([3, 4])); expect(calls).toEqual(expect.arrayContaining([3, 4]))
}); })
test('empty string', () => { test('empty string', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <h1 title=""></h1>; return () => <h1 title=""></h1>
}, },
}); })
expect(wrapper.html()).toBe('<h1 title=""></h1>'); expect(wrapper.html()).toBe('<h1 title=""></h1>')
}); })
}); })
describe('directive', () => { describe('directive', () => {
test('vHtml', () => { test('vHtml', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
const html = '<div>foo</div>'; const html = '<div>foo</div>'
return () => <h1 v-html={html}></h1>; return () => <h1 v-html={html}></h1>
}, },
}); })
expect(wrapper.html()).toBe('<h1>\n <div>foo</div>\n</h1>'); expect(wrapper.html()).toBe('<h1>\n <div>foo</div>\n</h1>')
}); })
test('vText', () => { test('vText', () => {
const text = 'foo'; const text = 'foo'
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div v-text={text}></div>; return () => <div v-text={text}></div>
}, },
}); })
expect(wrapper.html()).toBe('<div>foo</div>'); expect(wrapper.html()).toBe('<div>foo</div>')
}); })
}); })
describe('slots', () => { describe('slots', () => {
test('with default', () => { test('with default', () => {
@@ -296,11 +296,11 @@ describe('slots', () => {
{slots.default?.()} {slots.default?.()}
{slots.foo?.('val')} {slots.foo?.('val')}
</div> </div>
); )
}, },
}); })
A.inheritAttrs = false; A.inheritAttrs = false
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
@@ -308,98 +308,98 @@ describe('slots', () => {
<A v-slots={{ foo: (val: string) => val }}> <A v-slots={{ foo: (val: string) => val }}>
<span>default</span> <span>default</span>
</A> </A>
); )
}, },
}); })
expect(wrapper.html()).toBe('<div><span>default</span>val</div>'); expect(wrapper.html()).toBe('<div><span>default</span>val</div>')
}); })
test('without default', () => { test('without default', () => {
const A = defineComponent({ const A = defineComponent({
setup(_, { slots }) { setup(_, { slots }) {
return () => <div>{slots.foo?.('foo')}</div>; return () => <div>{slots.foo?.('foo')}</div>
}, },
}); })
A.inheritAttrs = false; A.inheritAttrs = false
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
return () => <A v-slots={{ foo: (val: string) => val }} />; return () => <A v-slots={{ foo: (val: string) => val }} />
}, },
}); })
expect(wrapper.html()).toBe('<div>foo</div>'); expect(wrapper.html()).toBe('<div>foo</div>')
}); })
}); })
describe('PatchFlags', () => { describe('PatchFlags', () => {
test('static', () => { test('static', () => {
const wrapper = shallowMount({ const wrapper = shallowMount({
setup() { setup() {
return () => <div class="static">static</div>; return () => <div class="static">static</div>
}, },
}); })
patchFlagExpect(wrapper, 0, null); patchFlagExpect(wrapper, 0, null)
}); })
test('props', async () => { test('props', async () => {
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
const visible = ref(true); const visible = ref(true)
const onClick = () => { const onClick = () => {
visible.value = false; visible.value = false
}; }
return () => ( return () => (
<div v-show={visible.value} onClick={onClick}> <div v-show={visible.value} onClick={onClick}>
NEED_PATCH NEED_PATCH
</div> </div>
); )
}, },
}); })
patchFlagExpect(wrapper, 8, ['onClick']); patchFlagExpect(wrapper, 8, ['onClick'])
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div style="display: none;">NEED_PATCH</div>'); expect(wrapper.html()).toBe('<div style="display: none;">NEED_PATCH</div>')
}); })
test('#728: template literals with expressions should be treated as dynamic', async () => { test('#728: template literals with expressions should be treated as dynamic', async () => {
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
const foo = ref(0); const foo = ref(0)
return () => ( return () => (
<button value={`${foo.value}`} onClick={() => foo.value++}></button> <button value={`${foo.value}`} onClick={() => foo.value++}></button>
); )
}, },
}); })
patchFlagExpect(wrapper, 8, ['value', 'onClick']); patchFlagExpect(wrapper, 8, ['value', 'onClick'])
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<button value="1"></button>'); expect(wrapper.html()).toBe('<button value="1"></button>')
}); })
test('full props', async () => { test('full props', async () => {
const wrapper = mount({ const wrapper = mount({
setup() { setup() {
const bindProps = reactive({ class: 'a', style: { marginTop: 10 } }); const bindProps = reactive({ class: 'a', style: { marginTop: 10 } })
const onClick = () => { const onClick = () => {
bindProps.class = 'b'; bindProps.class = 'b'
}; }
return () => ( return () => (
<div {...bindProps} class="static" onClick={onClick}> <div {...bindProps} class="static" onClick={onClick}>
full props full props
</div> </div>
); )
}, },
}); })
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', () => { describe('variables outside slots', () => {
const A = defineComponent({ const A = defineComponent({
@@ -407,11 +407,11 @@ describe('variables outside slots', () => {
inc: Function, inc: Function,
}, },
render() { render() {
return this.$slots.default?.(); return this.$slots.default?.()
}, },
}); })
A.inheritAttrs = false; A.inheritAttrs = false
test('internal', async () => { test('internal', async () => {
const wrapper = mount( const wrapper = mount(
@@ -419,17 +419,17 @@ describe('variables outside slots', () => {
data() { data() {
return { return {
val: 0, val: 0,
}; }
}, },
methods: { methods: {
inc() { inc() {
this.val += 1; this.val += 1
}, },
}, },
render() { render() {
const attrs = { const attrs = {
innerHTML: `${this.val}`, innerHTML: `${this.val}`,
}; }
return ( return (
<A inc={this.inc}> <A inc={this.inc}>
<div> <div>
@@ -441,15 +441,15 @@ describe('variables outside slots', () => {
+1 +1
</button> </button>
</A> </A>
); )
}, },
}) }),
); )
expect(wrapper.get('#textarea').element.innerHTML).toBe('0'); expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
await wrapper.get('#button').trigger('click'); await wrapper.get('#button').trigger('click')
expect(wrapper.get('#textarea').element.innerHTML).toBe('1'); expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
}); })
test('forwarded', async () => { test('forwarded', async () => {
const wrapper = mount( const wrapper = mount(
@@ -457,18 +457,18 @@ describe('variables outside slots', () => {
data() { data() {
return { return {
val: 0, val: 0,
}; }
}, },
methods: { methods: {
inc() { inc() {
this.val += 1; this.val += 1
}, },
}, },
render() { render() {
const attrs = { const attrs = {
innerHTML: `${this.val}`, innerHTML: `${this.val}`,
}; }
const textarea = <textarea id="textarea" {...attrs} />; const textarea = <textarea id="textarea" {...attrs} />
return ( return (
<A inc={this.inc}> <A inc={this.inc}>
<div>{textarea}</div> <div>{textarea}</div>
@@ -476,38 +476,38 @@ describe('variables outside slots', () => {
+1 +1
</button> </button>
</A> </A>
); )
}, },
}) }),
); )
expect(wrapper.get('#textarea').element.innerHTML).toBe('0'); expect(wrapper.get('#textarea').element.innerHTML).toBe('0')
await wrapper.get('#button').trigger('click'); await wrapper.get('#button').trigger('click')
expect(wrapper.get('#textarea').element.innerHTML).toBe('1'); expect(wrapper.get('#textarea').element.innerHTML).toBe('1')
}); })
}); })
test('reassign variable as component should work', () => { test('reassign variable as component should work', () => {
let a: any = 1; let a: any = 1
const A = defineComponent({ const A = defineComponent({
setup(_, { slots }) { setup(_, { slots }) {
return () => <span>{slots.default!()}</span>; return () => <span>{slots.default!()}</span>
}, },
}); })
const _a2 = 2; const _a2 = 2
a = _a2; a = _a2
a = <A>{a}</A>; a = <A>{a}</A>
const wrapper = mount({ const wrapper = mount({
render() { render() {
return a; return a
}, },
}); })
expect(wrapper.html()).toBe('<span>2</span>'); expect(wrapper.html()).toBe('<span>2</span>')
}); })
describe('should support passing object slots via JSX children', () => { describe('should support passing object slots via JSX children', () => {
const A = defineComponent({ const A = defineComponent({
@@ -517,78 +517,78 @@ describe('should support passing object slots via JSX children', () => {
{slots.default?.()} {slots.default?.()}
{slots.foo?.()} {slots.foo?.()}
</span> </span>
); )
}, },
}); })
test('single expression, variable', () => { test('single expression, variable', () => {
const slots = { default: () => 1, foo: () => 2 }; const slots = { default: () => 1, foo: () => 2 }
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{slots}</A>; return <A>{slots}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>12</span>'); expect(wrapper.html()).toBe('<span>12</span>')
}); })
test('single expression, object literal', () => { test('single expression, object literal', () => {
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{{ default: () => 1, foo: () => 2 }}</A>; return <A>{{ default: () => 1, foo: () => 2 }}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>12</span>'); expect(wrapper.html()).toBe('<span>12</span>')
}); })
test('single expression, object literal', () => { test('single expression, object literal', () => {
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{{ default: () => 1, foo: () => 2 }}</A>; return <A>{{ default: () => 1, foo: () => 2 }}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>12</span>'); expect(wrapper.html()).toBe('<span>12</span>')
}); })
test('single expression, non-literal value', () => { test('single expression, non-literal value', () => {
const foo = () => 1; const foo = () => 1
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{foo()}</A>; return <A>{foo()}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>1<!----></span>'); expect(wrapper.html()).toBe('<span>1<!----></span>')
}); })
test('single expression, function expression', () => { test('single expression, function expression', () => {
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{() => 'foo'}</A>; return <A>{() => 'foo'}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>foo<!----></span>'); expect(wrapper.html()).toBe('<span>foo<!----></span>')
}); })
test('single expression, function expression variable', () => { test('single expression, function expression variable', () => {
const foo = () => 'foo'; const foo = () => 'foo'
const wrapper = mount({ const wrapper = mount({
render() { render() {
return <A>{foo}</A>; return <A>{foo}</A>
}, },
}); })
expect(wrapper.html()).toBe('<span>foo<!----></span>'); expect(wrapper.html()).toBe('<span>foo<!----></span>')
}); })
test('single expression, array map expression', () => { test('single expression, array map expression', () => {
const data = ['A', 'B', 'C']; const data = ['A', 'B', 'C']
const wrapper = mount({ const wrapper = mount({
render() { render() {
@@ -600,9 +600,9 @@ describe('should support passing object slots via JSX children', () => {
</A> </A>
))} ))}
</> </>
); )
}, },
}); })
expect(wrapper.html()).toMatchInlineSnapshot( expect(wrapper.html()).toMatchInlineSnapshot(
` `
@@ -612,12 +612,12 @@ describe('should support passing object slots via JSX children', () => {
<!----></span> <!----></span>
<span><span>C</span> <span><span>C</span>
<!----></span>" <!----></span>"
` `,
); )
}); })
test('xx', () => { test('xx', () => {
const data = ['A', 'B', 'C']; const data = ['A', 'B', 'C']
const wrapper = mount({ const wrapper = mount({
render() { render() {
@@ -627,9 +627,9 @@ describe('should support passing object slots via JSX children', () => {
<A>{() => <span>{item}</span>}</A> <A>{() => <span>{item}</span>}</A>
))} ))}
</> </>
); )
}, },
}); })
expect(wrapper.html()).toMatchInlineSnapshot( expect(wrapper.html()).toMatchInlineSnapshot(
` `
@@ -639,7 +639,7 @@ describe('should support passing object slots via JSX children', () => {
<!----></span> <!----></span>
<span><span>C</span> <span><span>C</span>
<!----></span>" <!----></span>"
` `,
); )
}); })
}); })

View File

@@ -1,7 +1,7 @@
import { transformAsync } from '@babel/core'; import { transformAsync } from '@babel/core'
// @ts-expect-error missing types // @ts-expect-error missing types
import typescript from '@babel/plugin-syntax-typescript'; import typescript from '@babel/plugin-syntax-typescript'
import VueJsx from '../src'; import VueJsx from '../src'
describe('resolve type', () => { describe('resolve type', () => {
describe('runtime props', () => { describe('runtime props', () => {
@@ -16,9 +16,9 @@ describe('resolve type', () => {
[typescript, { isTSX: true }], [typescript, { isTSX: true }],
[VueJsx, { resolveType: true }], [VueJsx, { resolveType: true }],
], ],
} },
); )
expect(result!.code).toMatchSnapshot(); expect(result!.code).toMatchSnapshot()
}); })
}); })
}); })

View File

@@ -1 +1 @@
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime'

View File

@@ -1,9 +1,9 @@
import { transform } from '@babel/core'; import { transform } from '@babel/core'
import JSX, { type VueJSXPluginOptions } from '../src'; import JSX, { type VueJSXPluginOptions } from '../src'
interface Test { interface Test {
name: string; name: string
from: string; from: string
} }
const transpile = (source: string, options: VueJSXPluginOptions = {}) => const transpile = (source: string, options: VueJSXPluginOptions = {}) =>
@@ -18,14 +18,14 @@ const transpile = (source: string, options: VueJSXPluginOptions = {}) =>
}, },
(error, result) => { (error, result) => {
if (error) { if (error) {
return reject(error); return reject(error)
} }
resolve(result?.code); resolve(result?.code)
} },
) ),
); )
[ ;[
{ {
name: 'input[type="checkbox"]', name: 'input[type="checkbox"]',
from: '<input type="checkbox" v-model={test} />', from: '<input type="checkbox" v-model={test} />',
@@ -222,10 +222,10 @@ const transpile = (source: string, options: VueJSXPluginOptions = {}) =>
].forEach(({ name, from }) => { ].forEach(({ name, from }) => {
test(name, async () => { test(name, async () => {
expect( expect(
await transpile(from, { optimize: true, enableObjectSlots: true }) await transpile(from, { optimize: true, enableObjectSlots: true }),
).toMatchSnapshot(name); ).toMatchSnapshot(name)
}); })
}); })
const overridePropsTests: Test[] = [ const overridePropsTests: Test[] = [
{ {
@@ -236,13 +236,13 @@ const overridePropsTests: Test[] = [
name: 'multiple', name: 'multiple',
from: '<A loading {...a} {...{ b: 1, c: { d: 2 } }} class="x" style={x} />', from: '<A loading {...a} {...{ b: 1, c: { d: 2 } }} class="x" style={x} />',
}, },
]; ]
overridePropsTests.forEach(({ name, from }) => { overridePropsTests.forEach(({ name, from }) => {
test(`override props ${name}`, async () => { test(`override props ${name}`, async () => {
expect(await transpile(from, { mergeProps: false })).toMatchSnapshot(name); expect(await transpile(from, { mergeProps: false })).toMatchSnapshot(name)
}); })
}); })
const slotsTests: Test[] = [ const slotsTests: Test[] = [
{ {
@@ -296,58 +296,58 @@ const slotsTests: Test[] = [
</> </>
`, `,
}, },
]; ]
slotsTests.forEach(({ name, from }) => { slotsTests.forEach(({ name, from }) => {
test(`passing object slots via JSX children ${name}`, async () => { test(`passing object slots via JSX children ${name}`, async () => {
expect( expect(
await transpile(from, { optimize: true, enableObjectSlots: true }) await transpile(from, { optimize: true, enableObjectSlots: true }),
).toMatchSnapshot(name); ).toMatchSnapshot(name)
}); })
}); })
const objectSlotsTests = [ const objectSlotsTests = [
{ {
name: 'defaultSlot', name: 'defaultSlot',
from: '<Badge>{slots.default()}</Badge>', from: '<Badge>{slots.default()}</Badge>',
}, },
]; ]
objectSlotsTests.forEach(({ name, from }) => { objectSlotsTests.forEach(({ name, from }) => {
test(`disable object slot syntax with ${name}`, async () => { test(`disable object slot syntax with ${name}`, async () => {
expect( expect(
await transpile(from, { optimize: true, enableObjectSlots: false }) await transpile(from, { optimize: true, enableObjectSlots: false }),
).toMatchSnapshot(name); ).toMatchSnapshot(name)
}); })
}); })
const pragmaTests = [ const pragmaTests = [
{ {
name: 'custom', name: 'custom',
from: '<div>pragma</div>', from: '<div>pragma</div>',
}, },
]; ]
pragmaTests.forEach(({ name, from }) => { pragmaTests.forEach(({ name, from }) => {
test(`set pragma to ${name}`, async () => { test(`set pragma to ${name}`, async () => {
expect(await transpile(from, { pragma: 'custom' })).toMatchSnapshot(name); expect(await transpile(from, { pragma: 'custom' })).toMatchSnapshot(name)
}); })
}); })
const isCustomElementTests = [ const isCustomElementTests = [
{ {
name: 'isCustomElement', name: 'isCustomElement',
from: '<foo><span>foo</span></foo>', from: '<foo><span>foo</span></foo>',
}, },
]; ]
isCustomElementTests.forEach(({ name, from }) => { isCustomElementTests.forEach(({ name, from }) => {
test(name, async () => { test(name, async () => {
expect( expect(
await transpile(from, { isCustomElement: (tag) => tag === 'foo' }) await transpile(from, { isCustomElement: (tag) => tag === 'foo' }),
).toMatchSnapshot(name); ).toMatchSnapshot(name)
}); })
}); })
const fragmentTests = [ const fragmentTests = [
{ {
@@ -358,10 +358,10 @@ const fragmentTests = [
const Root2 = () => <_Fragment>root2</_Fragment> const Root2 = () => <_Fragment>root2</_Fragment>
`, `,
}, },
]; ]
fragmentTests.forEach(({ name, from }) => { fragmentTests.forEach(({ name, from }) => {
test(name, async () => { test(name, async () => {
expect(await transpile(from)).toMatchSnapshot(name); expect(await transpile(from)).toMatchSnapshot(name)
}); })
}); })

View File

@@ -1,5 +1,5 @@
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils'
import { type VNode, defineComponent } from 'vue'; import { type VNode, defineComponent } from 'vue'
test('input[type="checkbox"] should work', async () => { test('input[type="checkbox"] should work', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -7,24 +7,24 @@ test('input[type="checkbox"] should work', async () => {
data() { data() {
return { return {
test: true, test: true,
}; }
}, },
render() { render() {
return <input type="checkbox" v-model={this.test} />; return <input type="checkbox" v-model={this.test} />
}, },
}), }),
{ attachTo: document.body } { attachTo: document.body },
); )
expect(wrapper.vm.$el.checked).toBe(true); expect(wrapper.vm.$el.checked).toBe(true)
wrapper.vm.test = false; wrapper.vm.test = false
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.vm.$el.checked).toBe(false); expect(wrapper.vm.$el.checked).toBe(false)
expect(wrapper.vm.test).toBe(false); expect(wrapper.vm.test).toBe(false)
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.vm.$el.checked).toBe(true); expect(wrapper.vm.$el.checked).toBe(true)
expect(wrapper.vm.test).toBe(true); expect(wrapper.vm.test).toBe(true)
}); })
test('input[type="radio"] should work', async () => { test('input[type="radio"] should work', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -38,24 +38,24 @@ test('input[type="radio"] should work', async () => {
<input type="radio" value="1" v-model={this.test} name="test" /> <input type="radio" value="1" v-model={this.test} name="test" />
<input type="radio" value="2" v-model={this.test} name="test" /> <input type="radio" value="2" v-model={this.test} name="test" />
</> </>
); )
}, },
}), }),
{ attachTo: document.body } { attachTo: document.body },
); )
const [a, b] = wrapper.vm.$.subTree.children as VNode[]; const [a, b] = wrapper.vm.$.subTree.children as VNode[]
expect(a.el!.checked).toBe(true); expect(a.el!.checked).toBe(true)
wrapper.vm.test = '2'; wrapper.vm.test = '2'
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(a.el!.checked).toBe(false); expect(a.el!.checked).toBe(false)
expect(b.el!.checked).toBe(true); expect(b.el!.checked).toBe(true)
await a.el!.click(); await a.el!.click()
expect(a.el!.checked).toBe(true); expect(a.el!.checked).toBe(true)
expect(b.el!.checked).toBe(false); expect(b.el!.checked).toBe(false)
expect(wrapper.vm.test).toBe('1'); expect(wrapper.vm.test).toBe('1')
}); })
test('select should work with value bindings', async () => { test('select should work with value bindings', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -70,28 +70,28 @@ test('select should work with value bindings', async () => {
<option value={2}>b</option> <option value={2}>b</option>
<option value={3}>c</option> <option value={3}>c</option>
</select> </select>
); )
}, },
}) }),
); )
const el = wrapper.vm.$el; const el = wrapper.vm.$el
expect(el.value).toBe('2'); expect(el.value).toBe('2')
expect(el.children[1].selected).toBe(true); expect(el.children[1].selected).toBe(true)
wrapper.vm.test = 3; wrapper.vm.test = 3
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(el.value).toBe('3'); expect(el.value).toBe('3')
expect(el.children[2].selected).toBe(true); expect(el.children[2].selected).toBe(true)
el.value = '1'; el.value = '1'
await wrapper.trigger('change'); await wrapper.trigger('change')
expect(wrapper.vm.test).toBe('1'); expect(wrapper.vm.test).toBe('1')
el.value = '2'; el.value = '2'
await wrapper.trigger('change'); await wrapper.trigger('change')
expect(wrapper.vm.test).toBe(2); expect(wrapper.vm.test).toBe(2)
}); })
test('textarea should update value both ways', async () => { test('textarea should update value both ways', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -100,20 +100,20 @@ test('textarea should update value both ways', async () => {
test: 'b', test: 'b',
}), }),
render() { render() {
return <textarea v-model={this.test} />; return <textarea v-model={this.test} />
}, },
}) }),
); )
const el = wrapper.vm.$el; const el = wrapper.vm.$el
expect(el.value).toBe('b'); expect(el.value).toBe('b')
wrapper.vm.test = 'a'; wrapper.vm.test = 'a'
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(el.value).toBe('a'); expect(el.value).toBe('a')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('input'); await wrapper.trigger('input')
expect(wrapper.vm.test).toBe('c'); expect(wrapper.vm.test).toBe('c')
}); })
test('input[type="text"] should update value both ways', async () => { test('input[type="text"] should update value both ways', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -122,20 +122,20 @@ test('input[type="text"] should update value both ways', async () => {
test: 'b', test: 'b',
}), }),
render() { render() {
return <input v-model={this.test} />; return <input v-model={this.test} />
}, },
}) }),
); )
const el = wrapper.vm.$el; const el = wrapper.vm.$el
expect(el.value).toBe('b'); expect(el.value).toBe('b')
wrapper.vm.test = 'a'; wrapper.vm.test = 'a'
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(el.value).toBe('a'); expect(el.value).toBe('a')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('input'); await wrapper.trigger('input')
expect(wrapper.vm.test).toBe('c'); expect(wrapper.vm.test).toBe('c')
}); })
test('input[type="text"] .lazy modifier', async () => { test('input[type="text"] .lazy modifier', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -144,21 +144,21 @@ test('input[type="text"] .lazy modifier', async () => {
test: 'b', test: 'b',
}), }),
render() { render() {
return <input v-model={[this.test, ['lazy']]} />; return <input v-model={[this.test, ['lazy']]} />
}, },
}) }),
); )
const el = wrapper.vm.$el; const el = wrapper.vm.$el
expect(el.value).toBe('b'); expect(el.value).toBe('b')
expect(wrapper.vm.test).toBe('b'); expect(wrapper.vm.test).toBe('b')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('input'); await wrapper.trigger('input')
expect(wrapper.vm.test).toBe('b'); expect(wrapper.vm.test).toBe('b')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('change'); await wrapper.trigger('change')
expect(wrapper.vm.test).toBe('c'); expect(wrapper.vm.test).toBe('c')
}); })
test('dynamic type should work', async () => { test('dynamic type should work', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -167,19 +167,19 @@ test('dynamic type should work', async () => {
return { return {
test: true, test: true,
type: 'checkbox', type: 'checkbox',
}; }
}, },
render() { render() {
return <input type={this.type} v-model={this.test} />; return <input type={this.type} v-model={this.test} />
}, },
}) }),
); )
expect(wrapper.vm.$el.checked).toBe(true); expect(wrapper.vm.$el.checked).toBe(true)
wrapper.vm.test = false; wrapper.vm.test = false
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.vm.$el.checked).toBe(false); expect(wrapper.vm.$el.checked).toBe(false)
}); })
test('underscore modifier should work', async () => { test('underscore modifier should work', async () => {
const wrapper = shallowMount( const wrapper = shallowMount(
@@ -188,21 +188,21 @@ test('underscore modifier should work', async () => {
test: 'b', test: 'b',
}), }),
render() { render() {
return <input v-model_lazy={this.test} />; return <input v-model_lazy={this.test} />
}, },
}) }),
); )
const el = wrapper.vm.$el; const el = wrapper.vm.$el
expect(el.value).toBe('b'); expect(el.value).toBe('b')
expect(wrapper.vm.test).toBe('b'); expect(wrapper.vm.test).toBe('b')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('input'); await wrapper.trigger('input')
expect(wrapper.vm.test).toBe('b'); expect(wrapper.vm.test).toBe('b')
el.value = 'c'; el.value = 'c'
await wrapper.trigger('change'); await wrapper.trigger('change')
expect(wrapper.vm.test).toBe('c'); expect(wrapper.vm.test).toBe('c')
}); })
test('underscore modifier should work in custom component', async () => { test('underscore modifier should work in custom component', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -218,38 +218,38 @@ test('underscore modifier should work in custom component', async () => {
}, },
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:modelValue', 3); emit('update:modelValue', 3)
}; }
return () => ( return () => (
<div onClick={handleClick}> <div onClick={handleClick}>
{props.modelModifiers.double {props.modelModifiers.double
? props.modelValue * 2 ? props.modelValue * 2
: props.modelValue} : props.modelValue}
</div> </div>
); )
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
data() { data() {
return { return {
foo: 1, foo: 1,
}; }
}, },
render() { render() {
return <Child v-model_double={this.foo} />; return <Child v-model_double={this.foo} />
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>2</div>'); expect(wrapper.html()).toBe('<div>2</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>4</div>'); expect(wrapper.html()).toBe('<div>4</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>6</div>'); expect(wrapper.html()).toBe('<div>6</div>')
}); })
test('Named model', async () => { test('Named model', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -262,11 +262,11 @@ test('Named model', async () => {
}, },
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:value', 2); emit('update:value', 2)
}; }
return () => <div onClick={handleClick}>{props.value}</div>; return () => <div onClick={handleClick}>{props.value}</div>
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
@@ -274,18 +274,18 @@ test('Named model', async () => {
foo: 0, foo: 0,
}), }),
render() { render() {
return <Child v-model:value={this.foo} />; return <Child v-model:value={this.foo} />
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>0</div>'); expect(wrapper.html()).toBe('<div>0</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>1</div>'); expect(wrapper.html()).toBe('<div>1</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>2</div>'); expect(wrapper.html()).toBe('<div>2</div>')
}); })
test('named model and underscore modifier should work in custom component', async () => { test('named model and underscore modifier should work in custom component', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -301,33 +301,33 @@ test('named model and underscore modifier should work in custom component', asyn
}, },
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:value', 3); emit('update:value', 3)
}; }
return () => ( return () => (
<div onClick={handleClick}> <div onClick={handleClick}>
{props.valueModifiers.double ? props.value * 2 : props.value} {props.valueModifiers.double ? props.value * 2 : props.value}
</div> </div>
); )
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
data() { data() {
return { return {
foo: 1, foo: 1,
}; }
}, },
render() { render() {
return <Child v-model:value_double={this.foo} />; return <Child v-model:value_double={this.foo} />
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>2</div>'); expect(wrapper.html()).toBe('<div>2</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>4</div>'); expect(wrapper.html()).toBe('<div>4</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>6</div>'); expect(wrapper.html()).toBe('<div>6</div>')
}); })

View File

@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils'
import { defineComponent } from 'vue'; import { defineComponent } from 'vue'
test('single value binding should work', async () => { test('single value binding should work', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -9,32 +9,32 @@ test('single value binding should work', async () => {
emits: ['update:foo'], emits: ['update:foo'],
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:foo', 3); emit('update:foo', 3)
}; }
return () => <div onClick={handleClick}>{props.foo}</div>; return () => <div onClick={handleClick}>{props.foo}</div>
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
data() { data() {
return { return {
foo: 1, foo: 1,
}; }
}, },
render() { render() {
return <Child v-models={[[this.foo, 'foo']]} />; return <Child v-models={[[this.foo, 'foo']]} />
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>1</div>'); expect(wrapper.html()).toBe('<div>1</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>2</div>'); expect(wrapper.html()).toBe('<div>2</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>3</div>'); expect(wrapper.html()).toBe('<div>3</div>')
}); })
test('multiple values binding should work', async () => { test('multiple values binding should work', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -45,16 +45,16 @@ test('multiple values binding should work', async () => {
emits: ['update:foo', 'update:bar'], emits: ['update:foo', 'update:bar'],
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:foo', 3); emit('update:foo', 3)
emit('update:bar', 2); emit('update:bar', 2)
}; }
return () => ( return () => (
<div onClick={handleClick}> <div onClick={handleClick}>
{props.foo},{props.bar} {props.foo},{props.bar}
</div> </div>
); )
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
@@ -62,7 +62,7 @@ test('multiple values binding should work', async () => {
return { return {
foo: 1, foo: 1,
bar: 0, bar: 0,
}; }
}, },
render() { render() {
return ( return (
@@ -72,19 +72,19 @@ test('multiple values binding should work', async () => {
[this.bar, 'bar'], [this.bar, 'bar'],
]} ]}
/> />
); )
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>1,0</div>'); expect(wrapper.html()).toBe('<div>1,0</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
wrapper.vm.$data.bar += 1; wrapper.vm.$data.bar += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>2,1</div>'); expect(wrapper.html()).toBe('<div>2,1</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>3,2</div>'); expect(wrapper.html()).toBe('<div>3,2</div>')
}); })
test('modifier should work', async () => { test('modifier should work', async () => {
const Child = defineComponent({ const Child = defineComponent({
@@ -100,33 +100,33 @@ test('modifier should work', async () => {
emits: ['update:foo'], emits: ['update:foo'],
setup(props, { emit }) { setup(props, { emit }) {
const handleClick = () => { const handleClick = () => {
emit('update:foo', 3); emit('update:foo', 3)
}; }
return () => ( return () => (
<div onClick={handleClick}> <div onClick={handleClick}>
{props.fooModifiers.double ? props.foo * 2 : props.foo} {props.fooModifiers.double ? props.foo * 2 : props.foo}
</div> </div>
); )
}, },
}); })
const wrapper = mount( const wrapper = mount(
defineComponent({ defineComponent({
data() { data() {
return { return {
foo: 1, foo: 1,
}; }
}, },
render() { render() {
return <Child v-models={[[this.foo, 'foo', ['double']]]} />; return <Child v-models={[[this.foo, 'foo', ['double']]]} />
}, },
}) }),
); )
expect(wrapper.html()).toBe('<div>2</div>'); expect(wrapper.html()).toBe('<div>2</div>')
wrapper.vm.$data.foo += 1; wrapper.vm.$data.foo += 1
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick()
expect(wrapper.html()).toBe('<div>4</div>'); expect(wrapper.html()).toBe('<div>4</div>')
await wrapper.trigger('click'); await wrapper.trigger('click')
expect(wrapper.html()).toBe('<div>6</div>'); expect(wrapper.html()).toBe('<div>6</div>')
}); })

View File

@@ -1,31 +1,31 @@
import type * as BabelCore from '@babel/core'; import type * as BabelCore from '@babel/core'
import { parseExpression } from '@babel/parser'; import { parseExpression } from '@babel/parser'
import { import {
type SimpleTypeResolveContext, type SimpleTypeResolveContext,
type SimpleTypeResolveOptions, type SimpleTypeResolveOptions,
extractRuntimeEmits, extractRuntimeEmits,
extractRuntimeProps, extractRuntimeProps,
} from '@vue/compiler-sfc'; } from '@vue/compiler-sfc'
import { codeFrameColumns } from '@babel/code-frame'; import { codeFrameColumns } from '@babel/code-frame'
import { addNamed } from '@babel/helper-module-imports'; import { addNamed } from '@babel/helper-module-imports'
import { declare } from '@babel/helper-plugin-utils'; import { declare } from '@babel/helper-plugin-utils'
export { SimpleTypeResolveOptions as Options }; export { SimpleTypeResolveOptions as Options }
const plugin: ( const plugin: (
api: object, api: object,
options: SimpleTypeResolveOptions | null | undefined, options: SimpleTypeResolveOptions | null | undefined,
dirname: string dirname: string,
) => BabelCore.PluginObj<BabelCore.PluginPass> = ) => BabelCore.PluginObj<BabelCore.PluginPass> =
declare<SimpleTypeResolveOptions>(({ types: t }, options) => { declare<SimpleTypeResolveOptions>(({ types: t }, options) => {
let ctx: SimpleTypeResolveContext | undefined; let ctx: SimpleTypeResolveContext | undefined
let helpers: Set<string> | undefined; let helpers: Set<string> | undefined
return { return {
name: 'babel-plugin-resolve-type', name: 'babel-plugin-resolve-type',
pre(file) { pre(file) {
const filename = file.opts.filename || 'unknown.js'; const filename = file.opts.filename || 'unknown.js'
helpers = new Set(); helpers = new Set()
ctx = { ctx = {
filename: filename, filename: filename,
source: file.code, source: file.code,
@@ -45,91 +45,89 @@ const plugin: (
line: node.loc!.end.line, line: node.loc!.end.line,
column: node.loc!.end.column + 1, column: node.loc!.end.column + 1,
}, },
} },
)}` )}`,
); )
}, },
helper(key) { helper(key) {
helpers!.add(key); helpers!.add(key)
return `_${key}`; return `_${key}`
}, },
getString(node) { getString(node) {
return file.code.slice(node.start!, node.end!); return file.code.slice(node.start!, node.end!)
}, },
propsTypeDecl: undefined, propsTypeDecl: undefined,
propsRuntimeDefaults: undefined, propsRuntimeDefaults: undefined,
propsDestructuredBindings: {}, propsDestructuredBindings: {},
emitsTypeDecl: undefined, emitsTypeDecl: undefined,
}; }
}, },
visitor: { visitor: {
CallExpression(path) { CallExpression(path) {
if (!ctx) { if (!ctx) {
throw new Error( throw new Error(
'[@vue/babel-plugin-resolve-type] context is not loaded.' '[@vue/babel-plugin-resolve-type] context is not loaded.',
); )
} }
const { node } = path; const { node } = path
if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return; if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return
if (!checkDefineComponent(path)) return; if (!checkDefineComponent(path)) return
const comp = node.arguments[0]; const comp = node.arguments[0]
if (!comp || !t.isFunction(comp)) return; if (!comp || !t.isFunction(comp)) return
let options = node.arguments[1]; let options = node.arguments[1]
if (!options) { if (!options) {
options = t.objectExpression([]); options = t.objectExpression([])
node.arguments.push(options); node.arguments.push(options)
} }
let propsGenerics: BabelCore.types.TSType | undefined; let propsGenerics: BabelCore.types.TSType | undefined
let emitsGenerics: BabelCore.types.TSType | undefined; let emitsGenerics: BabelCore.types.TSType | undefined
if (node.typeParameters && node.typeParameters.params.length > 0) { if (node.typeParameters && node.typeParameters.params.length > 0) {
propsGenerics = node.typeParameters.params[0]; propsGenerics = node.typeParameters.params[0]
emitsGenerics = node.typeParameters.params[1]; emitsGenerics = node.typeParameters.params[1]
} }
node.arguments[1] = node.arguments[1] =
processProps(comp, propsGenerics, options) || options; processProps(comp, propsGenerics, options) || options
node.arguments[1] = node.arguments[1] =
processEmits(comp, emitsGenerics, node.arguments[1]) || options; processEmits(comp, emitsGenerics, node.arguments[1]) || options
}, },
VariableDeclarator(path) { VariableDeclarator(path) {
inferComponentName(path); inferComponentName(path)
}, },
}, },
post(file) { post(file) {
for (const helper of helpers!) { for (const helper of helpers!) {
addNamed(file.path, `_${helper}`, 'vue'); addNamed(file.path, `_${helper}`, 'vue')
} }
}, },
}; }
function inferComponentName( function inferComponentName(
path: BabelCore.NodePath<BabelCore.types.VariableDeclarator> path: BabelCore.NodePath<BabelCore.types.VariableDeclarator>,
) { ) {
const id = path.get('id'); const id = path.get('id')
const init = path.get('init'); const init = path.get('init')
if (!id || !id.isIdentifier() || !init || !init.isCallExpression()) if (!id || !id.isIdentifier() || !init || !init.isCallExpression()) return
return;
if (!init.get('callee')?.isIdentifier({ name: 'defineComponent' })) if (!init.get('callee')?.isIdentifier({ name: 'defineComponent' })) return
return; if (!checkDefineComponent(init)) return
if (!checkDefineComponent(init)) return;
const nameProperty = t.objectProperty( const nameProperty = t.objectProperty(
t.identifier('name'), t.identifier('name'),
t.stringLiteral(id.node.name) t.stringLiteral(id.node.name),
); )
const { arguments: args } = init.node; const { arguments: args } = init.node
if (args.length === 0) return; if (args.length === 0) return
if (args.length === 1) { if (args.length === 1) {
init.node.arguments.push(t.objectExpression([])); init.node.arguments.push(t.objectExpression([]))
} }
args[1] = addProperty(t, args[1], nameProperty); args[1] = addProperty(t, args[1], nameProperty)
} }
function processProps( function processProps(
@@ -138,39 +136,39 @@ const plugin: (
options: options:
| BabelCore.types.ArgumentPlaceholder | BabelCore.types.ArgumentPlaceholder
| BabelCore.types.SpreadElement | BabelCore.types.SpreadElement
| BabelCore.types.Expression | BabelCore.types.Expression,
) { ) {
const props = comp.params[0]; const props = comp.params[0]
if (!props) return; if (!props) return
if (props.type === 'AssignmentPattern') { if (props.type === 'AssignmentPattern') {
if (generics) { if (generics) {
ctx!.propsTypeDecl = resolveTypeReference(generics); ctx!.propsTypeDecl = resolveTypeReference(generics)
} else { } else {
ctx!.propsTypeDecl = getTypeAnnotation(props.left); ctx!.propsTypeDecl = getTypeAnnotation(props.left)
} }
ctx!.propsRuntimeDefaults = props.right; ctx!.propsRuntimeDefaults = props.right
} else { } else {
if (generics) { if (generics) {
ctx!.propsTypeDecl = resolveTypeReference(generics); ctx!.propsTypeDecl = resolveTypeReference(generics)
} else { } else {
ctx!.propsTypeDecl = getTypeAnnotation(props); ctx!.propsTypeDecl = getTypeAnnotation(props)
} }
} }
if (!ctx!.propsTypeDecl) return; if (!ctx!.propsTypeDecl) return
const runtimeProps = extractRuntimeProps(ctx!); const runtimeProps = extractRuntimeProps(ctx!)
if (!runtimeProps) { if (!runtimeProps) {
return; return
} }
const ast = parseExpression(runtimeProps); const ast = parseExpression(runtimeProps)
return addProperty( return addProperty(
t, t,
options, options,
t.objectProperty(t.identifier('props'), ast) t.objectProperty(t.identifier('props'), ast),
); )
} }
function processEmits( function processEmits(
@@ -179,92 +177,92 @@ const plugin: (
options: options:
| BabelCore.types.ArgumentPlaceholder | BabelCore.types.ArgumentPlaceholder
| BabelCore.types.SpreadElement | BabelCore.types.SpreadElement
| BabelCore.types.Expression | BabelCore.types.Expression,
) { ) {
let emitType: BabelCore.types.Node | undefined; let emitType: BabelCore.types.Node | undefined
if (generics) { if (generics) {
emitType = resolveTypeReference(generics); emitType = resolveTypeReference(generics)
} }
const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1]); const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1])
if ( if (
!emitType && !emitType &&
setupCtx && setupCtx &&
t.isTSTypeReference(setupCtx) && t.isTSTypeReference(setupCtx) &&
t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' }) t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
) { ) {
emitType = setupCtx.typeParameters?.params[0]; emitType = setupCtx.typeParameters?.params[0]
} }
if (!emitType) return; if (!emitType) return
ctx!.emitsTypeDecl = emitType; ctx!.emitsTypeDecl = emitType
const runtimeEmits = extractRuntimeEmits(ctx!); const runtimeEmits = extractRuntimeEmits(ctx!)
const ast = t.arrayExpression( const ast = t.arrayExpression(
Array.from(runtimeEmits).map((e) => t.stringLiteral(e)) Array.from(runtimeEmits).map((e) => t.stringLiteral(e)),
); )
return addProperty( return addProperty(
t, t,
options, options,
t.objectProperty(t.identifier('emits'), ast) t.objectProperty(t.identifier('emits'), ast),
); )
} }
function resolveTypeReference(typeNode: BabelCore.types.TSType) { function resolveTypeReference(typeNode: BabelCore.types.TSType) {
if (!ctx) return; if (!ctx) return
if (t.isTSTypeReference(typeNode)) { if (t.isTSTypeReference(typeNode)) {
const typeName = getTypeReferenceName(typeNode); const typeName = getTypeReferenceName(typeNode)
if (typeName) { if (typeName) {
const typeDeclaration = findTypeDeclaration(typeName); const typeDeclaration = findTypeDeclaration(typeName)
if (typeDeclaration) { if (typeDeclaration) {
return typeDeclaration; return typeDeclaration
} }
} }
} }
return; return
} }
function getTypeReferenceName(typeRef: BabelCore.types.TSTypeReference) { function getTypeReferenceName(typeRef: BabelCore.types.TSTypeReference) {
if (t.isIdentifier(typeRef.typeName)) { if (t.isIdentifier(typeRef.typeName)) {
return typeRef.typeName.name; return typeRef.typeName.name
} else if (t.isTSQualifiedName(typeRef.typeName)) { } else if (t.isTSQualifiedName(typeRef.typeName)) {
const parts: string[] = []; const parts: string[] = []
let current: BabelCore.types.TSEntityName = typeRef.typeName; let current: BabelCore.types.TSEntityName = typeRef.typeName
while (t.isTSQualifiedName(current)) { while (t.isTSQualifiedName(current)) {
if (t.isIdentifier(current.right)) { if (t.isIdentifier(current.right)) {
parts.unshift(current.right.name); parts.unshift(current.right.name)
} }
current = current.left; current = current.left
} }
if (t.isIdentifier(current)) { if (t.isIdentifier(current)) {
parts.unshift(current.name); parts.unshift(current.name)
} }
return parts.join('.'); return parts.join('.')
} }
return null; return null
} }
function findTypeDeclaration(typeName: string) { function findTypeDeclaration(typeName: string) {
if (!ctx) return null; if (!ctx) return null
for (const statement of ctx.ast) { for (const statement of ctx.ast) {
if ( if (
t.isTSInterfaceDeclaration(statement) && t.isTSInterfaceDeclaration(statement) &&
statement.id.name === typeName statement.id.name === typeName
) { ) {
return t.tsTypeLiteral(statement.body.body); return t.tsTypeLiteral(statement.body.body)
} }
if ( if (
t.isTSTypeAliasDeclaration(statement) && t.isTSTypeAliasDeclaration(statement) &&
statement.id.name === typeName statement.id.name === typeName
) { ) {
return statement.typeAnnotation; return statement.typeAnnotation
} }
if (t.isExportNamedDeclaration(statement) && statement.declaration) { if (t.isExportNamedDeclaration(statement) && statement.declaration) {
@@ -272,22 +270,22 @@ const plugin: (
t.isTSInterfaceDeclaration(statement.declaration) && t.isTSInterfaceDeclaration(statement.declaration) &&
statement.declaration.id.name === typeName statement.declaration.id.name === typeName
) { ) {
return t.tsTypeLiteral(statement.declaration.body.body); return t.tsTypeLiteral(statement.declaration.body.body)
} }
if ( if (
t.isTSTypeAliasDeclaration(statement.declaration) && t.isTSTypeAliasDeclaration(statement.declaration) &&
statement.declaration.id.name === typeName statement.declaration.id.name === typeName
) { ) {
return statement.declaration.typeAnnotation; return statement.declaration.typeAnnotation
} }
} }
} }
return null; return null
} }
}); })
export default plugin; export default plugin
function getTypeAnnotation(node: BabelCore.types.Node) { function getTypeAnnotation(node: BabelCore.types.Node) {
if ( if (
@@ -295,33 +293,32 @@ function getTypeAnnotation(node: BabelCore.types.Node) {
node.typeAnnotation && node.typeAnnotation &&
node.typeAnnotation.type === 'TSTypeAnnotation' node.typeAnnotation.type === 'TSTypeAnnotation'
) { ) {
return node.typeAnnotation.typeAnnotation; return node.typeAnnotation.typeAnnotation
} }
} }
function checkDefineComponent( function checkDefineComponent(
path: BabelCore.NodePath<BabelCore.types.CallExpression> path: BabelCore.NodePath<BabelCore.types.CallExpression>,
) { ) {
const defineCompImport = const defineCompImport = path.scope.getBinding('defineComponent')?.path.parent
path.scope.getBinding('defineComponent')?.path.parent; if (!defineCompImport) return true
if (!defineCompImport) return true;
return ( return (
defineCompImport.type === 'ImportDeclaration' && defineCompImport.type === 'ImportDeclaration' &&
/^@?vue(\/|$)/.test(defineCompImport.source.value) /^@?vue(\/|$)/.test(defineCompImport.source.value)
); )
} }
function addProperty<T extends BabelCore.types.Node>( function addProperty<T extends BabelCore.types.Node>(
t: (typeof BabelCore)['types'], t: (typeof BabelCore)['types'],
object: T, object: T,
property: BabelCore.types.ObjectProperty property: BabelCore.types.ObjectProperty,
) { ) {
if (t.isObjectExpression(object)) { if (t.isObjectExpression(object)) {
object.properties.unshift(property); object.properties.unshift(property)
} else if (t.isExpression(object)) { } else if (t.isExpression(object)) {
return t.objectExpression([property, t.spreadElement(object)]); return t.objectExpression([property, t.spreadElement(object)])
} }
return object; return object
} }
export { plugin as 'module.exports' }; export { plugin as 'module.exports' }

View File

@@ -1,13 +1,13 @@
import { transformAsync } from '@babel/core'; import { transformAsync } from '@babel/core'
// @ts-expect-error missing types // @ts-expect-error missing types
import typescript from '@babel/plugin-syntax-typescript'; import typescript from '@babel/plugin-syntax-typescript'
import ResolveType from '../src'; import ResolveType from '../src'
async function transform(code: string): Promise<string> { async function transform(code: string): Promise<string> {
const result = await transformAsync(code, { const result = await transformAsync(code, {
plugins: [[typescript, { isTSX: true }], ResolveType], plugins: [[typescript, { isTSX: true }], ResolveType],
}); })
return result!.code!; return result!.code!
} }
describe('resolve type', () => { describe('resolve type', () => {
@@ -26,10 +26,10 @@ describe('resolve type', () => {
defineComponent((props: Props & Props2) => { defineComponent((props: Props & Props2) => {
return () => h('div', props.msg); return () => h('div', props.msg);
}) })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('with generic', async () => { test('with generic', async () => {
const result = await transform( const result = await transform(
@@ -42,10 +42,10 @@ describe('resolve type', () => {
defineComponent<Props>((props) => { defineComponent<Props>((props) => {
return () => h('div', props.msg); return () => h('div', props.msg);
}) })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('with static default value and generic', async () => { test('with static default value and generic', async () => {
const result = await transform( const result = await transform(
@@ -58,10 +58,10 @@ describe('resolve type', () => {
defineComponent<Props>((props = { msg: 'hello' }) => { defineComponent<Props>((props = { msg: 'hello' }) => {
return () => h('div', props.msg); return () => h('div', props.msg);
}) })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('with static default value', async () => { test('with static default value', async () => {
const result = await transform( const result = await transform(
@@ -70,10 +70,10 @@ describe('resolve type', () => {
defineComponent((props: { msg?: string } = { msg: 'hello' }) => { defineComponent((props: { msg?: string } = { msg: 'hello' }) => {
return () => h('div', props.msg); return () => h('div', props.msg);
}) })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('with dynamic default value', async () => { test('with dynamic default value', async () => {
const result = await transform( const result = await transform(
@@ -83,11 +83,11 @@ describe('resolve type', () => {
defineComponent((props: { msg?: string } = defaults) => { defineComponent((props: { msg?: string } = defaults) => {
return () => h('div', props.msg); return () => h('div', props.msg);
}) })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
}); })
describe('runtime emits', () => { describe('runtime emits', () => {
test('basic', async () => { test('basic', async () => {
@@ -103,10 +103,10 @@ describe('resolve type', () => {
return () => {}; return () => {};
} }
); );
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('with generic emit type', async () => { test('with generic emit type', async () => {
const result = await transform( const result = await transform(
@@ -122,11 +122,11 @@ describe('resolve type', () => {
return () => {}; return () => {};
} }
); );
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
}); })
test('w/ tsx', async () => { test('w/ tsx', async () => {
const result = await transform( const result = await transform(
@@ -135,10 +135,10 @@ describe('resolve type', () => {
defineComponent(() => { defineComponent(() => {
return () => <div/ >; return () => <div/ >;
}); });
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
describe('defineComponent scope', () => { describe('defineComponent scope', () => {
test('fake', async () => { test('fake', async () => {
@@ -148,10 +148,10 @@ describe('resolve type', () => {
defineComponent((props: { msg?: string }) => { defineComponent((props: { msg?: string }) => {
return () => <div/ >; return () => <div/ >;
}); });
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('w/o import', async () => { test('w/o import', async () => {
const result = await transform( const result = await transform(
@@ -159,10 +159,10 @@ describe('resolve type', () => {
defineComponent((props: { msg?: string }) => { defineComponent((props: { msg?: string }) => {
return () => <div/ >; return () => <div/ >;
}); });
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('import sub-package', async () => { test('import sub-package', async () => {
const result = await transform( const result = await transform(
@@ -171,11 +171,11 @@ describe('resolve type', () => {
defineComponent((props: { msg?: string }) => { defineComponent((props: { msg?: string }) => {
return () => <div/ >; return () => <div/ >;
}); });
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
}); })
describe('infer component name', () => { describe('infer component name', () => {
test('no options', async () => { test('no options', async () => {
@@ -183,39 +183,39 @@ describe('resolve type', () => {
` `
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
const Foo = defineComponent(() => {}) const Foo = defineComponent(() => {})
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('object options', async () => { test('object options', async () => {
const result = await transform( const result = await transform(
` `
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
const Foo = defineComponent(() => {}, { foo: 'bar' }) const Foo = defineComponent(() => {}, { foo: 'bar' })
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('identifier options', async () => { test('identifier options', async () => {
const result = await transform( const result = await transform(
` `
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
const Foo = defineComponent(() => {}, opts) const Foo = defineComponent(() => {}, opts)
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
test('rest param', async () => { test('rest param', async () => {
const result = await transform( const result = await transform(
` `
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
const Foo = defineComponent(() => {}, ...args) const Foo = defineComponent(() => {}, ...args)
` `,
); )
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot()
}); })
}); })
}); })

View File

@@ -1,13 +1,13 @@
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
// @ts-ignore // @ts-ignore
self.MonacoEnvironment = { self.MonacoEnvironment = {
globalAPI: true, globalAPI: true,
getWorker(_: any, label: string) { getWorker(_: any, label: string) {
if (label === 'typescript' || label === 'javascript') { if (label === 'typescript' || label === 'javascript') {
return new tsWorker(); return new tsWorker()
} }
return new editorWorker(); return new editorWorker()
}, },
}; }

View File

@@ -1,30 +1,30 @@
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor'
import { watchEffect } from 'vue'; import { watchEffect } from 'vue'
import { transform } from '@babel/standalone'; import { transform } from '@babel/standalone'
import babelPluginJsx from '@vue/babel-plugin-jsx'; import babelPluginJsx from '@vue/babel-plugin-jsx'
// @ts-expect-error missing types // @ts-expect-error missing types
import typescript from '@babel/plugin-syntax-typescript'; import typescript from '@babel/plugin-syntax-typescript'
import { import {
type VueJSXPluginOptions, type VueJSXPluginOptions,
compilerOptions, compilerOptions,
initOptions, initOptions,
} from './options'; } from './options'
import './editor.worker'; import './editor.worker'
import './index.css'; import './index.css'
main(); main()
interface PersistedState { interface PersistedState {
src: string; src: string
options: VueJSXPluginOptions; options: VueJSXPluginOptions
} }
function main() { function main() {
const persistedState: PersistedState = JSON.parse( const persistedState: PersistedState = JSON.parse(
localStorage.getItem('state') || '{}' localStorage.getItem('state') || '{}',
); )
Object.assign(compilerOptions, persistedState.options); Object.assign(compilerOptions, persistedState.options)
const sharedEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions = const sharedEditorOptions: monaco.editor.IStandaloneEditorConstructionOptions =
{ {
@@ -39,11 +39,11 @@ function main() {
minimap: { minimap: {
enabled: false, enabled: false,
}, },
}; }
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true, noSemanticValidation: true,
}); })
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
allowJs: true, allowJs: true,
allowNonTsExtensions: true, allowNonTsExtensions: true,
@@ -51,7 +51,7 @@ function main() {
target: monaco.languages.typescript.ScriptTarget.Latest, target: monaco.languages.typescript.ScriptTarget.Latest,
module: monaco.languages.typescript.ModuleKind.ESNext, module: monaco.languages.typescript.ModuleKind.ESNext,
isolatedModules: true, isolatedModules: true,
}); })
const editor = monaco.editor.create(document.getElementById('source')!, { const editor = monaco.editor.create(document.getElementById('source')!, {
...sharedEditorOptions, ...sharedEditorOptions,
@@ -62,9 +62,9 @@ function main() {
const App = defineComponent((props) => <div>Hello World</div>)`, const App = defineComponent((props) => <div>Hello World</div>)`,
'typescript', 'typescript',
monaco.Uri.parse('file:///app.tsx') monaco.Uri.parse('file:///app.tsx'),
), ),
}); })
const output = monaco.editor.create(document.getElementById('output')!, { const output = monaco.editor.create(document.getElementById('output')!, {
readOnly: true, readOnly: true,
@@ -72,19 +72,19 @@ const App = defineComponent((props) => <div>Hello World</div>)`,
model: monaco.editor.createModel( model: monaco.editor.createModel(
'', '',
'typescript', 'typescript',
monaco.Uri.parse('file:///output.tsx') monaco.Uri.parse('file:///output.tsx'),
), ),
}); })
const reCompile = () => { const reCompile = () => {
const src = editor.getValue(); const src = editor.getValue()
const state = JSON.stringify({ const state = JSON.stringify({
src, src,
options: compilerOptions, options: compilerOptions,
}); })
localStorage.setItem('state', state); localStorage.setItem('state', state)
window.location.hash = encodeURIComponent(src); window.location.hash = encodeURIComponent(src)
console.clear(); console.clear()
try { try {
const res = transform(src, { const res = transform(src, {
babelrc: false, babelrc: false,
@@ -93,37 +93,37 @@ const App = defineComponent((props) => <div>Hello World</div>)`,
[typescript, { isTSX: true }], [typescript, { isTSX: true }],
], ],
ast: true, ast: true,
}); })
console.log('AST', res.ast!); console.log('AST', res.ast!)
output.setValue(res.code!); output.setValue(res.code!)
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err)
output.setValue(err.message!); output.setValue(err.message!)
} }
}; }
// handle resize // handle resize
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
editor.layout(); editor.layout()
output.layout(); output.layout()
}); })
initOptions(); initOptions()
watchEffect(reCompile); watchEffect(reCompile)
// update compile output when input changes // update compile output when input changes
editor.onDidChangeModelContent(debounce(reCompile)); editor.onDidChangeModelContent(debounce(reCompile))
} }
function debounce<T extends (...args: any[]) => any>(fn: T, delay = 300): T { function debounce<T extends (...args: any[]) => any>(fn: T, delay = 300): T {
let prevTimer: number | null = null; let prevTimer: number | null = null
return ((...args: any[]) => { return ((...args: any[]) => {
if (prevTimer) { if (prevTimer) {
clearTimeout(prevTimer); clearTimeout(prevTimer)
} }
prevTimer = window.setTimeout(() => { prevTimer = window.setTimeout(() => {
fn(...args); fn(...args)
prevTimer = null; prevTimer = null
}, delay); }, delay)
}) as any; }) as any
} }

View File

@@ -1,7 +1,7 @@
import { createApp, defineComponent, reactive } from 'vue'; import { createApp, defineComponent, reactive } from 'vue'
import { type VueJSXPluginOptions } from '@vue/babel-plugin-jsx'; import { type VueJSXPluginOptions } from '@vue/babel-plugin-jsx'
export { VueJSXPluginOptions }; export { VueJSXPluginOptions }
export const compilerOptions: VueJSXPluginOptions = reactive({ export const compilerOptions: VueJSXPluginOptions = reactive({
mergeProps: true, mergeProps: true,
@@ -9,7 +9,7 @@ export const compilerOptions: VueJSXPluginOptions = reactive({
transformOn: false, transformOn: false,
enableObjectSlots: true, enableObjectSlots: true,
resolveType: false, resolveType: false,
}); })
const App = defineComponent({ const App = defineComponent({
setup() { setup() {
@@ -34,7 +34,7 @@ const App = defineComponent({
onChange={(e: Event) => { onChange={(e: Event) => {
compilerOptions.mergeProps = ( compilerOptions.mergeProps = (
e.target as HTMLInputElement e.target as HTMLInputElement
).checked; ).checked
}} }}
/> />
<label for="mergeProps">mergeProps</label> <label for="mergeProps">mergeProps</label>
@@ -49,7 +49,7 @@ const App = defineComponent({
onChange={(e: Event) => { onChange={(e: Event) => {
compilerOptions.optimize = ( compilerOptions.optimize = (
e.target as HTMLInputElement e.target as HTMLInputElement
).checked; ).checked
}} }}
/> />
<label for="optimize">optimize</label> <label for="optimize">optimize</label>
@@ -64,7 +64,7 @@ const App = defineComponent({
onChange={(e: Event) => { onChange={(e: Event) => {
compilerOptions.transformOn = ( compilerOptions.transformOn = (
e.target as HTMLInputElement e.target as HTMLInputElement
).checked; ).checked
}} }}
/> />
<label for="transformOn">transformOn</label> <label for="transformOn">transformOn</label>
@@ -79,7 +79,7 @@ const App = defineComponent({
onChange={(e: Event) => { onChange={(e: Event) => {
compilerOptions.enableObjectSlots = ( compilerOptions.enableObjectSlots = (
e.target as HTMLInputElement e.target as HTMLInputElement
).checked; ).checked
}} }}
/> />
<label for="enableObjectSlots">enableObjectSlots</label> <label for="enableObjectSlots">enableObjectSlots</label>
@@ -94,7 +94,7 @@ const App = defineComponent({
onChange={(e: Event) => { onChange={(e: Event) => {
compilerOptions.resolveType = ( compilerOptions.resolveType = (
e.target as HTMLInputElement e.target as HTMLInputElement
).checked; ).checked
}} }}
/> />
<label for="resolveType">resolveType</label> <label for="resolveType">resolveType</label>
@@ -102,10 +102,10 @@ const App = defineComponent({
</ul> </ul>
</div> </div>
</>, </>,
]; ]
}, },
}); })
export function initOptions() { export function initOptions() {
createApp(App).mount(document.getElementById('header')!); createApp(App).mount(document.getElementById('header')!)
} }

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
import VueJSX from '@vitejs/plugin-vue-jsx'; import VueJSX from '@vitejs/plugin-vue-jsx'
export default defineConfig({ export default defineConfig({
resolve: { resolve: {
@@ -11,4 +11,4 @@ export default defineConfig({
'process.env.BABEL_TYPES_8_BREAKING': 'false', 'process.env.BABEL_TYPES_8_BREAKING': 'false',
}, },
plugins: [VueJSX()], plugins: [VueJSX()],
}); })

View File

@@ -1,4 +1,4 @@
import { defineConfig } from 'tsdown'; import { defineConfig } from 'tsdown'
export default defineConfig({ export default defineConfig({
workspace: [ workspace: [
@@ -14,4 +14,4 @@ export default defineConfig({
devExports: 'dev', devExports: 'dev',
}, },
fixedExtension: true, fixedExtension: true,
}); })

View File

@@ -1,6 +1,6 @@
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config'
import { babel } from '@rollup/plugin-babel'; import { babel } from '@rollup/plugin-babel'
import Jsx from './packages/babel-plugin-jsx/src'; import Jsx from './packages/babel-plugin-jsx/src'
export default defineConfig({ export default defineConfig({
resolve: { resolve: {
@@ -25,4 +25,4 @@ export default defineConfig({
globals: true, globals: true,
environment: 'jsdom', environment: 'jsdom',
}, },
}); })