Skip to content

Commit ef49d79

Browse files
committed
feat: add v-for
1 parent 0cb69b5 commit ef49d79

File tree

4 files changed

+127
-41
lines changed

4 files changed

+127
-41
lines changed

playground/App.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,52 @@ export default defineComponent({
1111
<form onSubmit_prevent>
1212
<input
1313
v-bind:value={count.value}
14-
onInput={(event: any) => count.value = event.target.value}
14+
onInput={count.value = $event.target.value}
1515
/>
1616
1717
<div>
1818
v-text:
1919
<span v-text={count.value} />
2020
</div>
21+
2122
<div>
2223
v-html:
2324
<span v-html={count.value} />
2425
</div>
26+
2527
<div>
2628
v-once:
2729
<span v-once>
2830
{count.value}
2931
</span>
3032
</div>
33+
3134
<div>
3235
v-pre:
3336
<span v-pre>
3437
{count.value}
3538
</span>
3639
</div>
40+
3741
<div>
3842
v-cloak:
3943
<span v-cloak>
4044
{count.value}
4145
</span>
4246
</div>
47+
48+
<div>
49+
v-for:
50+
{
51+
Array.from({ length: 10 }).map(() => {
52+
return (
53+
<span>
54+
1
55+
</span>
56+
)
57+
})
58+
}
59+
</div>
4360
</form>
4461
</>
4562
)

src/core/transfrom.ts renamed to src/core/transform.ts

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
parseSFC,
88
walkAST,
99
} from '@vue-macros/common'
10-
import type { JSXElement, JSXFragment, Node, Program } from '@babel/types'
10+
import type { CallExpression, JSXElement, JSXFragment, Node, Program } from '@babel/types'
1111
import { compile } from 'vue/vapor'
12+
import { transformVFor } from './v-for'
1213

