Skip to content

Commit 284c48f

Browse files
committed
feat(compiler)!: support native v-for directive
1 parent 1e428d2 commit 284c48f

File tree

9 files changed

+627
-38
lines changed

9 files changed

+627
-38
lines changed

packages/compiler/src/compile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type RootIRNode,
2626
type RootNode,
2727
} from './ir'
28+
import { transformVFor } from './transforms/vFor'
2829
import type { CompilerOptions as BaseCompilerOptions } from '@vue/compiler-dom'
2930
import type { JSXElement, JSXFragment } from '@babel/types'
3031

@@ -97,6 +98,7 @@ export type TransformPreset = [
9798
export function getBaseTransformPreset(): TransformPreset {
9899
return [
99100
[
101+
transformVFor,
100102
transformTemplateRef,
101103
transformText,
102104
transformElement,

packages/compiler/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export { transformVSlot } from './transforms/vSlot'
2020
export { transformVModel } from './transforms/vModel'
2121
export { transformVShow } from './transforms/vShow'
2222
export { transformVHtml } from './transforms/vHtml'
23+
export { transformVFor } from './transforms/vFor'

packages/compiler/src/transform.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
defaultOnError,
77
defaultOnWarn,
88
} from '@vue/compiler-dom'
9-
import { EMPTY_OBJ, NOOP, extend, isArray } from '@vue/shared'
9+
import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
1010
import {
1111
type BlockIRNode,
1212
DynamicFlag,
@@ -19,7 +19,7 @@ import {
1919
type RootNode,
2020
} from './ir'
2121
import { newBlock, newDynamic } from './transforms/utils'
22-
import { isConstantExpression } from './utils'
22+
import { findProp, getText, isConstantExpression } from './utils'
2323
import type { JSXAttribute, JSXElement, JSXFragment } from '@babel/types'
2424

2525
export type NodeTransform = (
@@ -43,6 +43,14 @@ export interface DirectiveTransformResult {
4343
modelModifiers?: string[]
4444
}
4545

46+
// A structural directive transform is technically also a NodeTransform;
47+
// Only v-if and v-for fall into this category.
48+
export type StructuralDirectiveTransform = (
49+
node: JSXElement,
50+
dir: JSXAttribute,
51+
context: TransformContext,
52+
) => void | (() => void)
53+
4654
export type TransformOptions = HackOptions<BaseTransformOptions>
4755
const defaultOptions = {
4856
filename: '',
@@ -248,3 +256,36 @@ export function transformNode(context: TransformContext<BlockIRNode['node']>) {
248256
context.registerTemplate()
249257
}
250258
}
259+
260+
export function createStructuralDirectiveTransform(
261+
name: string | string[],
262+
fn: StructuralDirectiveTransform,
263+
): NodeTransform {
264+
const matches = (n: string) =>
265+
isString(name) ? n === name : name.includes(n)
266+
267+
return (node, context) => {
268+
if (node.type === 'JSXElement') {
269+
const {
270+
openingElement: { attributes, name },
271+
} = node
272+
// structural directive transforms are not concerned with slots
273+
// as they are handled separately in vSlot.ts
274+
if (getText(name, context) === 'template' && findProp(node, 'v-slot')) {
275+
return
276+
}
277+
const exitFns = []
278+
for (const prop of attributes) {
279+
if (prop.type !== 'JSXAttribute') continue
280+
const propName = getText(prop.name, context)
281+
if (propName.startsWith('v-') && matches(propName.slice(2))) {
282+
attributes.splice(attributes.indexOf(prop), 1)
283+
const onExit = fn(node, prop, context as TransformContext)
284+
if (onExit) exitFns.push(onExit)
285+
break
286+
}
287+
}
288+
return exitFns
289+
}
290+
}
291+
}

packages/compiler/src/transforms/utils.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ export function createBranch(
4545
context: TransformContext,
4646
isVFor?: boolean,
4747
): [BlockIRNode, () => void] {
48-
context.node = node = wrapFragment(node, isVFor)
49-
48+
context.node = node = wrapFragment(node)
5049
const branch: BlockIRNode = newBlock(node)
5150
const exitBlock = context.enterBlock(branch, isVFor)
5251
context.reference()
@@ -55,37 +54,11 @@ export function createBranch(
5554

5655
export function wrapFragment(
5756
node: JSXElement | JSXFragment | Expression,
58-
isVFor?: boolean,
5957
): JSXFragment {
6058
if (node.type === 'JSXFragment') {
6159
return node
6260
}
6361

64-
if (
65-
isVFor &&
66-
(node.type === 'ArrowFunctionExpression' ||
67-
node.type === 'FunctionExpression')
68-
) {
69-
if (isJSXElement(node.body)) {
70-
node = node.body
71-
} else if (
72-
node.body.type === 'BlockStatement' &&
73-
node.body.body[0].type === 'ReturnStatement' &&
74-
node.body.body[0].argument
75-
) {
76-
node = node.body.body[0].argument
77-
} else {
78-
node = {
79-
...callExpression(
80-
parenthesizedExpression(arrowFunctionExpression([], node.body)),
81-
[],
82-
),
83-
start: node.body.start,
84-
end: node.body.end,
85-
}
86-
}
87-
}
88-
8962
return jsxFragment(jsxOpeningFragment(), jsxClosingFragment(), [
9063
node.type === 'JSXElement' ? node : jsxExpressionContainer(node),
9164
])
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
ErrorCodes,
3+
type SimpleExpressionNode,
4+
createCompilerError,
5+
forAliasRE,
6+
isConstantNode,
7+
} from '@vue/compiler-dom'
8+
import { parseExpression } from '@babel/parser'
9+
import { DynamicFlag, IRNodeTypes } from '../ir'
10+
import {
11+
findProp,
12+
getText,
13+
isJSXComponent,
14+
propToExpression,
15+
resolveExpression,
16+
resolveLocation,
17+
resolveSimpleExpression,
18+
} from '../utils'
19+
import {
20+
type NodeTransform,
21+
type TransformContext,
22+
createStructuralDirectiveTransform,
23+
} from '../transform'
24+
import { createBranch } from './utils'
25+
import type { JSXAttribute, JSXElement, Node } from '@babel/types'
26+
27+
export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
28+
'for',
29+
processFor,
30+
)
31+
32+
export function processFor(
33+
node: JSXElement,
34+
dir: JSXAttribute,
35+
context: TransformContext,
36+
) {
37+
if (!dir.value) {
38+
context.options.onError(
39+
createCompilerError(
40+
ErrorCodes.X_V_FOR_NO_EXPRESSION,
41+
resolveLocation(dir.loc, context),
42+
),
43+
)
44+
return
45+
}
46+
if (!forAliasRE) {
47+
context.options.onError(
48+
createCompilerError(
49+
ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION,
50+
resolveLocation(dir.loc, context),
51+
),
52+
)
53+
return
54+
}
55+
56+
let value: SimpleExpressionNode | undefined,
57+
index: SimpleExpressionNode | undefined,
58+
key: SimpleExpressionNode | undefined,
59+
source: SimpleExpressionNode
60+
if (
61+
dir.value.type === 'JSXExpressionContainer' &&
62+
dir.value.expression.type === 'BinaryExpression'
63+
) {
64+
if (dir.value.expression.left.type === 'SequenceExpression') {
65+
const expressions = dir.value.expression.left.expressions
66+
value = expressions[0] && resolveValueExpression(expressions[0], context)
67+
key = expressions[1] && resolveExpression(expressions[1], context)
68+
index = expressions[2] && resolveExpression(expressions[2], context)
69+
} else {
70+
value = resolveValueExpression(dir.value.expression.left, context)
71+
}
72+
source = resolveExpression(dir.value.expression.right, context)
73+
}
74+
75+
const keyProp = findProp(node, 'key')
76+
const keyProperty = keyProp && propToExpression(keyProp, context)
77+
const isComponent = isJSXComponent(node)
78+
const id = context.reference()
79+
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
80+
const [render, exitBlock] = createBranch(node, context, true)
81+
return (): void => {
82+
exitBlock()
83+
84+
const { parent } = context
85+
86+
// if v-for is the only child of a parent element, it can go the fast path
87+
// when the entire list is emptied
88+
const isOnlyChild =
89+
parent &&
90+
parent.block.node !== parent.node &&
91+
parent.node.children.length === 1
92+
93+
context.registerOperation({
94+
type: IRNodeTypes.FOR,
95+
id,
96+
source,
97+
value,
98+
key,
99+
index,
100+
keyProp: keyProperty,
101+
render,
102+
once: context.inVOnce || !!(source.ast && isConstantNode(source.ast, {})),
103+
component: isComponent,
104+
onlyChild: !!isOnlyChild,
105+
})
106+
}
107+
}
108+
109+
function resolveValueExpression(node: Node, context: TransformContext) {
110+
const text = getText(node, context)
111+
return node.type === 'Identifier'
112+
? resolveSimpleExpression(text, false, node.loc)
113+
: resolveSimpleExpression(
114+
text,
115+
false,
116+
node.loc,
117+
parseExpression(`(${text})=>{}`, {
118+
plugins: ['typescript'],
119+
}),
120+
)
121+
}

packages/compiler/src/utils.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import { EMPTY_EXPRESSION } from './transforms/utils'
2929
import type { TransformContext } from './transform'
3030
import type { VaporDirectiveNode } from './ir'
3131

32-
export function propToExpression(prop: AttributeNode | VaporDirectiveNode) {
33-
return prop.type === NodeTypes.ATTRIBUTE
34-
? prop.value
35-
? createSimpleExpression(prop.value.content, true, prop.value.loc)
36-
: EMPTY_EXPRESSION
37-
: prop.exp
32+
export function propToExpression(
33+
prop: JSXAttribute,
34+
context: TransformContext,
35+
) {
36+
return prop.type === 'JSXAttribute' &&
37+
prop.value?.type === 'JSXExpressionContainer'
38+
? resolveExpression(prop.value.expression, context)
39+
: EMPTY_EXPRESSION
3840
}
3941

4042
export function isConstantExpression(exp: SimpleExpressionNode) {
@@ -320,3 +322,7 @@ export function isJSXElement(
320322
): node is JSXElement | JSXFragment {
321323
return !!node && (node.type === 'JSXElement' || node.type === 'JSXFragment')
322324
}
325+
326+
export function getText(node: Node, content: TransformContext) {
327+
return content.ir.source.slice(node.start!, node.end!)
328+
}

0 commit comments

Comments
 (0)