Procházet zdrojové kódy

wip(compiler-dom): v-model runtime

Evan You před 6 roky
rodič
revize
d376439167

+ 1 - 1
packages/compiler-core/__tests__/transforms/transformElement.spec.ts

@@ -271,7 +271,7 @@ describe('compiler: element transform', () => {
         foo(dir) {
         foo(dir) {
           _dir = dir
           _dir = dir
           return {
           return {
-            props: createObjectProperty(dir.arg!, dir.exp!),
+            props: [createObjectProperty(dir.arg!, dir.exp!)],
             needRuntime: false
             needRuntime: false
           }
           }
         }
         }

+ 14 - 15
packages/compiler-core/src/ast.ts

@@ -2,7 +2,6 @@ import { isString } from '@vue/shared'
 import { ForParseResult } from './transforms/vFor'
 import { ForParseResult } from './transforms/vFor'
 import {
 import {
   CREATE_VNODE,
   CREATE_VNODE,
-  RuntimeHelper,
   APPLY_DIRECTIVES,
   APPLY_DIRECTIVES,
   RENDER_SLOT,
   RENDER_SLOT,
   CREATE_SLOTS,
   CREATE_SLOTS,
@@ -88,7 +87,7 @@ export type TemplateChildNode =
 export interface RootNode extends Node {
 export interface RootNode extends Node {
   type: NodeTypes.ROOT
   type: NodeTypes.ROOT
   children: TemplateChildNode[]
   children: TemplateChildNode[]
-  helpers: RuntimeHelper[]
+  helpers: symbol[]
   components: string[]
   components: string[]
   directives: string[]
   directives: string[]
   hoists: JSChildNode[]
   hoists: JSChildNode[]
@@ -184,7 +183,7 @@ export interface CompoundExpressionNode extends Node {
     | InterpolationNode
     | InterpolationNode
     | TextNode
     | TextNode
     | string
     | string
-    | RuntimeHelper)[]
+    | symbol)[]
   // an expression parsed as the params of a function will track
   // an expression parsed as the params of a function will track
   // the identifiers declared inside the function body.
   // the identifiers declared inside the function body.
   identifiers?: string[]
   identifiers?: string[]
@@ -226,10 +225,10 @@ export type JSChildNode =
 
 
 export interface CallExpression extends Node {
 export interface CallExpression extends Node {
   type: NodeTypes.JS_CALL_EXPRESSION
   type: NodeTypes.JS_CALL_EXPRESSION
-  callee: string | RuntimeHelper
+  callee: string | symbol
   arguments: (
   arguments: (
     | string
     | string
-    | RuntimeHelper
+    | symbol
     | JSChildNode
     | JSChildNode
     | TemplateChildNode
     | TemplateChildNode
     | TemplateChildNode[])[]
     | TemplateChildNode[])[]
@@ -276,17 +275,17 @@ export interface ConditionalExpression extends Node {
 export interface PlainElementCodegenNode extends CallExpression {
 export interface PlainElementCodegenNode extends CallExpression {
   callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
   callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
   arguments:  // tag, props, children, patchFlag, dynamicProps
   arguments:  // tag, props, children, patchFlag, dynamicProps
-    | [string | RuntimeHelper]
-    | [string | RuntimeHelper, PropsExpression]
-    | [string | RuntimeHelper, 'null' | PropsExpression, TemplateChildNode[]]
+    | [string | symbol]
+    | [string | symbol, PropsExpression]
+    | [string | symbol, 'null' | PropsExpression, TemplateChildNode[]]
     | [
     | [
-        string | RuntimeHelper,
+        string | symbol,
         'null' | PropsExpression,
         'null' | PropsExpression,
         'null' | TemplateChildNode[],
         'null' | TemplateChildNode[],
         string
         string
       ]
       ]
     | [
     | [
-        string | RuntimeHelper,
+        string | symbol,
         'null' | PropsExpression,
         'null' | PropsExpression,
         'null' | TemplateChildNode[],
         'null' | TemplateChildNode[],
         string,
         string,
@@ -302,17 +301,17 @@ export type ElementCodegenNode =
 export interface PlainComponentCodegenNode extends CallExpression {
 export interface PlainComponentCodegenNode extends CallExpression {
   callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
   callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
   arguments:  // Comp, props, slots, patchFlag, dynamicProps
   arguments:  // Comp, props, slots, patchFlag, dynamicProps
-    | [string | RuntimeHelper]
-    | [string | RuntimeHelper, PropsExpression]
-    | [string | RuntimeHelper, 'null' | PropsExpression, SlotsExpression]
+    | [string | symbol]
+    | [string | symbol, PropsExpression]
+    | [string | symbol, 'null' | PropsExpression, SlotsExpression]
     | [
     | [
-        string | RuntimeHelper,
+        string | symbol,
         'null' | PropsExpression,
         'null' | PropsExpression,
         'null' | SlotsExpression,
         'null' | SlotsExpression,
         string
         string
       ]
       ]
     | [
     | [
-        string | RuntimeHelper,
+        string | symbol,
         'null' | PropsExpression,
         'null' | PropsExpression,
         'null' | SlotsExpression,
         'null' | SlotsExpression,
         string,
         string,

+ 4 - 8
packages/compiler-core/src/codegen.ts

@@ -33,8 +33,7 @@ import {
   COMMENT,
   COMMENT,
   helperNameMap,
   helperNameMap,
   RESOLVE_COMPONENT,
   RESOLVE_COMPONENT,
-  RESOLVE_DIRECTIVE,
-  RuntimeHelper
+  RESOLVE_DIRECTIVE
 } from './runtimeHelpers'
 } from './runtimeHelpers'
 
 
 type CodegenNode = TemplateChildNode | JSChildNode
 type CodegenNode = TemplateChildNode | JSChildNode
@@ -74,7 +73,7 @@ export interface CodegenContext extends Required<CodegenOptions> {
   offset: number
   offset: number
   indentLevel: number
   indentLevel: number
   map?: SourceMapGenerator
   map?: SourceMapGenerator
-  helper(key: RuntimeHelper): string
+  helper(key: symbol): string
   push(code: string, node?: CodegenNode, openOnly?: boolean): void
   push(code: string, node?: CodegenNode, openOnly?: boolean): void
   resetMapping(loc: SourceLocation): void
   resetMapping(loc: SourceLocation): void
   indent(): void
   indent(): void
@@ -338,7 +337,7 @@ function genNodeListAsArray(
 }
 }
 
 
 function genNodeList(
 function genNodeList(
-  nodes: (string | RuntimeHelper | CodegenNode | TemplateChildNode[])[],
+  nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
   context: CodegenContext,
   context: CodegenContext,
   multilines: boolean = false
   multilines: boolean = false
 ) {
 ) {
@@ -363,10 +362,7 @@ function genNodeList(
   }
   }
 }
 }
 
 
-function genNode(
-  node: CodegenNode | RuntimeHelper | string,
-  context: CodegenContext
-) {
+function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
   if (isString(node)) {
   if (isString(node)) {
     context.push(node)
     context.push(node)
     return
     return

+ 6 - 0
packages/compiler-core/src/index.ts

@@ -98,3 +98,9 @@ export {
   createCompilerError
   createCompilerError
 } from './errors'
 } from './errors'
 export * from './ast'
 export * from './ast'
+export * from './utils'
+export { registerRuntimeHelpers } from './runtimeHelpers'
+
+// expose transforms so higher-order compilers can import and extend them
+export { transformModel } from './transforms/vModel'
+export { transformOn } from './transforms/vOn'

+ 8 - 22
packages/compiler-core/src/runtimeHelpers.ts

@@ -18,30 +18,10 @@ export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
 export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
 export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
 export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
 export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
 
 
-export type RuntimeHelper =
-  | typeof FRAGMENT
-  | typeof PORTAL
-  | typeof COMMENT
-  | typeof TEXT
-  | typeof SUSPENSE
-  | typeof EMPTY
-  | typeof OPEN_BLOCK
-  | typeof CREATE_BLOCK
-  | typeof CREATE_VNODE
-  | typeof RESOLVE_COMPONENT
-  | typeof RESOLVE_DIRECTIVE
-  | typeof APPLY_DIRECTIVES
-  | typeof RENDER_LIST
-  | typeof RENDER_SLOT
-  | typeof CREATE_SLOTS
-  | typeof TO_STRING
-  | typeof MERGE_PROPS
-  | typeof TO_HANDLERS
-  | typeof CAMELIZE
-
 // Name mapping for runtime helpers that need to be imported from 'vue' in
 // Name mapping for runtime helpers that need to be imported from 'vue' in
 // generated code. Make sure these are correctly exported in the runtime!
 // generated code. Make sure these are correctly exported in the runtime!
-export const helperNameMap = {
+// Using `any` here because TS doesn't allow symbols as index type.
+export const helperNameMap: any = {
   [FRAGMENT]: `Fragment`,
   [FRAGMENT]: `Fragment`,
   [PORTAL]: `Portal`,
   [PORTAL]: `Portal`,
   [COMMENT]: `Comment`,
   [COMMENT]: `Comment`,
@@ -62,3 +42,9 @@ export const helperNameMap = {
   [TO_HANDLERS]: `toHandlers`,
   [TO_HANDLERS]: `toHandlers`,
   [CAMELIZE]: `camelize`
   [CAMELIZE]: `camelize`
 }
 }
+
+export function registerRuntimeHelpers(helpers: any) {
+  Object.getOwnPropertySymbols(helpers).forEach(s => {
+    helperNameMap[s] = helpers[s]
+  })
+}

+ 5 - 6
packages/compiler-core/src/transform.ts

@@ -22,7 +22,6 @@ import {
   COMMENT,
   COMMENT,
   CREATE_VNODE,
   CREATE_VNODE,
   FRAGMENT,
   FRAGMENT,
-  RuntimeHelper,
   helperNameMap,
   helperNameMap,
   APPLY_DIRECTIVES,
   APPLY_DIRECTIVES,
   CREATE_BLOCK
   CREATE_BLOCK
@@ -48,8 +47,8 @@ export type DirectiveTransform = (
   node: ElementNode,
   node: ElementNode,
   context: TransformContext
   context: TransformContext
 ) => {
 ) => {
-  props: Property | Property[]
-  needRuntime: boolean
+  props: Property[]
+  needRuntime: boolean | symbol
 }
 }
 
 
 // A structural directive transform is a technically a NodeTransform;
 // A structural directive transform is a technically a NodeTransform;
@@ -70,7 +69,7 @@ export interface TransformOptions {
 
 
 export interface TransformContext extends Required<TransformOptions> {
 export interface TransformContext extends Required<TransformOptions> {
   root: RootNode
   root: RootNode
-  helpers: Set<RuntimeHelper>
+  helpers: Set<symbol>
   components: Set<string>
   components: Set<string>
   directives: Set<string>
   directives: Set<string>
   hoists: JSChildNode[]
   hoists: JSChildNode[]
@@ -84,8 +83,8 @@ export interface TransformContext extends Required<TransformOptions> {
   parent: ParentNode | null
   parent: ParentNode | null
   childIndex: number
   childIndex: number
   currentNode: RootNode | TemplateChildNode | null
   currentNode: RootNode | TemplateChildNode | null
-  helper<T extends RuntimeHelper>(name: T): T
-  helperString(name: RuntimeHelper): string
+  helper<T extends symbol>(name: T): T
+  helperString(name: symbol): string
   replaceNode(node: TemplateChildNode): void
   replaceNode(node: TemplateChildNode): void
   removeNode(node?: TemplateChildNode): void
   removeNode(node?: TemplateChildNode): void
   onNodeRemoved: () => void
   onNodeRemoved: () => void

+ 28 - 18
packages/compiler-core/src/transforms/transformElement.ts

@@ -15,7 +15,7 @@ import {
   createObjectExpression,
   createObjectExpression,
   Property
   Property
 } from '../ast'
 } from '../ast'
-import { isArray, PatchFlags, PatchFlagNames } from '@vue/shared'
+import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
 import {
   CREATE_VNODE,
   CREATE_VNODE,
@@ -28,6 +28,10 @@ import {
 import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
 import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
 import { buildSlots } from './vSlot'
 import { buildSlots } from './vSlot'
 
 
+// some directive transforms (e.g. v-model) may return a symbol for runtime
+// import, which should be used instead of a resolveDirective call.
+const directiveImportMap = new WeakMap<DirectiveNode, symbol>()
+
 // generate a JavaScript AST for this element's codegen
 // generate a JavaScript AST for this element's codegen
 export const transformElement: NodeTransform = (node, context) => {
 export const transformElement: NodeTransform = (node, context) => {
   if (node.type === NodeTypes.ELEMENT) {
   if (node.type === NodeTypes.ELEMENT) {
@@ -137,9 +141,7 @@ export const transformElement: NodeTransform = (node, context) => {
             [
             [
               vnode,
               vnode,
               createArrayExpression(
               createArrayExpression(
-                runtimeDirectives.map(dir => {
-                  return createDirectiveArgs(dir, context)
-                }),
+                runtimeDirectives.map(dir => createDirectiveArgs(dir, context)),
                 loc
                 loc
               )
               )
             ],
             ],
@@ -274,15 +276,13 @@ export function buildProps(
       if (directiveTransform) {
       if (directiveTransform) {
         // has built-in directive transform.
         // has built-in directive transform.
         const { props, needRuntime } = directiveTransform(prop, node, context)
         const { props, needRuntime } = directiveTransform(prop, node, context)
-        if (isArray(props)) {
-          properties.push(...props)
-          properties.forEach(analyzePatchFlag)
-        } else {
-          properties.push(props)
-          analyzePatchFlag(props)
-        }
+        props.forEach(analyzePatchFlag)
+        properties.push(...props)
         if (needRuntime) {
         if (needRuntime) {
           runtimeDirectives.push(prop)
           runtimeDirectives.push(prop)
+          if (isSymbol(needRuntime)) {
+            directiveImportMap.set(prop, needRuntime)
+          }
         }
         }
       } else {
       } else {
         // no built-in transform, this is a user custom directive.
         // no built-in transform, this is a user custom directive.
@@ -362,7 +362,12 @@ function dedupeProperties(properties: Property[]): Property[] {
     const name = prop.key.content
     const name = prop.key.content
     const existing = knownProps[name]
     const existing = knownProps[name]
     if (existing) {
     if (existing) {
-      if (name.startsWith('on') || name === 'style' || name === 'class') {
+      if (
+        name === 'style' ||
+        name === 'class' ||
+        name.startsWith('on') ||
+        name.startsWith('vnode')
+      ) {
         mergeAsArray(existing, prop)
         mergeAsArray(existing, prop)
       }
       }
       // unexpected duplicate, should have emitted error during parse
       // unexpected duplicate, should have emitted error during parse
@@ -389,12 +394,17 @@ function createDirectiveArgs(
   dir: DirectiveNode,
   dir: DirectiveNode,
   context: TransformContext
   context: TransformContext
 ): ArrayExpression {
 ): ArrayExpression {
-  // inject statement for resolving directive
-  context.helper(RESOLVE_DIRECTIVE)
-  context.directives.add(dir.name)
-  const dirArgs: ArrayExpression['elements'] = [
-    toValidAssetId(dir.name, `directive`)
-  ]
+  const dirArgs: ArrayExpression['elements'] = []
+  const runtime = directiveImportMap.get(dir)
+  if (runtime) {
+    context.helper(runtime)
+    dirArgs.push(context.helperString(runtime))
+  } else {
+    // inject statement for resolving directive
+    context.helper(RESOLVE_DIRECTIVE)
+    context.directives.add(dir.name)
+    dirArgs.push(toValidAssetId(dir.name, `directive`))
+  }
   const { loc } = dir
   const { loc } = dir
   if (dir.exp) dirArgs.push(dir.exp)
   if (dir.exp) dirArgs.push(dir.exp)
   if (dir.arg) dirArgs.push(dir.arg)
   if (dir.arg) dirArgs.push(dir.arg)

+ 3 - 4
packages/compiler-core/src/transforms/vBind.ts

@@ -28,10 +28,9 @@ export const transformBind: DirectiveTransform = (dir, node, context) => {
     }
     }
   }
   }
   return {
   return {
-    props: createObjectProperty(
-      arg!,
-      exp || createSimpleExpression('', true, loc)
-    ),
+    props: [
+      createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
+    ],
     needRuntime: false
     needRuntime: false
   }
   }
 }
 }

+ 8 - 2
packages/compiler-core/src/transforms/vModel.ts

@@ -38,7 +38,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
         ])
         ])
     : createSimpleExpression('onUpdate:modelValue', true)
     : createSimpleExpression('onUpdate:modelValue', true)
 
 
-  return createTransformProps([
+  const props = [
     createObjectProperty(propName, dir.exp!),
     createObjectProperty(propName, dir.exp!),
     createObjectProperty(
     createObjectProperty(
       eventName,
       eventName,
@@ -48,7 +48,13 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
         ` = $event)`
         ` = $event)`
       ])
       ])
     )
     )
-  ])
+  ]
+
+  if (dir.modifiers.length) {
+    // TODO add modelModifiers prop
+  }
+
+  return createTransformProps(props)
 }
 }
 
 
 function createTransformProps(props: Property[] = []) {
 function createTransformProps(props: Property[] = []) {

+ 6 - 4
packages/compiler-core/src/transforms/vOn.ts

@@ -69,10 +69,12 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
   }
   }
 
 
   return {
   return {
-    props: createObjectProperty(
-      eventName,
-      dir.exp || createSimpleExpression(`() => {}`, false, loc)
-    ),
+    props: [
+      createObjectProperty(
+        eventName,
+        dir.exp || createSimpleExpression(`() => {}`, false, loc)
+      )
+    ],
     needRuntime: false
     needRuntime: false
   }
   }
 }
 }

+ 6 - 4
packages/compiler-core/src/transforms/vOnce.ts

@@ -6,10 +6,12 @@ import {
 
 
 export const transformOnce: DirectiveTransform = dir => {
 export const transformOnce: DirectiveTransform = dir => {
   return {
   return {
-    props: createObjectProperty(
-      createSimpleExpression(`$once`, true, dir.loc),
-      createSimpleExpression('true', false)
-    ),
+    props: [
+      createObjectProperty(
+        createSimpleExpression(`$once`, true, dir.loc),
+        createSimpleExpression('true', false)
+      )
+    ],
     needRuntime: false
     needRuntime: false
   }
   }
 }
 }

+ 6 - 2
packages/compiler-dom/src/errors.ts

@@ -24,12 +24,16 @@ export const enum DOMErrorCodes {
   X_V_HTML_NO_EXPRESSION = ErrorCodes.__EXTEND_POINT__,
   X_V_HTML_NO_EXPRESSION = ErrorCodes.__EXTEND_POINT__,
   X_V_HTML_WITH_CHILDREN,
   X_V_HTML_WITH_CHILDREN,
   X_V_TEXT_NO_EXPRESSION,
   X_V_TEXT_NO_EXPRESSION,
-  X_V_TEXT_WITH_CHILDREN
+  X_V_TEXT_WITH_CHILDREN,
+  X_V_MODEL_ON_INVALID_ELEMENT,
+  X_V_MODEL_ARG_ON_ELEMENT
 }
 }
 
 
 export const DOMErrorMessages: { [code: number]: string } = {
 export const DOMErrorMessages: { [code: number]: string } = {
   [DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
   [DOMErrorCodes.X_V_HTML_NO_EXPRESSION]: `v-html is missing expression.`,
   [DOMErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
   [DOMErrorCodes.X_V_HTML_WITH_CHILDREN]: `v-html will override element children.`,
   [DOMErrorCodes.X_V_TEXT_NO_EXPRESSION]: `v-text is missing expression.`,
   [DOMErrorCodes.X_V_TEXT_NO_EXPRESSION]: `v-text is missing expression.`,
-  [DOMErrorCodes.X_V_TEXT_WITH_CHILDREN]: `v-text will override element children.`
+  [DOMErrorCodes.X_V_TEXT_WITH_CHILDREN]: `v-text will override element children.`,
+  [DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT]: `v-model can only be used on <input>, <textarea> and <select> elements.`,
+  [DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT]: `v-model argument is not supported on plain elements.`
 }
 }

+ 2 - 0
packages/compiler-dom/src/index.ts

@@ -5,6 +5,7 @@ import { transformStyle } from './transforms/transformStyle'
 import { transformCloak } from './transforms/vCloak'
 import { transformCloak } from './transforms/vCloak'
 import { transformVHtml } from './transforms/vHtml'
 import { transformVHtml } from './transforms/vHtml'
 import { transformVText } from './transforms/vText'
 import { transformVText } from './transforms/vText'
+import { transformModel } from './transforms/vModel'
 
 
 export function compile(
 export function compile(
   template: string,
   template: string,
@@ -18,6 +19,7 @@ export function compile(
       cloak: transformCloak,
       cloak: transformCloak,
       html: transformVHtml,
       html: transformVHtml,
       text: transformVText,
       text: transformVText,
+      model: transformModel, // override compiler-core
       ...(options.directiveTransforms || {})
       ...(options.directiveTransforms || {})
     }
     }
   })
   })

+ 15 - 0
packages/compiler-dom/src/runtimeHelpers.ts

@@ -0,0 +1,15 @@
+import { registerRuntimeHelpers } from '@vue/compiler-core'
+
+export const V_MODEL_RADIO = Symbol(__DEV__ ? `vModelRadio` : ``)
+export const V_MODEL_CHECKBOX = Symbol(__DEV__ ? `vModelCheckbox` : ``)
+export const V_MODEL_TEXT = Symbol(__DEV__ ? `vModelText` : ``)
+export const V_MODEL_SELECT = Symbol(__DEV__ ? `vModelSelect` : ``)
+export const V_MODEL_DYNAMIC = Symbol(__DEV__ ? `vModelDynamic` : ``)
+
+registerRuntimeHelpers({
+  [V_MODEL_RADIO]: `vModelRadio`,
+  [V_MODEL_CHECKBOX]: `vModelCheckbox`,
+  [V_MODEL_TEXT]: `vModelText`,
+  [V_MODEL_SELECT]: `vModelSelect`,
+  [V_MODEL_DYNAMIC]: `vModelDynamic`
+})

+ 6 - 4
packages/compiler-dom/src/transforms/vHtml.ts

@@ -19,10 +19,12 @@ export const transformVHtml: DirectiveTransform = (dir, node, context) => {
     node.children.length = 0
     node.children.length = 0
   }
   }
   return {
   return {
-    props: createObjectProperty(
-      createSimpleExpression(`innerHTML`, true, loc),
-      exp || createSimpleExpression('', true)
-    ),
+    props: [
+      createObjectProperty(
+        createSimpleExpression(`innerHTML`, true, loc),
+        exp || createSimpleExpression('', true)
+      )
+    ],
     needRuntime: false
     needRuntime: false
   }
   }
 }
 }

+ 66 - 1
packages/compiler-dom/src/transforms/vModel.ts

@@ -1 +1,66 @@
-// TODO
+import {
+  transformModel as baseTransform,
+  DirectiveTransform,
+  ElementTypes,
+  findProp,
+  NodeTypes
+} from '@vue/compiler-core'
+import { createDOMCompilerError, DOMErrorCodes } from '../errors'
+import {
+  V_MODEL_CHECKBOX,
+  V_MODEL_RADIO,
+  V_MODEL_SELECT,
+  V_MODEL_TEXT,
+  V_MODEL_DYNAMIC
+} from '../runtimeHelpers'
+
+export const transformModel: DirectiveTransform = (dir, node, context) => {
+  const res = baseTransform(dir, node, context)
+  const { tag, tagType } = node
+  if (tagType === ElementTypes.ELEMENT) {
+    if (dir.arg) {
+      context.onError(
+        createDOMCompilerError(
+          DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
+          dir.arg.loc
+        )
+      )
+    }
+
+    if (tag === 'input' || tag === 'textarea' || tag === 'select') {
+      let directiveToUse = V_MODEL_TEXT
+      if (tag === 'input') {
+        const type = findProp(node, `type`)
+        if (type) {
+          if (type.type === NodeTypes.DIRECTIVE) {
+            // :type="foo"
+            directiveToUse = V_MODEL_DYNAMIC
+          } else if (type.value) {
+            switch (type.value.content) {
+              case 'radio':
+                directiveToUse = V_MODEL_RADIO
+                break
+              case 'checkbox':
+                directiveToUse = V_MODEL_CHECKBOX
+                break
+            }
+          }
+        }
+      } else if (tag === 'select') {
+        directiveToUse = V_MODEL_SELECT
+      }
+      // inject runtime directive
+      // by returning the helper symbol via needRuntime
+      // the import will replaced a resovleDirective call.
+      res.needRuntime = context.helper(directiveToUse)
+    } else {
+      context.onError(
+        createDOMCompilerError(
+          DOMErrorCodes.X_V_MODEL_ON_INVALID_ELEMENT,
+          dir.loc
+        )
+      )
+    }
+  }
+  return res
+}

+ 6 - 4
packages/compiler-dom/src/transforms/vText.ts

@@ -19,10 +19,12 @@ export const transformVText: DirectiveTransform = (dir, node, context) => {
     node.children.length = 0
     node.children.length = 0
   }
   }
   return {
   return {
-    props: createObjectProperty(
-      createSimpleExpression(`textContent`, true, loc),
-      exp || createSimpleExpression('', true)
-    ),
+    props: [
+      createObjectProperty(
+        createSimpleExpression(`textContent`, true, loc),
+        exp || createSimpleExpression('', true)
+      )
+    ],
     needRuntime: false
     needRuntime: false
   }
   }
 }
 }

+ 1 - 0
packages/runtime-core/src/index.ts

@@ -29,6 +29,7 @@ export { getCurrentInstance } from './component'
 
 
 // For custom renderers
 // For custom renderers
 export { createRenderer } from './createRenderer'
 export { createRenderer } from './createRenderer'
+export { warn } from './warning'
 export {
 export {
   handleError,
   handleError,
   callWithErrorHandling,
   callWithErrorHandling,

+ 40 - 0
packages/runtime-dom/src/directives/vModel.ts

@@ -0,0 +1,40 @@
+import { Directive } from '@vue/runtime-core'
+
+// We are exporting the v-model runtime directly as vnode hooks so that it can
+// be tree-shaken in case v-model is never used.
+export const vModelText: Directive = {
+  beforeMount(el, binding) {
+    el.value = binding.value
+  },
+  mounted(el, binding, vnode) {},
+  beforeUpdate(el, binding, vnode, prevVNode) {},
+  updated(el, binding, vnode) {}
+}
+
+export const vModelRadio: Directive = {
+  beforeMount(el, binding, vnode) {},
+  mounted(el, binding, vnode) {},
+  beforeUpdate(el, binding, vnode, prevVNode) {},
+  updated(el, binding, vnode) {}
+}
+
+export const vModelCheckbox: Directive = {
+  beforeMount(el, binding, vnode) {},
+  mounted(el, binding, vnode) {},
+  beforeUpdate(el, binding, vnode, prevVNode) {},
+  updated(el, binding, vnode) {}
+}
+
+export const vModelSelect: Directive = {
+  beforeMount(el, binding, vnode) {},
+  mounted(el, binding, vnode) {},
+  beforeUpdate(el, binding, vnode, prevVNode) {},
+  updated(el, binding, vnode) {}
+}
+
+export const vModelDynamic: Directive = {
+  beforeMount(el, binding, vnode) {},
+  mounted(el, binding, vnode) {},
+  beforeUpdate(el, binding, vnode, prevVNode) {},
+  updated(el, binding, vnode) {}
+}

+ 10 - 0
packages/runtime-dom/src/index.ts

@@ -9,10 +9,20 @@ const { render, createApp } = createRenderer<Node, Element>({
 
 
 export { render, createApp }
 export { render, createApp }
 
 
+// DOM-only runtime helpers
+export {
+  vModelText,
+  vModelCheckbox,
+  vModelRadio,
+  vModelSelect,
+  vModelDynamic
+} from './directives/vModel'
+
 // re-export everything from core
 // re-export everything from core
 // h, Component, reactivity API, nextTick, flags & types
 // h, Component, reactivity API, nextTick, flags & types
 export * from '@vue/runtime-core'
 export * from '@vue/runtime-core'
 
 
+// Type augmentations
 export interface ComponentPublicInstance {
 export interface ComponentPublicInstance {
   $el: Element
   $el: Element
 }
 }

+ 4 - 0
packages/runtime-dom/src/patchProp.ts

@@ -29,6 +29,10 @@ export function patchProp(
     case 'style':
     case 'style':
       patchStyle(el, prevValue, nextValue)
       patchStyle(el, prevValue, nextValue)
       break
       break
+    case 'modelValue':
+    case 'onUpdate:modelValue':
+      // Do nothing. This is handled by v-model directives.
+      break
     default:
     default:
       if (isOn(key)) {
       if (isOn(key)) {
         patchEvent(
         patchEvent(