Skip to content

Commit 3b35951

Browse files
committed
feat(svelte-template-compiler): first implementation svelte binding
1 parent a45e3f7 commit 3b35951

File tree

8 files changed

+301
-13
lines changed

8 files changed

+301
-13
lines changed

packages/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
declare let __BROWSER__: boolean
22
declare let __COMMIT__: string
33
declare let __TEST__: string
4+
declare let __DEV__: boolean

packages/svelte-template-compiler/src/ir/converter.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,32 @@ describe('convertProps', () => {
537537
})
538538
})
539539

540+
describe('Svelte Binding Node', () => {
541+
test('basic: <input bind:value={name} />', () => {
542+
const el = getSvelteElement('<input bind:value={name} />')
543+
expect(convertProps(el!)).toMatchObject([
544+
{
545+
type: NodeTypes.DIRECTIVE,
546+
name: 'model',
547+
rawName: 'v-model',
548+
modifiers: [],
549+
loc: {
550+
// TODO: we want to map for svelte code correctly...
551+
// source: 'v-model:value={name}'
552+
},
553+
arg: undefined,
554+
exp: {
555+
// ast: null,
556+
type: NodeTypes.SIMPLE_EXPRESSION,
557+
content: 'name',
558+
constType: ConstantTypes.NOT_CONSTANT,
559+
isStatic: false
560+
}
561+
}
562+
])
563+
})
564+
})
565+
540566
test('no attribute', () => {
541567
const el = getSvelteElement('<div />')
542568
expect(convertProps(el!)).toMatchObject([])

packages/svelte-template-compiler/src/ir/converter.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NodeTypes, createSimpleExpression } from '@vue-vapor/compiler-dom'
33
import { generate } from 'astring'
44
import {
55
isSvelteAttribute,
6+
isSvelteBindingDirective,
67
isSvelteEventHandler,
78
isSvelteMustacheTag,
89
isSvelteShorthandAttribute,
@@ -31,7 +32,11 @@ export function convertProps(node: SvelteElement): (VaporDirectiveNode | Attribu
3132
} else {
3233
props.push(convertSvelteAttribute(attr))
3334
}
34-
} else if (isSvelteSpreadAttribute(attr) || isSvelteEventHandler(attr)) {
35+
} else if (
36+
isSvelteSpreadAttribute(attr) ||
37+
isSvelteEventHandler(attr) ||
38+
isSvelteBindingDirective(attr)
39+
) {
3540
props.push(convertVaporDirective(attr))
3641
}
3742
}
@@ -161,11 +166,24 @@ function convertVaporDirective(
161166
type: NodeTypes.DIRECTIVE,
162167
name: 'on',
163168
rawName: `v-on:${node.name}${vaporModifiers}`,
169+
// @ts-expect-error -- FIXME
164170
modifiers,
165171
loc: convertSvelteLocation(directiveLoc, directiveLocSource),
166172
arg,
167173
exp
168174
}
175+
} else if (isSvelteBindingDirective(node)) {
176+
const content = node.expression ? generate(node.expression) : ''
177+
return {
178+
type: NodeTypes.DIRECTIVE,
179+
name: 'model',
180+
rawName: 'v-model',
181+
// @ts-expect-error -- FIXME
182+
modifiers: node.modifiers,
183+
loc: convertSvelteLocation(node, `v-model="${content}"`),
184+
exp: createSimpleExpression(content, false, convertSvelteLocation(node, content)),
185+
arg: undefined
186+
}
169187
} else {
170188
// TODO: we should consider error strategy
171189
throw new Error('unexpected node type')

packages/svelte-template-compiler/src/ir/svelte.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ export function isIfBlockOnElseBlock(node: SvelteIfBlock): boolean {
100100
return node.type === 'IfBlock' && !!node.elseif
101101
}
102102

103+
export function isSvelteBindingDirective(node: unknown): node is SvelteBaseExpressionDirective {
104+
return isObject(node) && 'type' in node && node.type === 'Binding'
105+
}
106+
103107
export interface SvelteCompileError {
104108
code: string
105109
start: {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`bind:property > simple binding > input > expected 1`] = `
4+
"import { vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, template as _template } from 'vue/vapor';
5+
const t0 = _template("<input>")
6+
7+
export function render(_ctx) {
8+
const n0 = t0()
9+
_withDirectives(n0, [[_vModelText, () => text]])
10+
_delegate(n0, "update:modelValue", () => $event => (text = $event))
11+
return n0
12+
}"
13+
`;
14+
15+
exports[`bind:property > simple binding > input > received 1`] = `
16+
"import { vModelText as _vModelText, withDirectives as _withDirectives, delegate as _delegate, template as _template } from 'vue/vapor';
17+
const t0 = _template("<input>")
18+
19+
export function render(_ctx) {
20+
const n0 = t0()
21+
_withDirectives(n0, [[_vModelText, () => text]])
22+
_delegate(n0, "update:modelValue", () => $event => (text = $event))
23+
return n0
24+
}"
25+
`;

packages/svelte-template-compiler/src/transforms/model.test.ts

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,82 @@
1+
import { NodeTypes } from '@vue-vapor/compiler-dom'
2+
import { compile as vaporCompile } from '@vue-vapor/compiler-vapor'
13
import { describe, expect, test } from 'vitest'
4+
import { IRNodeTypes } from '../ir/index.ts'
5+
import { makeCompile } from './_utils.ts'
6+
import { transformVBind } from './bind.ts'
7+
import { transformChildren } from './children.ts'
8+
import { transformComment } from './comment.ts'
9+
import { transformElement } from './element.ts'
10+
import { transformVModel } from './model.ts'
11+
import { transformVOn } from './on.ts'
12+
import { transformText } from './text.ts'
13+
14+
const compileWithVModel = makeCompile({
15+
prefixIdentifiers: false,
16+
nodeTransforms: [transformElement, transformChildren, transformText, transformComment],
17+
directiveTransforms: {
18+
bind: transformVBind,
19+
on: transformVOn,
20+
model: transformVModel
21+
}
22+
})
223

324
describe('bind:property', () => {
4-
describe.todo('simple binding', () => {
25+
describe('simple binding', () => {
526
test('input', () => {
627
const source1 = `<input bind:value={text} />`
7-
expect(source1).toBe('todo')
28+
const source2 = `<input v-model="text" />`
29+
const { code, vaporHelpers, ir, helpers } = compileWithVModel(source1)
30+
const expectedResult = vaporCompile(source2)
31+
32+
expect(code).toMatchSnapshot('received')
33+
expect(expectedResult.code).toMatchSnapshot('expected')
34+
35+
expect(code).contains(`_withDirectives(n0, [[_vModelText, () => text]])`)
36+
expect(code).contains(`"update:modelValue", () => $event => (text = $event))`)
37+
38+
expect(vaporHelpers).toContain('vModelText')
39+
expect(helpers.size).toBe(0)
40+
expect(ir.template).toEqual(['<input>'])
41+
expect(ir.block.operation).toMatchObject([
42+
{
43+
type: IRNodeTypes.SET_MODEL_VALUE,
44+
element: 0,
45+
isComponent: false,
46+
key: {
47+
type: NodeTypes.SIMPLE_EXPRESSION,
48+
content: 'modelValue',
49+
isStatic: true
50+
},
51+
value: {
52+
type: NodeTypes.SIMPLE_EXPRESSION,
53+
content: 'text',
54+
isStatic: false
55+
}
56+
},
57+
{
58+
type: IRNodeTypes.WITH_DIRECTIVE,
59+
name: 'vModelText',
60+
element: 0,
61+
builtin: true,
62+
dir: {
63+
arg: undefined,
64+
exp: {
65+
type: NodeTypes.SIMPLE_EXPRESSION,
66+
content: 'text',
67+
isStatic: false
68+
}
69+
}
70+
}
71+
])
872
})
973

10-
test('textarea', () => {
74+
test.todo('textarea', () => {
1175
const source1 = `<textarea bind:value={text} />`
1276
expect(source1).toBe('todo')
1377
})
1478

15-
test('checkbox', () => {
79+
test.todo('checkbox', () => {
1680
const source1 = `<input type="checkbox" bind:checked={yes} />`
1781
expect(source1).toBe('todo')
1882
})
@@ -84,7 +148,7 @@ describe.todo('Binding <select> value', () => {
84148
})
85149
})
86150

87-
describe('contenteditable', () => {
151+
describe.todo('contenteditable', () => {
88152
test('innerHTML', () => {
89153
const source1 = `<div contenteditable="true" bind:innerHTML={html} />`
90154
expect(source1).toBe('todo')

packages/svelte-template-compiler/src/transforms/model.ts

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,157 @@
55
// Repository url: https://github.com/vuejs/core-vapor
66
// Code url: https://github.com/vuejs/core-vapor/blob/6608bb31973d35973428cae4fbd62026db068365/packages/compiler-vapor/src/transforms/vModel.ts
77

8+
import { BindingTypes, createSimpleExpression, isMemberExpression } from '@vue-vapor/compiler-dom'
9+
import { IRNodeTypes } from '../ir/index.ts'
10+
import { getExpSource } from './utils.ts'
11+
12+
import type { VaporHelper } from '@vue-vapor/compiler-vapor'
813
import type { DirectiveTransform } from './types.ts'
914

10-
export const transformVModel: DirectiveTransform = (_dir, _node, _context) => {
11-
// TODO: transform vapor v-model from svelte some bindings
12-
// https://svelte.dev/docs/element-directives#bind-property
13-
// https://svelte.dev/docs/element-directives#binding-select-value
14-
// ... and more on https://svelte.dev/docs/element-directives
15+
// TODO: transform vapor v-model from svelte some bindings
16+
// https://svelte.dev/docs/element-directives#bind-property
17+
// https://svelte.dev/docs/element-directives#binding-select-value
18+
// ... and more on https://svelte.dev/docs/element-directives
19+
export const transformVModel: DirectiveTransform = (dir, node, context) => {
20+
console.log('transformVModel', dir, node)
21+
22+
const { exp, arg } = dir
23+
if (!exp) {
24+
// TODO: should throw error on `onError`
25+
return
26+
}
27+
28+
// we assume v-model directives are always parsed
29+
// (not artificially created by a transform)
30+
const rawExp = exp.loc.source
31+
console.log('rawExp', rawExp)
32+
console.log('bindingMetadata', context.options.bindingMetadata)
33+
34+
// TODO: do we need to handle from `bindingMetadata`?
35+
const bindingType = (context.options.bindingMetadata || {})[rawExp]
36+
console.log('bindingType', bindingType)
37+
38+
// check props
39+
if (bindingType === BindingTypes.PROPS || bindingType === BindingTypes.PROPS_ALIASED) {
40+
// TODO: should throw error on `onError`
41+
return
42+
}
43+
44+
const expString = exp.content
45+
const maybeRef =
46+
!__BROWSER__ &&
47+
context.options.inline &&
48+
(bindingType === BindingTypes.SETUP_LET ||
49+
bindingType === BindingTypes.SETUP_REF ||
50+
bindingType === BindingTypes.SETUP_MAYBE_REF)
51+
console.log('maybeRef', maybeRef)
52+
console.log('expString', expString)
53+
54+
if (!expString.trim() || (!isMemberExpression(getExpSource(exp), context.options) && !maybeRef)) {
55+
// TODO: should throw error on `onError`
56+
return
57+
}
58+
59+
const isComponent = node.type === 'InlineComponent'
60+
if (isComponent) {
61+
return {
62+
// eslint-disable-next-line unicorn/prefer-logical-operator-over-ternary
63+
key: arg ? arg : createSimpleExpression('modelValue', true),
64+
value: exp,
65+
model: true,
66+
67+
modelModifiers: dir.modifiers.map(m => m.content)
68+
}
69+
}
70+
71+
const { name: tag } = node
72+
const isCustomElement = context.options.isCustomElement(tag)
73+
74+
let runtimeDirective: VaporHelper | undefined = 'vModelText'
75+
if (tag === 'input' || tag === 'textarea' || tag === 'select' || isCustomElement) {
76+
if (tag === 'input' || isCustomElement) {
77+
/*
78+
const type = findProp(node, 'type')
79+
if (type) {
80+
if (type.type === NodeTypes.DIRECTIVE) {
81+
// :type="foo"
82+
runtimeDirective = 'vModelDynamic'
83+
} else if (type.value) {
84+
switch (type.value.content) {
85+
case 'radio':
86+
runtimeDirective = 'vModelRadio'
87+
break
88+
case 'checkbox':
89+
runtimeDirective = 'vModelCheckbox'
90+
break
91+
case 'file':
92+
runtimeDirective = undefined
93+
context.options.onError(
94+
createDOMCompilerError(
95+
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
96+
dir.loc,
97+
),
98+
)
99+
break
100+
default:
101+
// text type
102+
__DEV__ && checkDuplicatedValue()
103+
break
104+
}
105+
}
106+
} else if (hasDynamicKeyVBind(node)) {
107+
// element has bindings with dynamic keys, which can possibly contain
108+
// "type".
109+
runtimeDirective = 'vModelDynamic'
110+
} else {
111+
// text type
112+
__DEV__ && checkDuplicatedValue()
113+
}
114+
*/
115+
} else if (tag === 'select') {
116+
runtimeDirective = 'vModelSelect'
117+
} else {
118+
// textarea
119+
// __DEV__ && checkDuplicatedValue()
120+
}
121+
} else {
122+
// TODO:
123+
// context.options.onError(
124+
// createDOMCompilerError(
125+
// DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
126+
// dir.loc,
127+
// ),
128+
// )
129+
}
130+
131+
context.registerOperation({
132+
type: IRNodeTypes.SET_MODEL_VALUE,
133+
element: context.reference(),
134+
key: arg || createSimpleExpression('modelValue', true),
135+
value: exp,
136+
isComponent
137+
})
138+
139+
if (runtimeDirective) {
140+
context.registerOperation({
141+
type: IRNodeTypes.WITH_DIRECTIVE,
142+
element: context.reference(),
143+
dir,
144+
name: runtimeDirective,
145+
builtin: true
146+
})
147+
}
148+
149+
// NOTE: we don't need duplicate value check, because svelte binding allows it
150+
// function checkDuplicatedValue() {
151+
// const value = findDir(node, 'bind')
152+
// if (value && isStaticArgOf(value.arg, 'value')) {
153+
// context.options.onError(
154+
// createDOMCompilerError(
155+
// DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
156+
// value.loc,
157+
// ),
158+
// )
159+
// }
160+
// }
15161
}

0 commit comments

Comments
 (0)