;
161 | });
162 | `,
163 | )
164 | expect(result).toMatchSnapshot()
165 | })
166 |
167 | test('import sub-package', async () => {
168 | const result = await transform(
169 | `
170 | import { defineComponent } from 'vue/dist/vue.esm-bundler';
171 | defineComponent((props: { msg?: string }) => {
172 | return () =>
;
173 | });
174 | `,
175 | )
176 | expect(result).toMatchSnapshot()
177 | })
178 | })
179 |
180 | describe('infer component name', () => {
181 | test('no options', async () => {
182 | const result = await transform(
183 | `
184 | import { defineComponent } from 'vue';
185 | const Foo = defineComponent(() => {})
186 | `,
187 | )
188 | expect(result).toMatchSnapshot()
189 | })
190 |
191 | test('object options', async () => {
192 | const result = await transform(
193 | `
194 | import { defineComponent } from 'vue';
195 | const Foo = defineComponent(() => {}, { foo: 'bar' })
196 | `,
197 | )
198 | expect(result).toMatchSnapshot()
199 | })
200 |
201 | test('identifier options', async () => {
202 | const result = await transform(
203 | `
204 | import { defineComponent } from 'vue';
205 | const Foo = defineComponent(() => {}, opts)
206 | `,
207 | )
208 | expect(result).toMatchSnapshot()
209 | })
210 |
211 | test('rest param', async () => {
212 | const result = await transform(
213 | `
214 | import { defineComponent } from 'vue';
215 | const Foo = defineComponent(() => {}, ...args)
216 | `,
217 | )
218 | expect(result).toMatchSnapshot()
219 | })
220 | })
221 | })
222 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/src/parseDirectives.ts:
--------------------------------------------------------------------------------
1 | import t from '@babel/types'
2 | import { createIdentifier } from './utils'
3 | import type { State } from './interface'
4 | import type { NodePath } from '@babel/traverse'
5 |
6 | export type Tag =
7 | | t.Identifier
8 | | t.MemberExpression
9 | | t.StringLiteral
10 | | t.CallExpression
11 |
12 | /**
13 | * Get JSX element type
14 | *
15 | * @param path Path
16 | */
17 | const getType = (path: NodePath) => {
18 | const typePath = path.get('attributes').find((attribute) => {
19 | if (!attribute.isJSXAttribute()) {
20 | return false
21 | }
22 | return (
23 | attribute.get('name').isJSXIdentifier() &&
24 | (attribute.get('name') as NodePath).node.name === 'type'
25 | )
26 | }) as NodePath | undefined
27 |
28 | return typePath ? typePath.get('value').node : null
29 | }
30 |
31 | const parseModifiers = (value: any): string[] =>
32 | t.isArrayExpression(value)
33 | ? value.elements
34 | .map((el) => (t.isStringLiteral(el) ? el.value : ''))
35 | .filter(Boolean)
36 | : []
37 |
38 | const parseDirectives = (params: {
39 | name: string
40 | path: NodePath
41 | value: t.Expression | null
42 | state: State
43 | tag: Tag
44 | isComponent: boolean
45 | }) => {
46 | const { path, value, state, tag, isComponent } = params
47 | const args: Array = []
48 | const vals: t.Expression[] = []
49 | const modifiersSet: Set[] = []
50 |
51 | let directiveName
52 | let directiveArgument
53 | let directiveModifiers
54 | if ('namespace' in path.node.name) {
55 | ;[directiveName, directiveArgument] = params.name.split(':')
56 | directiveName = path.node.name.namespace.name
57 | directiveArgument = path.node.name.name.name
58 | directiveModifiers = directiveArgument.split('_').slice(1)
59 | } else {
60 | const underscoreModifiers = params.name.split('_')
61 | directiveName = underscoreModifiers.shift() || ''
62 | directiveModifiers = underscoreModifiers
63 | }
64 | directiveName = directiveName
65 | .replace(/^v/, '')
66 | .replace(/^-/, '')
67 | .replace(/^\S/, (s: string) => s.toLowerCase())
68 |
69 | if (directiveArgument) {
70 | args.push(t.stringLiteral(directiveArgument.split('_')[0]))
71 | }
72 |
73 | const isVModels = directiveName === 'models'
74 | const isVModel = directiveName === 'model'
75 | if (isVModel && !path.get('value').isJSXExpressionContainer()) {
76 | throw new Error('You have to use JSX Expression inside your v-model')
77 | }
78 |
79 | if (isVModels && !isComponent) {
80 | throw new Error('v-models can only use in custom components')
81 | }
82 |
83 | const shouldResolve =
84 | !['html', 'text', 'model', 'slots', 'models'].includes(directiveName) ||
85 | (isVModel && !isComponent)
86 |
87 | let modifiers = directiveModifiers
88 |
89 | if (t.isArrayExpression(value)) {
90 | const elementsList = isVModels ? value.elements! : [value]
91 |
92 | elementsList.forEach((element) => {
93 | if (isVModels && !t.isArrayExpression(element)) {
94 | throw new Error('You should pass a Two-dimensional Arrays to v-models')
95 | }
96 |
97 | const { elements } = element as t.ArrayExpression
98 | const [first, second, third] = elements
99 |
100 | if (
101 | second &&
102 | !t.isArrayExpression(second) &&
103 | !t.isSpreadElement(second)
104 | ) {
105 | args.push(second)
106 | modifiers = parseModifiers(third as t.ArrayExpression)
107 | } else if (t.isArrayExpression(second)) {
108 | if (!shouldResolve) {
109 | args.push(t.nullLiteral())
110 | }
111 | modifiers = parseModifiers(second)
112 | } else if (!shouldResolve) {
113 | // work as v-model={[value]} or v-models={[[value]]}
114 | args.push(t.nullLiteral())
115 | }
116 | modifiersSet.push(new Set(modifiers))
117 | vals.push(first as t.Expression)
118 | })
119 | } else if (isVModel && !shouldResolve) {
120 | // work as v-model={value}
121 | args.push(t.nullLiteral())
122 | modifiersSet.push(new Set(directiveModifiers))
123 | } else {
124 | modifiersSet.push(new Set(directiveModifiers))
125 | }
126 |
127 | return {
128 | directiveName,
129 | modifiers: modifiersSet,
130 | values: vals.length ? vals : [value],
131 | args,
132 | directive: shouldResolve
133 | ? ([
134 | resolveDirective(path, state, tag, directiveName),
135 | vals[0] || value,
136 | modifiersSet[0]?.size
137 | ? args[0] || t.unaryExpression('void', t.numericLiteral(0), true)
138 | : args[0],
139 | !!modifiersSet[0]?.size &&
140 | t.objectExpression(
141 | [...modifiersSet[0]].map((modifier) =>
142 | t.objectProperty(
143 | t.identifier(modifier),
144 | t.booleanLiteral(true),
145 | ),
146 | ),
147 | ),
148 | ].filter(Boolean) as t.Expression[])
149 | : undefined,
150 | }
151 | }
152 |
153 | const resolveDirective = (
154 | path: NodePath,
155 | state: State,
156 | tag: Tag,
157 | directiveName: string,
158 | ) => {
159 | if (directiveName === 'show') {
160 | return createIdentifier(state, 'vShow')
161 | }
162 | if (directiveName === 'model') {
163 | let modelToUse
164 | const type = getType(path.parentPath as NodePath)
165 | switch ((tag as t.StringLiteral).value) {
166 | case 'select':
167 | modelToUse = createIdentifier(state, 'vModelSelect')
168 | break
169 | case 'textarea':
170 | modelToUse = createIdentifier(state, 'vModelText')
171 | break
172 | default:
173 | if (t.isStringLiteral(type) || !type) {
174 | switch ((type as t.StringLiteral)?.value) {
175 | case 'checkbox':
176 | modelToUse = createIdentifier(state, 'vModelCheckbox')
177 | break
178 | case 'radio':
179 | modelToUse = createIdentifier(state, 'vModelRadio')
180 | break
181 | default:
182 | modelToUse = createIdentifier(state, 'vModelText')
183 | }
184 | } else {
185 | modelToUse = createIdentifier(state, 'vModelDynamic')
186 | }
187 | }
188 | return modelToUse
189 | }
190 | const referenceName = `v${directiveName[0].toUpperCase()}${directiveName.slice(1)}`
191 | if (path.scope.references[referenceName]) {
192 | return t.identifier(referenceName)
193 | }
194 | return t.callExpression(createIdentifier(state, 'resolveDirective'), [
195 | t.stringLiteral(directiveName),
196 | ])
197 | }
198 |
199 | export default parseDirectives
200 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/src/index.ts:
--------------------------------------------------------------------------------
1 | import { addNamed, addNamespace, isModule } from '@babel/helper-module-imports'
2 | import { declare } from '@babel/helper-plugin-utils'
3 | // @ts-expect-error
4 | import _syntaxJsx from '@babel/plugin-syntax-jsx'
5 | import _template from '@babel/template'
6 | import t from '@babel/types'
7 | import ResolveType from '@vue/babel-plugin-resolve-type'
8 | import sugarFragment from './sugar-fragment'
9 | import transformVueJSX from './transform-vue-jsx'
10 | import type { State, VueJSXPluginOptions } from './interface'
11 | import type * as BabelCore from '@babel/core'
12 | import type { NodePath, Visitor } from '@babel/traverse'
13 |
14 | export type { VueJSXPluginOptions }
15 |
16 | const hasJSX = (parentPath: NodePath) => {
17 | let fileHasJSX = false
18 | parentPath.traverse({
19 | JSXElement(path) {
20 | // skip ts error
21 | fileHasJSX = true
22 | path.stop()
23 | },
24 | JSXFragment(path) {
25 | fileHasJSX = true
26 | path.stop()
27 | },
28 | })
29 |
30 | return fileHasJSX
31 | }
32 |
33 | const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+(\S+)/
34 |
35 | /* #__NO_SIDE_EFFECTS__ */
36 | function interopDefault(m: any) {
37 | return m.default || m
38 | }
39 |
40 | const syntaxJsx = /*#__PURE__*/ interopDefault(_syntaxJsx)
41 | const template = /*#__PURE__*/ interopDefault(_template)
42 |
43 | const plugin: (
44 | api: object,
45 | options: VueJSXPluginOptions | null | undefined,
46 | dirname: string,
47 | ) => BabelCore.PluginObj = declare<
48 | VueJSXPluginOptions,
49 | BabelCore.PluginObj
50 | >((api, opt, dirname) => {
51 | const { types } = api
52 | let resolveType: BabelCore.PluginObj | undefined
53 | if (opt.resolveType) {
54 | if (typeof opt.resolveType === 'boolean') opt.resolveType = {}
55 | resolveType = ResolveType(api, opt.resolveType, dirname)
56 | }
57 | return {
58 | ...resolveType,
59 | name: 'babel-plugin-jsx',
60 | inherits: /*#__PURE__*/ interopDefault(syntaxJsx),
61 | visitor: {
62 | ...(resolveType?.visitor as Visitor),
63 | ...transformVueJSX,
64 | ...sugarFragment,
65 | Program: {
66 | enter(path, state) {
67 | if (hasJSX(path)) {
68 | const importNames = [
69 | 'createVNode',
70 | 'Fragment',
71 | 'resolveComponent',
72 | 'withDirectives',
73 | 'vShow',
74 | 'vModelSelect',
75 | 'vModelText',
76 | 'vModelCheckbox',
77 | 'vModelRadio',
78 | 'vModelText',
79 | 'vModelDynamic',
80 | 'resolveDirective',
81 | 'mergeProps',
82 | 'createTextVNode',
83 | 'isVNode',
84 | ]
85 | if (isModule(path)) {
86 | // import { createVNode } from "vue";
87 | const importMap: Record<
88 | string,
89 | t.MemberExpression | t.Identifier
90 | > = {}
91 | importNames.forEach((name) => {
92 | state.set(name, () => {
93 | if (importMap[name]) {
94 | return types.cloneNode(importMap[name])
95 | }
96 | const identifier = addNamed(path, name, 'vue', {
97 | ensureLiveReference: true,
98 | })
99 | importMap[name] = identifier
100 | return identifier
101 | })
102 | })
103 | const { enableObjectSlots = true } = state.opts
104 | if (enableObjectSlots) {
105 | state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
106 | if (importMap.runtimeIsSlot) {
107 | return importMap.runtimeIsSlot
108 | }
109 | const { name: isVNodeName } = state.get(
110 | 'isVNode',
111 | )() as t.Identifier
112 | const isSlot = path.scope.generateUidIdentifier('isSlot')
113 | const ast = template.ast`
114 | function ${isSlot.name}(s) {
115 | return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${isVNodeName}(s));
116 | }
117 | `
118 | const lastImport = (path.get('body') as NodePath[]).findLast(
119 | (p) => p.isImportDeclaration(),
120 | )
121 | if (lastImport) {
122 | lastImport.insertAfter(ast)
123 | }
124 | importMap.runtimeIsSlot = isSlot
125 | return isSlot
126 | })
127 | }
128 | } else {
129 | // var _vue = require('vue');
130 | let sourceName: t.Identifier
131 | importNames.forEach((name) => {
132 | state.set(name, () => {
133 | if (!sourceName) {
134 | sourceName = addNamespace(path, 'vue', {
135 | ensureLiveReference: true,
136 | })
137 | }
138 | return t.memberExpression(sourceName, t.identifier(name))
139 | })
140 | })
141 |
142 | const helpers: Record = {}
143 |
144 | const { enableObjectSlots = true } = state.opts
145 | if (enableObjectSlots) {
146 | state.set('@vue/babel-plugin-jsx/runtimeIsSlot', () => {
147 | if (helpers.runtimeIsSlot) {
148 | return helpers.runtimeIsSlot
149 | }
150 | const isSlot = path.scope.generateUidIdentifier('isSlot')
151 | const { object: objectName } = state.get(
152 | 'isVNode',
153 | )() as t.MemberExpression
154 | const ast = template.ast`
155 | function ${isSlot.name}(s) {
156 | return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${
157 | (objectName as t.Identifier).name
158 | }.isVNode(s));
159 | }
160 | `
161 |
162 | const nodePaths = path.get('body') as NodePath[]
163 | const lastImport = nodePaths.findLast(
164 | (p) =>
165 | p.isVariableDeclaration() &&
166 | p.node.declarations.some(
167 | (d) => (d.id as t.Identifier)?.name === sourceName.name,
168 | ),
169 | )
170 | if (lastImport) {
171 | lastImport.insertAfter(ast)
172 | }
173 | return isSlot
174 | })
175 | }
176 | }
177 |
178 | const {
179 | opts: { pragma = '' },
180 | file,
181 | } = state
182 |
183 | if (pragma) {
184 | state.set('createVNode', () => t.identifier(pragma))
185 | }
186 |
187 | if (file.ast.comments) {
188 | for (const comment of file.ast.comments) {
189 | const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value)
190 | if (jsxMatches) {
191 | state.set('createVNode', () => t.identifier(jsxMatches[1]))
192 | }
193 | }
194 | }
195 | }
196 | },
197 | },
198 | },
199 | }
200 | })
201 |
202 | export default plugin
203 | export { plugin as 'module.exports' }
204 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/README-zh_CN.md:
--------------------------------------------------------------------------------
1 | # Vue 3 Babel JSX 插件
2 |
3 | [](https://www.npmjs.com/package/@vue/babel-plugin-jsx)
4 | [](https://github.com/actions-cool/issues-helper)
5 |
6 | 以 JSX 的方式来编写 Vue 代码
7 |
8 | [English](/packages/babel-plugin-jsx/README.md) | 简体中文
9 |
10 | ## 安装
11 |
12 | 安装插件
13 |
14 | ```bash
15 | npm install @vue/babel-plugin-jsx -D
16 | ```
17 |
18 | 配置 Babel
19 |
20 | ```json
21 | {
22 | "plugins": ["@vue/babel-plugin-jsx"]
23 | }
24 | ```
25 |
26 | ## 使用
27 |
28 | ### 参数
29 |
30 | #### transformOn
31 |
32 | Type: `boolean`
33 |
34 | Default: `false`
35 |
36 | 把 `on: { click: xx }` 转成 `onClick: xxx`
37 |
38 | #### optimize
39 |
40 | Type: `boolean`
41 |
42 | Default: `false`
43 |
44 | 开启此选项后,JSX 插件会尝试使用 [`PatchFlags`](https://cn.vuejs.org/guide/extras/rendering-mechanism#patch-flags) 和 [`SlotFlags`](https://github.com/vuejs/core/blob/v3.5.13/packages/runtime-core/src/componentSlots.ts#L69-L77) 来优化运行时代码,从而提升渲染性能。需要注意的是,JSX 的灵活性远高于模板语法,这使得编译优化的可能性相对有限,其优化效果会比 Vue 官方模板编译器更为有限。
45 |
46 | 优化后的代码会选择性地跳过一些重渲染操作以提高性能。因此,建议在开启此选项后对应用进行完整的测试,确保所有功能都能正常工作。
47 |
48 | #### isCustomElement
49 |
50 | Type: `(tag: string) => boolean`
51 |
52 | Default: `undefined`
53 |
54 | 自定义元素
55 |
56 | #### mergeProps
57 |
58 | Type: `boolean`
59 |
60 | Default: `true`
61 |
62 | 合并 class / style / onXXX handlers
63 |
64 | #### enableObjectSlots
65 |
66 | 使用 `enableObjectSlots` (文档下面会提到)。虽然在 JSX 中比较好使,但是会增加一些 `_isSlot` 的运行时条件判断,这会增加你的项目体积。即使你关闭了 `enableObjectSlots`,`v-slots` 还是可以使用
67 |
68 | #### pragma
69 |
70 | Type: `string`
71 |
72 | Default: `createVNode`
73 |
74 | 替换编译 JSX 表达式的时候使用的函数
75 |
76 | #### resolveType
77 |
78 | Type: `boolean`
79 |
80 | Default: `false`
81 |
82 | (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`). This is an experimental feature and may not work in all cases.
83 |
84 | ## 表达式
85 |
86 | ### 内容
87 |
88 | 函数式组件
89 |
90 | ```jsx
91 | const App = () =>
92 | ```
93 |
94 | 在 render 中使用
95 |
96 | ```jsx
97 | const App = {
98 | render() {
99 | return Vue 3.0
100 | },
101 | }
102 | ```
103 |
104 | ```jsx
105 | import { defineComponent, withModifiers } from 'vue'
106 |
107 | const App = defineComponent({
108 | setup() {
109 | const count = ref(0)
110 |
111 | const inc = () => {
112 | count.value++
113 | }
114 |
115 | return () => {count.value}
116 | },
117 | })
118 | ```
119 |
120 | Fragment
121 |
122 | ```jsx
123 | const App = () => (
124 | <>
125 | I'm
126 | Fragment
127 | >
128 | )
129 | ```
130 |
131 | ### Attributes / Props
132 |
133 | ```jsx
134 | const App = () =>
135 | ```
136 |
137 | 动态绑定:
138 |
139 | ```jsx
140 | const placeholderText = 'email'
141 | const App = () =>
142 | ```
143 |
144 | ### 指令
145 |
146 | #### v-show
147 |
148 | ```jsx
149 | const App = {
150 | data() {
151 | return { visible: true }
152 | },
153 | render() {
154 | return
155 | },
156 | }
157 | ```
158 |
159 | #### v-model
160 |
161 | > 注意:如果想要使用 `arg`, 第二个参数需要为字符串
162 |
163 | ```jsx
164 |
165 | ```
166 |
167 | ```jsx
168 |
169 | ```
170 |
171 | ```jsx
172 | ;
173 | // 或者
174 | ;
175 | ```
176 |
177 | ```jsx
178 | ;
179 | // 或者
180 | ;
181 | ```
182 |
183 | 会编译成:
184 |
185 | ```js
186 | h(A, {
187 | argument: val,
188 | argumentModifiers: {
189 | modifier: true,
190 | },
191 | 'onUpdate:argument': ($event) => (val = $event),
192 | })
193 | ```
194 |
195 | #### v-models (从 1.1.0 开始不推荐使用)
196 |
197 | > 注意: 你应该传递一个二维数组给 v-models。
198 |
199 | ```jsx
200 |
201 | ```
202 |
203 | ```jsx
204 |
210 | ```
211 |
212 | ```jsx
213 |
219 | ```
220 |
221 | 会编译成:
222 |
223 | ```js
224 | h(A, {
225 | modelValue: foo,
226 | modelModifiers: {
227 | modifier: true,
228 | },
229 | 'onUpdate:modelValue': ($event) => (foo = $event),
230 | bar,
231 | barModifiers: {
232 | modifier: true,
233 | },
234 | 'onUpdate:bar': ($event) => (bar = $event),
235 | })
236 | ```
237 |
238 | #### 自定义指令
239 |
240 | 只有 argument 的时候推荐使用
241 |
242 | ```jsx
243 | const App = {
244 | directives: { custom: customDirective },
245 | setup() {
246 | return () =>
247 | },
248 | }
249 | ```
250 |
251 | ```jsx
252 | const App = {
253 | directives: { custom: customDirective },
254 | setup() {
255 | return () =>
256 | },
257 | }
258 | ```
259 |
260 | ### 插槽
261 |
262 | > 注意: 在 `jsx` 中,应该使用 **`v-slots`** 代替 _`v-slot`_
263 |
264 | ```jsx
265 | const A = (props, { slots }) => (
266 | <>
267 | {slots.default ? slots.default() : 'foo'}
268 | {slots.bar?.()}
269 | >
270 | )
271 |
272 | const App = {
273 | setup() {
274 | const slots = {
275 | bar: () => B,
276 | }
277 | return () => (
278 |
279 | A
280 |
281 | )
282 | },
283 | }
284 |
285 | // or
286 |
287 | const App2 = {
288 | setup() {
289 | const slots = {
290 | default: () => A
,
291 | bar: () => B,
292 | }
293 | return () =>
294 | },
295 | }
296 |
297 | // 或者,当 `enableObjectSlots` 不是 `false` 时,您可以使用对象插槽
298 | const App3 = {
299 | setup() {
300 | return () => (
301 | <>
302 |
303 | {{
304 | default: () => A
,
305 | bar: () => B,
306 | }}
307 |
308 | {() => 'foo'}
309 | >
310 | )
311 | },
312 | }
313 | ```
314 |
315 | ### 在 TypeScript 中使用
316 |
317 | `tsconfig.json`:
318 |
319 | ```json
320 | {
321 | "compilerOptions": {
322 | "jsx": "preserve"
323 | }
324 | }
325 | ```
326 |
327 | ## 谁在使用
328 |
329 |
378 |
379 | ## 兼容性
380 |
381 | 要求:
382 |
383 | - **Babel 7+**
384 | - **Vue 3+**
385 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/README.md:
--------------------------------------------------------------------------------
1 | # Babel Plugin JSX for Vue 3
2 |
3 | [](https://www.npmjs.com/package/@vue/babel-plugin-jsx)
4 | [](https://github.com/actions-cool/issues-helper)
5 |
6 | To add Vue JSX support.
7 |
8 | English | [简体中文](/packages/babel-plugin-jsx/README-zh_CN.md)
9 |
10 | ## Installation
11 |
12 | Install the plugin with:
13 |
14 | ```bash
15 | npm install @vue/babel-plugin-jsx -D
16 | ```
17 |
18 | Then add the plugin to your babel config:
19 |
20 | ```json
21 | {
22 | "plugins": ["@vue/babel-plugin-jsx"]
23 | }
24 | ```
25 |
26 | ## Usage
27 |
28 | ### options
29 |
30 | #### transformOn
31 |
32 | Type: `boolean`
33 |
34 | Default: `false`
35 |
36 | transform `on: { click: xx }` to `onClick: xxx`
37 |
38 | #### optimize
39 |
40 | Type: `boolean`
41 |
42 | Default: `false`
43 |
44 | When enabled, this plugin generates optimized runtime code using [`PatchFlags`](https://vuejs.org/guide/extras/rendering-mechanism#patch-flags) and [`SlotFlags`](https://github.com/vuejs/core/blob/v3.5.13/packages/runtime-core/src/componentSlots.ts#L69-L77) to improve rendering performance. However, due to JSX's dynamic nature, the optimizations are not as comprehensive as those in Vue's official template compiler.
45 |
46 | Since the optimized code may skip certain re-renders to improve performance, we strongly recommend thorough testing of your application after enabling this option to ensure everything works as expected.
47 |
48 | #### isCustomElement
49 |
50 | Type: `(tag: string) => boolean`
51 |
52 | Default: `undefined`
53 |
54 | configuring custom elements
55 |
56 | #### mergeProps
57 |
58 | Type: `boolean`
59 |
60 | Default: `true`
61 |
62 | merge static and dynamic class / style attributes / onXXX handlers
63 |
64 | #### enableObjectSlots
65 |
66 | Type: `boolean`
67 |
68 | Default: `true`
69 |
70 | Whether to enable `object slots` (mentioned below the document) syntax". It might be useful in JSX, but it will add a lot of `_isSlot` condition expressions which increase your bundle size. And `v-slots` is still available even if `enableObjectSlots` is turned off.
71 |
72 | #### pragma
73 |
74 | Type: `string`
75 |
76 | Default: `createVNode`
77 |
78 | Replace the function used when compiling JSX expressions.
79 |
80 | #### resolveType
81 |
82 | Type: `boolean`
83 |
84 | Default: `false`
85 |
86 | (**Experimental**) Infer component metadata from types (e.g. `props`, `emits`, `name`). This is an experimental feature and may not work in all cases.
87 |
88 | ## Syntax
89 |
90 | ### Content
91 |
92 | functional component
93 |
94 | ```jsx
95 | const App = () => Vue 3.0
96 | ```
97 |
98 | with render
99 |
100 | ```jsx
101 | const App = {
102 | render() {
103 | return Vue 3.0
104 | },
105 | }
106 | ```
107 |
108 | ```jsx
109 | import { defineComponent, withModifiers } from 'vue'
110 |
111 | const App = defineComponent({
112 | setup() {
113 | const count = ref(0)
114 |
115 | const inc = () => {
116 | count.value++
117 | }
118 |
119 | return () => {count.value}
120 | },
121 | })
122 | ```
123 |
124 | Fragment
125 |
126 | ```jsx
127 | const App = () => (
128 | <>
129 | I'm
130 | Fragment
131 | >
132 | )
133 | ```
134 |
135 | ### Attributes / Props
136 |
137 | ```jsx
138 | const App = () =>
139 | ```
140 |
141 | with a dynamic binding:
142 |
143 | ```jsx
144 | const placeholderText = 'email'
145 | const App = () =>
146 | ```
147 |
148 | ### Directives
149 |
150 | #### v-show
151 |
152 | ```jsx
153 | const App = {
154 | data() {
155 | return { visible: true }
156 | },
157 | render() {
158 | return
159 | },
160 | }
161 | ```
162 |
163 | #### v-model
164 |
165 | > Note: You should pass the second param as string for using `arg`.
166 |
167 | ```jsx
168 |
169 | ```
170 |
171 | ```jsx
172 |
173 | ```
174 |
175 | ```jsx
176 | ;
177 | // Or
178 | ;
179 | ```
180 |
181 | ```jsx
182 | ;
183 | // Or
184 | ;
185 | ```
186 |
187 | Will compile to:
188 |
189 | ```js
190 | h(A, {
191 | argument: val,
192 | argumentModifiers: {
193 | modifier: true,
194 | },
195 | 'onUpdate:argument': ($event) => (val = $event),
196 | })
197 | ```
198 |
199 | #### v-models (Not recommended since v1.1.0)
200 |
201 | > Note: You should pass a Two-dimensional Arrays to v-models.
202 |
203 | ```jsx
204 |
205 | ```
206 |
207 | ```jsx
208 |
214 | ```
215 |
216 | ```jsx
217 |
223 | ```
224 |
225 | Will compile to:
226 |
227 | ```js
228 | h(A, {
229 | modelValue: foo,
230 | modelModifiers: {
231 | modifier: true,
232 | },
233 | 'onUpdate:modelValue': ($event) => (foo = $event),
234 | bar,
235 | barModifiers: {
236 | modifier: true,
237 | },
238 | 'onUpdate:bar': ($event) => (bar = $event),
239 | })
240 | ```
241 |
242 | #### custom directive
243 |
244 | Recommended when using string arguments
245 |
246 | ```jsx
247 | const App = {
248 | directives: { custom: customDirective },
249 | setup() {
250 | return () =>
251 | },
252 | }
253 | ```
254 |
255 | ```jsx
256 | const App = {
257 | directives: { custom: customDirective },
258 | setup() {
259 | return () =>
260 | },
261 | }
262 | ```
263 |
264 | ### Slot
265 |
266 | > Note: In `jsx`, _`v-slot`_ should be replaced with **`v-slots`**
267 |
268 | ```jsx
269 | const A = (props, { slots }) => (
270 | <>
271 | {slots.default ? slots.default() : 'foo'}
272 | {slots.bar?.()}
273 | >
274 | )
275 |
276 | const App = {
277 | setup() {
278 | const slots = {
279 | bar: () => B,
280 | }
281 | return () => (
282 |
283 | A
284 |
285 | )
286 | },
287 | }
288 |
289 | // or
290 |
291 | const App2 = {
292 | setup() {
293 | const slots = {
294 | default: () => A
,
295 | bar: () => B,
296 | }
297 | return () =>
298 | },
299 | }
300 |
301 | // or you can use object slots when `enableObjectSlots` is not false.
302 | const App3 = {
303 | setup() {
304 | return () => (
305 | <>
306 |
307 | {{
308 | default: () => A
,
309 | bar: () => B,
310 | }}
311 |
312 | {() => 'foo'}
313 | >
314 | )
315 | },
316 | }
317 | ```
318 |
319 | ### In TypeScript
320 |
321 | `tsconfig.json`:
322 |
323 | ```json
324 | {
325 | "compilerOptions": {
326 | "jsx": "preserve"
327 | }
328 | }
329 | ```
330 |
331 | ## Who is using
332 |
333 |
382 |
383 | ## Compatibility
384 |
385 | This repo is only compatible with:
386 |
387 | - **Babel 7+**
388 | - **Vue 3+**
389 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/test/v-model.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallowMount } from '@vue/test-utils'
2 | import { defineComponent, type VNode } from 'vue'
3 |
4 | test('input[type="checkbox"] should work', async () => {
5 | const wrapper = shallowMount(
6 | defineComponent({
7 | data() {
8 | return {
9 | test: true,
10 | }
11 | },
12 | render() {
13 | return
14 | },
15 | }),
16 | { attachTo: document.body },
17 | )
18 |
19 | expect(wrapper.vm.$el.checked).toBe(true)
20 | wrapper.vm.test = false
21 | await wrapper.vm.$nextTick()
22 | expect(wrapper.vm.$el.checked).toBe(false)
23 | expect(wrapper.vm.test).toBe(false)
24 | await wrapper.trigger('click')
25 | expect(wrapper.vm.$el.checked).toBe(true)
26 | expect(wrapper.vm.test).toBe(true)
27 | })
28 |
29 | test('input[type="radio"] should work', async () => {
30 | const wrapper = shallowMount(
31 | defineComponent({
32 | data: () => ({
33 | test: '1',
34 | }),
35 | render() {
36 | return (
37 | <>
38 |
39 |
40 | >
41 | )
42 | },
43 | }),
44 | { attachTo: document.body },
45 | )
46 |
47 | const [a, b] = wrapper.vm.$.subTree.children as VNode[]
48 |
49 | expect(a.el!.checked).toBe(true)
50 | wrapper.vm.test = '2'
51 | await wrapper.vm.$nextTick()
52 | expect(a.el!.checked).toBe(false)
53 | expect(b.el!.checked).toBe(true)
54 | await a.el!.click()
55 | expect(a.el!.checked).toBe(true)
56 | expect(b.el!.checked).toBe(false)
57 | expect(wrapper.vm.test).toBe('1')
58 | })
59 |
60 | test('select should work with value bindings', async () => {
61 | const wrapper = shallowMount(
62 | defineComponent({
63 | data: () => ({
64 | test: 2,
65 | }),
66 | render() {
67 | return (
68 |
73 | )
74 | },
75 | }),
76 | )
77 |
78 | const el = wrapper.vm.$el
79 |
80 | expect(el.value).toBe('2')
81 | expect(el.children[1].selected).toBe(true)
82 | wrapper.vm.test = 3
83 | await wrapper.vm.$nextTick()
84 | expect(el.value).toBe('3')
85 | expect(el.children[2].selected).toBe(true)
86 |
87 | el.value = '1'
88 | await wrapper.trigger('change')
89 | expect(wrapper.vm.test).toBe('1')
90 |
91 | el.value = '2'
92 | await wrapper.trigger('change')
93 | expect(wrapper.vm.test).toBe(2)
94 | })
95 |
96 | test('textarea should update value both ways', async () => {
97 | const wrapper = shallowMount(
98 | defineComponent({
99 | data: () => ({
100 | test: 'b',
101 | }),
102 | render() {
103 | return
104 | },
105 | }),
106 | )
107 | const el = wrapper.vm.$el
108 |
109 | expect(el.value).toBe('b')
110 | wrapper.vm.test = 'a'
111 | await wrapper.vm.$nextTick()
112 | expect(el.value).toBe('a')
113 | el.value = 'c'
114 | await wrapper.trigger('input')
115 | expect(wrapper.vm.test).toBe('c')
116 | })
117 |
118 | test('input[type="text"] should update value both ways', async () => {
119 | const wrapper = shallowMount(
120 | defineComponent({
121 | data: () => ({
122 | test: 'b',
123 | }),
124 | render() {
125 | return
126 | },
127 | }),
128 | )
129 | const el = wrapper.vm.$el
130 |
131 | expect(el.value).toBe('b')
132 | wrapper.vm.test = 'a'
133 | await wrapper.vm.$nextTick()
134 | expect(el.value).toBe('a')
135 | el.value = 'c'
136 | await wrapper.trigger('input')
137 | expect(wrapper.vm.test).toBe('c')
138 | })
139 |
140 | test('input[type="text"] .lazy modifier', async () => {
141 | const wrapper = shallowMount(
142 | defineComponent({
143 | data: () => ({
144 | test: 'b',
145 | }),
146 | render() {
147 | return
148 | },
149 | }),
150 | )
151 | const el = wrapper.vm.$el
152 |
153 | expect(el.value).toBe('b')
154 | expect(wrapper.vm.test).toBe('b')
155 | el.value = 'c'
156 | await wrapper.trigger('input')
157 | expect(wrapper.vm.test).toBe('b')
158 | el.value = 'c'
159 | await wrapper.trigger('change')
160 | expect(wrapper.vm.test).toBe('c')
161 | })
162 |
163 | test('dynamic type should work', async () => {
164 | const wrapper = shallowMount(
165 | defineComponent({
166 | data() {
167 | return {
168 | test: true,
169 | type: 'checkbox',
170 | }
171 | },
172 | render() {
173 | return
174 | },
175 | }),
176 | )
177 |
178 | expect(wrapper.vm.$el.checked).toBe(true)
179 | wrapper.vm.test = false
180 | await wrapper.vm.$nextTick()
181 | expect(wrapper.vm.$el.checked).toBe(false)
182 | })
183 |
184 | test('underscore modifier should work', async () => {
185 | const wrapper = shallowMount(
186 | defineComponent({
187 | data: () => ({
188 | test: 'b',
189 | }),
190 | render() {
191 | return
192 | },
193 | }),
194 | )
195 | const el = wrapper.vm.$el
196 |
197 | expect(el.value).toBe('b')
198 | expect(wrapper.vm.test).toBe('b')
199 | el.value = 'c'
200 | await wrapper.trigger('input')
201 | expect(wrapper.vm.test).toBe('b')
202 | el.value = 'c'
203 | await wrapper.trigger('change')
204 | expect(wrapper.vm.test).toBe('c')
205 | })
206 |
207 | test('underscore modifier should work in custom component', async () => {
208 | const Child = defineComponent({
209 | emits: ['update:modelValue'],
210 | props: {
211 | modelValue: {
212 | type: Number,
213 | default: 0,
214 | },
215 | modelModifiers: {
216 | default: () => ({ double: false }),
217 | },
218 | },
219 | setup(props, { emit }) {
220 | const handleClick = () => {
221 | emit('update:modelValue', 3)
222 | }
223 | return () => (
224 |
225 | {props.modelModifiers.double
226 | ? props.modelValue * 2
227 | : props.modelValue}
228 |
229 | )
230 | },
231 | })
232 |
233 | const wrapper = mount(
234 | defineComponent({
235 | data() {
236 | return {
237 | foo: 1,
238 | }
239 | },
240 | render() {
241 | return
242 | },
243 | }),
244 | )
245 |
246 | expect(wrapper.html()).toBe('2
')
247 | wrapper.vm.$data.foo += 1
248 | await wrapper.vm.$nextTick()
249 | expect(wrapper.html()).toBe('4
')
250 | await wrapper.trigger('click')
251 | expect(wrapper.html()).toBe('6
')
252 | })
253 |
254 | test('Named model', async () => {
255 | const Child = defineComponent({
256 | emits: ['update:value'],
257 | props: {
258 | value: {
259 | type: Number,
260 | default: 0,
261 | },
262 | },
263 | setup(props, { emit }) {
264 | const handleClick = () => {
265 | emit('update:value', 2)
266 | }
267 | return () => {props.value}
268 | },
269 | })
270 |
271 | const wrapper = mount(
272 | defineComponent({
273 | data: () => ({
274 | foo: 0,
275 | }),
276 | render() {
277 | return
278 | },
279 | }),
280 | )
281 |
282 | expect(wrapper.html()).toBe('0
')
283 | wrapper.vm.$data.foo += 1
284 | await wrapper.vm.$nextTick()
285 | expect(wrapper.html()).toBe('1
')
286 | await wrapper.trigger('click')
287 | expect(wrapper.html()).toBe('2
')
288 | })
289 |
290 | test('named model and underscore modifier should work in custom component', async () => {
291 | const Child = defineComponent({
292 | emits: ['update:value'],
293 | props: {
294 | value: {
295 | type: Number,
296 | default: 0,
297 | },
298 | valueModifiers: {
299 | default: () => ({ double: false }),
300 | },
301 | },
302 | setup(props, { emit }) {
303 | const handleClick = () => {
304 | emit('update:value', 3)
305 | }
306 | return () => (
307 |
308 | {props.valueModifiers.double ? props.value * 2 : props.value}
309 |
310 | )
311 | },
312 | })
313 |
314 | const wrapper = mount(
315 | defineComponent({
316 | data() {
317 | return {
318 | foo: 1,
319 | }
320 | },
321 | render() {
322 | return
323 | },
324 | }),
325 | )
326 |
327 | expect(wrapper.html()).toBe('2
')
328 | wrapper.vm.$data.foo += 1
329 | await wrapper.vm.$nextTick()
330 | expect(wrapper.html()).toBe('4
')
331 | await wrapper.trigger('click')
332 | expect(wrapper.html()).toBe('6
')
333 | })
334 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/test/snapshot.test.ts:
--------------------------------------------------------------------------------
1 | import { transform } from '@babel/core'
2 | import JSX, { type VueJSXPluginOptions } from '../src'
3 |
4 | interface Test {
5 | name: string
6 | from: string
7 | }
8 |
9 | const transpile = (source: string, options: VueJSXPluginOptions = {}) =>
10 | new Promise((resolve, reject) =>
11 | transform(
12 | source,
13 | {
14 | filename: '',
15 | presets: null,
16 | plugins: [[JSX, options]],
17 | configFile: false,
18 | },
19 | (error, result) => {
20 | if (error) {
21 | return reject(error)
22 | }
23 | resolve(result?.code)
24 | },
25 | ),
26 | )
27 |
28 | ;[
29 | {
30 | name: 'input[type="checkbox"]',
31 | from: '',
32 | },
33 | {
34 | name: 'input[type="radio"]',
35 | from: `
36 | <>
37 |
38 |
39 | >
40 | `,
41 | },
42 | {
43 | name: 'select',
44 | from: `
45 |
50 | `,
51 | },
52 | {
53 | name: 'textarea',
54 | from: '',
55 | },
56 | {
57 | name: 'input[type="text"]',
58 | from: '',
59 | },
60 | {
61 | name: 'dynamic type in input',
62 | from: '',
63 | },
64 | {
65 | name: 'v-show',
66 | from: 'vShow
',
67 | },
68 | {
69 | name: 'input[type="text"] .lazy modifier',
70 | from: `
71 |
72 | `,
73 | },
74 | {
75 | name: 'custom directive',
76 | from: '',
77 | },
78 | {
79 | name: 'vHtml',
80 | from: '',
81 | },
82 | {
83 | name: 'vText',
84 | from: '',
85 | },
86 | {
87 | name: 'Without props',
88 | from: 'a',
89 | },
90 | {
91 | name: 'MereProps Order',
92 | from: '',
93 | },
94 | {
95 | name: 'Merge class/ style attributes into array',
96 | from: '',
97 | },
98 | {
99 | name: 'single no need for a mergeProps call',
100 | from: 'single
',
101 | },
102 | {
103 | name: 'should keep `import * as Vue from "vue"`',
104 | from: `
105 | import * as Vue from 'vue';
106 |
107 | Vue
108 | `,
109 | },
110 | {
111 | name: 'specifiers should be merged into a single importDeclaration',
112 | from: `
113 | import { createVNode, Fragment as _Fragment } from 'vue';
114 | import { vShow } from 'vue'
115 |
116 | <_Fragment />
117 | `,
118 | },
119 | {
120 | name: 'Without JSX should work',
121 | from: `
122 | import { createVNode } from 'vue';
123 | createVNode('div', null, ['Without JSX should work']);
124 | `,
125 | },
126 | {
127 | name: 'reassign variable as component',
128 | from: `
129 | import { defineComponent } from 'vue';
130 | let a = 1;
131 | const A = defineComponent({
132 | setup(_, { slots }) {
133 | return () => {slots.default()};
134 | },
135 | });
136 |
137 | const _a2 = 2;
138 |
139 | a = _a2;
140 |
141 | a = {a};
142 | `,
143 | },
144 | {
145 | name: 'custom directive',
146 | from: `
147 | <>
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | >
156 | `,
157 | },
158 | {
159 | name: 'directive in scope',
160 | from: `
161 | const vXxx = {};
162 |
163 | `,
164 | },
165 | {
166 | name: 'vModels',
167 | from: '',
168 | },
169 | {
170 | name: 'use "model" as the prop name',
171 | from: '',
172 | },
173 | {
174 | name: 'named import specifier `Keep Alive`',
175 | from: `
176 | import { KeepAlive } from 'vue';
177 |
178 | 123
179 | `,
180 | },
181 | {
182 | name: 'namespace specifier `Keep Alive`',
183 | from: `
184 | import * as Vue from 'vue';
185 |
186 | 123
187 | `,
188 | },
189 | {
190 | name: 'use "@jsx" comment specify pragma',
191 | from: `
192 | /* @jsx custom */
193 | Hello
194 | `,
195 | },
196 | {
197 | name: 'v-model target value support variable',
198 | from: `
199 | const foo = 'foo';
200 |
201 | const a = () => 'a';
202 |
203 | const b = { c: 'c' };
204 | <>
205 |
206 |
207 |
208 |
209 |
210 |
211 | >
212 | `,
213 | },
214 | {
215 | name: 'using v-slots without children should not be spread',
216 | from: '',
217 | },
218 | {
219 | name: 'TemplateLiteral prop and event co-usage',
220 | from: ' foo.value++}>
',
221 | },
222 | ].forEach(({ name, from }) => {
223 | test(name, async () => {
224 | expect(
225 | await transpile(from, { optimize: true, enableObjectSlots: true }),
226 | ).toMatchSnapshot(name)
227 | })
228 | })
229 |
230 | const overridePropsTests: Test[] = [
231 | {
232 | name: 'single',
233 | from: '',
234 | },
235 | {
236 | name: 'multiple',
237 | from: '',
238 | },
239 | ]
240 |
241 | overridePropsTests.forEach(({ name, from }) => {
242 | test(`override props ${name}`, async () => {
243 | expect(await transpile(from, { mergeProps: false })).toMatchSnapshot(name)
244 | })
245 | })
246 |
247 | const slotsTests: Test[] = [
248 | {
249 | name: 'multiple expressions',
250 | from: '{foo}{bar}',
251 | },
252 | {
253 | name: 'single expression, function expression',
254 | from: `
255 | {() => "foo"}
256 | `,
257 | },
258 | {
259 | name: 'single expression, non-literal value: runtime check',
260 | from: `
261 | const foo = () => 1;
262 | {foo()};
263 | `,
264 | },
265 | {
266 | name: 'no directive in slot',
267 | from: `
268 | <>
269 | {foo}
270 |
271 | {foo}
272 |
273 | >
274 | `,
275 | },
276 | {
277 | name: 'directive in slot',
278 | from: `
279 | <>
280 | {foo}
281 |
282 | {foo}
283 |
284 | >
285 | `,
286 | },
287 | {
288 | name: 'directive in slot, in scope',
289 | from: `
290 | const vXxx = {};
291 | <>
292 | {foo}
293 |
294 | {foo}
295 |
296 | >
297 | `,
298 | },
299 | ]
300 |
301 | slotsTests.forEach(({ name, from }) => {
302 | test(`passing object slots via JSX children ${name}`, async () => {
303 | expect(
304 | await transpile(from, { optimize: true, enableObjectSlots: true }),
305 | ).toMatchSnapshot(name)
306 | })
307 | })
308 |
309 | const objectSlotsTests = [
310 | {
311 | name: 'defaultSlot',
312 | from: '{slots.default()}',
313 | },
314 | ]
315 |
316 | objectSlotsTests.forEach(({ name, from }) => {
317 | test(`disable object slot syntax with ${name}`, async () => {
318 | expect(
319 | await transpile(from, { optimize: true, enableObjectSlots: false }),
320 | ).toMatchSnapshot(name)
321 | })
322 | })
323 |
324 | const pragmaTests = [
325 | {
326 | name: 'custom',
327 | from: 'pragma
',
328 | },
329 | ]
330 |
331 | pragmaTests.forEach(({ name, from }) => {
332 | test(`set pragma to ${name}`, async () => {
333 | expect(await transpile(from, { pragma: 'custom' })).toMatchSnapshot(name)
334 | })
335 | })
336 |
337 | const isCustomElementTests = [
338 | {
339 | name: 'isCustomElement',
340 | from: 'foo',
341 | },
342 | ]
343 |
344 | isCustomElementTests.forEach(({ name, from }) => {
345 | test(name, async () => {
346 | expect(
347 | await transpile(from, { isCustomElement: (tag) => tag === 'foo' }),
348 | ).toMatchSnapshot(name)
349 | })
350 | })
351 |
352 | const fragmentTests = [
353 | {
354 | name: '_Fragment already imported',
355 | from: `
356 | import { Fragment as _Fragment } from 'vue'
357 | const Root1 = () => <>root1>
358 | const Root2 = () => <_Fragment>root2
359 | `,
360 | },
361 | ]
362 |
363 | fragmentTests.forEach(({ name, from }) => {
364 | test(name, async () => {
365 | expect(await transpile(from)).toMatchSnapshot(name)
366 | })
367 | })
368 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/src/utils.ts:
--------------------------------------------------------------------------------
1 | import t from '@babel/types'
2 | import { isHTMLTag, isSVGTag } from '@vue/shared'
3 | import { SlotFlags } from './slotFlags'
4 | import type { State } from './interface'
5 | import type { NodePath } from '@babel/traverse'
6 | export const JSX_HELPER_KEY = 'JSX_HELPER_KEY'
7 | export const FRAGMENT = 'Fragment'
8 | export const KEEP_ALIVE = 'KeepAlive'
9 |
10 | export const createIdentifier = (
11 | state: State,
12 | name: string,
13 | ): t.Identifier | t.MemberExpression => state.get(name)()
14 |
15 | /**
16 | * Checks if string is describing a directive
17 | * @param src string
18 | */
19 | export const isDirective = (src: string): boolean =>
20 | src.startsWith('v-') ||
21 | (src.startsWith('v') && src.length >= 2 && src[1] >= 'A' && src[1] <= 'Z')
22 |
23 | /**
24 | * Should transformed to slots
25 | * @param tag string
26 | * @returns boolean
27 | */
28 | // if _Fragment is already imported, it will end with number
29 | export const shouldTransformedToSlots = (tag: string) =>
30 | !new RegExp(String.raw`^_?${FRAGMENT}\d*$`).test(tag) && tag !== KEEP_ALIVE
31 |
32 | /**
33 | * Check if a Node is a component
34 | */
35 | export const checkIsComponent = (
36 | path: NodePath,
37 | state: State,
38 | ): boolean => {
39 | const namePath = path.get('name')
40 |
41 | if (namePath.isJSXMemberExpression()) {
42 | return shouldTransformedToSlots(namePath.node.property.name) // For withCtx
43 | }
44 |
45 | const tag = (namePath as NodePath).node.name
46 |
47 | return (
48 | !state.opts.isCustomElement?.(tag) &&
49 | shouldTransformedToSlots(tag) &&
50 | !isHTMLTag(tag) &&
51 | !isSVGTag(tag)
52 | )
53 | }
54 |
55 | /**
56 | * Transform JSXMemberExpression to MemberExpression
57 | * @param path JSXMemberExpression
58 | * @returns MemberExpression
59 | */
60 | export const transformJSXMemberExpression = (
61 | path: NodePath,
62 | ): t.MemberExpression => {
63 | const objectPath = path.node.object
64 | const propertyPath = path.node.property
65 | const transformedObject = t.isJSXMemberExpression(objectPath)
66 | ? transformJSXMemberExpression(
67 | path.get('object') as NodePath,
68 | )
69 | : t.isJSXIdentifier(objectPath)
70 | ? t.identifier(objectPath.name)
71 | : t.nullLiteral()
72 | const transformedProperty = t.identifier(propertyPath.name)
73 | return t.memberExpression(transformedObject, transformedProperty)
74 | }
75 |
76 | /**
77 | * Get tag (first attribute for h) from JSXOpeningElement
78 | * @param path JSXElement
79 | * @param state State
80 | * @returns Identifier | StringLiteral | MemberExpression | CallExpression
81 | */
82 | export const getTag = (
83 | path: NodePath,
84 | state: State,
85 | ): t.Identifier | t.CallExpression | t.StringLiteral | t.MemberExpression => {
86 | const namePath = path.get('openingElement').get('name')
87 | if (namePath.isJSXIdentifier()) {
88 | const { name } = namePath.node
89 | if (!isHTMLTag(name) && !isSVGTag(name)) {
90 | return name === FRAGMENT
91 | ? createIdentifier(state, FRAGMENT)
92 | : path.scope.hasBinding(name)
93 | ? t.identifier(name)
94 | : state.opts.isCustomElement?.(name)
95 | ? t.stringLiteral(name)
96 | : t.callExpression(createIdentifier(state, 'resolveComponent'), [
97 | t.stringLiteral(name),
98 | ])
99 | }
100 |
101 | return t.stringLiteral(name)
102 | }
103 |
104 | if (namePath.isJSXMemberExpression()) {
105 | return transformJSXMemberExpression(namePath)
106 | }
107 | throw new Error(`getTag: ${namePath.type} is not supported`)
108 | }
109 |
110 | export const getJSXAttributeName = (path: NodePath): string => {
111 | const nameNode = path.node.name
112 | if (t.isJSXIdentifier(nameNode)) {
113 | return nameNode.name
114 | }
115 |
116 | return `${nameNode.namespace.name}:${nameNode.name.name}`
117 | }
118 |
119 | /**
120 | * Transform JSXText to StringLiteral
121 | * @param path JSXText
122 | * @returns StringLiteral | null
123 | */
124 | export const transformJSXText = (
125 | path: NodePath,
126 | ): t.StringLiteral | null => {
127 | const str = transformText(path.node.value)
128 | return str === '' ? null : t.stringLiteral(str)
129 | }
130 |
131 | export const transformText = (text: string) => {
132 | const lines = text.split(/\r\n|\n|\r/)
133 |
134 | let lastNonEmptyLine = 0
135 |
136 | for (const [i, line] of lines.entries()) {
137 | if (/[^ \t]/.test(line)) {
138 | lastNonEmptyLine = i
139 | }
140 | }
141 |
142 | let str = ''
143 |
144 | for (let i = 0; i < lines.length; i++) {
145 | const line = lines[i]
146 |
147 | const isFirstLine = i === 0
148 | const isLastLine = i === lines.length - 1
149 | const isLastNonEmptyLine = i === lastNonEmptyLine
150 |
151 | // replace rendered whitespace tabs with spaces
152 | let trimmedLine = line.replaceAll('\t', ' ')
153 |
154 | // trim whitespace touching a newline
155 | if (!isFirstLine) {
156 | trimmedLine = trimmedLine.replace(/^ +/, '')
157 | }
158 |
159 | // trim whitespace touching an endline
160 | if (!isLastLine) {
161 | trimmedLine = trimmedLine.replace(/ +$/, '')
162 | }
163 |
164 | if (trimmedLine) {
165 | if (!isLastNonEmptyLine) {
166 | trimmedLine += ' '
167 | }
168 |
169 | str += trimmedLine
170 | }
171 | }
172 |
173 | return str
174 | }
175 |
176 | /**
177 | * Transform JSXExpressionContainer to Expression
178 | * @param path JSXExpressionContainer
179 | * @returns Expression
180 | */
181 | export const transformJSXExpressionContainer = (
182 | path: NodePath,
183 | ): t.Expression => path.get('expression').node as t.Expression
184 |
185 | /**
186 | * Transform JSXSpreadChild
187 | * @param path JSXSpreadChild
188 | * @returns SpreadElement
189 | */
190 | export const transformJSXSpreadChild = (
191 | path: NodePath,
192 | ): t.SpreadElement => t.spreadElement(path.get('expression').node)
193 |
194 | export const walksScope = (
195 | path: NodePath,
196 | name: string,
197 | slotFlag: SlotFlags,
198 | ): void => {
199 | if (path.scope.hasBinding(name) && path.parentPath) {
200 | if (t.isJSXElement(path.parentPath.node)) {
201 | path.parentPath.setData('slotFlag', slotFlag)
202 | }
203 | walksScope(path.parentPath, name, slotFlag)
204 | }
205 | }
206 |
207 | export const buildIIFE = (
208 | path: NodePath,
209 | children: t.Expression[],
210 | ) => {
211 | const { parentPath } = path
212 | if (parentPath.isAssignmentExpression()) {
213 | const { left } = parentPath.node as t.AssignmentExpression
214 | if (t.isIdentifier(left)) {
215 | return children.map((child) => {
216 | if (t.isIdentifier(child) && child.name === left.name) {
217 | const insertName = path.scope.generateUidIdentifier(child.name)
218 | parentPath.insertBefore(
219 | t.variableDeclaration('const', [
220 | t.variableDeclarator(
221 | insertName,
222 | t.callExpression(
223 | t.functionExpression(
224 | null,
225 | [],
226 | t.blockStatement([t.returnStatement(child)]),
227 | ),
228 | [],
229 | ),
230 | ),
231 | ]),
232 | )
233 | return insertName
234 | }
235 | return child
236 | })
237 | }
238 | }
239 | return children
240 | }
241 |
242 | const onRE = /^on[^a-z]/
243 |
244 | export const isOn = (key: string) => onRE.test(key)
245 |
246 | const mergeAsArray = (
247 | existing: t.ObjectProperty,
248 | incoming: t.ObjectProperty,
249 | ) => {
250 | if (t.isArrayExpression(existing.value)) {
251 | existing.value.elements.push(incoming.value as t.Expression)
252 | } else {
253 | existing.value = t.arrayExpression([
254 | existing.value as t.Expression,
255 | incoming.value as t.Expression,
256 | ])
257 | }
258 | }
259 |
260 | export const dedupeProperties = (
261 | properties: t.ObjectProperty[] = [],
262 | mergeProps?: boolean,
263 | ) => {
264 | if (!mergeProps) {
265 | return properties
266 | }
267 | const knownProps = new Map()
268 | const deduped: t.ObjectProperty[] = []
269 | properties.forEach((prop) => {
270 | if (t.isStringLiteral(prop.key)) {
271 | const { value: name } = prop.key
272 | const existing = knownProps.get(name)
273 | if (existing) {
274 | if (name === 'style' || name === 'class' || name.startsWith('on')) {
275 | mergeAsArray(existing, prop)
276 | }
277 | } else {
278 | knownProps.set(name, prop)
279 | deduped.push(prop)
280 | }
281 | } else {
282 | // v-model target with variable
283 | deduped.push(prop)
284 | }
285 | })
286 |
287 | return deduped
288 | }
289 |
290 | /**
291 | * Check if an attribute value is constant
292 | * @param node
293 | * @returns boolean
294 | */
295 | export const isConstant = (
296 | node: t.Expression | t.Identifier | t.Literal | t.SpreadElement | null,
297 | ): boolean => {
298 | if (t.isIdentifier(node)) {
299 | return node.name === 'undefined'
300 | }
301 | if (t.isArrayExpression(node)) {
302 | const { elements } = node
303 | return elements.every((element) => element && isConstant(element))
304 | }
305 | if (t.isObjectExpression(node)) {
306 | return node.properties.every((property) =>
307 | isConstant((property as any).value),
308 | )
309 | }
310 | if (
311 | t.isTemplateLiteral(node) ? !node.expressions.length : t.isLiteral(node)
312 | ) {
313 | return true
314 | }
315 | return false
316 | }
317 |
318 | export const transformJSXSpreadAttribute = (
319 | nodePath: NodePath,
320 | path: NodePath,
321 | mergeProps: boolean,
322 | args: (t.ObjectProperty | t.Expression | t.SpreadElement)[],
323 | ) => {
324 | const argument = path.get('argument') as NodePath<
325 | t.ObjectExpression | t.Identifier
326 | >
327 | const properties = t.isObjectExpression(argument.node)
328 | ? argument.node.properties
329 | : undefined
330 | if (!properties) {
331 | if (argument.isIdentifier()) {
332 | walksScope(
333 | nodePath,
334 | (argument.node as t.Identifier).name,
335 | SlotFlags.DYNAMIC,
336 | )
337 | }
338 | args.push(mergeProps ? argument.node : t.spreadElement(argument.node))
339 | } else if (mergeProps) {
340 | args.push(t.objectExpression(properties))
341 | } else {
342 | args.push(...(properties as t.ObjectProperty[]))
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/packages/babel-plugin-resolve-type/src/index.ts:
--------------------------------------------------------------------------------
1 | import { codeFrameColumns } from '@babel/code-frame'
2 | import { addNamed } from '@babel/helper-module-imports'
3 | import { declare } from '@babel/helper-plugin-utils'
4 | import { parseExpression } from '@babel/parser'
5 | import {
6 | extractRuntimeEmits,
7 | extractRuntimeProps,
8 | type SimpleTypeResolveContext,
9 | type SimpleTypeResolveOptions,
10 | } from '@vue/compiler-sfc'
11 | import type * as BabelCore from '@babel/core'
12 |
13 | export type { SimpleTypeResolveOptions as Options }
14 |
15 | const plugin: (
16 | api: object,
17 | options: SimpleTypeResolveOptions | null | undefined,
18 | dirname: string,
19 | ) => BabelCore.PluginObj =
20 | declare(({ types: t }, options) => {
21 | let ctx: SimpleTypeResolveContext | undefined
22 | let helpers: Set | undefined
23 |
24 | return {
25 | name: 'babel-plugin-resolve-type',
26 | pre(file) {
27 | const filename = file.opts.filename || 'unknown.js'
28 | helpers = new Set()
29 | ctx = {
30 | filename,
31 | source: file.code,
32 | options,
33 | ast: file.ast.program.body,
34 | isCE: false,
35 | error(msg, node) {
36 | throw new Error(
37 | `[@vue/babel-plugin-resolve-type] ${msg}\n\n${filename}\n${codeFrameColumns(
38 | file.code,
39 | {
40 | start: {
41 | line: node.loc!.start.line,
42 | column: node.loc!.start.column + 1,
43 | },
44 | end: {
45 | line: node.loc!.end.line,
46 | column: node.loc!.end.column + 1,
47 | },
48 | },
49 | )}`,
50 | )
51 | },
52 | helper(key) {
53 | helpers!.add(key)
54 | return `_${key}`
55 | },
56 | getString(node) {
57 | return file.code.slice(node.start!, node.end!)
58 | },
59 | propsTypeDecl: undefined,
60 | propsRuntimeDefaults: undefined,
61 | propsDestructuredBindings: {},
62 | emitsTypeDecl: undefined,
63 | }
64 | },
65 | visitor: {
66 | CallExpression(path) {
67 | if (!ctx) {
68 | throw new Error(
69 | '[@vue/babel-plugin-resolve-type] context is not loaded.',
70 | )
71 | }
72 |
73 | const { node } = path
74 |
75 | if (!t.isIdentifier(node.callee, { name: 'defineComponent' })) return
76 | if (!checkDefineComponent(path)) return
77 |
78 | const comp = node.arguments[0]
79 | if (!comp || !t.isFunction(comp)) return
80 |
81 | let options = node.arguments[1]
82 | if (!options) {
83 | options = t.objectExpression([])
84 | node.arguments.push(options)
85 | }
86 |
87 | let propsGenerics: BabelCore.types.TSType | undefined
88 | let emitsGenerics: BabelCore.types.TSType | undefined
89 | if (node.typeParameters && node.typeParameters.params.length > 0) {
90 | propsGenerics = node.typeParameters.params[0]
91 | emitsGenerics = node.typeParameters.params[1]
92 | }
93 |
94 | node.arguments[1] =
95 | processProps(comp, propsGenerics, options) || options
96 | node.arguments[1] =
97 | processEmits(comp, emitsGenerics, node.arguments[1]) || options
98 | },
99 | VariableDeclarator(path) {
100 | inferComponentName(path)
101 | },
102 | },
103 | post(file) {
104 | for (const helper of helpers!) {
105 | addNamed(file.path, `_${helper}`, 'vue')
106 | }
107 | },
108 | }
109 |
110 | function inferComponentName(
111 | path: BabelCore.NodePath,
112 | ) {
113 | const id = path.get('id')
114 | const init = path.get('init')
115 | if (!id || !id.isIdentifier() || !init || !init.isCallExpression()) return
116 |
117 | if (!init.get('callee')?.isIdentifier({ name: 'defineComponent' })) return
118 | if (!checkDefineComponent(init)) return
119 |
120 | const nameProperty = t.objectProperty(
121 | t.identifier('name'),
122 | t.stringLiteral(id.node.name),
123 | )
124 | const { arguments: args } = init.node
125 | if (args.length === 0) return
126 |
127 | if (args.length === 1) {
128 | init.node.arguments.push(t.objectExpression([]))
129 | }
130 | args[1] = addProperty(t, args[1], nameProperty)
131 | }
132 |
133 | function processProps(
134 | comp: BabelCore.types.Function,
135 | generics: BabelCore.types.TSType | undefined,
136 | options:
137 | | BabelCore.types.ArgumentPlaceholder
138 | | BabelCore.types.SpreadElement
139 | | BabelCore.types.Expression,
140 | ) {
141 | const props = comp.params[0]
142 | if (!props) return
143 |
144 | if (props.type === 'AssignmentPattern') {
145 | if (generics) {
146 | ctx!.propsTypeDecl = resolveTypeReference(generics)
147 | } else {
148 | ctx!.propsTypeDecl = getTypeAnnotation(props.left)
149 | }
150 | ctx!.propsRuntimeDefaults = props.right
151 | } else if (generics) {
152 | ctx!.propsTypeDecl = resolveTypeReference(generics)
153 | } else {
154 | ctx!.propsTypeDecl = getTypeAnnotation(props)
155 | }
156 |
157 | if (!ctx!.propsTypeDecl) return
158 |
159 | const runtimeProps = extractRuntimeProps(ctx!)
160 | if (!runtimeProps) {
161 | return
162 | }
163 |
164 | const ast = parseExpression(runtimeProps)
165 | return addProperty(
166 | t,
167 | options,
168 | t.objectProperty(t.identifier('props'), ast),
169 | )
170 | }
171 |
172 | function processEmits(
173 | comp: BabelCore.types.Function,
174 | generics: BabelCore.types.TSType | undefined,
175 | options:
176 | | BabelCore.types.ArgumentPlaceholder
177 | | BabelCore.types.SpreadElement
178 | | BabelCore.types.Expression,
179 | ) {
180 | let emitType: BabelCore.types.Node | undefined
181 | if (generics) {
182 | emitType = resolveTypeReference(generics)
183 | }
184 |
185 | const setupCtx = comp.params[1] && getTypeAnnotation(comp.params[1])
186 | if (
187 | !emitType &&
188 | setupCtx &&
189 | t.isTSTypeReference(setupCtx) &&
190 | t.isIdentifier(setupCtx.typeName, { name: 'SetupContext' })
191 | ) {
192 | emitType = setupCtx.typeParameters?.params[0]
193 | }
194 | if (!emitType) return
195 |
196 | ctx!.emitsTypeDecl = emitType
197 | const runtimeEmits = extractRuntimeEmits(ctx!)
198 |
199 | const ast = t.arrayExpression(
200 | Array.from(runtimeEmits).map((e) => t.stringLiteral(e)),
201 | )
202 | return addProperty(
203 | t,
204 | options,
205 | t.objectProperty(t.identifier('emits'), ast),
206 | )
207 | }
208 |
209 | function resolveTypeReference(typeNode: BabelCore.types.TSType) {
210 | if (!ctx) return
211 |
212 | if (t.isTSTypeReference(typeNode)) {
213 | const typeName = getTypeReferenceName(typeNode)
214 | if (typeName) {
215 | const typeDeclaration = findTypeDeclaration(typeName)
216 | if (typeDeclaration) {
217 | return typeDeclaration
218 | }
219 | }
220 | }
221 |
222 | return
223 | }
224 |
225 | function getTypeReferenceName(typeRef: BabelCore.types.TSTypeReference) {
226 | if (t.isIdentifier(typeRef.typeName)) {
227 | return typeRef.typeName.name
228 | } else if (t.isTSQualifiedName(typeRef.typeName)) {
229 | const parts: string[] = []
230 | let current: BabelCore.types.TSEntityName = typeRef.typeName
231 |
232 | while (t.isTSQualifiedName(current)) {
233 | if (t.isIdentifier(current.right)) {
234 | parts.unshift(current.right.name)
235 | }
236 | current = current.left
237 | }
238 |
239 | if (t.isIdentifier(current)) {
240 | parts.unshift(current.name)
241 | }
242 |
243 | return parts.join('.')
244 | }
245 | return null
246 | }
247 |
248 | function findTypeDeclaration(typeName: string) {
249 | if (!ctx) return null
250 |
251 | for (const statement of ctx.ast) {
252 | if (
253 | t.isTSInterfaceDeclaration(statement) &&
254 | statement.id.name === typeName
255 | ) {
256 | return t.tsTypeLiteral(statement.body.body)
257 | }
258 |
259 | if (
260 | t.isTSTypeAliasDeclaration(statement) &&
261 | statement.id.name === typeName
262 | ) {
263 | return statement.typeAnnotation
264 | }
265 |
266 | if (t.isExportNamedDeclaration(statement) && statement.declaration) {
267 | if (
268 | t.isTSInterfaceDeclaration(statement.declaration) &&
269 | statement.declaration.id.name === typeName
270 | ) {
271 | return t.tsTypeLiteral(statement.declaration.body.body)
272 | }
273 |
274 | if (
275 | t.isTSTypeAliasDeclaration(statement.declaration) &&
276 | statement.declaration.id.name === typeName
277 | ) {
278 | return statement.declaration.typeAnnotation
279 | }
280 | }
281 | }
282 |
283 | return null
284 | }
285 | })
286 | export default plugin
287 |
288 | function getTypeAnnotation(node: BabelCore.types.Node) {
289 | if (
290 | 'typeAnnotation' in node &&
291 | node.typeAnnotation &&
292 | node.typeAnnotation.type === 'TSTypeAnnotation'
293 | ) {
294 | return node.typeAnnotation.typeAnnotation
295 | }
296 | }
297 |
298 | function checkDefineComponent(
299 | path: BabelCore.NodePath,
300 | ) {
301 | const defineCompImport = path.scope.getBinding('defineComponent')?.path.parent
302 | if (!defineCompImport) return true
303 |
304 | return (
305 | defineCompImport.type === 'ImportDeclaration' &&
306 | /^@?vue(?:\/|$)/.test(defineCompImport.source.value)
307 | )
308 | }
309 |
310 | function addProperty(
311 | t: (typeof BabelCore)['types'],
312 | object: T,
313 | property: BabelCore.types.ObjectProperty,
314 | ) {
315 | if (t.isObjectExpression(object)) {
316 | object.properties.unshift(property)
317 | } else if (t.isExpression(object)) {
318 | return t.objectExpression([property, t.spreadElement(object)])
319 | }
320 | return object
321 | }
322 | export { plugin as 'module.exports' }
323 |
--------------------------------------------------------------------------------
/packages/babel-plugin-jsx/test/index.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallowMount, type VueWrapper } from '@vue/test-utils'
2 | import {
3 | defineComponent,
4 | reactive,
5 | ref,
6 | Transition,
7 | type ComponentPublicInstance,
8 | type CSSProperties,
9 | } from 'vue'
10 |
11 | const patchFlagExpect = (
12 | wrapper: VueWrapper,
13 | flag: number,
14 | dynamic: string[] | null,
15 | ) => {
16 | const { patchFlag, dynamicProps } = wrapper.vm.$.subTree as any
17 |
18 | expect(patchFlag).toBe(flag)
19 | expect(dynamicProps).toEqual(dynamic)
20 | }
21 |
22 | describe('Transform JSX', () => {
23 | test('should render with render function', () => {
24 | const wrapper = shallowMount({
25 | render() {
26 | return 123
27 | },
28 | })
29 | expect(wrapper.text()).toBe('123')
30 | })
31 |
32 | test('should render with setup', () => {
33 | const wrapper = shallowMount({
34 | setup() {
35 | return () => 123
36 | },
37 | })
38 | expect(wrapper.text()).toBe('123')
39 | })
40 |
41 | test('Extracts attrs', () => {
42 | const wrapper = shallowMount({
43 | setup() {
44 | return () =>
45 | },
46 | })
47 | expect(wrapper.element.id).toBe('hi')
48 | })
49 |
50 | test('Binds attrs', () => {
51 | const id = 'foo'
52 | const wrapper = shallowMount({
53 | setup() {
54 | return () => {id}
55 | },
56 | })
57 | expect(wrapper.text()).toBe('foo')
58 | })
59 |
60 | test('should not fallthrough with inheritAttrs: false', () => {
61 | const Child = defineComponent({
62 | props: {
63 | foo: Number,
64 | },
65 | setup(props) {
66 | return () => {props.foo}
67 | },
68 | })
69 |
70 | Child.inheritAttrs = false
71 |
72 | const wrapper = mount({
73 | render() {
74 | return
75 | },
76 | })
77 | expect(wrapper.classes()).toStrictEqual([])
78 | expect(wrapper.text()).toBe('1')
79 | })
80 |
81 | test('Fragment', () => {
82 | const Child = () => 123
83 |
84 | Child.inheritAttrs = false
85 |
86 | const wrapper = mount({
87 | setup() {
88 | return () => (
89 | <>
90 |
91 | 456
92 | >
93 | )
94 | },
95 | })
96 |
97 | expect(wrapper.html()).toBe('123
\n456
')
98 | })
99 |
100 | test('nested component', () => {
101 | const A = {
102 | B: defineComponent({
103 | setup() {
104 | return () => 123
105 | },
106 | }),
107 | }
108 |
109 | A.B.inheritAttrs = false
110 |
111 | const wrapper = mount(() => )
112 |
113 | expect(wrapper.html()).toBe('123
')
114 | })
115 |
116 | test('xlink:href', () => {
117 | const wrapper = shallowMount({
118 | setup() {
119 | return () =>
120 | },
121 | })
122 | expect(wrapper.attributes()['xlink:href']).toBe('#name')
123 | })
124 |
125 | test('Merge class', () => {
126 | const wrapper = shallowMount({
127 | setup() {
128 | // @ts-expect-error
129 | return () =>
130 | },
131 | })
132 | expect(wrapper.classes().toSorted()).toEqual(['a', 'b'].toSorted())
133 | })
134 |
135 | test('Merge style', () => {
136 | const propsA = {
137 | style: {
138 | color: 'red',
139 | } satisfies CSSProperties,
140 | }
141 | const propsB = {
142 | style: {
143 | color: 'blue',
144 | width: '300px',
145 | height: '300px',
146 | } satisfies CSSProperties,
147 | }
148 | const wrapper = shallowMount({
149 | setup() {
150 | // @ts-ignore
151 | return () =>
152 | },
153 | })
154 | expect(wrapper.html()).toBe(
155 | '',
156 | )
157 | })
158 |
159 | test('JSXSpreadChild', () => {
160 | const a = ['1', '2']
161 | const wrapper = shallowMount({
162 | setup() {
163 | return () => {[...a]}
164 | },
165 | })
166 | expect(wrapper.text()).toBe('12')
167 | })
168 |
169 | test('domProps input[value]', () => {
170 | const val = 'foo'
171 | const wrapper = shallowMount({
172 | setup() {
173 | return () =>
174 | },
175 | })
176 | expect(wrapper.html()).toBe('')
177 | })
178 |
179 | test('domProps input[checked]', () => {
180 | const val = true
181 | const wrapper = shallowMount({
182 | setup() {
183 | return () =>
184 | },
185 | })
186 |
187 | expect(wrapper.vm.$.subTree?.props?.checked).toBe(val)
188 | })
189 |
190 | test('domProps option[selected]', () => {
191 | const val = true
192 | const wrapper = shallowMount({
193 | render() {
194 | return
195 | },
196 | })
197 | expect(wrapper.vm.$.subTree?.props?.selected).toBe(val)
198 | })
199 |
200 | test('domProps video[muted]', () => {
201 | const val = true
202 | const wrapper = shallowMount({
203 | render() {
204 | return
205 | },
206 | })
207 |
208 | expect(wrapper.vm.$.subTree?.props?.muted).toBe(val)
209 | })
210 |
211 | test('Spread (single object expression)', () => {
212 | const props = {
213 | id: '1',
214 | }
215 | const wrapper = shallowMount({
216 | render() {
217 | return 123
218 | },
219 | })
220 | expect(wrapper.html()).toBe('123
')
221 | })
222 |
223 | test('Spread (mixed)', async () => {
224 | const calls: number[] = []
225 | const data = {
226 | id: 'hehe',
227 | onClick() {
228 | calls.push(3)
229 | },
230 | innerHTML: '2',
231 | class: ['a', 'b'],
232 | }
233 |
234 | const wrapper = shallowMount({
235 | setup() {
236 | return () => (
237 |