Skip to content

Commit d4c3fa3

Browse files
committed
feat(compiler): support v-bind
1 parent 78f8917 commit d4c3fa3

File tree

10 files changed

+780
-54
lines changed

10 files changed

+780
-54
lines changed

src/core/compiler/compile.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type RootNode,
2626
} from './ir'
2727
import { transformText } from './transforms/transformText'
28+
import { transformVBind } from './transforms/vBind'
2829
import type { JSXElement, JSXFragment, Program } from '@babel/types'
2930

3031
export interface VaporCodegenResult
@@ -121,5 +122,10 @@ export type TransformPreset = [
121122
export function getBaseTransformPreset(
122123
prefixIdentifiers?: boolean,
123124
): TransformPreset {
124-
return [[transformText, transformElement, transformChildren], {}]
125+
return [
126+
[transformText, transformElement, transformChildren],
127+
{
128+
bind: transformVBind,
129+
},
130+
]
125131
}

src/core/compiler/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export {
88
} from './compile'
99
export * from './transform'
1010

11+
export * from './ir'
12+
1113
export { transformText } from './transforms/transformText'
1214
export { transformElement } from './transforms/transformElement'
1315
export { transformChildren } from './transforms/transformChildren'
16+
export { transformVBind } from './transforms/vBind'

src/core/compiler/transform.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,16 @@ import {
1919
type OperationNode,
2020
type RootIRNode,
2121
type RootNode,
22-
type VaporDirectiveNode,
2322
} from './ir/index'
24-
import type { JSXElement, JSXFragment } from '@babel/types'
23+
import type { JSXAttribute, JSXElement, JSXFragment } from '@babel/types'
2524

2625
export type NodeTransform = (
2726
node: BlockIRNode['node'],
2827
context: TransformContext<BlockIRNode['node']>,
2928
) => void | (() => void) | (() => void)[]
3029

3130
export type DirectiveTransform = (
32-
dir: VaporDirectiveNode,
31+
dir: JSXAttribute,
3332
node: JSXElement,
3433
context: TransformContext<JSXElement>,
3534
) => DirectiveTransformResult | void

src/core/compiler/transforms/transformElement.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,21 +235,20 @@ function transformProp(
235235
context: TransformContext<JSXElement>,
236236
): DirectiveTransformResult | void {
237237
if (prop.type === 'JSXSpreadAttribute') return
238-
239238
let name = prop.name.type === 'JSXIdentifier' ? prop.name.name : ''
240-
const value = prop.value?.type === 'StringLiteral' ? prop.value.value : ''
241239

242-
if (prop.type === 'JSXAttribute') {
240+
if (!prop.value || prop.value.type === 'StringLiteral') {
243241
if (isReservedProp(name)) return
244242
return {
245243
key: resolveSimpleExpression(name, true, prop.name.loc!),
246244
value:
247245
prop.value && prop.value.type === 'StringLiteral'
248-
? resolveSimpleExpression(value, true, prop.value.loc!)
246+
? resolveSimpleExpression(prop.value.value, true, prop.value.loc!)
249247
: EMPTY_EXPRESSION,
250248
}
251249
}
252250

251+
name = /^on[A-Z]/.test(name) ? 'on' : 'bind'
253252
const directiveTransform = context.options.directiveTransforms[name]
254253
if (directiveTransform) {
255254
return directiveTransform(prop, node, context)

src/core/compiler/transforms/transformText.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
getLiteralExpressionValue,
1010
isComponent,
1111
isConstantExpression,
12-
resolveSimpleExpression,
12+
resolveExpression,
1313
} from '../utils'
1414
import type {
1515
Expression,
@@ -88,25 +88,7 @@ function processTextLikeContainer(
8888

8989
function createTextLikeExpression(node: TextLike, context: TransformContext) {
9090
seen.get(context.root)!.add(node)
91-
if (node.type === 'JSXText') {
92-
return resolveSimpleExpression(node.value, true, node.loc!)
93-
} else {
94-
const source = context.ir.source.slice(
95-
node.expression.start!,
96-
node.expression.end!,
97-
)
98-
let ast: false | ParseResult<Expression> = false
99-
if (context.options.prefixIdentifiers) {
100-
ast = parseExpression(` ${source}`, {
101-
sourceType: 'module',
102-
plugins: context.options.expressionPlugins,
103-
})
104-
}
105-
106-
const result = resolveSimpleExpression(source, false, node.loc!)
107-
result.ast = ast
108-
return result
109-
}
91+
return resolveExpression(node, context)
11092
}
11193

11294
function isAllTextLike(children: Node[]): children is TextLike[] {

src/core/compiler/transforms/vBind.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
ErrorCodes,
3+
type SimpleExpressionNode,
4+
createCompilerError,
5+
} from '@vue/compiler-dom'
6+
import { camelize, extend } from '@vue/shared'
7+
import {
8+
resolveExpression,
9+
resolveLocation,
10+
resolveSimpleExpression,
11+
} from '../utils'
12+
import { isReservedProp } from './transformElement'
13+
import type { DirectiveTransform } from '../transform'
14+
15+
const __BROWSER__ = false
16+
export const transformVBind: DirectiveTransform = (dir, node, context) => {
17+
const { loc } = dir
18+
if (!loc || dir.name.type === 'JSXNamespacedName') return
19+
20+
const [name, ...modifiers] = dir.name.name.split('_')
21+
22+
let exp: SimpleExpressionNode
23+
if (!name.trim() && loc) {
24+
if (!__BROWSER__) {
25+
// #10280 only error against empty expression in non-browser build
26+
// because :foo in in-DOM templates will be parsed into :foo="" by the
27+
// browser
28+
context.options.onError(
29+
createCompilerError(
30+
ErrorCodes.X_V_BIND_NO_EXPRESSION,
31+
resolveLocation(loc, name),
32+
),
33+
)
34+
}
35+
exp = resolveSimpleExpression('', true, loc)
36+
}
37+
38+
exp = resolveExpression(dir.value, context)
39+
let arg = resolveExpression(dir.name, context)
40+
41+
if (arg.isStatic && isReservedProp(arg.content)) return
42+
43+
let camel = false
44+
if (modifiers.includes('camel')) {
45+
if (arg.isStatic) {
46+
arg = extend({}, arg, { content: camelize(arg.content) })
47+
} else {
48+
camel = true
49+
}
50+
}
51+
52+
return {
53+
key: arg,
54+
value: exp,
55+
loc,
56+
runtimeCamelize: camel,
57+
modifier: modifiers.includes('prop')
58+
? '.'
59+
: modifiers.includes('attr')
60+
? '^'
61+
: undefined,
62+
}
63+
}

src/core/compiler/utils.ts

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import {
1111
} from '@vue/compiler-dom'
1212
import htmlTags, { type HtmlTags } from 'html-tags'
1313
import svgTags from 'svg-tags'
14+
import { type ParseResult, parseExpression } from '@babel/parser'
1415
import { EMPTY_EXPRESSION } from './transforms/utils'
16+
import type { TransformContext } from './transform'
1517
import type { VaporDirectiveNode } from './ir'
1618
import type {
1719
BigIntLiteral,
20+
Expression,
21+
JSXAttribute,
22+
JSXIdentifier,
23+
JSXText,
1824
Node,
1925
NumericLiteral,
2026
SourceLocation,
@@ -53,16 +59,6 @@ export function isConstantExpression(exp: SimpleExpressionNode) {
5359
)
5460
}
5561

56-
export function resolveExpression(exp: SimpleExpressionNode) {
57-
if (!exp.isStatic) {
58-
const value = getLiteralExpressionValue(exp)
59-
if (value !== null) {
60-
return createSimpleExpression(`${value}`, true, exp.loc)
61-
}
62-
}
63-
return exp
64-
}
65-
6662
export function getLiteralExpressionValue(
6763
exp: SimpleExpressionNode,
6864
): number | string | boolean | null {
@@ -83,24 +79,73 @@ export function getLiteralExpressionValue(
8379
return exp.isStatic ? exp.content : null
8480
}
8581

82+
export function resolveExpression(
83+
node: JSXAttribute['value'] | JSXText | JSXIdentifier,
84+
context: TransformContext,
85+
) {
86+
const isStatic =
87+
!!node &&
88+
(node.type === 'StringLiteral' ||
89+
node.type === 'JSXText' ||
90+
node.type === 'JSXIdentifier')
91+
const source = !node
92+
? ''
93+
: node.type === 'JSXIdentifier'
94+
? node.name
95+
: isStatic
96+
? node.value
97+
: node.type === 'JSXExpressionContainer'
98+
? node.expression.type === 'Identifier'
99+
? node.expression.name
100+
: context.ir.source.slice(
101+
node.expression.start!,
102+
node.expression.end!,
103+
)
104+
: ''
105+
const location = node ? node.loc : null
106+
let ast: false | ParseResult<Expression> = false
107+
if (!isStatic && context.options.prefixIdentifiers) {
108+
ast = parseExpression(` ${source}`, {
109+
sourceType: 'module',
110+
plugins: context.options.expressionPlugins,
111+
})
112+
}
113+
return resolveSimpleExpression(source, isStatic, location, ast)
114+
}
115+
86116
export function resolveSimpleExpression(
87117
source: string,
88118
isStatic: boolean,
89-
location: SourceLocation,
119+
location?: SourceLocation | null,
120+
ast?: false | ParseResult<Expression>,
90121
) {
91-
return createSimpleExpression(source, isStatic, {
92-
start: {
93-
line: location.start.line,
94-
column: location.start.column,
95-
offset: location.start.index,
96-
},
97-
end: {
98-
line: location.end.line,
99-
column: location.end.column,
100-
offset: location.end.index,
101-
},
122+
const result = createSimpleExpression(
123+
source,
124+
isStatic,
125+
resolveLocation(location, source),
126+
)
127+
result.ast = ast ?? null
128+
return result
129+
}
130+
131+
export function resolveLocation(location?: SourceLocation | null, source = '') {
132+
return {
133+
start: location
134+
? {
135+
line: location.start.line,
136+
column: location.start.column + 1,
137+
offset: location.start.index,
138+
}
139+
: { line: 1, column: 1, offset: 0 },
140+
end: location
141+
? {
142+
line: location.end.line,
143+
column: location.end.column + 1,
144+
offset: location.end.index,
145+
}
146+
: { line: 1, column: 1, offset: 0 },
102147
source,
103-
})
148+
}
104149
}
105150

106151
export function isComponent(node: Node) {

test/compile.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('compile', () => {
7070
return n0
7171
})()"
7272
`)
73-
// expect(code).contains('a + b.value')
73+
expect(code).contains('a + b.value')
7474
})
7575
})
7676
})

test/transforms/transformChildren.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ describe('compiler: children transform', () => {
2222

2323
test('children & sibling references', () => {
2424
const { code, vaporHelpers } = compileWithElementTransform(
25-
`<div id>
25+
`<div>
2626
<p>{ first }</p>
2727
{ second }
2828
<p>{ forth }</p>
2929
</div>`,
3030
)
3131
expect(code).toMatchInlineSnapshot(`
3232
"import { next as _next, createTextNode as _createTextNode, insert as _insert, renderEffect as _renderEffect, setText as _setText, template as _template } from 'vue/vapor';
33-
const t0 = _template("<div id><p></p> <!><p></p></div>")
33+
const t0 = _template("<div><p></p> <!><p></p></div>")
3434
3535
export function render(_ctx) {
3636
const n4 = t0()

0 commit comments

Comments
 (0)