foo(dir) {
_dir = dir
return {
- props: createObjectProperty(dir.arg!, dir.exp!),
+ props: [createObjectProperty(dir.arg!, dir.exp!)],
needRuntime: false
}
}
import { ForParseResult } from './transforms/vFor'
import {
CREATE_VNODE,
- RuntimeHelper,
APPLY_DIRECTIVES,
RENDER_SLOT,
CREATE_SLOTS,
export interface RootNode extends Node {
type: NodeTypes.ROOT
children: TemplateChildNode[]
- helpers: RuntimeHelper[]
+ helpers: symbol[]
components: string[]
directives: string[]
hoists: JSChildNode[]
| InterpolationNode
| TextNode
| string
- | RuntimeHelper)[]
+ | symbol)[]
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
- callee: string | RuntimeHelper
+ callee: string | symbol
arguments: (
| string
- | RuntimeHelper
+ | symbol
| JSChildNode
| TemplateChildNode
| TemplateChildNode[])[]
export interface PlainElementCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
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' | TemplateChildNode[],
string
]
| [
- string | RuntimeHelper,
+ string | symbol,
'null' | PropsExpression,
'null' | TemplateChildNode[],
string,
export interface PlainComponentCodegenNode extends CallExpression {
callee: typeof CREATE_VNODE | typeof CREATE_BLOCK
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' | SlotsExpression,
string
]
| [
- string | RuntimeHelper,
+ string | symbol,
'null' | PropsExpression,
'null' | SlotsExpression,
string,
COMMENT,
helperNameMap,
RESOLVE_COMPONENT,
- RESOLVE_DIRECTIVE,
- RuntimeHelper
+ RESOLVE_DIRECTIVE
} from './runtimeHelpers'
type CodegenNode = TemplateChildNode | JSChildNode
offset: number
indentLevel: number
map?: SourceMapGenerator
- helper(key: RuntimeHelper): string
+ helper(key: symbol): string
push(code: string, node?: CodegenNode, openOnly?: boolean): void
resetMapping(loc: SourceLocation): void
indent(): void
}
function genNodeList(
- nodes: (string | RuntimeHelper | CodegenNode | TemplateChildNode[])[],
+ nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
context: CodegenContext,
multilines: boolean = false
) {
}
}
-function genNode(
- node: CodegenNode | RuntimeHelper | string,
- context: CodegenContext
-) {
+function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
createCompilerError
} from './errors'
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'
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
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
// 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`,
[PORTAL]: `Portal`,
[COMMENT]: `Comment`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`
}
+
+export function registerRuntimeHelpers(helpers: any) {
+ Object.getOwnPropertySymbols(helpers).forEach(s => {
+ helperNameMap[s] = helpers[s]
+ })
+}
COMMENT,
CREATE_VNODE,
FRAGMENT,
- RuntimeHelper,
helperNameMap,
APPLY_DIRECTIVES,
CREATE_BLOCK
node: ElementNode,
context: TransformContext
) => {
- props: Property | Property[]
- needRuntime: boolean
+ props: Property[]
+ needRuntime: boolean | symbol
}
// A structural directive transform is a technically a NodeTransform;
export interface TransformContext extends Required<TransformOptions> {
root: RootNode
- helpers: Set<RuntimeHelper>
+ helpers: Set<symbol>
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
parent: ParentNode | null
childIndex: number
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
removeNode(node?: TemplateChildNode): void
onNodeRemoved: () => void
createObjectExpression,
Property
} from '../ast'
-import { isArray, PatchFlags, PatchFlagNames } from '@vue/shared'
+import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import {
CREATE_VNODE,
import { getInnerRange, isVSlot, toValidAssetId } from '../utils'
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
export const transformElement: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
[
vnode,
createArrayExpression(
- runtimeDirectives.map(dir => {
- return createDirectiveArgs(dir, context)
- }),
+ runtimeDirectives.map(dir => createDirectiveArgs(dir, context)),
loc
)
],
if (directiveTransform) {
// has built-in directive transform.
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) {
runtimeDirectives.push(prop)
+ if (isSymbol(needRuntime)) {
+ directiveImportMap.set(prop, needRuntime)
+ }
}
} else {
// no built-in transform, this is a user custom directive.
const name = prop.key.content
const existing = knownProps[name]
if (existing) {
- if (name.startsWith('on') || name === 'style' || name === 'class') {
+ if (
+ name === 'style' ||
+ name === 'class' ||
+ name.startsWith('on') ||
+ name.startsWith('vnode')
+ ) {
mergeAsArray(existing, prop)
}
// unexpected duplicate, should have emitted error during parse
dir: DirectiveNode,
context: TransformContext
): 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
if (dir.exp) dirArgs.push(dir.exp)
if (dir.arg) dirArgs.push(dir.arg)
}
}
return {
- props: createObjectProperty(
- arg!,
- exp || createSimpleExpression('', true, loc)
- ),
+ props: [
+ createObjectProperty(arg!, exp || createSimpleExpression('', true, loc))
+ ],
needRuntime: false
}
}
])
: createSimpleExpression('onUpdate:modelValue', true)
- return createTransformProps([
+ const props = [
createObjectProperty(propName, dir.exp!),
createObjectProperty(
eventName,
` = $event)`
])
)
- ])
+ ]
+
+ if (dir.modifiers.length) {
+ // TODO add modelModifiers prop
+ }
+
+ return createTransformProps(props)
}
function createTransformProps(props: Property[] = []) {
}
return {
- props: createObjectProperty(
- eventName,
- dir.exp || createSimpleExpression(`() => {}`, false, loc)
- ),
+ props: [
+ createObjectProperty(
+ eventName,
+ dir.exp || createSimpleExpression(`() => {}`, false, loc)
+ )
+ ],
needRuntime: false
}
}
export const transformOnce: DirectiveTransform = dir => {
return {
- props: createObjectProperty(
- createSimpleExpression(`$once`, true, dir.loc),
- createSimpleExpression('true', false)
- ),
+ props: [
+ createObjectProperty(
+ createSimpleExpression(`$once`, true, dir.loc),
+ createSimpleExpression('true', false)
+ )
+ ],
needRuntime: false
}
}
X_V_HTML_NO_EXPRESSION = ErrorCodes.__EXTEND_POINT__,
X_V_HTML_WITH_CHILDREN,
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 } = {
[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_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.`
}
import { transformCloak } from './transforms/vCloak'
import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
+import { transformModel } from './transforms/vModel'
export function compile(
template: string,
cloak: transformCloak,
html: transformVHtml,
text: transformVText,
+ model: transformModel, // override compiler-core
...(options.directiveTransforms || {})
}
})
--- /dev/null
+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`
+})
node.children.length = 0
}
return {
- props: createObjectProperty(
- createSimpleExpression(`innerHTML`, true, loc),
- exp || createSimpleExpression('', true)
- ),
+ props: [
+ createObjectProperty(
+ createSimpleExpression(`innerHTML`, true, loc),
+ exp || createSimpleExpression('', true)
+ )
+ ],
needRuntime: false
}
}
-// 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
+}
node.children.length = 0
}
return {
- props: createObjectProperty(
- createSimpleExpression(`textContent`, true, loc),
- exp || createSimpleExpression('', true)
- ),
+ props: [
+ createObjectProperty(
+ createSimpleExpression(`textContent`, true, loc),
+ exp || createSimpleExpression('', true)
+ )
+ ],
needRuntime: false
}
}
// For custom renderers
export { createRenderer } from './createRenderer'
+export { warn } from './warning'
export {
handleError,
callWithErrorHandling,
--- /dev/null
+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) {}
+}
export { render, createApp }
+// DOM-only runtime helpers
+export {
+ vModelText,
+ vModelCheckbox,
+ vModelRadio,
+ vModelSelect,
+ vModelDynamic
+} from './directives/vModel'
+
// re-export everything from core
// h, Component, reactivity API, nextTick, flags & types
export * from '@vue/runtime-core'
+// Type augmentations
export interface ComponentPublicInstance {
$el: Element
}
case 'style':
patchStyle(el, prevValue, nextValue)
break
+ case 'modelValue':
+ case 'onUpdate:modelValue':
+ // Do nothing. This is handled by v-model directives.
+ break
default:
if (isOn(key)) {
patchEvent(