1314
export function transformVueJsxVapor(code: string, id: string) {
1415
const lang = getLang(id)
@@ -35,71 +36,91 @@ export function transformVueJsxVapor(code: string, id: string) {
3536
}
3637

3738
const s = new MagicString(code)
39+
const importSet = new Set()
3840
for (const { ast, offset } of asts) {
3941
const rootElements: (JSXElement | JSXFragment)[] = []
42+
const vForNodes: CallExpression[] = []
4043
walkAST<Node>(ast, {
4144
enter(node, parent) {
42-
if (node.type === 'JSXElement') {
43-
if (
45+
if (
46+
(node.type === 'JSXElement'
47+
&& (
4448
parent?.type === 'VariableDeclarator'
4549
|| parent?.type === 'ArrowFunctionExpression'
4650
|| parent?.type === 'CallExpression'
4751
|| parent?.type === 'ReturnStatement'
48-
)
49-
rootElements.push(node)
52+
))
53+
|| node.type === 'JSXFragment'
54+
) {
55+
rootElements.push(node)
56+
this.skip()
57+
}
58+
},
59+
})
5060

61+
for (const rootElement of rootElements) {
62+
walkAST<Node>(rootElement, {
63+
enter(node, parent) {
5164
if (
52-
node.openingElement.attributes.find(
65+
node.type === 'JSXElement'
66+
&& node.openingElement.attributes.find(
5367
attr =>
5468
attr.type === 'JSXAttribute'
5569
&& s.sliceNode(attr.name, { offset }) === 'v-pre',
5670
)
57-
)
71+
) {
5872
return this.skip()
59-
}
60-
else if (node.type === 'JSXFragment') {
61-
rootElements.push(node)
62-
}
63-
else if (node.type === 'JSXAttribute') {
64-
let name = s.sliceNode(node.name, { offset })
65-
if (/^on[A-Z]/.test(name)) {
66-
name = name.replace(
67-
/^(on)([A-Z])/,
68-
(_, __, str) => `@${str.toLowerCase()}`,
69-
)
7073
}
71-
else if (!name.startsWith('v-')) {
72-
name = `:${name}`
74+
else if (
75+
node.type === 'JSXExpressionContainer'
76+
&& parent?.type === 'JSXElement'
77+
) {
78+
if (node.expression.type === 'CallExpression'
79+
&& node.expression.callee.type === 'MemberExpression'
80+
&& node.expression.callee.property.type === 'Identifier'
81+
&& node.expression.callee.property.name === 'map') {
82+
vForNodes.push(node.expression)
83+
s.remove(node.start! + offset, node.expression.start! + offset)
84+
s.remove(node.expression.end! + offset, node.end! + offset)
85+
}
86+
else {
87+
s.appendLeft(node.start! + offset, '{')
88+
s.appendRight(node.end! + offset, '}')
89+
}
7390
}
74-
s.overwriteNode(node.name, `${name.replaceAll('_', '.')}`, {
75-
offset,
76-
})
91+
else if (node.type === 'JSXAttribute') {
92+
let name = s.sliceNode(node.name, { offset })
93+
if (/^on[A-Z]/.test(name)) {
94+
name = name.replace(
95+
/^(on)([A-Z])/,
96+
(_, __, str) => `@${str.toLowerCase()}`,
97+
)
98+
}
99+
else if (!name.startsWith('v-')) {
100+
name = `:${name}`
101+
}
102+
s.overwriteNode(node.name, `${name.replaceAll('_', '.')}`, {
103+
offset,
104+
})
77105

78-
if (node.value && node.value.type !== 'StringLiteral') {
79-
s.overwriteNode(
80-
node.value,
106+
if (node.value && node.value.type !== 'StringLiteral') {
107+
s.overwriteNode(
108+
node.value,
81109
`"${
82110
s.slice(
83111
node.value.start! + offset + 1,
84112
node.value.end! + offset - 1,
85113
)
86114
}"`,
87115
{ offset },
88-
)
116+
)
117+
}
89118
}
90-
}
91-
else if (
92-
node.type === 'JSXExpressionContainer'
93-
&& parent?.type === 'JSXElement'
94-
) {
95-
s.appendLeft(node.start! + offset, '{')
96-
s.appendRight(node.end! + offset, '}')
97-
}
98-
},
99-
})
119+
},
120+
})
121+
122+
transformVFor(vForNodes, s, offset)
100123

101-
const importSet = new Set()
102-
for (const rootElement of rootElements) {
103124
const { code } = compile(
104125
s.sliceNode(
105126
rootElement.type === 'JSXFragment'

src/core/v-for.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { MagicString } from '@vue-macros/common'
2+
import type { CallExpression, Expression, Node } from '@babel/types'
3+
4+
export function transformVFor(
5+
nodes: CallExpression[],
6+
s: MagicString,
7+
offset: number,
8+
) {
9+
nodes.forEach((node) => {
10+
const [argument] = node.arguments
11+
if (!argument)
12+
return
13+
14+
let left
15+
let returnExpression
16+
if (
17+
argument.type === 'FunctionExpression'
18+
|| argument.type === 'ArrowFunctionExpression'
19+
) {
20+
left = s.sliceNode(argument.params, { offset })
21+
if (argument.body.type !== 'BlockStatement') {
22+
returnExpression = argument.body
23+
}
24+
else {
25+
for (const statement of argument.body.body) {
26+
if (statement.type === 'ReturnStatement' && statement.argument)
27+
returnExpression = statement.argument
28+
}
29+
}
30+
}
31+
if (!returnExpression)
32+
return
33+
34+
const right = node.callee.type === 'MemberExpression'
35+
? s.sliceNode(node.callee.object, { offset })
36+
: null
37+
if (!right)
38+
return
39+
40+
const tagName = returnExpression.type === 'JSXElement' ? returnExpression.openingElement.name : returnExpression.type === 'JSXFragment' ? returnExpression.openingFragment : null
41+
if (!tagName)
42+
return
43+
if (tagName.end)
44+
s.appendRight(tagName.end, ` v-for="(${left}) in ${right}"`)
45+
s.remove(node.start! + offset, returnExpression.start! + offset - 1)
46+
s.remove(returnExpression.end! + offset + 1, node.end! + offset)
47+
})
48+
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { UnpluginFactory } from 'unplugin'
22
import { createUnplugin } from 'unplugin'
33
import { createFilter } from 'vite'
44
import type { Options } from './types'
5-
import { transformVueJsxVapor } from './core/transfrom'
5+
import { transformVueJsxVapor } from './core/transform'
66

77
export const unpluginFactory: UnpluginFactory<Options | undefined> = options => ({
88
enforce: 'pre',

0 commit comments

Comments
 (0)