chore: Merge branch 'v2-compat'

This commit is contained in:
Evan You 2021-04-28 12:30:57 -04:00
commit cd33714935
93 changed files with 5386 additions and 346 deletions

View File

@ -41,7 +41,7 @@ module.exports = {
}, },
// Packages targeting DOM // Packages targeting DOM
{ {
files: ['packages/{vue,runtime-dom}/**'], files: ['packages/{vue,vue-compat,runtime-dom}/**'],
rules: { rules: {
'no-restricted-globals': ['error', ...NodeGlobals] 'no-restricted-globals': ['error', ...NodeGlobals]
} }

View File

@ -12,7 +12,8 @@ module.exports = {
__NODE_JS__: true, __NODE_JS__: true,
__FEATURE_OPTIONS_API__: true, __FEATURE_OPTIONS_API__: true,
__FEATURE_SUSPENSE__: true, __FEATURE_SUSPENSE__: true,
__FEATURE_PROD_DEVTOOLS__: false __FEATURE_PROD_DEVTOOLS__: false,
__COMPAT__: true
}, },
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
coverageReporters: ['html', 'lcov', 'text'], coverageReporters: ['html', 'lcov', 'text'],
@ -34,6 +35,7 @@ module.exports = {
watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'], watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
moduleNameMapper: { moduleNameMapper: {
'@vue/compat': '<rootDir>/packages/vue-compat/src',
'^@vue/(.*?)$': '<rootDir>/packages/$1/src', '^@vue/(.*?)$': '<rootDir>/packages/$1/src',
vue: '<rootDir>/packages/vue/src' vue: '<rootDir>/packages/vue/src'
}, },

View File

@ -1736,20 +1736,26 @@ foo
}) })
}) })
describe('whitespace management', () => { describe('whitespace management when adopting strategy condense', () => {
const parse = (content: string, options?: ParserOptions) =>
baseParse(content, {
whitespace: 'condense',
...options
})
it('should remove whitespaces at start/end inside an element', () => { it('should remove whitespaces at start/end inside an element', () => {
const ast = baseParse(`<div> <span/> </div>`) const ast = parse(`<div> <span/> </div>`)
expect((ast.children[0] as ElementNode).children.length).toBe(1) expect((ast.children[0] as ElementNode).children.length).toBe(1)
}) })
it('should remove whitespaces w/ newline between elements', () => { it('should remove whitespaces w/ newline between elements', () => {
const ast = baseParse(`<div/> \n <div/> \n <div/>`) const ast = parse(`<div/> \n <div/> \n <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true) expect(ast.children.every(c => c.type === NodeTypes.ELEMENT)).toBe(true)
}) })
it('should remove whitespaces adjacent to comments', () => { it('should remove whitespaces adjacent to comments', () => {
const ast = baseParse(`<div/> \n <!--foo--> <div/>`) const ast = parse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@ -1757,7 +1763,7 @@ foo
}) })
it('should remove whitespaces w/ newline between comments and elements', () => { it('should remove whitespaces w/ newline between comments and elements', () => {
const ast = baseParse(`<div/> \n <!--foo--> \n <div/>`) const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.ELEMENT) expect(ast.children[0].type).toBe(NodeTypes.ELEMENT)
expect(ast.children[1].type).toBe(NodeTypes.COMMENT) expect(ast.children[1].type).toBe(NodeTypes.COMMENT)
@ -1765,7 +1771,7 @@ foo
}) })
it('should NOT remove whitespaces w/ newline between interpolations', () => { it('should NOT remove whitespaces w/ newline between interpolations', () => {
const ast = baseParse(`{{ foo }} \n {{ bar }}`) const ast = parse(`{{ foo }} \n {{ bar }}`)
expect(ast.children.length).toBe(3) expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION) expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
expect(ast.children[1]).toMatchObject({ expect(ast.children[1]).toMatchObject({
@ -1776,7 +1782,7 @@ foo
}) })
it('should NOT remove whitespaces w/o newline between elements', () => { it('should NOT remove whitespaces w/o newline between elements', () => {
const ast = baseParse(`<div/> <div/> <div/>`) const ast = parse(`<div/> <div/> <div/>`)
expect(ast.children.length).toBe(5) expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([ expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT, NodeTypes.ELEMENT,
@ -1788,7 +1794,7 @@ foo
}) })
it('should condense consecutive whitespaces in text', () => { it('should condense consecutive whitespaces in text', () => {
const ast = baseParse(` foo \n bar baz `) const ast = parse(` foo \n bar baz `)
expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `) expect((ast.children[0] as TextNode).content).toBe(` foo bar baz `)
}) })
@ -1824,6 +1830,84 @@ foo
}) })
}) })
describe('whitespace management when adopting strategy preserve', () => {
const parse = (content: string, options?: ParserOptions) =>
baseParse(content, {
whitespace: 'preserve',
...options
})
it('should still remove whitespaces at start/end inside an element', () => {
const ast = parse(`<div> <span/> </div>`)
expect((ast.children[0] as ElementNode).children.length).toBe(1)
})
it('should preserve whitespaces w/ newline between elements', () => {
const ast = parse(`<div/> \n <div/> \n <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT
])
})
it('should preserve whitespaces adjacent to comments', () => {
const ast = parse(`<div/> \n <!--foo--> <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.COMMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT
])
})
it('should preserve whitespaces w/ newline between comments and elements', () => {
const ast = parse(`<div/> \n <!--foo--> \n <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.COMMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT
])
})
it('should preserve whitespaces w/ newline between interpolations', () => {
const ast = parse(`{{ foo }} \n {{ bar }}`)
expect(ast.children.length).toBe(3)
expect(ast.children[0].type).toBe(NodeTypes.INTERPOLATION)
expect(ast.children[1]).toMatchObject({
type: NodeTypes.TEXT,
content: ' '
})
expect(ast.children[2].type).toBe(NodeTypes.INTERPOLATION)
})
it('should preserve whitespaces w/o newline between elements', () => {
const ast = parse(`<div/> <div/> <div/>`)
expect(ast.children.length).toBe(5)
expect(ast.children.map(c => c.type)).toMatchObject([
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT,
NodeTypes.TEXT,
NodeTypes.ELEMENT
])
})
it('should preserve consecutive whitespaces in text', () => {
const content = ` foo \n bar baz `
const ast = parse(content)
expect((ast.children[0] as TextNode).content).toBe(content)
})
})
describe('Errors', () => { describe('Errors', () => {
const patterns: { const patterns: {
[key: string]: Array<{ [key: string]: Array<{

View File

@ -306,6 +306,7 @@ describe('compiler: v-if', () => {
code: ErrorCodes.X_V_IF_SAME_KEY code: ErrorCodes.X_V_IF_SAME_KEY
} }
]) ])
expect('unnecessary key usage on v-if').toHaveBeenWarned()
}) })
}) })

View File

@ -11,6 +11,7 @@
], ],
"buildOptions": { "buildOptions": {
"name": "VueCompilerCore", "name": "VueCompilerCore",
"compat": true,
"formats": [ "formats": [
"esm-bundler", "esm-bundler",
"cjs" "cjs"

View File

@ -109,6 +109,9 @@ export interface RootNode extends Node {
temps: number temps: number
ssrHelpers?: symbol[] ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
// v2 compat only
filters?: string[]
} }
export type ElementNode = export type ElementNode =

View File

@ -50,7 +50,8 @@ import {
CREATE_BLOCK, CREATE_BLOCK,
OPEN_BLOCK, OPEN_BLOCK,
CREATE_STATIC, CREATE_STATIC,
WITH_CTX WITH_CTX,
RESOLVE_FILTER
} from './runtimeHelpers' } from './runtimeHelpers'
import { ImportItem } from './transform' import { ImportItem } from './transform'
@ -274,6 +275,12 @@ export function generate(
newline() newline()
} }
} }
if (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
newline()
}
if (ast.temps > 0) { if (ast.temps > 0) {
push(`let `) push(`let `)
for (let i = 0; i < ast.temps; i++) { for (let i = 0; i < ast.temps; i++) {
@ -458,11 +465,15 @@ function genModulePreamble(
function genAssets( function genAssets(
assets: string[], assets: string[],
type: 'component' | 'directive', type: 'component' | 'directive' | 'filter',
{ helper, push, newline }: CodegenContext { helper, push, newline }: CodegenContext
) { ) {
const resolver = helper( const resolver = helper(
type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE __COMPAT__ && type === 'filter'
? RESOLVE_FILTER
: type === 'component'
? RESOLVE_COMPONENT
: RESOLVE_DIRECTIVE
) )
for (let i = 0; i < assets.length; i++) { for (let i = 0; i < assets.length; i++) {
let id = assets[i] let id = assets[i]
@ -727,14 +738,12 @@ function genExpressionAsPropertyKey(
} }
function genComment(node: CommentNode, context: CodegenContext) { function genComment(node: CommentNode, context: CodegenContext) {
if (__DEV__) {
const { push, helper, pure } = context const { push, helper, pure } = context
if (pure) { if (pure) {
push(PURE_ANNOTATION) push(PURE_ANNOTATION)
} }
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node) push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
} }
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) { function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure } = context const { push, helper, pure } = context

View File

@ -0,0 +1,167 @@
import { SourceLocation } from '../ast'
import { CompilerError } from '../errors'
import { ParserContext } from '../parse'
import { TransformContext } from '../transform'
export type CompilerCompatConfig = Partial<
Record<CompilerDeprecationTypes, boolean | 'suppress-warning'>
> & {
MODE?: 2 | 3
}
export interface CompilerCompatOptions {
compatConfig?: CompilerCompatConfig
}
export const enum CompilerDeprecationTypes {
COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC',
COMPILER_V_BIND_PROP = 'COMPILER_V_BIND_PROP',
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
COMPILER_V_ON_NATIVE = 'COMPILER_V_ON_NATIVE',
COMPILER_V_IF_V_FOR_PRECEDENCE = 'COMPILER_V_IF_V_FOR_PRECEDENCE',
COMPILER_V_FOR_REF = 'COMPILER_V_FOR_REF',
COMPILER_NATIVE_TEMPLATE = 'COMPILER_NATIVE_TEMPLATE',
COMPILER_INLINE_TEMPLATE = 'COMPILER_INLINE_TEMPLATE',
COMPILER_FILTERS = 'COMPILER_FILTER'
}
type DeprecationData = {
message: string | ((...args: any[]) => string)
link?: string
}
const deprecationData: Record<CompilerDeprecationTypes, DeprecationData> = {
[CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT]: {
message:
`Platform-native elements with "is" prop will no longer be ` +
`treated as components in Vue 3 unless the "is" value is explicitly ` +
`prefixed with "vue:".`,
link: `https://v3.vuejs.org/guide/migration/custom-elements-interop.html`
},
[CompilerDeprecationTypes.COMPILER_V_BIND_SYNC]: {
message: key =>
`.sync modifier for v-bind has been removed. Use v-model with ` +
`argument instead. \`v-bind:${key}.sync\` should be changed to ` +
`\`v-model:${key}\`.`,
link: `https://v3.vuejs.org/guide/migration/v-model.html`
},
[CompilerDeprecationTypes.COMPILER_V_BIND_PROP]: {
message:
`.prop modifier for v-bind has been removed and no longer necessary. ` +
`Vue 3 will automatically set a binding as DOM property when appropriate.`
},
[CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER]: {
message:
`v-bind="obj" usage is now order sensitive and behaves like JavaScript ` +
`object spread: it will now overwrite an existing non-mergeable attribute ` +
`that appears before v-bind in the case of conflict. ` +
`To retain 2.x behavior, move v-bind to make it the first attribute. ` +
`You can also suppress this warning if the usage is intended.`,
link: `https://v3.vuejs.org/guide/migration/v-bind.html`
},
[CompilerDeprecationTypes.COMPILER_V_ON_NATIVE]: {
message: `.native modifier for v-on has been removed as is no longer necessary.`,
link: `https://v3.vuejs.org/guide/migration/v-on-native-modifier-removed.html`
},
[CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE]: {
message:
`v-if / v-for precedence when used on the same element has changed ` +
`in Vue 3: v-if now takes higher precedence and will no longer have ` +
`access to v-for scope variables. It is best to avoid the ambiguity ` +
`with <template> tags or use a computed property that filters v-for ` +
`data source.`,
link: `https://v3.vuejs.org/guide/migration/v-if-v-for.html`
},
[CompilerDeprecationTypes.COMPILER_V_FOR_REF]: {
message:
`Ref usage on v-for no longer creates array ref values in Vue 3. ` +
`Consider using function refs or refactor to avoid ref usage altogether.`,
link: `https://v3.vuejs.org/guide/migration/array-refs.html`
},
[CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE]: {
message:
`<template> with no special directives will render as a native template ` +
`element instead of its inner content in Vue 3.`
},
[CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE]: {
message: `"inline-template" has been removed in Vue 3.`,
link: `https://v3.vuejs.org/guide/migration/inline-template-attribute.html`
},
[CompilerDeprecationTypes.COMPILER_FILTERS]: {
message:
`filters have been removed in Vue 3. ` +
`The "|" symbol will be treated as native JavaScript bitwise OR operator. ` +
`Use method calls or computed properties instead.`,
link: `https://v3.vuejs.org/guide/migration/filters.html`
}
}
function getCompatValue(
key: CompilerDeprecationTypes | 'MODE',
context: ParserContext | TransformContext
) {
const config = (context as ParserContext).options
? (context as ParserContext).options.compatConfig
: (context as TransformContext).compatConfig
const value = config && config[key]
if (key === 'MODE') {
return value || 3 // compiler defaults to v3 behavior
} else {
return value
}
}
export function isCompatEnabled(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext
) {
const mode = getCompatValue('MODE', context)
const value = getCompatValue(key, context)
// in v3 mode, only enable if explicitly set to true
// otherwise enable for any non-false value
return mode === 3 ? value === true : value !== false
}
export function checkCompatEnabled(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext,
loc: SourceLocation | null,
...args: any[]
): boolean {
const enabled = isCompatEnabled(key, context)
if (__DEV__ && enabled) {
warnDeprecation(key, context, loc, ...args)
}
return enabled
}
export function warnDeprecation(
key: CompilerDeprecationTypes,
context: ParserContext | TransformContext,
loc: SourceLocation | null,
...args: any[]
) {
const val = getCompatValue(key, context)
if (val === 'suppress-warning') {
return
}
const { message, link } = deprecationData[key]
const msg = `(deprecation ${key}) ${
typeof message === 'function' ? message(...args) : message
}${link ? `\n Details: ${link}` : ``}`
const err = new SyntaxError(msg) as CompilerError
err.code = key
if (loc) err.loc = loc
context.onWarn(err)
}

View File

@ -0,0 +1,194 @@
import { RESOLVE_FILTER } from '../runtimeHelpers'
import {
AttributeNode,
DirectiveNode,
NodeTransform,
NodeTypes,
SimpleExpressionNode,
toValidAssetId,
TransformContext
} from '@vue/compiler-core'
import {
CompilerDeprecationTypes,
isCompatEnabled,
warnDeprecation
} from './compatConfig'
import { ExpressionNode } from '../ast'
const validDivisionCharRE = /[\w).+\-_$\]]/
export const transformFilter: NodeTransform = (node, context) => {
if (!isCompatEnabled(CompilerDeprecationTypes.COMPILER_FILTERS, context)) {
return
}
if (node.type === NodeTypes.INTERPOLATION) {
// filter rewrite is applied before expression transform so only
// simple expressions are possible at this stage
rewriteFilter(node.content, context)
}
if (node.type === NodeTypes.ELEMENT) {
node.props.forEach((prop: AttributeNode | DirectiveNode) => {
if (
prop.type === NodeTypes.DIRECTIVE &&
prop.name !== 'for' &&
prop.exp
) {
rewriteFilter(prop.exp, context)
}
})
}
}
function rewriteFilter(node: ExpressionNode, context: TransformContext) {
if (node.type === NodeTypes.SIMPLE_EXPRESSION) {
parseFilter(node, context)
} else {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i]
if (typeof child !== 'object') continue
if (child.type === NodeTypes.SIMPLE_EXPRESSION) {
parseFilter(child, context)
} else if (child.type === NodeTypes.COMPOUND_EXPRESSION) {
rewriteFilter(node, context)
} else if (child.type === NodeTypes.INTERPOLATION) {
rewriteFilter(child.content, context)
}
}
}
}
function parseFilter(node: SimpleExpressionNode, context: TransformContext) {
const exp = node.content
let inSingle = false
let inDouble = false
let inTemplateString = false
let inRegex = false
let curly = 0
let square = 0
let paren = 0
let lastFilterIndex = 0
let c,
prev,
i: number,
expression,
filters: string[] = []
for (i = 0; i < exp.length; i++) {
prev = c
c = exp.charCodeAt(i)
if (inSingle) {
if (c === 0x27 && prev !== 0x5c) inSingle = false
} else if (inDouble) {
if (c === 0x22 && prev !== 0x5c) inDouble = false
} else if (inTemplateString) {
if (c === 0x60 && prev !== 0x5c) inTemplateString = false
} else if (inRegex) {
if (c === 0x2f && prev !== 0x5c) inRegex = false
} else if (
c === 0x7c && // pipe
exp.charCodeAt(i + 1) !== 0x7c &&
exp.charCodeAt(i - 1) !== 0x7c &&
!curly &&
!square &&
!paren
) {
if (expression === undefined) {
// first filter, end of expression
lastFilterIndex = i + 1
expression = exp.slice(0, i).trim()
} else {
pushFilter()
}
} else {
switch (c) {
case 0x22:
inDouble = true
break // "
case 0x27:
inSingle = true
break // '
case 0x60:
inTemplateString = true
break // `
case 0x28:
paren++
break // (
case 0x29:
paren--
break // )
case 0x5b:
square++
break // [
case 0x5d:
square--
break // ]
case 0x7b:
curly++
break // {
case 0x7d:
curly--
break // }
}
if (c === 0x2f) {
// /
let j = i - 1
let p
// find first non-whitespace prev char
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
}
if (expression === undefined) {
expression = exp.slice(0, i).trim()
} else if (lastFilterIndex !== 0) {
pushFilter()
}
function pushFilter() {
filters.push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
if (
filters.length &&
warnDeprecation(
CompilerDeprecationTypes.COMPILER_FILTERS,
context,
node.loc
)
) {
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i], context)
}
node.content = expression
}
}
function wrapFilter(
exp: string,
filter: string,
context: TransformContext
): string {
context.helper(RESOLVE_FILTER)
const i = filter.indexOf('(')
if (i < 0) {
context.filters!.add(filter)
return `${toValidAssetId(filter, 'filter')}(${exp})`
} else {
const name = filter.slice(0, i)
const args = filter.slice(i + 1)
context.filters!.add(name)
return `${toValidAssetId(name, 'filter')}(${exp}${
args !== ')' ? ',' + args : args
}`
}
}

View File

@ -15,6 +15,7 @@ import { trackSlotScopes, trackVForSlotScopes } from './transforms/vSlot'
import { transformText } from './transforms/transformText' import { transformText } from './transforms/transformText'
import { transformOnce } from './transforms/vOnce' import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
export type TransformPreset = [ export type TransformPreset = [
@ -30,6 +31,7 @@ export function getBaseTransformPreset(
transformOnce, transformOnce,
transformIf, transformIf,
transformFor, transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers ...(!__BROWSER__ && prefixIdentifiers
? [ ? [
// order is important // order is important

View File

@ -1,7 +1,7 @@
import { SourceLocation } from './ast' import { SourceLocation } from './ast'
export interface CompilerError extends SyntaxError { export interface CompilerError extends SyntaxError {
code: number code: number | string
loc?: SourceLocation loc?: SourceLocation
} }
@ -13,6 +13,10 @@ export function defaultOnError(error: CompilerError) {
throw error throw error
} }
export function defaultOnWarn(msg: CompilerError) {
__DEV__ && console.warn(`[Vue warn] ${msg.message}`)
}
export function createCompilerError<T extends number>( export function createCompilerError<T extends number>(
code: T, code: T,
loc?: SourceLocation, loc?: SourceLocation,
@ -87,13 +91,16 @@ export const enum ErrorCodes {
X_CACHE_HANDLER_NOT_SUPPORTED, X_CACHE_HANDLER_NOT_SUPPORTED,
X_SCOPE_ID_NOT_SUPPORTED, X_SCOPE_ID_NOT_SUPPORTED,
// warnings
X_V_IF_KEY,
// Special value for higher-order compilers to pick up the last code // Special value for higher-order compilers to pick up the last code
// to avoid collision of error codes. This should always be kept as the last // to avoid collision of error codes. This should always be kept as the last
// item. // item.
__EXTEND_POINT__ __EXTEND_POINT__
} }
export const errorMessages: { [code: number]: string } = { export const errorMessages: Record<ErrorCodes, string> = {
// parse errors // parse errors
[ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.', [ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT]: 'Illegal comment.',
[ErrorCodes.CDATA_IN_HTML_CONTENT]: [ErrorCodes.CDATA_IN_HTML_CONTENT]:
@ -124,6 +131,7 @@ export const errorMessages: { [code: number]: string } = {
"Attribute name cannot start with '='.", "Attribute name cannot start with '='.",
[ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]: [ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME]:
"'<?' is allowed only in XML context.", "'<?' is allowed only in XML context.",
[ErrorCodes.UNEXPECTED_NULL_CHARACTER]: `Unexpected null cahracter.`,
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.", [ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
// Vue-specific parse errors // Vue-specific parse errors
@ -164,5 +172,13 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, [ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`, [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`,
[ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`, [ErrorCodes.X_CACHE_HANDLER_NOT_SUPPORTED]: `"cacheHandlers" option is only supported when the "prefixIdentifiers" option is enabled.`,
[ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.` [ErrorCodes.X_SCOPE_ID_NOT_SUPPORTED]: `"scopeId" option is only supported in module mode.`,
// warnings
[ErrorCodes.X_V_IF_KEY]:
`unnecessary key usage on v-if/else branches. ` +
`Vue will automatically generate unique keys for each branch.`,
// just to fullfill types
[ErrorCodes.__EXTEND_POINT__]: ``
} }

View File

@ -57,3 +57,10 @@ export {
} from './transforms/transformElement' } from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet' export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { generateCodeFrame } from '@vue/shared' export { generateCodeFrame } from '@vue/shared'
// v2 compat only
export {
checkCompatEnabled,
warnDeprecation,
CompilerDeprecationTypes
} from './compat/compatConfig'

View File

@ -6,9 +6,17 @@ import {
DirectiveTransform, DirectiveTransform,
TransformContext TransformContext
} from './transform' } from './transform'
import { CompilerCompatOptions } from './compat/compatConfig'
import { ParserPlugin } from '@babel/parser' import { ParserPlugin } from '@babel/parser'
export interface ParserOptions { export interface ErrorHandlingOptions {
onWarn?: (warning: CompilerError) => void
onError?: (error: CompilerError) => void
}
export interface ParserOptions
extends ErrorHandlingOptions,
CompilerCompatOptions {
/** /**
* e.g. platform native elements, e.g. `<div>` for browsers * e.g. platform native elements, e.g. `<div>` for browsers
*/ */
@ -44,11 +52,14 @@ export interface ParserOptions {
* @default ['{{', '}}'] * @default ['{{', '}}']
*/ */
delimiters?: [string, string] delimiters?: [string, string]
/**
* Whitespace handling strategy
*/
whitespace?: 'preserve' | 'condense'
/** /**
* Only needed for DOM compilers * Only needed for DOM compilers
*/ */
decodeEntities?: (rawText: string, asAttr: boolean) => string decodeEntities?: (rawText: string, asAttr: boolean) => string
onError?: (error: CompilerError) => void
/** /**
* Keep comments in the templates AST, even in production * Keep comments in the templates AST, even in production
*/ */
@ -138,7 +149,10 @@ interface SharedTransformCodegenOptions {
filename?: string filename?: string
} }
export interface TransformOptions extends SharedTransformCodegenOptions { export interface TransformOptions
extends SharedTransformCodegenOptions,
ErrorHandlingOptions,
CompilerCompatOptions {
/** /**
* An array of node transforms to be applied to every AST node. * An array of node transforms to be applied to every AST node.
*/ */
@ -213,7 +227,6 @@ export interface TransformOptions extends SharedTransformCodegenOptions {
* needed to render inline CSS variables on component root * needed to render inline CSS variables on component root
*/ */
ssrCssVars?: string ssrCssVars?: string
onError?: (error: CompilerError) => void
} }
export interface CodegenOptions extends SharedTransformCodegenOptions { export interface CodegenOptions extends SharedTransformCodegenOptions {

View File

@ -1,6 +1,11 @@
import { ParserOptions } from './options' import { ErrorHandlingOptions, ParserOptions } from './options'
import { NO, isArray, makeMap, extend } from '@vue/shared' import { NO, isArray, makeMap, extend } from '@vue/shared'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors' import {
ErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn
} from './errors'
import { import {
assert, assert,
advancePositionWithMutation, advancePositionWithMutation,
@ -25,8 +30,18 @@ import {
createRoot, createRoot,
ConstantTypes ConstantTypes
} from './ast' } from './ast'
import {
checkCompatEnabled,
CompilerCompatOptions,
CompilerDeprecationTypes,
warnDeprecation
} from './compat/compatConfig'
type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent' type OptionalOptions =
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
| keyof CompilerCompatOptions
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> & type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
Pick<ParserOptions, OptionalOptions> Pick<ParserOptions, OptionalOptions>
type AttributeValue = type AttributeValue =
@ -59,6 +74,7 @@ export const defaultParserOptions: MergedParserOptions = {
decodeEntities: (rawText: string): string => decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]), rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError, onError: defaultOnError,
onWarn: defaultOnWarn,
comments: false comments: false
} }
@ -80,6 +96,7 @@ export interface ParserContext {
column: number column: number
inPre: boolean // HTML <pre> tag, preserve whitespaces inPre: boolean // HTML <pre> tag, preserve whitespaces
inVPre: boolean // v-pre, do not process directives and interpolations inVPre: boolean // v-pre, do not process directives and interpolations
onWarn: NonNullable<ErrorHandlingOptions['onWarn']>
} }
export function baseParse( export function baseParse(
@ -111,7 +128,8 @@ function createParserContext(
originalSource: content, originalSource: content,
source: content, source: content,
inPre: false, inPre: false,
inVPre: false inVPre: false,
onWarn: options.onWarn
} }
} }
@ -202,38 +220,39 @@ function parseChildren(
} }
} }
// Whitespace management for more efficient output // Whitespace handling strategy like v2
// (same as v2 whitespace: 'condense')
let removedWhitespace = false let removedWhitespace = false
if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) { if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
const preserve = context.options.whitespace === 'preserve'
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i] const node = nodes[i]
if (!context.inPre && node.type === NodeTypes.TEXT) { if (!context.inPre && node.type === NodeTypes.TEXT) {
if (!/[^\t\r\n\f ]/.test(node.content)) { if (!/[^\t\r\n\f ]/.test(node.content)) {
const prev = nodes[i - 1] const prev = nodes[i - 1]
const next = nodes[i + 1] const next = nodes[i + 1]
// If: // Remove if:
// - the whitespace is the first or last node, or: // - the whitespace is the first or last node, or:
// - the whitespace is adjacent to a comment, or: // - (condense mode) the whitespace is adjacent to a comment, or:
// - the whitespace is between two elements AND contains newline // - (condense mode) the whitespace is between two elements AND contains newline
// Then the whitespace is ignored.
if ( if (
!prev || !prev ||
!next || !next ||
prev.type === NodeTypes.COMMENT || (!preserve &&
(prev.type === NodeTypes.COMMENT ||
next.type === NodeTypes.COMMENT || next.type === NodeTypes.COMMENT ||
(prev.type === NodeTypes.ELEMENT && (prev.type === NodeTypes.ELEMENT &&
next.type === NodeTypes.ELEMENT && next.type === NodeTypes.ELEMENT &&
/[\r\n]/.test(node.content)) /[\r\n]/.test(node.content))))
) { ) {
removedWhitespace = true removedWhitespace = true
nodes[i] = null as any nodes[i] = null as any
} else { } else {
// Otherwise, condensed consecutive whitespace inside the text // Otherwise, the whitespace is condensed into a single space
// down to a single space
node.content = ' ' node.content = ' '
} }
} else { } else if (!preserve) {
// in condense mode, consecutive whitespaces in text are condensed
// down to a single space.
node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ') node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
} }
} }
@ -389,6 +408,27 @@ function parseElement(
const children = parseChildren(context, mode, ancestors) const children = parseChildren(context, mode, ancestors)
ancestors.pop() ancestors.pop()
// 2.x inline-template compat
if (__COMPAT__) {
const inlineTemplateProp = element.props.find(
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
) as AttributeNode
if (
inlineTemplateProp &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
context,
inlineTemplateProp.loc
)
) {
inlineTemplateProp.value!.content = getSelection(
context,
element.loc.end
).source
console.log(inlineTemplateProp)
}
}
element.children = children element.children = children
// End tag. // End tag.
@ -427,11 +467,21 @@ const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
/** /**
* Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag). * Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
*/ */
function parseTag(
context: ParserContext,
type: TagType.Start,
parent: ElementNode | undefined
): ElementNode
function parseTag(
context: ParserContext,
type: TagType.End,
parent: ElementNode | undefined
): void
function parseTag( function parseTag(
context: ParserContext, context: ParserContext,
type: TagType, type: TagType,
parent: ElementNode | undefined parent: ElementNode | undefined
): ElementNode { ): ElementNode | undefined {
__TEST__ && assert(/^<\/?[a-z]/i.test(context.source)) __TEST__ && assert(/^<\/?[a-z]/i.test(context.source))
__TEST__ && __TEST__ &&
assert( assert(
@ -461,6 +511,7 @@ function parseTag(
// check v-pre // check v-pre
if ( if (
type === TagType.Start &&
!context.inVPre && !context.inVPre &&
props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre') props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
) { ) {
@ -484,12 +535,58 @@ function parseTag(
advanceBy(context, isSelfClosing ? 2 : 1) advanceBy(context, isSelfClosing ? 2 : 1)
} }
if (type === TagType.End) {
return
}
// 2.x deprecation checks
if (__COMPAT__ && __DEV__ && !__TEST__) {
let hasIf = false
let hasFor = false
for (let i = 0; i < props.length; i++) {
const p = props[i]
if (p.type === NodeTypes.DIRECTIVE) {
if (p.name === 'if') {
hasIf = true
} else if (p.name === 'for') {
hasFor = true
}
}
if (hasIf && hasFor) {
warnDeprecation(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
context,
getSelection(context, start)
)
}
}
}
let tagType = ElementTypes.ELEMENT let tagType = ElementTypes.ELEMENT
const options = context.options const options = context.options
if (!context.inVPre && !options.isCustomElement(tag)) { if (!context.inVPre && !options.isCustomElement(tag)) {
const hasVIs = props.some( const hasVIs = props.some(p => {
p => p.type === NodeTypes.DIRECTIVE && p.name === 'is' if (p.name !== 'is') return
// v-is="xxx" (TODO: deprecate)
if (p.type === NodeTypes.DIRECTIVE) {
return true
}
// is="vue:xxx"
if (p.value && p.value.content.startsWith('vue:')) {
return true
}
// in compat mode, any is usage is considered a component
if (
__COMPAT__ &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
context,
p.loc
) )
) {
return true
}
})
if (options.isNativeTag && !hasVIs) { if (options.isNativeTag && !hasVIs) {
if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
} else if ( } else if (
@ -506,11 +603,10 @@ function parseTag(
tagType = ElementTypes.SLOT tagType = ElementTypes.SLOT
} else if ( } else if (
tag === 'template' && tag === 'template' &&
props.some(p => { props.some(
return ( p =>
p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name) p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
) )
})
) { ) {
tagType = ElementTypes.TEMPLATE tagType = ElementTypes.TEMPLATE
} }
@ -615,10 +711,9 @@ function parseAttribute(
name name
)! )!
const dirName = let dirName =
match[1] || match[1] ||
(startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot') (startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
let arg: ExpressionNode | undefined let arg: ExpressionNode | undefined
if (match[2]) { if (match[2]) {
@ -673,6 +768,32 @@ function parseAttribute(
valueLoc.source = valueLoc.source.slice(1, -1) valueLoc.source = valueLoc.source.slice(1, -1)
} }
const modifiers = match[3] ? match[3].substr(1).split('.') : []
// 2.x compat v-bind:foo.sync -> v-model:foo
if (__COMPAT__ && dirName === 'bind' && arg) {
if (
modifiers.includes('sync') &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
context,
loc,
arg.loc.source
)
) {
dirName = 'model'
modifiers.splice(modifiers.indexOf('sync'), 1)
}
if (__DEV__ && modifiers.includes('prop')) {
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_PROP,
context,
loc
)
}
}
return { return {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name: dirName, name: dirName,
@ -686,7 +807,7 @@ function parseAttribute(
loc: value.loc loc: value.loc
}, },
arg, arg,
modifiers: match[3] ? match[3].substr(1).split('.') : [], modifiers,
loc loc
} }
} }

View File

@ -14,6 +14,7 @@ export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
__DEV__ ? `resolveDynamicComponent` : `` __DEV__ ? `resolveDynamicComponent` : ``
) )
export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``) export const RESOLVE_DIRECTIVE = Symbol(__DEV__ ? `resolveDirective` : ``)
export const RESOLVE_FILTER = Symbol(__DEV__ ? `resolveFilter` : ``)
export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``) export const WITH_DIRECTIVES = Symbol(__DEV__ ? `withDirectives` : ``)
export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``) export const RENDER_LIST = Symbol(__DEV__ ? `renderList` : ``)
export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``) export const RENDER_SLOT = Symbol(__DEV__ ? `renderSlot` : ``)
@ -50,6 +51,7 @@ export const helperNameMap: any = {
[RESOLVE_COMPONENT]: `resolveComponent`, [RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`, [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`, [RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
[WITH_DIRECTIVES]: `withDirectives`, [WITH_DIRECTIVES]: `withDirectives`,
[RENDER_LIST]: `renderList`, [RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`, [RENDER_SLOT]: `renderSlot`,

View File

@ -28,7 +28,7 @@ import {
capitalize, capitalize,
camelize camelize
} from '@vue/shared' } from '@vue/shared'
import { defaultOnError } from './errors' import { defaultOnError, defaultOnWarn } from './errors'
import { import {
TO_DISPLAY_STRING, TO_DISPLAY_STRING,
FRAGMENT, FRAGMENT,
@ -40,6 +40,7 @@ import {
} from './runtimeHelpers' } from './runtimeHelpers'
import { isVSlot } from './utils' import { isVSlot } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic' import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { CompilerCompatOptions } from './compat/compatConfig'
// There are two types of transforms: // There are two types of transforms:
// //
@ -83,7 +84,10 @@ export interface ImportItem {
} }
export interface TransformContext export interface TransformContext
extends Required<Omit<TransformOptions, 'filename'>> { extends Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>,
CompilerCompatOptions {
selfName: string | null selfName: string | null
root: RootNode root: RootNode
helpers: Map<symbol, number> helpers: Map<symbol, number>
@ -114,6 +118,9 @@ export interface TransformContext
hoist(exp: JSChildNode): SimpleExpressionNode hoist(exp: JSChildNode): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
constantCache: Map<TemplateChildNode, ConstantTypes> constantCache: Map<TemplateChildNode, ConstantTypes>
// 2.x Compat only
filters?: Set<string>
} }
export function createTransformContext( export function createTransformContext(
@ -136,7 +143,9 @@ export function createTransformContext(
bindingMetadata = EMPTY_OBJ, bindingMetadata = EMPTY_OBJ,
inline = false, inline = false,
isTS = false, isTS = false,
onError = defaultOnError onError = defaultOnError,
onWarn = defaultOnWarn,
compatConfig
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/) const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
@ -160,6 +169,8 @@ export function createTransformContext(
inline, inline,
isTS, isTS,
onError, onError,
onWarn,
compatConfig,
// state // state
root, root,
@ -281,6 +292,10 @@ export function createTransformContext(
} }
} }
if (__COMPAT__) {
context.filters = new Set()
}
function addId(id: string) { function addId(id: string) {
const { identifiers } = context const { identifiers } = context
if (identifiers[id] === undefined) { if (identifiers[id] === undefined) {
@ -313,6 +328,10 @@ export function transform(root: RootNode, options: TransformOptions) {
root.hoists = context.hoists root.hoists = context.hoists
root.temps = context.temps root.temps = context.temps
root.cached = context.cached root.cached = context.cached
if (__COMPAT__) {
root.filters = [...context.filters!]
}
} }
function createRootCodegen(root: RootNode, context: TransformContext) { function createRootCodegen(root: RootNode, context: TransformContext) {

View File

@ -41,7 +41,8 @@ import {
TELEPORT, TELEPORT,
KEEP_ALIVE, KEEP_ALIVE,
SUSPENSE, SUSPENSE,
UNREF UNREF,
FRAGMENT
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { import {
getInnerRange, getInnerRange,
@ -55,6 +56,11 @@ import {
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { getConstantType } from './hoistStatic' import { getConstantType } from './hoistStatic'
import { BindingTypes } from '../options' import { BindingTypes } from '../options'
import {
checkCompatEnabled,
CompilerDeprecationTypes,
isCompatEnabled
} from '../compat/compatConfig'
// some directive transforms (e.g. v-model) may return a symbol for runtime // some directive transforms (e.g. v-model) may return a symbol for runtime
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
@ -82,9 +88,23 @@ export const transformElement: NodeTransform = (node, context) => {
// The goal of the transform is to create a codegenNode implementing the // The goal of the transform is to create a codegenNode implementing the
// VNodeCall interface. // VNodeCall interface.
const vnodeTag = isComponent let vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context) ? resolveComponentType(node as ComponentNode, context)
: `"${tag}"` : `"${tag}"`
// 2.x <template> with no directives compat
if (
__COMPAT__ &&
tag === 'template' &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
context,
node.loc
)
) {
vnodeTag = context.helper(FRAGMENT)
}
const isDynamicComponent = const isDynamicComponent =
isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT
@ -230,13 +250,20 @@ export function resolveComponentType(
context: TransformContext, context: TransformContext,
ssr = false ssr = false
) { ) {
const { tag } = node let { tag } = node
// 1. dynamic component // 1. dynamic component
const isProp = isComponentTag(tag) const isExplicitDynamic = isComponentTag(tag)
? findProp(node, 'is') const isProp =
: findDir(node, 'is') findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is'))
if (isProp) { if (isProp) {
if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) {
// <button is="vue:xxx">
// if not <component>, only is value that starts with "vue:" will be
// treated as component by the parse phase and reach here, unless it's
// compat mode where all is values are considered components
tag = isProp.value!.content.replace(/^vue:/, '')
} else {
const exp = const exp =
isProp.type === NodeTypes.ATTRIBUTE isProp.type === NodeTypes.ATTRIBUTE
? isProp.value && createSimpleExpression(isProp.value.content, true) ? isProp.value && createSimpleExpression(isProp.value.content, true)
@ -247,6 +274,7 @@ export function resolveComponentType(
]) ])
} }
} }
}
// 2. built-in components (Teleport, Transition, KeepAlive, Suspense...) // 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag) const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
@ -416,8 +444,11 @@ export function buildProps(
isStatic = false isStatic = false
} }
} }
// skip :is on <component> // skip is on <component>, or is="vue:xxx"
if (name === 'is' && isComponentTag(tag)) { if (
name === 'is' &&
(isComponentTag(tag) || (value && value.content.startsWith('vue:')))
) {
continue continue
} }
properties.push( properties.push(
@ -437,8 +468,8 @@ export function buildProps(
} else { } else {
// directives // directives
const { name, arg, exp, loc } = prop const { name, arg, exp, loc } = prop
const isBind = name === 'bind' const isVBind = name === 'bind'
const isOn = name === 'on' const isVOn = name === 'on'
// skip v-slot - it is handled by its dedicated transform. // skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') { if (name === 'slot') {
@ -456,17 +487,17 @@ export function buildProps(
// skip v-is and :is on <component> // skip v-is and :is on <component>
if ( if (
name === 'is' || name === 'is' ||
(isBind && isComponentTag(tag) && isBindKey(arg, 'is')) (isVBind && isComponentTag(tag) && isBindKey(arg, 'is'))
) { ) {
continue continue
} }
// skip v-on in SSR compilation // skip v-on in SSR compilation
if (isOn && ssr) { if (isVOn && ssr) {
continue continue
} }
// special case for v-bind and v-on with no argument // special case for v-bind and v-on with no argument
if (!arg && (isBind || isOn)) { if (!arg && (isVBind || isVOn)) {
hasDynamicKeys = true hasDynamicKeys = true
if (exp) { if (exp) {
if (properties.length) { if (properties.length) {
@ -475,7 +506,50 @@ export function buildProps(
) )
properties = [] properties = []
} }
if (isBind) { if (isVBind) {
if (__COMPAT__) {
// 2.x v-bind object order compat
if (__DEV__) {
const hasOverridableKeys = mergeArgs.some(arg => {
if (arg.type === NodeTypes.JS_OBJECT_EXPRESSION) {
return arg.properties.some(({ key }) => {
if (
key.type !== NodeTypes.SIMPLE_EXPRESSION ||
!key.isStatic
) {
return true
}
return (
key.content !== 'class' &&
key.content !== 'style' &&
!isOn(key.content)
)
})
} else {
// dynamic expression
return true
}
})
if (hasOverridableKeys) {
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
context,
loc
)
}
}
if (
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_OBJECT_ORDER,
context
)
) {
mergeArgs.unshift(exp)
continue
}
}
mergeArgs.push(exp) mergeArgs.push(exp)
} else { } else {
// v-on="obj" -> toHandlers(obj) // v-on="obj" -> toHandlers(obj)
@ -489,7 +563,7 @@ export function buildProps(
} else { } else {
context.onError( context.onError(
createCompilerError( createCompilerError(
isBind isVBind
? ErrorCodes.X_V_BIND_NO_EXPRESSION ? ErrorCodes.X_V_BIND_NO_EXPRESSION
: ErrorCodes.X_V_ON_NO_EXPRESSION, : ErrorCodes.X_V_ON_NO_EXPRESSION,
loc loc
@ -516,6 +590,25 @@ export function buildProps(
runtimeDirectives.push(prop) runtimeDirectives.push(prop)
} }
} }
if (
__COMPAT__ &&
prop.type === NodeTypes.ATTRIBUTE &&
prop.name === 'ref' &&
context.scopes.vFor > 0 &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_FOR_REF,
context,
prop.loc
)
) {
properties.push(
createObjectProperty(
createSimpleExpression('refInFor', true),
createSimpleExpression('true', false)
)
)
}
} }
let propsExpression: PropsExpression | undefined = undefined let propsExpression: PropsExpression | undefined = undefined

View File

@ -254,6 +254,11 @@ export function processExpression(
parent && parentStack.push(parent) parent && parentStack.push(parent)
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
if (!isDuplicate(node)) { if (!isDuplicate(node)) {
// v2 wrapped filter call
if (__COMPAT__ && node.name.startsWith('_filter_')) {
return
}
const needPrefix = shouldPrefix(node, parent!, parentStack) const needPrefix = shouldPrefix(node, parent!, parentStack)
if (!knownIds[node.name] && needPrefix) { if (!knownIds[node.name] && needPrefix) {
if (isStaticProperty(parent!) && parent.shorthand) { if (isStaticProperty(parent!) && parent.shorthand) {

View File

@ -63,7 +63,11 @@ export const transformText: NodeTransform = (node, context) => {
(children.length === 1 && (children.length === 1 &&
(node.type === NodeTypes.ROOT || (node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT && (node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT))) node.tagType === ElementTypes.ELEMENT &&
// in compat mode, <template> tags with no special directives
// will be rendered as a fragment so its children must be
// converted into vnodes.
!(__COMPAT__ && node.tag === 'template'))))
) { ) {
return return
} }

View File

@ -7,7 +7,7 @@ import { CAMELIZE } from '../runtimeHelpers'
// v-bind without arg is handled directly in ./transformElements.ts due to it affecting // v-bind without arg is handled directly in ./transformElements.ts due to it affecting
// codegen for the entire props object. This transform here is only for v-bind // codegen for the entire props object. This transform here is only for v-bind
// *with* args. // *with* args.
export const transformBind: DirectiveTransform = (dir, node, context) => { export const transformBind: DirectiveTransform = (dir, _node, context) => {
const { exp, modifiers, loc } = dir const { exp, modifiers, loc } = dir
const arg = dir.arg! const arg = dir.arg!

View File

@ -110,7 +110,7 @@ export function processIf(
} }
if (dir.name === 'if') { if (dir.name === 'if') {
const branch = createIfBranch(node, dir) const branch = createIfBranch(node, dir, context)
const ifNode: IfNode = { const ifNode: IfNode = {
type: NodeTypes.IF, type: NodeTypes.IF,
loc: node.loc, loc: node.loc,
@ -145,7 +145,7 @@ export function processIf(
if (sibling && sibling.type === NodeTypes.IF) { if (sibling && sibling.type === NodeTypes.IF) {
// move the node to the if node's branches // move the node to the if node's branches
context.removeNode() context.removeNode()
const branch = createIfBranch(node, dir) const branch = createIfBranch(node, dir, context)
if (__DEV__ && comments.length) { if (__DEV__ && comments.length) {
branch.children = [...comments, ...branch.children] branch.children = [...comments, ...branch.children]
} }
@ -187,7 +187,16 @@ export function processIf(
} }
} }
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode { function createIfBranch(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
): IfBranchNode {
const userKey = findProp(node, `key`)
if (__DEV__ && userKey) {
context.onWarn(createCompilerError(ErrorCodes.X_V_IF_KEY, userKey.loc))
}
return { return {
type: NodeTypes.IF_BRANCH, type: NodeTypes.IF_BRANCH,
loc: node.loc, loc: node.loc,
@ -196,7 +205,7 @@ function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for') node.tagType === ElementTypes.TEMPLATE && !findDir(node, 'for')
? node.children ? node.children
: [node], : [node],
userKey: findProp(node, `key`) userKey
} }
} }

View File

@ -271,7 +271,7 @@ export function injectProp(
export function toValidAssetId( export function toValidAssetId(
name: string, name: string,
type: 'component' | 'directive' type: 'component' | 'directive' | 'filter'
): string { ): string {
return `_${type}_${name.replace(/[^\w]/g, '_')}` return `_${type}_${name.replace(/[^\w]/g, '_')}`
} }

View File

@ -14,6 +14,7 @@
"sideEffects": false, "sideEffects": false,
"buildOptions": { "buildOptions": {
"name": "VueCompilerDOM", "name": "VueCompilerDOM",
"compat": true,
"formats": [ "formats": [
"esm-bundler", "esm-bundler",
"esm-browser", "esm-browser",

View File

@ -12,12 +12,12 @@ export interface DOMCompilerError extends CompilerError {
export function createDOMCompilerError( export function createDOMCompilerError(
code: DOMErrorCodes, code: DOMErrorCodes,
loc?: SourceLocation loc?: SourceLocation
): DOMCompilerError { ) {
return createCompilerError( return createCompilerError(
code, code,
loc, loc,
__DEV__ || !__BROWSER__ ? DOMErrorMessages : undefined __DEV__ || !__BROWSER__ ? DOMErrorMessages : undefined
) ) as DOMCompilerError
} }
export const enum DOMErrorCodes { export const enum DOMErrorCodes {

View File

@ -8,7 +8,9 @@ import {
createCompoundExpression, createCompoundExpression,
ExpressionNode, ExpressionNode,
SimpleExpressionNode, SimpleExpressionNode,
isStaticExp isStaticExp,
warnDeprecation,
CompilerDeprecationTypes
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers' import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers'
import { makeMap, capitalize } from '@vue/shared' import { makeMap, capitalize } from '@vue/shared'
@ -92,6 +94,14 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
const { modifiers } = dir const { modifiers } = dir
if (!modifiers.length) return baseResult if (!modifiers.length) return baseResult
if (__COMPAT__ && __DEV__ && modifiers.includes('native')) {
warnDeprecation(
CompilerDeprecationTypes.COMPILER_V_ON_NATIVE,
context,
dir.loc
)
}
let { key, value: handlerExp } = baseResult.props[0] let { key, value: handlerExp } = baseResult.props[0]
const { const {
keyModifiers, keyModifiers,

View File

@ -155,6 +155,18 @@ export function parse(
false false
) as SFCTemplateBlock) ) as SFCTemplateBlock)
templateBlock.ast = node templateBlock.ast = node
// warn against 2.x <template functional>
if (templateBlock.attrs.functional) {
const err = new SyntaxError(
`<template functional> is no longer supported in Vue 3, since ` +
`functional components no longer have significant performance ` +
`difference from stateful ones. Just use a normal <template> ` +
`instead.`
) as CompilerError
err.loc = node.props.find(p => p.name === 'functional')!.loc
errors.push(err)
}
} else { } else {
errors.push(createDuplicateBlockError(node)) errors.push(createDuplicateBlockError(node))
} }

View File

@ -12,8 +12,8 @@ export interface SSRCompilerError extends CompilerError {
export function createSSRCompilerError( export function createSSRCompilerError(
code: SSRErrorCodes, code: SSRErrorCodes,
loc?: SourceLocation loc?: SourceLocation
): SSRCompilerError { ) {
return createCompilerError(code, loc, SSRErrorMessages) return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError
} }
export const enum SSRErrorCodes { export const enum SSRErrorCodes {

View File

@ -8,6 +8,7 @@ declare var __ESM_BROWSER__: boolean
declare var __NODE_JS__: boolean declare var __NODE_JS__: boolean
declare var __COMMIT__: string declare var __COMMIT__: string
declare var __VERSION__: string declare var __VERSION__: string
declare var __COMPAT__: boolean
// Feature flags // Feature flags
declare var __FEATURE_OPTIONS_API__: boolean declare var __FEATURE_OPTIONS_API__: boolean

View File

@ -481,4 +481,7 @@ describe('api: createApp', () => {
app.mount(root) app.mount(root)
expect(serializeInner(root)).toBe('hello') expect(serializeInner(root)).toBe('hello')
}) })
// config.compilerOptions is tested in packages/vue since it is only
// supported in the full build.
}) })

View File

@ -6,7 +6,7 @@ import {
createApp, createApp,
shallowReadonly shallowReadonly
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { ComponentInternalInstance } from '../src/component' import { ComponentInternalInstance, ComponentOptions } from '../src/component'
describe('component: proxy', () => { describe('component: proxy', () => {
test('data', () => { test('data', () => {
@ -93,7 +93,7 @@ describe('component: proxy', () => {
expect(instanceProxy.$root).toBe(instance!.root.proxy) expect(instanceProxy.$root).toBe(instance!.root.proxy)
expect(instanceProxy.$emit).toBe(instance!.emit) expect(instanceProxy.$emit).toBe(instance!.emit)
expect(instanceProxy.$el).toBe(instance!.vnode.el) expect(instanceProxy.$el).toBe(instance!.vnode.el)
expect(instanceProxy.$options).toBe(instance!.type) expect(instanceProxy.$options).toBe(instance!.type as ComponentOptions)
expect(() => (instanceProxy.$data = {})).toThrow(TypeError) expect(() => (instanceProxy.$data = {})).toThrow(TypeError)
expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned() expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()

View File

@ -82,7 +82,7 @@ describe('component: slots', () => {
expect(slots.default()).toMatchObject([normalizeVNode(h('span'))]) expect(slots.default()).toMatchObject([normalizeVNode(h('span'))])
}) })
test('updateSlots: instance.slots should be update correctly (when slotType is number)', async () => { test('updateSlots: instance.slots should be updated correctly (when slotType is number)', async () => {
const flag1 = ref(true) const flag1 = ref(true)
let instance: any let instance: any
@ -124,7 +124,7 @@ describe('component: slots', () => {
expect(instance.slots).toHaveProperty('two') expect(instance.slots).toHaveProperty('two')
}) })
test('updateSlots: instance.slots should be update correctly (when slotType is null)', async () => { test('updateSlots: instance.slots should be updated correctly (when slotType is null)', async () => {
const flag1 = ref(true) const flag1 = ref(true)
let instance: any let instance: any

View File

@ -4,17 +4,20 @@ import {
validateComponentName, validateComponentName,
Component Component
} from './component' } from './component'
import { ComponentOptions } from './componentOptions' import { ComponentOptions, RuntimeCompilerOptions } from './componentOptions'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives' import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer' import { RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject' import { InjectionKey } from './apiInject'
import { isFunction, NO, isObject } from '@vue/shared'
import { warn } from './warning' import { warn } from './warning'
import { createVNode, cloneVNode, VNode } from './vnode' import { createVNode, cloneVNode, VNode } from './vnode'
import { RootHydrateFunction } from './hydration' import { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
import { isFunction, NO, isObject } from '@vue/shared'
import { version } from '.' import { version } from '.'
import { installCompatMount } from './compat/global'
import { installLegacyConfigProperties } from './compat/globalConfig'
import { installGlobalFilterMethod } from './compat/filter'
export interface App<HostElement = any> { export interface App<HostElement = any> {
version: string version: string
@ -39,6 +42,17 @@ export interface App<HostElement = any> {
_props: Data | null _props: Data | null
_container: HostElement | null _container: HostElement | null
_context: AppContext _context: AppContext
/**
* v2 compat only
*/
filter?(name: string): Function | undefined
filter?(name: string, filter: Function): this
/**
* @internal v3 compat only
*/
_createRoot?(options: ComponentOptions): ComponentPublicInstance
} }
export type OptionMergeFunction = ( export type OptionMergeFunction = (
@ -55,7 +69,6 @@ export interface AppConfig {
performance: boolean performance: boolean
optionMergeStrategies: Record<string, OptionMergeFunction> optionMergeStrategies: Record<string, OptionMergeFunction>
globalProperties: Record<string, any> globalProperties: Record<string, any>
isCustomElement: (tag: string) => boolean
errorHandler?: ( errorHandler?: (
err: unknown, err: unknown,
instance: ComponentPublicInstance | null, instance: ComponentPublicInstance | null,
@ -66,6 +79,17 @@ export interface AppConfig {
instance: ComponentPublicInstance | null, instance: ComponentPublicInstance | null,
trace: string trace: string
) => void ) => void
/**
* @deprecated use config.compilerOptions.isCustomElement
*/
isCustomElement?: (tag: string) => boolean
/**
* Options to pass to @vue/compiler-dom.
* Only supported in runtime compiler build.
*/
compilerOptions: RuntimeCompilerOptions
} }
export interface AppContext { export interface AppContext {
@ -85,6 +109,11 @@ export interface AppContext {
* @internal * @internal
*/ */
reload?: () => void reload?: () => void
/**
* v2 compat only
* @internal
*/
filters?: Record<string, Function>
} }
type PluginInstallFunction = (app: App, ...options: any[]) => any type PluginInstallFunction = (app: App, ...options: any[]) => any
@ -103,9 +132,11 @@ export function createAppContext(): AppContext {
performance: false, performance: false,
globalProperties: {}, globalProperties: {},
optionMergeStrategies: {}, optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined, errorHandler: undefined,
warnHandler: undefined warnHandler: undefined,
compilerOptions: {
isCustomElement: NO
}
}, },
mixins: [], mixins: [],
components: {}, components: {},
@ -298,6 +329,12 @@ export function createAppAPI<HostElement>(
} }
}) })
if (__COMPAT__) {
installCompatMount(app, context, render, hydrate)
installGlobalFilterMethod(app, context)
if (__DEV__) installLegacyConfigProperties(app.config)
}
return app return app
} }
} }

View File

@ -18,7 +18,8 @@ import {
NOOP, NOOP,
remove, remove,
isMap, isMap,
isSet isSet,
isPlainObject
} from '@vue/shared' } from '@vue/shared'
import { import {
currentInstance, currentInstance,
@ -33,6 +34,9 @@ import {
} from './errorHandling' } from './errorHandling'
import { queuePostRenderEffect } from './renderer' import { queuePostRenderEffect } from './renderer'
import { warn } from './warning' import { warn } from './warning'
import { DeprecationTypes } from './compat/compatConfig'
import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
import { ObjectWatchOptionItem } from './componentOptions'
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
@ -217,6 +221,21 @@ function doWatch(
__DEV__ && warnInvalidSource(source) __DEV__ && warnInvalidSource(source)
} }
// 2.x array mutation watch compat
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
if (cb && deep) { if (cb && deep) {
const baseGetter = getter const baseGetter = getter
getter = () => traverse(baseGetter()) getter = () => traverse(baseGetter())
@ -254,7 +273,14 @@ function doWatch(
if (cb) { if (cb) {
// watch(source, cb) // watch(source, cb)
const newValue = runner() const newValue = runner()
if (deep || forceTrigger || hasChanged(newValue, oldValue)) { if (
deep ||
forceTrigger ||
hasChanged(newValue, oldValue) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again // cleanup before running cb again
if (cleanup) { if (cleanup) {
cleanup() cleanup()
@ -329,7 +355,7 @@ function doWatch(
export function instanceWatch( export function instanceWatch(
this: ComponentInternalInstance, this: ComponentInternalInstance,
source: string | Function, source: string | Function,
cb: WatchCallback, value: WatchCallback | ObjectWatchOptionItem,
options?: WatchOptions options?: WatchOptions
): WatchStopHandle { ): WatchStopHandle {
const publicThis = this.proxy as any const publicThis = this.proxy as any
@ -338,6 +364,13 @@ export function instanceWatch(
? createPathGetter(publicThis, source) ? createPathGetter(publicThis, source)
: () => publicThis[source] : () => publicThis[source]
: source.bind(publicThis) : source.bind(publicThis)
let cb
if (isFunction(value)) {
cb = value
} else {
cb = value.handler as Function
options = value
}
return doWatch(getter, cb.bind(publicThis), options, this) return doWatch(getter, cb.bind(publicThis), options, this)
} }
@ -367,9 +400,9 @@ function traverse(value: unknown, seen: Set<unknown> = new Set()) {
value.forEach((v: any) => { value.forEach((v: any) => {
traverse(v, seen) traverse(v, seen)
}) })
} else { } else if (isPlainObject(value)) {
for (const key in value) { for (const key in value) {
traverse(value[key], seen) traverse((value as any)[key], seen)
} }
} }
return value return value

View File

@ -0,0 +1,336 @@
import Vue from '@vue/compat'
import { effect, isReactive } from '@vue/reactivity'
import {
DeprecationTypes,
deprecationData,
toggleDeprecationWarning
} from '../compatConfig'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 2 })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
describe('GLOBAL_MOUNT', () => {
test('new Vue() with el', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
el,
compatConfig: { GLOBAL_MOUNT: true },
data() {
return {
msg: 'hello'
}
}
})
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(el.innerHTML).toBe('hello')
})
test('new Vue() + $mount', () => {
const el = document.createElement('div')
el.innerHTML = `{{ msg }}`
new Vue({
data() {
return {
msg: 'hello'
}
}
}).$mount(el)
expect(el.innerHTML).toBe('hello')
})
})
describe('GLOBAL_MOUNT_CONTAINER', () => {
test('should warn', () => {
toggleDeprecationWarning(true)
const el = document.createElement('div')
el.innerHTML = `test`
el.setAttribute('v-bind:id', 'foo')
new Vue().$mount(el)
// warning only
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT_CONTAINER].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_EXTEND', () => {
// https://github.com/vuejs/vue/blob/dev/test/unit/features/global-api/extend.spec.js
it('should correctly merge options', () => {
toggleDeprecationWarning(true)
const Test = Vue.extend({
name: 'test',
a: 1,
b: 2
})
expect(Test.options.a).toBe(1)
expect(Test.options.b).toBe(2)
expect(Test.super).toBe(Vue)
const t = new Test({
a: 2
})
expect(t.$options.a).toBe(2)
expect(t.$options.b).toBe(2)
// inheritance
const Test2 = Test.extend({
a: 2
})
expect(Test2.options.a).toBe(2)
expect(Test2.options.b).toBe(2)
const t2 = new Test2({
a: 3
})
expect(t2.$options.a).toBe(3)
expect(t2.$options.b).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_EXTEND].message
).toHaveBeenWarned()
})
it('should work when used as components', () => {
const foo = Vue.extend({
template: '<span>foo</span>'
})
const bar = Vue.extend({
template: '<span>bar</span>'
})
const vm = new Vue({
template: '<div><foo></foo><bar></bar></div>',
components: { foo, bar }
}).$mount()
expect(vm.$el.innerHTML).toBe('<span>foo</span><span>bar</span>')
})
it('should merge lifecycle hooks', () => {
const calls: number[] = []
const A = Vue.extend({
created() {
calls.push(1)
}
})
const B = A.extend({
created() {
calls.push(2)
}
})
new B({
created() {
calls.push(3)
}
})
expect(calls).toEqual([1, 2, 3])
})
it('should not merge nested mixins created with Vue.extend', () => {
const A = Vue.extend({
created: () => {}
})
const B = Vue.extend({
mixins: [A],
created: () => {}
})
const C = Vue.extend({
extends: B,
created: () => {}
})
const D = Vue.extend({
mixins: [C],
created: () => {}
})
expect(D.options.created!.length).toBe(4)
})
it('should merge methods', () => {
const A = Vue.extend({
methods: {
a() {
return this.n
}
}
})
const B = A.extend({
methods: {
b() {
return this.n + 1
}
}
})
const b = new B({
data: () => ({ n: 0 }),
methods: {
c() {
return this.n + 2
}
}
}) as any
expect(b.a()).toBe(0)
expect(b.b()).toBe(1)
expect(b.c()).toBe(2)
})
it('should merge assets', () => {
const A = Vue.extend({
components: {
aa: {
template: '<div>A</div>'
}
}
})
const B = A.extend({
components: {
bb: {
template: '<div>B</div>'
}
}
})
const b = new B({
template: '<div><aa></aa><bb></bb></div>'
}).$mount()
expect(b.$el.innerHTML).toBe('<div>A</div><div>B</div>')
})
it('caching', () => {
const options = {
template: '<div></div>'
}
const A = Vue.extend(options)
const B = Vue.extend(options)
expect(A).toBe(B)
})
it('extended options should use different identify from parent', () => {
const A = Vue.extend({ computed: {} })
const B = A.extend()
B.options.computed.b = () => 'foo'
expect(B.options.computed).not.toBe(A.options.computed)
expect(A.options.computed.b).toBeUndefined()
})
})
describe('GLOBAL_PROTOTYPE', () => {
test('plain properties', () => {
toggleDeprecationWarning(true)
Vue.prototype.$test = 1
const vm = new Vue() as any
expect(vm.$test).toBe(1)
delete Vue.prototype.$test
expect(
deprecationData[DeprecationTypes.GLOBAL_MOUNT].message
).toHaveBeenWarned()
expect(
deprecationData[DeprecationTypes.GLOBAL_PROTOTYPE].message
).toHaveBeenWarned()
})
test('method this context', () => {
Vue.prototype.$test = function() {
return this.msg
}
const vm = new Vue({
data() {
return { msg: 'method' }
}
}) as any
expect(vm.$test()).toBe('method')
delete Vue.prototype.$test
})
test('defined properties', () => {
Object.defineProperty(Vue.prototype, '$test', {
configurable: true,
get() {
return this.msg
}
})
const vm = new Vue({
data() {
return { msg: 'getter' }
}
}) as any
expect(vm.$test).toBe('getter')
delete Vue.prototype.$test
})
test('extended prototype', async () => {
const Foo = Vue.extend()
Foo.prototype.$test = 1
const vm = new Foo() as any
expect(vm.$test).toBe(1)
const plain = new Vue() as any
expect(plain.$test).toBeUndefined()
})
})
describe('GLOBAL_SET/DELETE', () => {
test('set', () => {
toggleDeprecationWarning(true)
const obj: any = {}
Vue.set(obj, 'foo', 1)
expect(obj.foo).toBe(1)
expect(
deprecationData[DeprecationTypes.GLOBAL_SET].message
).toHaveBeenWarned()
})
test('delete', () => {
toggleDeprecationWarning(true)
const obj: any = { foo: 1 }
Vue.delete(obj, 'foo')
expect('foo' in obj).toBe(false)
expect(
deprecationData[DeprecationTypes.GLOBAL_DELETE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_OBSERVABLE', () => {
test('should work', () => {
toggleDeprecationWarning(true)
const obj = Vue.observable({})
expect(isReactive(obj)).toBe(true)
expect(
deprecationData[DeprecationTypes.GLOBAL_OBSERVABLE].message
).toHaveBeenWarned()
})
})
describe('GLOBAL_PRIVATE_UTIL', () => {
test('defineReactive', () => {
toggleDeprecationWarning(true)
const obj: any = {}
// @ts-ignore
Vue.util.defineReactive(obj, 'test', 1)
let n
effect(() => {
n = obj.test
})
expect(n).toBe(1)
obj.test++
expect(n).toBe(2)
expect(
deprecationData[DeprecationTypes.GLOBAL_PRIVATE_UTIL].message
).toHaveBeenWarned()
})
})

View File

@ -0,0 +1,77 @@
import Vue from '@vue/compat'
import { toggleDeprecationWarning } from '../compatConfig'
beforeEach(() => {
toggleDeprecationWarning(false)
Vue.configureCompat({ MODE: 2 })
})
afterEach(() => {
Vue.configureCompat({ MODE: 3 })
toggleDeprecationWarning(false)
})
function triggerEvent(
target: Element,
event: string,
process?: (e: any) => any
) {
const e = document.createEvent('HTMLEvents')
e.initEvent(event, true, true)
if (process) process(e)
target.dispatchEvent(e)
return e
}
// only testing config options that affect runtime behavior.
test('GLOBAL_KEY_CODES', () => {
Vue.config.keyCodes = {
foo: 86,
bar: [38, 87]
}
const onFoo = jest.fn()
const onBar = jest.fn()
const el = document.createElement('div')
new Vue({
el,
template: `<input type="text" @keyup.foo="onFoo" @keyup.bar="onBar">`,
methods: {
onFoo,
onBar
}
})
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 86
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(0)
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 38
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(1)
triggerEvent(el.children[0], 'keyup', e => {
e.key = '_'
e.keyCode = 87
})
expect(onFoo).toHaveBeenCalledTimes(1)
expect(onBar).toHaveBeenCalledTimes(2)
})
test('GLOBAL_IGNORED_ELEMENTS', () => {
Vue.config.ignoredElements = [/^v-/, 'foo']
const el = document.createElement('div')
new Vue({
el,
template: `<v-foo/><foo/>`
})
expect(el.innerHTML).toBe(`<v-foo></v-foo><foo></foo>`)
})

View File

@ -0,0 +1,178 @@
import { ShapeFlags } from '@vue/shared/src'
import { createComponentInstance } from '../../component'
import { setCurrentRenderingInstance } from '../../componentRenderContext'
import { DirectiveBinding } from '../../directives'
import { createVNode } from '../../vnode'
import { compatH as h } from '../renderFn'
describe('compat: render function', () => {
const mockDir = {}
const mockChildComp = {}
const mockComponent = {
directives: {
mockDir
},
components: {
foo: mockChildComp
}
}
const mockInstance = createComponentInstance(
createVNode(mockComponent),
null,
null
)
beforeEach(() => {
setCurrentRenderingInstance(mockInstance)
})
afterEach(() => {
setCurrentRenderingInstance(null)
})
test('string component lookup', () => {
expect(h('foo')).toMatchObject({
type: mockChildComp
})
})
test('class / style / attrs / domProps / props', () => {
expect(
h('div', {
class: 'foo',
style: { color: 'red' },
attrs: {
id: 'foo'
},
domProps: {
innerHTML: 'hi'
},
props: {
myProp: 'foo'
}
})
).toMatchObject({
props: {
class: 'foo',
style: { color: 'red' },
id: 'foo',
innerHTML: 'hi',
myProp: 'foo'
}
})
})
test('staticClass + class', () => {
expect(
h('div', {
class: { foo: true },
staticClass: 'bar'
})
).toMatchObject({
props: {
class: 'bar foo'
}
})
})
test('staticStyle + style', () => {
expect(
h('div', {
style: { color: 'red' },
staticStyle: { fontSize: '14px' }
})
).toMatchObject({
props: {
style: {
color: 'red',
fontSize: '14px'
}
}
})
})
test('on / nativeOn', () => {
const fn = () => {}
expect(
h('div', {
on: {
click: fn,
fooBar: fn
},
nativeOn: {
click: fn,
'bar-baz': fn
}
})
).toMatchObject({
props: {
onClick: fn, // should dedupe
onFooBar: fn,
'onBar-baz': fn
}
})
})
test('directives', () => {
expect(
h('div', {
directives: [
{
name: 'mock-dir',
value: '2',
// expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
]
})
).toMatchObject({
dirs: [
{
dir: mockDir,
instance: mockInstance.proxy,
value: '2',
oldValue: void 0,
arg: 'foo',
modifiers: {
bar: true
}
}
] as DirectiveBinding[]
})
})
test('scopedSlots', () => {
const scopedSlots = {
default() {}
}
const vnode = h(mockComponent, {
scopedSlots
})
expect(vnode).toMatchObject({
children: scopedSlots
})
expect('scopedSlots' in vnode.props!).toBe(false)
expect(vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN).toBeTruthy()
})
test('legacy named slot', () => {
const vnode = h(mockComponent, [
'text',
h('div', { slot: 'foo' }, 'one'),
h('div', { slot: 'bar' }, 'two'),
h('div', { slot: 'foo' }, 'three'),
h('div', 'four')
])
expect(vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN).toBeTruthy()
const slots = vnode.children as any
// default
expect(slots.default()).toMatchObject(['text', { children: 'four' }])
expect(slots.foo()).toMatchObject([
{ children: 'one' },
{ children: 'three' }
])
expect(slots.bar()).toMatchObject([{ children: 'two' }])
})
})

View File

@ -0,0 +1,27 @@
import { isOn } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { DeprecationTypes, isCompatEnabled } from './compatConfig'
export function shouldSkipAttr(
key: string,
value: any,
instance: ComponentInternalInstance
): boolean {
if (
(key === 'class' || key === 'style') &&
isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance)
) {
return true
}
if (
isOn(key) &&
isCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance)
) {
return true
}
// vue-router
if (key.startsWith('routerView') || key === 'registerRouteInstance') {
return true
}
return false
}

View File

@ -0,0 +1,636 @@
import { extend, hasOwn, isArray } from '@vue/shared'
import {
ComponentInternalInstance,
ComponentOptions,
formatComponentName,
getComponentName,
getCurrentInstance,
isRuntimeOnly
} from '../component'
import { warn } from '../warning'
export const enum DeprecationTypes {
GLOBAL_MOUNT = 'GLOBAL_MOUNT',
GLOBAL_MOUNT_CONTAINER = 'GLOBAL_MOUNT_CONTAINER',
GLOBAL_EXTEND = 'GLOBAL_EXTEND',
GLOBAL_PROTOTYPE = 'GLOBAL_PROTOTYPE',
GLOBAL_SET = 'GLOBAL_SET',
GLOBAL_DELETE = 'GLOBAL_DELETE',
GLOBAL_OBSERVABLE = 'GLOBAL_OBSERVABLE',
GLOBAL_PRIVATE_UTIL = 'GLOBAL_PRIVATE_UTIL',
CONFIG_SILENT = 'CONFIG_SILENT',
CONFIG_DEVTOOLS = 'CONFIG_DEVTOOLS',
CONFIG_KEY_CODES = 'CONFIG_KEY_CODES',
CONFIG_PRODUCTION_TIP = 'CONFIG_PRODUCTION_TIP',
CONFIG_IGNORED_ELEMENTS = 'CONFIG_IGNORED_ELEMENTS',
CONFIG_WHITESPACE = 'CONFIG_WHITESPACE',
INSTANCE_SET = 'INSTANCE_SET',
INSTANCE_DELETE = 'INSTANCE_DELETE',
INSTANCE_DESTROY = 'INSTANCE_DESTROY',
INSTANCE_EVENT_EMITTER = 'INSTANCE_EVENT_EMITTER',
INSTANCE_EVENT_HOOKS = 'INSTANCE_EVENT_HOOKS',
INSTANCE_CHILDREN = 'INSTANCE_CHILDREN',
INSTANCE_LISTENERS = 'INSTANCE_LISTENERS',
INSTANCE_SCOPED_SLOTS = 'INSTANCE_SCOPED_SLOTS',
INSTANCE_ATTRS_CLASS_STYLE = 'INSTANCE_ATTRS_CLASS_STYLE',
OPTIONS_DATA_FN = 'OPTIONS_DATA_FN',
OPTIONS_DATA_MERGE = 'OPTIONS_DATA_MERGE',
OPTIONS_BEFORE_DESTROY = 'OPTIONS_BEFORE_DESTROY',
OPTIONS_DESTROYED = 'OPTIONS_DESTROYED',
WATCH_ARRAY = 'WATCH_ARRAY',
PROPS_DEFAULT_THIS = 'PROPS_DEFAULT_THIS',
V_FOR_REF = 'V_FOR_REF',
V_ON_KEYCODE_MODIFIER = 'V_ON_KEYCODE_MODIFIER',
CUSTOM_DIR = 'CUSTOM_DIR',
ATTR_FALSE_VALUE = 'ATTR_FALSE_VALUE',
ATTR_ENUMERATED_COERSION = 'ATTR_ENUMERATED_COERSION',
TRANSITION_CLASSES = 'TRANSITION_CLASSES',
TRANSITION_GROUP_ROOT = 'TRANSITION_GROUP_ROOT',
COMPONENT_ASYNC = 'COMPONENT_ASYNC',
COMPONENT_FUNCTIONAL = 'COMPONENT_FUNCTIONAL',
COMPONENT_V_MODEL = 'COMPONENT_V_MODEL',
RENDER_FUNCTION = 'RENDER_FUNCTION',
FILTERS = 'FILTERS',
PRIVATE_APIS = 'PRIVATE_APIS'
}
type DeprecationData = {
message: string | ((...args: any[]) => string)
link?: string
}
export const deprecationData: Record<DeprecationTypes, DeprecationData> = {
[DeprecationTypes.GLOBAL_MOUNT]: {
message:
`The global app bootstrapping API has changed: vm.$mount() and the "el" ` +
`option have been removed. Use createApp(RootComponent).mount() instead.`,
link: `https://v3.vuejs.org/guide/migration/global-api.html#mounting-app-instance`
},
[DeprecationTypes.GLOBAL_MOUNT_CONTAINER]: {
message:
`Vue detected directives on the mount container. ` +
`In Vue 3, the container is no longer considered part of the template ` +
`and will not be processed/replaced.`,
link: `https://v3.vuejs.org/guide/migration/mount-changes.html`
},
[DeprecationTypes.GLOBAL_EXTEND]: {
message:
`Vue.extend() has been removed in Vue 3. ` +
`Use defineComponent() instead.`,
link: `https://v3.vuejs.org/api/global-api.html#definecomponent`
},
[DeprecationTypes.GLOBAL_PROTOTYPE]: {
message:
`Vue.prototype is no longer available in Vue 3. ` +
`Use config.globalProperties instead.`,
link: `https://v3.vuejs.org/guide/migration/global-api.html#vue-prototype-replaced-by-config-globalproperties`
},
[DeprecationTypes.GLOBAL_SET]: {
message:
`Vue.set() has been removed as it is no longer needed in Vue 3. ` +
`Simply use native JavaScript mutations.`
},
[DeprecationTypes.GLOBAL_DELETE]: {
message:
`Vue.delete() has been removed as it is no longer needed in Vue 3. ` +
`Simply use native JavaScript mutations.`
},
[DeprecationTypes.GLOBAL_OBSERVABLE]: {
message:
`Vue.observable() has been removed. ` +
`Use \`import { reactive } from "vue"\` from Composition API instead.`,
link: `https://v3.vuejs.org/api/basic-reactivity.html`
},
[DeprecationTypes.GLOBAL_PRIVATE_UTIL]: {
message:
`Vue.util has been removed. Please refactor to avoid its usage ` +
`since it was an internal API even in Vue 2.`
},
[DeprecationTypes.CONFIG_SILENT]: {
message:
`config.silent has been removed because it is not good practice to ` +
`intentionally suppress warnings. You can use your browser console's ` +
`filter features to focus on relevant messages.`
},
[DeprecationTypes.CONFIG_DEVTOOLS]: {
message:
`config.devtools has been removed. To enable devtools for ` +
`production, configure the __VUE_PROD_DEVTOOLS__ compile-time flag.`,
link: `https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags`
},
[DeprecationTypes.CONFIG_KEY_CODES]: {
message:
`config.keyCodes has been removed. ` +
`In Vue 3, you can directly use the kebab-case key names as v-on modifiers.`,
link: `https://v3.vuejs.org/guide/migration/keycode-modifiers.html`
},
[DeprecationTypes.CONFIG_PRODUCTION_TIP]: {
message: `config.productionTip has been removed.`,
link: `https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed`
},
[DeprecationTypes.CONFIG_IGNORED_ELEMENTS]: {
message: () => {
let msg = `config.ignoredElements has been removed.`
if (isRuntimeOnly()) {
msg += ` Pass the "isCustomElement" option to @vue/compiler-dom instead.`
} else {
msg += ` Use config.isCustomElement instead.`
}
return msg
},
link: `https://v3.vuejs.org/guide/migration/global-api.html#config-ignoredelements-is-now-config-iscustomelement`
},
[DeprecationTypes.CONFIG_WHITESPACE]: {
// this warning is only relevant in the full build when using runtime
// compilation, so it's put in the runtime compatConfig list.
message:
`Vue 3 compiler's whitespace option will default to "condense" instead of ` +
`"preserve". To suppress this warning, provide an explicit value for ` +
`\`config.compilerOptions.whitespace\`.`
},
[DeprecationTypes.INSTANCE_SET]: {
message:
`vm.$set() has been removed as it is no longer needed in Vue 3. ` +
`Simply use native JavaScript mutations.`
},
[DeprecationTypes.INSTANCE_DELETE]: {
message:
`vm.$delete() has been removed as it is no longer needed in Vue 3. ` +
`Simply use native JavaScript mutations.`
},
[DeprecationTypes.INSTANCE_DESTROY]: {
message: `vm.$destroy() has been removed. Use app.unmount() instead.`,
link: `https://v3.vuejs.org/api/application-api.html#unmount`
},
[DeprecationTypes.INSTANCE_EVENT_EMITTER]: {
message:
`vm.$on/$once/$off() have been removed. ` +
`Use an external event emitter library instead.`,
link: `https://v3.vuejs.org/guide/migration/events-api.html`
},
[DeprecationTypes.INSTANCE_EVENT_HOOKS]: {
message: event =>
`"${event}" lifecycle events are no longer supported. From templates, ` +
`use the "vnode" prefix instead of "hook:". For example, @${event} ` +
`should be changed to @vnode-${event.slice(5)}. ` +
`From JavaScript, use Composition API to dynamically register lifecycle ` +
`hooks.`,
link: `https://v3.vuejs.org/guide/migration/vnode-lifecycle-events.html`
},
[DeprecationTypes.INSTANCE_CHILDREN]: {
message:
`vm.$children has been removed. Consider refactoring your logic ` +
`to avoid relying on direct access to child components.`,
link: `https://v3.vuejs.org/guide/migration/children.html`
},
[DeprecationTypes.INSTANCE_LISTENERS]: {
message:
`vm.$listeners has been removed. In Vue 3, parent v-on listeners are ` +
`included in vm.$attrs and it is no longer necessary to separately use ` +
`v-on="$listeners" if you are already using v-bind="$attrs". ` +
`(Note: the Vue 3 behavior only applies if this compat config is disabled)`,
link: `https://v3.vuejs.org/guide/migration/listeners-removed.html`
},
[DeprecationTypes.INSTANCE_SCOPED_SLOTS]: {
message: `vm.$scopedSlots has been removed. Use vm.$slots instead.`,
link: `https://v3.vuejs.org/guide/migration/slots-unification.html`
},
[DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE]: {
message: componentName =>
`Component <${componentName}> has \`inheritAttrs: false\` but is ` +
`relying on class/style fallthrough from parent. In Vue 3, class/style ` +
`are now included in $attrs and will no longer fallthrough when ` +
`inheritAttrs is false. If you are already using v-bind="$attrs" on ` +
`component root it should render the same end result. ` +
`If you are binding $attrs to a non-root element and expecting ` +
`class/style to fallthrough on root, you will need to now manually bind ` +
`them on root via :class="$attrs.class".`,
link: `https://v3.vuejs.org/guide/migration/attrs-includes-class-style.html`
},
[DeprecationTypes.OPTIONS_DATA_FN]: {
message:
`The "data" option can no longer be a plain object. ` +
`Always use a function.`,
link: `https://v3.vuejs.org/guide/migration/data-option.html`
},
[DeprecationTypes.OPTIONS_DATA_MERGE]: {
message: (key: string) =>
`Detected conflicting key "${key}" when merging data option values. ` +
`In Vue 3, data keys are merged shallowly and will override one another.`,
link: `https://v3.vuejs.org/guide/migration/data-option.html#mixin-merge-behavior-change`
},
[DeprecationTypes.OPTIONS_BEFORE_DESTROY]: {
message: `\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`
},
[DeprecationTypes.OPTIONS_DESTROYED]: {
message: `\`destroyed\` has been renamed to \`unmounted\`.`
},
[DeprecationTypes.WATCH_ARRAY]: {
message:
`"watch" option or vm.$watch on an array value will no longer ` +
`trigger on array mutation unless the "deep" option is specified. ` +
`If current usage is intended, you can disable the compat behavior and ` +
`suppress this warning with:` +
`\n\n configureCompat({ ${DeprecationTypes.WATCH_ARRAY}: false })\n`,
link: `https://v3.vuejs.org/guide/migration/watch.html`
},
[DeprecationTypes.PROPS_DEFAULT_THIS]: {
message: (key: string) =>
`props default value function no longer has access to "this". The compat ` +
`build only offers access to this.$options.` +
`(found in prop "${key}")`,
link: `https://v3.vuejs.org/guide/migration/props-default-this.html`
},
[DeprecationTypes.CUSTOM_DIR]: {
message: (legacyHook: string, newHook: string) =>
`Custom directive hook "${legacyHook}" has been removed. ` +
`Use "${newHook}" instead.`,
link: `https://v3.vuejs.org/guide/migration/custom-directives.html`
},
[DeprecationTypes.V_FOR_REF]: {
message:
`Ref usage on v-for no longer creates array ref values in Vue 3. ` +
`Consider using function refs or refactor to avoid ref usage altogether.`,
link: `https://v3.vuejs.org/guide/migration/array-refs.html`
},
[DeprecationTypes.V_ON_KEYCODE_MODIFIER]: {
message:
`Using keyCode as v-on modifier is no longer supported. ` +
`Use kebab-case key name modifiers instead.`,
link: `https://v3.vuejs.org/guide/migration/keycode-modifiers.html`
},
[DeprecationTypes.ATTR_FALSE_VALUE]: {
message: (name: string) =>
`Attribute "${name}" with v-bind value \`false\` will render ` +
`${name}="false" instead of removing it in Vue 3. To remove the attribute, ` +
`use \`null\` or \`undefined\` instead. If the usage is intended, ` +
`you can disable the compat behavior and suppress this warning with:` +
`\n\n configureCompat({ ${
DeprecationTypes.ATTR_FALSE_VALUE
}: false })\n`,
link: `https://v3.vuejs.org/guide/migration/attribute-coercion.html`
},
[DeprecationTypes.ATTR_ENUMERATED_COERSION]: {
message: (name: string, value: any, coerced: string) =>
`Enumerated attribute "${name}" with v-bind value \`${value}\` will ` +
`${
value === null ? `be removed` : `render the value as-is`
} instead of coercing the value to "${coerced}" in Vue 3. ` +
`Always use explicit "true" or "false" values for enumerated attributes. ` +
`If the usage is intended, ` +
`you can disable the compat behavior and suppress this warning with:` +
`\n\n configureCompat({ ${
DeprecationTypes.ATTR_ENUMERATED_COERSION
}: false })\n`,
link: `https://v3.vuejs.org/guide/migration/attribute-coercion.html`
},
[DeprecationTypes.TRANSITION_CLASSES]: {
message: `` // this feature cannot be runtime-detected
},
[DeprecationTypes.TRANSITION_GROUP_ROOT]: {
message:
`<TransitionGroup> no longer renders a root <span> element by ` +
`default if no "tag" prop is specified. If you do not rely on the span ` +
`for styling, you can disable the compat behavior and suppress this ` +
`warning with:` +
`\n\n configureCompat({ ${
DeprecationTypes.TRANSITION_GROUP_ROOT
}: false })\n`,
link: `https://v3.vuejs.org/guide/migration/transition-group.html`
},
[DeprecationTypes.COMPONENT_ASYNC]: {
message: (comp: any) => {
const name = getComponentName(comp)
return (
`Async component${
name ? ` <${name}>` : `s`
} should be explicitly created via \`defineAsyncComponent()\` ` +
`in Vue 3. Plain functions will be treated as functional components in ` +
`non-compat build. If you have already migrated all async component ` +
`usage and intend to use plain functions for functional components, ` +
`you can disable the compat behavior and suppress this ` +
`warning with:` +
`\n\n configureCompat({ ${
DeprecationTypes.COMPONENT_ASYNC
}: false })\n`
)
},
link: `https://v3.vuejs.org/guide/migration/async-components.html`
},
[DeprecationTypes.COMPONENT_FUNCTIONAL]: {
message: (comp: any) => {
const name = getComponentName(comp)
return (
`Functional component${
name ? ` <${name}>` : `s`
} should be defined as a plain function in Vue 3. The "functional" ` +
`option has been removed. NOTE: Before migrating to use plain ` +
`functions for functional components, first make sure that all async ` +
`components usage have been migrated and its compat behavior has ` +
`been disabled.`
)
},
link: `https://v3.vuejs.org/guide/migration/functional-components.html`
},
[DeprecationTypes.COMPONENT_V_MODEL]: {
message: (comp: ComponentOptions) => {
const configMsg =
`opt-in to ` +
`Vue 3 behavior on a per-component basis with \`compatConfig: { ${
DeprecationTypes.COMPONENT_V_MODEL
}: false }\`.`
if (
comp.props && isArray(comp.props)
? comp.props.includes('modelValue')
: hasOwn(comp.props, 'modelValue')
) {
return (
`Component delcares "modelValue" prop, which is Vue 3 usage, but ` +
`is running under Vue 2 compat v-model behavior. You can ${configMsg}`
)
}
return (
`v-model usage on component has changed in Vue 3. Component that expects ` +
`to work with v-model should now use the "modelValue" prop and emit the ` +
`"update:modelValue" event. You can update the usage and then ${configMsg}`
)
},
link: `https://v3.vuejs.org/guide/migration/v-model.html`
},
[DeprecationTypes.RENDER_FUNCTION]: {
message:
`Vue 3's render function API has changed. ` +
`You can opt-in to the new API with:` +
`\n\n configureCompat({ ${
DeprecationTypes.RENDER_FUNCTION
}: false })\n` +
`\n (This can also be done per-component via the "compatConfig" option.)`,
link: `https://v3.vuejs.org/guide/migration/render-function-api.html`
},
[DeprecationTypes.FILTERS]: {
message:
`filters have been removed in Vue 3. ` +
`The "|" symbol will be treated as native JavaScript bitwise OR operator. ` +
`Use method calls or computed properties instead.`,
link: `https://v3.vuejs.org/guide/migration/filters.html`
},
[DeprecationTypes.PRIVATE_APIS]: {
message: name =>
`"${name}" is a Vue 2 private API that no longer exists in Vue 3. ` +
`If you are seeing this warning only due to a dependency, you can ` +
`suppress this warning via { PRIVATE_APIS: 'supress-warning' }.`
}
}
const instanceWarned: Record<string, true> = Object.create(null)
const warnCount: Record<string, number> = Object.create(null)
// test only
let warningEnabled = true
export function toggleDeprecationWarning(flag: boolean) {
warningEnabled = flag
}
export function warnDeprecation(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
...args: any[]
) {
if (!__DEV__) {
return
}
if (__TEST__ && !warningEnabled) {
return
}
instance = instance || getCurrentInstance()
// check user config
const config = getCompatConfigForKey(key, instance)
if (config === 'suppress-warning') {
return
}
const dupKey = key + args.join('')
let compId: string | number | null =
instance && formatComponentName(instance, instance.type)
if (compId === 'Anonymous' && instance) {
compId = instance.uid
}
// skip if the same warning is emitted for the same component type
const componentDupKey = dupKey + compId
if (!__TEST__ && componentDupKey in instanceWarned) {
return
}
instanceWarned[componentDupKey] = true
// same warning, but different component. skip the long message and just
// log the key and count.
if (!__TEST__ && dupKey in warnCount) {
warn(`(deprecation ${key}) (${++warnCount[dupKey] + 1})`)
return
}
warnCount[dupKey] = 0
const { message, link } = deprecationData[key]
warn(
`(deprecation ${key}) ${
typeof message === 'function' ? message(...args) : message
}${link ? `\n Details: ${link}` : ``}`
)
if (!isCompatEnabled(key, instance)) {
console.error(
`^ The above deprecation's compat behavior is disabled and will likely ` +
`lead to runtime errors.`
)
}
}
export type CompatConfig = Partial<
Record<DeprecationTypes, boolean | 'suppress-warning'>
> & {
MODE?: 2 | 3
}
export const globalCompatConfig: CompatConfig = {
MODE: 2
}
export function configureCompat(config: CompatConfig) {
if (__DEV__) {
validateCompatConfig(config)
}
extend(globalCompatConfig, config)
}
const seenConfigObjects = /*#__PURE__*/ new WeakSet<CompatConfig>()
const warnedInvalidKeys: Record<string, boolean> = {}
// dev only
export function validateCompatConfig(config: CompatConfig) {
if (seenConfigObjects.has(config)) {
return
}
seenConfigObjects.add(config)
for (const key of Object.keys(config)) {
if (
key !== 'MODE' &&
!(key in deprecationData) &&
!(key in warnedInvalidKeys)
) {
if (key.startsWith('COMPILER_')) {
if (isRuntimeOnly()) {
warn(
`Depreaction config "${key}" is compiler-specific and you are ` +
`running a runtime-only build of Vue. This deprecation should be ` +
`configured via compiler options in your build setup instead.`
// TODO link to migration build docs on build setup
)
}
} else {
warn(`Invalid deprecation config "${key}".`)
}
warnedInvalidKeys[key] = true
}
}
}
export function getCompatConfigForKey(
key: DeprecationTypes | 'MODE',
instance: ComponentInternalInstance | null
) {
const instanceConfig =
instance && (instance.type as ComponentOptions).compatConfig
if (instanceConfig && key in instanceConfig) {
return instanceConfig[key]
}
return globalCompatConfig[key]
}
export function isCompatEnabled(
key: DeprecationTypes,
instance: ComponentInternalInstance | null
): boolean {
// skip compat for built-in components
if (instance && instance.type.__isBuiltIn) {
return false
}
const mode = getCompatConfigForKey('MODE', instance) || 2
const val = getCompatConfigForKey(key, instance)
if (mode === 2) {
return val !== false
} else {
return val === true || val === 'suppress-warning'
}
}
/**
* Use this for features that are completely removed in non-compat build.
*/
export function assertCompatEnabled(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
...args: any[]
) {
if (!isCompatEnabled(key, instance)) {
throw new Error(`${key} compat has been disabled.`)
} else if (__DEV__) {
warnDeprecation(key, instance, ...args)
}
}
/**
* Use this for features where legacy usage is still possible, but will likely
* lead to runtime error if compat is disabled. (warn in all cases)
*/
export function softAssertCompatEnabled(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
...args: any[]
) {
if (__DEV__) {
warnDeprecation(key, instance, ...args)
}
return isCompatEnabled(key, instance)
}
/**
* Use this for features with the same syntax but with mutually exclusive
* behavior in 2 vs 3. Only warn if compat is enabled.
* e.g. render function
*/
export function checkCompatEnabled(
key: DeprecationTypes,
instance: ComponentInternalInstance | null,
...args: any[]
) {
const enabled = isCompatEnabled(key, instance)
if (__DEV__ && enabled) {
warnDeprecation(key, instance, ...args)
}
return enabled
}
// run tests in v3 mode by default
if (__TEST__) {
configureCompat({
MODE: 3
})
}

View File

@ -0,0 +1,164 @@
import { isArray, isFunction, isObject, isPromise } from '@vue/shared'
import { defineAsyncComponent } from '../apiAsyncComponent'
import {
Component,
ComponentInternalInstance,
ComponentOptions,
FunctionalComponent,
getCurrentInstance
} from '../component'
import { resolveInjections } from '../componentOptions'
import { InternalSlots } from '../componentSlots'
import { isVNode } from '../vnode'
import {
checkCompatEnabled,
softAssertCompatEnabled,
DeprecationTypes
} from './compatConfig'
import { getCompatListeners } from './instanceListeners'
import { compatH } from './renderFn'
export function convertLegacyComponent(
comp: any,
instance: ComponentInternalInstance | null
): Component {
if (comp.__isBuiltIn) {
return comp
}
// 2.x constructor
if (isFunction(comp) && comp.cid) {
comp = comp.options
}
// 2.x async component
if (
isFunction(comp) &&
checkCompatEnabled(DeprecationTypes.COMPONENT_ASYNC, instance, comp)
) {
// since after disabling this, plain functions are still valid usage, do not
// use softAssert here.
return convertLegacyAsyncComponent(comp)
}
// 2.x functional component
if (
isObject(comp) &&
comp.functional &&
softAssertCompatEnabled(
DeprecationTypes.COMPONENT_FUNCTIONAL,
instance,
comp
)
) {
return convertLegacyFunctionalComponent(comp)
}
return comp
}
interface LegacyAsyncOptions {
component: Promise<Component>
loading?: Component
error?: Component
delay?: number
timeout?: number
}
type LegacyAsyncReturnValue = Promise<Component> | LegacyAsyncOptions
type LegacyAsyncComponent = (
resolve?: (res: LegacyAsyncReturnValue) => void,
reject?: (reason?: any) => void
) => LegacyAsyncReturnValue | undefined
const normalizedAsyncComponentMap = new Map<LegacyAsyncComponent, Component>()
function convertLegacyAsyncComponent(comp: LegacyAsyncComponent) {
if (normalizedAsyncComponentMap.has(comp)) {
return normalizedAsyncComponentMap.get(comp)!
}
// we have to call the function here due to how v2's API won't expose the
// options until we call it
let resolve: (res: LegacyAsyncReturnValue) => void
let reject: (reason?: any) => void
const fallbackPromise = new Promise<Component>((r, rj) => {
;(resolve = r), (reject = rj)
})
const res = comp(resolve!, reject!)
let converted: Component
if (isPromise(res)) {
converted = defineAsyncComponent(() => res)
} else if (isObject(res) && !isVNode(res) && !isArray(res)) {
converted = defineAsyncComponent({
loader: () => res.component,
loadingComponent: res.loading,
errorComponent: res.error,
delay: res.delay,
timeout: res.timeout
})
} else if (res == null) {
converted = defineAsyncComponent(() => fallbackPromise)
} else {
converted = comp as any // probably a v3 functional comp
}
normalizedAsyncComponentMap.set(comp, converted)
return converted
}
const normalizedFunctionalComponentMap = new Map<
ComponentOptions,
FunctionalComponent
>()
export const legacySlotProxyHandlers: ProxyHandler<InternalSlots> = {
get(target, key: string) {
const slot = target[key]
return slot && slot()
}
}
function convertLegacyFunctionalComponent(comp: ComponentOptions) {
if (normalizedFunctionalComponentMap.has(comp)) {
return normalizedFunctionalComponentMap.get(comp)!
}
const legacyFn = comp.render as any
const Func: FunctionalComponent = (props, ctx) => {
const instance = getCurrentInstance()!
const legacyCtx = {
props,
children: instance.vnode.children || [],
data: instance.vnode.props || {},
scopedSlots: ctx.slots,
parent: instance.parent && instance.parent.proxy,
slots() {
return new Proxy(ctx.slots, legacySlotProxyHandlers)
},
get listeners() {
return getCompatListeners(instance)
},
get injections() {
if (comp.inject) {
const injections = {}
resolveInjections(comp.inject, {})
return injections
}
return {}
}
}
return legacyFn(compatH, legacyCtx)
}
Func.props = comp.props
Func.displayName = comp.name
// v2 functional components do not inherit attrs
Func.inheritAttrs = false
normalizedFunctionalComponentMap.set(comp, Func)
return Func
}

View File

@ -0,0 +1,60 @@
import { isArray } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { ObjectDirective, DirectiveHook } from '../directives'
import { softAssertCompatEnabled, DeprecationTypes } from './compatConfig'
export interface LegacyDirective {
bind?: DirectiveHook
inserted?: DirectiveHook
update?: DirectiveHook
componentUpdated?: DirectiveHook
unbind?: DirectiveHook
}
const legacyDirectiveHookMap: Partial<
Record<
keyof ObjectDirective,
keyof LegacyDirective | (keyof LegacyDirective)[]
>
> = {
beforeMount: 'bind',
mounted: 'inserted',
updated: ['update', 'componentUpdated'],
unmounted: 'unbind'
}
export function mapCompatDirectiveHook(
name: keyof ObjectDirective,
dir: ObjectDirective & LegacyDirective,
instance: ComponentInternalInstance | null
): DirectiveHook | DirectiveHook[] | undefined {
const mappedName = legacyDirectiveHookMap[name]
if (mappedName) {
if (isArray(mappedName)) {
const hook: DirectiveHook[] = []
mappedName.forEach(name => {
const mappedHook = dir[name]
if (mappedHook) {
softAssertCompatEnabled(
DeprecationTypes.CUSTOM_DIR,
instance,
mappedName,
name
)
hook.push(mappedHook)
}
})
return hook.length ? hook : undefined
} else {
if (dir[mappedName]) {
softAssertCompatEnabled(
DeprecationTypes.CUSTOM_DIR,
instance,
mappedName,
name
)
}
return dir[mappedName]
}
}
}

View File

@ -0,0 +1,38 @@
import { isFunction, isPlainObject } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function deepMergeData(
to: any,
from: any,
instance: ComponentInternalInstance
) {
for (const key in from) {
const toVal = to[key]
const fromVal = from[key]
if (key in to && isPlainObject(toVal) && isPlainObject(fromVal)) {
__DEV__ &&
warnDeprecation(DeprecationTypes.OPTIONS_DATA_MERGE, instance, key)
deepMergeData(toVal, fromVal, instance)
} else {
to[key] = fromVal
}
}
}
export function mergeDataOption(to: any, from: any) {
if (!from) {
return to
}
if (!to) {
return from
}
return function mergedDataFn(this: ComponentPublicInstance) {
return deepMergeData(
isFunction(to) ? to.call(this, this) : to,
isFunction(from) ? from.call(this, this) : from,
this.$
)
}
}

View File

@ -0,0 +1,18 @@
import { App, AppContext } from '../apiCreateApp'
import { warn } from '../warning'
import { assertCompatEnabled, DeprecationTypes } from './compatConfig'
export function installGlobalFilterMethod(app: App, context: AppContext) {
context.filters = {}
app.filter = (name: string, filter?: Function): any => {
assertCompatEnabled(DeprecationTypes.FILTERS, null)
if (!filter) {
return context.filters![name]
}
if (__DEV__ && context.filters![name]) {
warn(`Filter "${name}" has already been registered.`)
}
context.filters![name] = filter
return app
}
}

View File

@ -0,0 +1,529 @@
import {
isReactive,
reactive,
track,
TrackOpTypes,
trigger,
TriggerOpTypes
} from '@vue/reactivity'
import {
isFunction,
extend,
NOOP,
EMPTY_OBJ,
isArray,
isObject,
isString
} from '@vue/shared'
import { warn } from '../warning'
import { cloneVNode, createVNode } from '../vnode'
import { RootRenderFunction } from '../renderer'
import { RootHydrateFunction } from '../hydration'
import {
App,
AppConfig,
AppContext,
CreateAppFunction,
Plugin
} from '../apiCreateApp'
import {
Component,
ComponentOptions,
createComponentInstance,
finishComponentSetup,
isRuntimeOnly,
setupComponent
} from '../component'
import { RenderFunction, mergeOptions } from '../componentOptions'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { devtoolsInitApp } from '../devtools'
import { Directive } from '../directives'
import { nextTick } from '../scheduler'
import { version } from '..'
import { LegacyConfig, legacyOptionMergeStrats } from './globalConfig'
import { LegacyDirective } from './customDirective'
import {
warnDeprecation,
DeprecationTypes,
assertCompatEnabled,
configureCompat,
isCompatEnabled,
softAssertCompatEnabled
} from './compatConfig'
import { LegacyPublicInstance } from './instance'
/**
* @deprecated the default `Vue` export has been removed in Vue 3. The type for
* the default export is provided only for migration purposes. Please use
* named imports instead - e.g. `import { createApp } from 'vue'`.
*/
export type CompatVue = Pick<App, 'version' | 'component' | 'directive'> & {
configureCompat: typeof configureCompat
// no inference here since these types are not meant for actual use - they
// are merely here to provide type checks for internal implementation and
// information for migration.
new (options?: ComponentOptions): LegacyPublicInstance
version: string
config: AppConfig & LegacyConfig
extend: (options?: ComponentOptions) => CompatVue
nextTick: typeof nextTick
use(plugin: Plugin, ...options: any[]): CompatVue
mixin(mixin: ComponentOptions): CompatVue
component(name: string): Component | undefined
component(name: string, component: Component): CompatVue
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): CompatVue
compile(template: string): RenderFunction
/**
* @deprecated Vue 3 no longer needs set() for adding new properties.
*/
set(target: any, key: string | number | symbol, value: any): void
/**
* @deprecated Vue 3 no longer needs delete() for property deletions.
*/
delete(target: any, key: string | number | symbol): void
/**
* @deprecated use `reactive` instead.
*/
observable: typeof reactive
/**
* @deprecated filters have been removed from Vue 3.
*/
filter(name: string, arg: any): null
/**
* @internal
*/
cid: number
/**
* @internal
*/
options: ComponentOptions
/**
* @internal
*/
super: CompatVue
}
export let isCopyingConfig = false
// Legacy global Vue constructor
export function createCompatVue(
createApp: CreateAppFunction<Element>
): CompatVue {
const Vue: CompatVue = function Vue(options: ComponentOptions = {}) {
return createCompatApp(options, Vue)
} as any
const singletonApp = createApp({})
function createCompatApp(options: ComponentOptions = {}, Ctor: any) {
assertCompatEnabled(DeprecationTypes.GLOBAL_MOUNT, null)
const { data } = options
if (
data &&
!isFunction(data) &&
softAssertCompatEnabled(DeprecationTypes.OPTIONS_DATA_FN, null)
) {
options.data = () => data
}
const app = createApp(options)
// copy over asset registries and deopt flag
;['mixins', 'components', 'directives', 'deopt'].forEach(key => {
// @ts-ignore
app._context[key] = singletonApp._context[key]
})
// copy over global config mutations
isCopyingConfig = true
for (const key in singletonApp.config) {
if (key === 'isNativeTag') continue
if (
isRuntimeOnly() &&
(key === 'isCustomElement' || key === 'compilerOptions')
) {
continue
}
const val = singletonApp.config[key as keyof AppConfig]
// @ts-ignore
app.config[key] = val
// compat for runtime ignoredElements -> isCustomElement
if (
key === 'ignoredElements' &&
isCompatEnabled(DeprecationTypes.CONFIG_IGNORED_ELEMENTS, null) &&
!isRuntimeOnly() &&
isArray(val)
) {
app.config.compilerOptions.isCustomElement = tag => {
return val.some(v => (isString(v) ? v === tag : v.test(tag)))
}
}
}
isCopyingConfig = false
// copy prototype augmentations as config.globalProperties
if (isCompatEnabled(DeprecationTypes.GLOBAL_PROTOTYPE, null)) {
app.config.globalProperties = Ctor.prototype
}
let hasPrototypeAugmentations = false
for (const key in Ctor.prototype) {
if (key !== 'constructor') {
hasPrototypeAugmentations = true
break
}
}
if (__DEV__ && hasPrototypeAugmentations) {
warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE, null)
}
const vm = app._createRoot!(options)
if (options.el) {
return (vm as any).$mount(options.el)
} else {
return vm
}
}
Vue.version = __VERSION__
Vue.config = singletonApp.config
Vue.nextTick = nextTick
Vue.options = { _base: Vue }
let cid = 1
Vue.cid = cid
const extendCache = new WeakMap()
function extendCtor(this: any, extendOptions: ComponentOptions = {}) {
assertCompatEnabled(DeprecationTypes.GLOBAL_EXTEND, null)
if (isFunction(extendOptions)) {
extendOptions = extendOptions.options
}
if (extendCache.has(extendOptions)) {
return extendCache.get(extendOptions)
}
const Super = this
function SubVue(inlineOptions?: ComponentOptions) {
if (!inlineOptions) {
return createCompatApp(SubVue.options, SubVue)
} else {
return createCompatApp(
mergeOptions(
extend({}, SubVue.options),
inlineOptions,
null,
legacyOptionMergeStrats as any
),
SubVue
)
}
}
SubVue.super = Super
SubVue.prototype = Object.create(Vue.prototype)
SubVue.prototype.constructor = SubVue
// clone non-primitive base option values for edge case of mutating
// extended options
const mergeBase: any = {}
for (const key in Super.options) {
const superValue = Super.options[key]
mergeBase[key] = isArray(superValue)
? superValue.slice()
: isObject(superValue)
? extend(Object.create(null), superValue)
: superValue
}
SubVue.options = mergeOptions(
mergeBase,
extendOptions,
null,
legacyOptionMergeStrats as any
)
SubVue.options._base = SubVue
SubVue.extend = extendCtor.bind(SubVue)
SubVue.mixin = Super.mixin
SubVue.use = Super.use
SubVue.cid = ++cid
extendCache.set(extendOptions, SubVue)
return SubVue
}
Vue.extend = extendCtor.bind(Vue) as any
Vue.set = (target, key, value) => {
assertCompatEnabled(DeprecationTypes.GLOBAL_SET, null)
target[key] = value
}
Vue.delete = (target, key) => {
assertCompatEnabled(DeprecationTypes.GLOBAL_DELETE, null)
delete target[key]
}
Vue.observable = (target: any) => {
assertCompatEnabled(DeprecationTypes.GLOBAL_OBSERVABLE, null)
return reactive(target)
}
Vue.use = (p, ...options) => {
if (p && isFunction(p.install)) {
p.install(Vue as any, ...options)
} else if (isFunction(p)) {
p(Vue as any, ...options)
}
return Vue
}
Vue.mixin = m => {
singletonApp.mixin(m)
return Vue
}
Vue.component = ((name: string, comp: Component) => {
if (comp) {
singletonApp.component(name, comp)
return Vue
} else {
return singletonApp.component(name)
}
}) as any
Vue.directive = ((name: string, dir: Directive | LegacyDirective) => {
if (dir) {
singletonApp.directive(name, dir as Directive)
return Vue
} else {
return singletonApp.directive(name)
}
}) as any
Vue.filter = ((name: string, filter: any) => {
// TODO deprecation warning
// TODO compiler warning for filters (maybe behavior compat?)
}) as any
// internal utils - these are technically internal but some plugins use it.
const util = {
warn: __DEV__ ? warn : NOOP,
extend,
mergeOptions: (parent: any, child: any, vm?: ComponentPublicInstance) =>
mergeOptions(
parent,
child,
vm && vm.$,
vm ? undefined : (legacyOptionMergeStrats as any)
),
defineReactive
}
Object.defineProperty(Vue, 'util', {
get() {
assertCompatEnabled(DeprecationTypes.GLOBAL_PRIVATE_UTIL, null)
return util
}
})
Vue.configureCompat = configureCompat
return Vue
}
export function installCompatMount(
app: App,
context: AppContext,
render: RootRenderFunction,
hydrate?: RootHydrateFunction
) {
let isMounted = false
/**
* Vue 2 supports the behavior of creating a component instance but not
* mounting it, which is no longer possible in Vue 3 - this internal
* function simulates that behavior.
*/
app._createRoot = options => {
const component = app._component
const vnode = createVNode(component, options.propsData || null)
vnode.appContext = context
const hasNoRender =
!isFunction(component) && !component.render && !component.template
const emptyRender = () => {}
// create root instance
const instance = createComponentInstance(vnode, null, null)
// suppress "missing render fn" warning since it can't be determined
// until $mount is called
if (hasNoRender) {
instance.render = emptyRender
}
setupComponent(instance)
vnode.component = instance
// $mount & $destroy
// these are defined on ctx and picked up by the $mount/$destroy
// public property getters on the instance proxy.
// Note: the following assumes DOM environment since the compat build
// only targets web. It essentially includes logic for app.mount from
// both runtime-core AND runtime-dom.
instance.ctx._compat_mount = (selectorOrEl?: string | Element) => {
if (isMounted) {
__DEV__ && warn(`Root instance is already mounted.`)
return
}
let container: Element
if (typeof selectorOrEl === 'string') {
// eslint-disable-next-line
const result = document.querySelector(selectorOrEl)
if (!result) {
__DEV__ &&
warn(
`Failed to mount root instance: selector "${selectorOrEl}" returned null.`
)
return
}
container = result
} else {
// eslint-disable-next-line
container = selectorOrEl || document.createElement('div')
}
const isSVG = container instanceof SVGElement
// HMR root reload
if (__DEV__) {
context.reload = () => {
const cloned = cloneVNode(vnode)
// compat mode will use instance if not reset to null
cloned.component = null
render(cloned, container, isSVG)
}
}
// resolve in-DOM template if component did not provide render
// and no setup/mixin render functions are provided (by checking
// that the instance is still using the placeholder render fn)
if (hasNoRender && instance.render === emptyRender) {
// root directives check
if (__DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
warnDeprecation(DeprecationTypes.GLOBAL_MOUNT_CONTAINER, null)
break
}
}
}
instance.render = null
;(component as ComponentOptions).template = container.innerHTML
finishComponentSetup(instance, false, true /* skip options */)
}
// clear content before mounting
container.innerHTML = ''
// TODO hydration
render(vnode, container, isSVG)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
isMounted = true
app._container = container
// for devtools and telemetry
;(container as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version)
}
return instance.proxy!
}
instance.ctx._compat_destroy = app.unmount
return instance.proxy!
}
}
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
const patched = new WeakSet<object>()
function defineReactive(obj: any, key: string, val: any) {
// it's possible for the orignial object to be mutated after being defined
// and expecting reactivity... we are covering it here because this seems to
// be a bit more common.
if (isObject(val) && !isReactive(val) && !patched.has(val)) {
const reactiveVal = reactive(val)
if (isArray(val)) {
methodsToPatch.forEach(m => {
// @ts-ignore
val[m] = (...args: any[]) => {
// @ts-ignore
Array.prototype[m].call(reactiveVal, ...args)
}
})
} else {
Object.keys(val).forEach(key => {
try {
defineReactiveSimple(val, key, val[key])
} catch (e) {}
})
}
}
const i = obj.$
if (i && obj === i.proxy) {
// Vue instance, add it to data
if (i.data === EMPTY_OBJ) {
i.data = reactive({})
}
i.data[key] = val
i.accessCache = Object.create(null)
} else if (isReactive(obj)) {
obj[key] = val
} else {
defineReactiveSimple(obj, key, val)
}
}
function defineReactiveSimple(obj: any, key: string, val: any) {
val = isObject(val) ? reactive(val) : val
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
track(obj, TrackOpTypes.GET, key)
return val
},
set(newVal) {
val = isObject(newVal) ? reactive(newVal) : newVal
trigger(obj, TriggerOpTypes.SET, key, newVal)
}
})
}

View File

@ -0,0 +1,105 @@
import { extend, isArray } from '@vue/shared'
import { AppConfig } from '../apiCreateApp'
import { mergeDataOption } from './data'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
import { isCopyingConfig } from './global'
// legacy config warnings
export type LegacyConfig = {
/**
* @deprecated `config.silent` option has been removed
*/
silent?: boolean
/**
* @deprecated use __VUE_PROD_DEVTOOLS__ compile-time feature flag instead
* https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags
*/
devtools?: boolean
/**
* @deprecated use `config.isCustomElement` instead
* https://v3.vuejs.org/guide/migration/global-api.html#config-ignoredelements-is-now-config-iscustomelement
*/
ignoredElements?: (string | RegExp)[]
/**
* @deprecated
* https://v3.vuejs.org/guide/migration/keycode-modifiers.html
*/
keyCodes?: Record<string, number | number[]>
/**
* @deprecated
* https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed
*/
productionTip?: boolean
}
// dev only
export function installLegacyConfigProperties(config: AppConfig) {
const legacyConfigOptions: Record<string, DeprecationTypes> = {
silent: DeprecationTypes.CONFIG_SILENT,
devtools: DeprecationTypes.CONFIG_DEVTOOLS,
ignoredElements: DeprecationTypes.CONFIG_IGNORED_ELEMENTS,
keyCodes: DeprecationTypes.CONFIG_KEY_CODES,
productionTip: DeprecationTypes.CONFIG_PRODUCTION_TIP
}
Object.keys(legacyConfigOptions).forEach(key => {
let val = (config as any)[key]
Object.defineProperty(config, key, {
enumerable: true,
get() {
return val
},
set(newVal) {
if (!isCopyingConfig) {
warnDeprecation(legacyConfigOptions[key], null)
}
val = newVal
}
})
})
// Internal merge strats which are no longer needed in v3, but we need to
// expose them because some v2 plugins will reuse these internal strats to
// merge their custom options.
extend(config.optionMergeStrategies, legacyOptionMergeStrats)
}
export const legacyOptionMergeStrats = {
data: mergeDataOption,
beforeCreate: mergeHook,
created: mergeHook,
beforeMount: mergeHook,
mounted: mergeHook,
beforeUpdate: mergeHook,
updated: mergeHook,
beforeDestroy: mergeHook,
destroyed: mergeHook,
activated: mergeHook,
deactivated: mergeHook,
errorCaptured: mergeHook,
serverPrefetch: mergeHook,
// assets
components: mergeObjectOptions,
directives: mergeObjectOptions,
filters: mergeObjectOptions,
// objects
props: mergeObjectOptions,
methods: mergeObjectOptions,
inject: mergeObjectOptions,
computed: mergeObjectOptions,
// watch has special merge behavior in v2, but isn't actually needed in v3.
// since we are only exposing these for compat and nobody should be relying
// on the watch-specific behavior, just expose the object merge strat.
watch: mergeObjectOptions
}
function mergeHook(
to: Function[] | Function | undefined,
from: Function | Function[]
) {
return Array.from(new Set([...(isArray(to) ? to : to ? [to] : []), from]))
}
function mergeObjectOptions(to: Object | undefined, from: Object | undefined) {
return to ? extend(extend(Object.create(null), to), from) : from
}

View File

@ -0,0 +1,154 @@
import {
extend,
looseEqual,
looseIndexOf,
NOOP,
toDisplayString,
toNumber
} from '@vue/shared'
import {
ComponentPublicInstance,
PublicPropertiesMap
} from '../componentPublicInstance'
import { getCompatChildren } from './instanceChildren'
import {
DeprecationTypes,
assertCompatEnabled,
isCompatEnabled
} from './compatConfig'
import { off, on, once } from './instanceEventEmitter'
import { getCompatListeners } from './instanceListeners'
import { shallowReadonly } from '@vue/reactivity'
import { legacySlotProxyHandlers } from './component'
import { compatH } from './renderFn'
import { createCommentVNode, createTextVNode } from '../vnode'
import { renderList } from '../helpers/renderList'
import {
legacyBindDynamicKeys,
legacyBindObjectListeners,
legacyBindObjectProps,
legacyCheckKeyCodes,
legacyMarkOnce,
legacyPrependModifier,
legacyRenderSlot,
legacyRenderStatic,
legacyresolveScopedSlots
} from './renderHelpers'
import { resolveFilter } from '../helpers/resolveAssets'
import { resolveMergedOptions } from '../componentOptions'
import { Slots } from '../componentSlots'
export type LegacyPublicInstance = ComponentPublicInstance &
LegacyPublicProperties
export interface LegacyPublicProperties {
$set(target: object, key: string, value: any): void
$delete(target: object, key: string): void
$mount(el?: string | Element): this
$destroy(): void
$scopedSlots: Slots
$on(event: string | string[], fn: Function): this
$once(event: string, fn: Function): this
$off(event?: string, fn?: Function): this
$children: LegacyPublicProperties[]
$listeners: Record<string, Function | Function[]>
}
export function installCompatInstanceProperties(map: PublicPropertiesMap) {
const set = (target: any, key: any, val: any) => {
target[key] = val
}
const del = (target: any, key: any) => {
delete target[key]
}
extend(map, {
$set: i => {
assertCompatEnabled(DeprecationTypes.INSTANCE_SET, i)
return set
},
$delete: i => {
assertCompatEnabled(DeprecationTypes.INSTANCE_DELETE, i)
return del
},
$mount: i => {
assertCompatEnabled(
DeprecationTypes.GLOBAL_MOUNT,
null /* this warning is global */
)
// root mount override from ./global.ts in installCompatMount
return i.ctx._compat_mount || NOOP
},
$destroy: i => {
assertCompatEnabled(DeprecationTypes.INSTANCE_DESTROY, i)
// root destroy override from ./global.ts in installCompatMount
return i.ctx._compat_destroy || NOOP
},
// overrides existing accessor
$slots: i => {
if (
isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, i) &&
i.render &&
i.render._compatWrapped
) {
return new Proxy(i.slots, legacySlotProxyHandlers)
}
return __DEV__ ? shallowReadonly(i.slots) : i.slots
},
$scopedSlots: i => {
assertCompatEnabled(DeprecationTypes.INSTANCE_SCOPED_SLOTS, i)
return __DEV__ ? shallowReadonly(i.slots) : i.slots
},
$on: i => on.bind(null, i),
$once: i => once.bind(null, i),
$off: i => off.bind(null, i),
$children: getCompatChildren,
$listeners: getCompatListeners
} as PublicPropertiesMap)
if (isCompatEnabled(DeprecationTypes.PRIVATE_APIS, null)) {
extend(map, {
$vnode: i => i.vnode,
// inject addtional properties into $options for compat
$options: i => {
let res = resolveMergedOptions(i)
if (res === i.type) res = i.type.__merged = extend({}, res)
res.parent = i.proxy!.$parent
res.propsData = i.vnode.props
return res
},
// v2 render helpers
$createElement: () => compatH,
_self: i => i.proxy,
_uid: i => i.uid,
_c: () => compatH,
_o: () => legacyMarkOnce,
_n: () => toNumber,
_s: () => toDisplayString,
_l: () => renderList,
_t: i => legacyRenderSlot.bind(null, i),
_q: () => looseEqual,
_i: () => looseIndexOf,
_m: i => legacyRenderStatic.bind(null, i),
_f: () => resolveFilter,
_k: i => legacyCheckKeyCodes.bind(null, i),
_b: () => legacyBindObjectProps,
_v: () => createTextVNode,
_e: () => createCommentVNode,
_u: () => legacyresolveScopedSlots,
_g: () => legacyBindObjectListeners,
_d: () => legacyBindDynamicKeys,
_p: () => legacyPrependModifier
} as PublicPropertiesMap)
}
}

View File

@ -0,0 +1,28 @@
import { ShapeFlags } from '@vue/shared/src'
import { ComponentInternalInstance } from '../component'
import { ComponentPublicInstance } from '../componentPublicInstance'
import { VNode } from '../vnode'
import { assertCompatEnabled, DeprecationTypes } from './compatConfig'
export function getCompatChildren(
instance: ComponentInternalInstance
): ComponentPublicInstance[] {
assertCompatEnabled(DeprecationTypes.INSTANCE_CHILDREN, instance)
const root = instance.subTree
const children: ComponentPublicInstance[] = []
if (root) {
walk(root, children)
}
return children
}
function walk(vnode: VNode, children: ComponentPublicInstance[]) {
if (vnode.component) {
children.push(vnode.component.proxy!)
} else if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
const vnodes = vnode.children as VNode[]
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], children)
}
}
}

View File

@ -0,0 +1,108 @@
import { isArray } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { assertCompatEnabled, DeprecationTypes } from './compatConfig'
interface EventRegistry {
[event: string]: Function[] | undefined
}
const eventRegistryMap = /*#__PURE__*/ new WeakMap<
ComponentInternalInstance,
EventRegistry
>()
export function getRegistry(
instance: ComponentInternalInstance
): EventRegistry {
let events = eventRegistryMap.get(instance)
if (!events) {
eventRegistryMap.set(instance, (events = Object.create(null)))
}
return events!
}
export function on(
instance: ComponentInternalInstance,
event: string | string[],
fn: Function
) {
if (isArray(event)) {
event.forEach(e => on(instance, e, fn))
} else {
if (event.startsWith('hook:')) {
assertCompatEnabled(
DeprecationTypes.INSTANCE_EVENT_HOOKS,
instance,
event
)
} else {
assertCompatEnabled(DeprecationTypes.INSTANCE_EVENT_EMITTER, instance)
}
const events = getRegistry(instance)
;(events[event] || (events[event] = [])).push(fn)
}
return instance.proxy
}
export function once(
instance: ComponentInternalInstance,
event: string,
fn: Function
) {
const wrapped = (...args: any[]) => {
off(instance, event, wrapped)
fn.call(instance.proxy, ...args)
}
wrapped.fn = fn
on(instance, event, wrapped)
return instance.proxy
}
export function off(
instance: ComponentInternalInstance,
event?: string,
fn?: Function
) {
assertCompatEnabled(DeprecationTypes.INSTANCE_EVENT_EMITTER, instance)
const vm = instance.proxy
// all
if (!arguments.length) {
eventRegistryMap.set(instance, Object.create(null))
return vm
}
// array of events
if (isArray(event)) {
event.forEach(e => off(instance, e, fn))
return vm
}
// specific event
const events = getRegistry(instance)
const cbs = events[event!]
if (!cbs) {
return vm
}
if (!fn) {
events[event!] = undefined
return vm
}
events[event!] = cbs.filter(cb => !(cb === fn || (cb as any).fn === fn))
return vm
}
export function emit(
instance: ComponentInternalInstance,
event: string,
...args: any[]
) {
const cbs = getRegistry(instance)[event]
if (cbs) {
callWithAsyncErrorHandling(
cbs,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
return instance.proxy
}

View File

@ -0,0 +1,19 @@
import { isOn } from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { assertCompatEnabled, DeprecationTypes } from './compatConfig'
export function getCompatListeners(instance: ComponentInternalInstance) {
assertCompatEnabled(DeprecationTypes.INSTANCE_LISTENERS, instance)
const listeners: Record<string, Function | Function[]> = {}
const rawProps = instance.vnode.props
if (!rawProps) {
return listeners
}
for (const key in rawProps) {
if (isOn(key)) {
listeners[key[2].toLowerCase() + key.slice(3)] = rawProps[key]
}
}
return listeners
}

View File

@ -0,0 +1,40 @@
import { isArray } from '@vue/shared'
import { inject } from '../apiInject'
import { ComponentInternalInstance, Data } from '../component'
import { ComponentOptions, resolveMergedOptions } from '../componentOptions'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function createPropsDefaultThis(
instance: ComponentInternalInstance,
rawProps: Data,
propKey: string
) {
return new Proxy(
{},
{
get(_, key: string) {
__DEV__ &&
warnDeprecation(DeprecationTypes.PROPS_DEFAULT_THIS, null, propKey)
// $options
if (key === '$options') {
return resolveMergedOptions(instance)
}
// props
if (key in rawProps) {
return rawProps[key]
}
// injections
const injections = (instance.type as ComponentOptions).inject
if (injections) {
if (isArray(injections)) {
if (injections.includes(key)) {
return inject(key)
}
} else if (key in injections) {
return inject(key)
}
}
}
}
)
}

View File

@ -0,0 +1,45 @@
import { isArray, remove } from '@vue/shared'
import { ComponentInternalInstance, Data } from '../component'
import { VNode } from '../vnode'
import { DeprecationTypes, warnDeprecation } from './compatConfig'
export function convertLegacyRefInFor(vnode: VNode) {
// refInFor
if (vnode.props && vnode.props.refInFor) {
delete vnode.props.refInFor
if (vnode.ref) {
if (isArray(vnode.ref)) {
vnode.ref.forEach(r => (r.f = true))
} else {
vnode.ref.f = true
}
}
}
}
export function registerLegacyRef(
refs: Data,
key: string,
value: any,
owner: ComponentInternalInstance,
isInFor: boolean | undefined,
isUnmount: boolean
) {
const existing = refs[key]
if (isUnmount) {
if (isArray(existing)) {
remove(existing, value)
} else {
refs[key] = null
}
} else if (isInFor) {
__DEV__ && warnDeprecation(DeprecationTypes.V_FOR_REF, owner)
if (!isArray(existing)) {
refs[key] = [value]
} else if (!existing.includes(value)) {
existing.push(value)
}
} else {
refs[key] = value
}
}

View File

@ -0,0 +1,344 @@
import {
extend,
hyphenate,
isArray,
isObject,
isString,
makeMap,
normalizeClass,
normalizeStyle,
ShapeFlags,
toHandlerKey
} from '@vue/shared'
import {
Component,
ComponentInternalInstance,
ComponentOptions,
Data,
InternalRenderFunction
} from '../component'
import { currentRenderingInstance } from '../componentRenderContext'
import { DirectiveArguments, withDirectives } from '../directives'
import {
resolveDirective,
resolveDynamicComponent
} from '../helpers/resolveAssets'
import {
Comment,
createVNode,
isVNode,
normalizeChildren,
VNode,
VNodeArrayChildren,
VNodeProps
} from '../vnode'
import {
checkCompatEnabled,
DeprecationTypes,
isCompatEnabled
} from './compatConfig'
import { compatModelEventPrefix } from './vModel'
const v3CompiledRenderFnRE = /^(?:function \w+)?\(_ctx, _cache/
export function convertLegacyRenderFn(instance: ComponentInternalInstance) {
const Component = instance.type as ComponentOptions
const render = Component.render as InternalRenderFunction | undefined
// v3 runtime compiled, or already checked / wrapped
if (!render || render._rc || render._compatChecked || render._compatWrapped) {
return
}
if (v3CompiledRenderFnRE.test(render.toString())) {
// v3 pre-compiled function
render._compatChecked = true
return
}
// v2 render function, try to provide compat
if (checkCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)) {
const wrapped = (Component.render = function compatRender() {
// @ts-ignore
return render.call(this, compatH)
})
// @ts-ignore
wrapped._compatWrapped = true
}
}
interface LegacyVNodeProps {
key?: string | number
ref?: string
refInFor?: boolean
staticClass?: string
class?: unknown
staticStyle?: Record<string, unknown>
style?: Record<string, unknown>
attrs?: Record<string, unknown>
domProps?: Record<string, unknown>
on?: Record<string, Function | Function[]>
nativeOn?: Record<string, Function | Function[]>
directives?: LegacyVNodeDirective[]
// component only
props?: Record<string, unknown>
slot?: string
scopedSlots?: Record<string, Function>
model?: {
value: any
callback: (v: any) => void
expression: string
}
}
interface LegacyVNodeDirective {
name: string
value: unknown
arg?: string
modifiers?: Record<string, boolean>
}
type LegacyVNodeChildren =
| string
| number
| boolean
| VNode
| VNodeArrayChildren
export function compatH(
type: string | Component,
children?: LegacyVNodeChildren
): VNode
export function compatH(
type: string | Component,
props?: LegacyVNodeProps,
children?: LegacyVNodeChildren
): VNode
export function compatH(
type: any,
propsOrChildren?: any,
children?: any
): VNode {
if (!type) {
type = Comment
}
// to support v2 string component name look!up
if (typeof type === 'string') {
const t = hyphenate(type)
if (t === 'transition' || t === 'transition-group' || t === 'keep-alive') {
// since transition and transition-group are runtime-dom-specific,
// we cannot import them directly here. Instead they are registered using
// special keys in @vue/compat entry.
type = `__compat__${t}`
}
type = resolveDynamicComponent(type)
}
const l = arguments.length
const is2ndArgArrayChildren = isArray(propsOrChildren)
if (l === 2 || is2ndArgArrayChildren) {
if (isObject(propsOrChildren) && !is2ndArgArrayChildren) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return convertLegacySlots(createVNode(type, null, [propsOrChildren]))
}
// props without children
return convertLegacySlots(
convertLegacyDirectives(
createVNode(type, convertLegacyProps(propsOrChildren, type)),
propsOrChildren
)
)
} else {
// omit props
return convertLegacySlots(createVNode(type, null, propsOrChildren))
}
} else {
if (isVNode(children)) {
children = [children]
}
return convertLegacySlots(
convertLegacyDirectives(
createVNode(type, convertLegacyProps(propsOrChildren, type), children),
propsOrChildren
)
)
}
}
const skipLegacyRootLevelProps = /*#__PURE__*/ makeMap(
'staticStyle,staticClass,directives,model,hook'
)
function convertLegacyProps(
legacyProps: LegacyVNodeProps | undefined,
type: any
): Data & VNodeProps | null {
if (!legacyProps) {
return null
}
const converted: Data & VNodeProps = {}
for (const key in legacyProps) {
if (key === 'attrs' || key === 'domProps' || key === 'props') {
extend(converted, legacyProps[key])
} else if (key === 'on' || key === 'nativeOn') {
const listeners = legacyProps[key]
for (const event in listeners) {
const handlerKey = convertLegacyEventKey(event)
const existing = converted[handlerKey]
const incoming = listeners[event]
if (existing !== incoming) {
if (existing) {
// for the rare case where the same handler is attached
// twice with/without .native modifier...
if (key === 'nativeOn' && String(existing) === String(incoming)) {
continue
}
converted[handlerKey] = [].concat(existing as any, incoming as any)
} else {
converted[handlerKey] = incoming
}
}
}
} else if (!skipLegacyRootLevelProps(key)) {
converted[key] = legacyProps[key as keyof LegacyVNodeProps]
}
}
if (legacyProps.staticClass) {
converted.class = normalizeClass([legacyProps.staticClass, converted.class])
}
if (legacyProps.staticStyle) {
converted.style = normalizeStyle([legacyProps.staticStyle, converted.style])
}
if (legacyProps.model && isObject(type)) {
// v2 compiled component v-model
const { prop = 'value', event = 'input' } = (type as any).model || {}
converted[prop] = legacyProps.model.value
converted[compatModelEventPrefix + event] = legacyProps.model.callback
}
return converted
}
function convertLegacyEventKey(event: string): string {
// normalize v2 event prefixes
if (event[0] === '&') {
event = event.slice(1) + 'Passive'
}
if (event[0] === '~') {
event = event.slice(1) + 'Once'
}
if (event[0] === '!') {
event = event.slice(1) + 'Capture'
}
return toHandlerKey(event)
}
function convertLegacyDirectives(
vnode: VNode,
props?: LegacyVNodeProps
): VNode {
if (props && props.directives) {
return withDirectives(
vnode,
props.directives.map(({ name, value, arg, modifiers }) => {
return [
resolveDirective(name)!,
value,
arg,
modifiers
] as DirectiveArguments[number]
})
)
}
return vnode
}
function convertLegacySlots(vnode: VNode): VNode {
const { props, children } = vnode
let slots: Record<string, any> | undefined
if (vnode.shapeFlag & ShapeFlags.COMPONENT && isArray(children)) {
slots = {}
// check "slot" property on vnodes and turn them into v3 function slots
for (let i = 0; i < children.length; i++) {
const child = children[i]
const slotName =
(isVNode(child) && child.props && child.props.slot) || 'default'
const slot = slots[slotName] || (slots[slotName] = [] as any[])
if (isVNode(child) && child.type === 'template') {
slot.push(child.children)
} else {
slot.push(child)
}
}
if (slots) {
for (const key in slots) {
const slotChildren = slots[key]
slots[key] = () => slotChildren
}
}
}
const scopedSlots = props && props.scopedSlots
if (scopedSlots) {
delete props!.scopedSlots
if (slots) {
extend(slots, scopedSlots)
} else {
slots = scopedSlots
}
}
if (slots) {
normalizeChildren(vnode, slots)
}
return vnode
}
export function defineLegacyVNodeProperties(vnode: VNode) {
if (
isCompatEnabled(
DeprecationTypes.RENDER_FUNCTION,
currentRenderingInstance
) &&
isCompatEnabled(DeprecationTypes.PRIVATE_APIS, currentRenderingInstance)
) {
const context = currentRenderingInstance
const getInstance = () => vnode.component && vnode.component.proxy
let componentOptions: any
Object.defineProperties(vnode, {
tag: { get: () => vnode.type },
data: { get: () => vnode.props, set: p => (vnode.props = p) },
elm: { get: () => vnode.el },
componentInstance: { get: getInstance },
child: { get: getInstance },
text: { get: () => (isString(vnode.children) ? vnode.children : null) },
context: { get: () => context && context.proxy },
componentOptions: {
get: () => {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
if (componentOptions) {
return componentOptions
}
return (componentOptions = {
Ctor: vnode.type,
propsData: vnode.props,
children: vnode.children
})
}
}
}
})
}
}

View File

@ -0,0 +1,182 @@
import {
camelize,
extend,
hyphenate,
isArray,
isObject,
isReservedProp,
normalizeClass
} from '@vue/shared'
import { ComponentInternalInstance } from '../component'
import { Slot } from '../componentSlots'
import { createSlots } from '../helpers/createSlots'
import { renderSlot } from '../helpers/renderSlot'
import { toHandlers } from '../helpers/toHandlers'
import { mergeProps, VNode } from '../vnode'
function toObject(arr: Array<any>): Object {
const res = {}
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}
export function legacyBindObjectProps(
data: any,
_tag: string,
value: any,
_asProp: boolean,
isSync?: boolean
) {
if (value && isObject(value)) {
if (isArray(value)) {
value = toObject(value)
}
for (const key in value) {
if (isReservedProp(key)) {
data[key] = value[key]
} else if (key === 'class') {
data.class = normalizeClass([data.class, value.class])
} else if (key === 'style') {
data.style = normalizeClass([data.style, value.style])
} else {
const attrs = data.attrs || (data.attrs = {})
const camelizedKey = camelize(key)
const hyphenatedKey = hyphenate(key)
if (!(camelizedKey in attrs) && !(hyphenatedKey in attrs)) {
attrs[key] = value[key]
if (isSync) {
const on = data.on || (data.on = {})
on[`update:${key}`] = function($event: any) {
value[key] = $event
}
}
}
}
}
}
return data
}
export function legacyBindObjectListeners(props: any, listeners: any) {
return mergeProps(props, toHandlers(listeners))
}
export function legacyRenderSlot(
instance: ComponentInternalInstance,
name: string,
fallback?: VNode[],
props?: any,
bindObject?: any
) {
if (bindObject) {
props = mergeProps(props, bindObject)
}
return renderSlot(instance.slots, name, props, fallback && (() => fallback))
}
type LegacyScopedSlotsData = Array<
| {
key: string
fn: Function
}
| LegacyScopedSlotsData
>
export function legacyresolveScopedSlots(
fns: LegacyScopedSlotsData,
raw?: Record<string, Slot>,
// the following are added in 2.6
hasDynamicKeys?: boolean
) {
// v2 default slot doesn't have name
return createSlots(
raw || ({ $stable: !hasDynamicKeys } as any),
mapKeyToName(fns)
)
}
function mapKeyToName(slots: LegacyScopedSlotsData) {
for (let i = 0; i < slots.length; i++) {
const fn = slots[i]
if (fn) {
if (isArray(fn)) {
mapKeyToName(fn)
} else {
;(fn as any).name = fn.key || 'default'
}
}
}
return slots as any
}
const staticCacheMap = /*#__PURE__*/ new WeakMap<
ComponentInternalInstance,
any[]
>()
export function legacyRenderStatic(
instance: ComponentInternalInstance,
index: number
) {
let cache = staticCacheMap.get(instance)
if (!cache) {
staticCacheMap.set(instance, (cache = []))
}
if (cache[index]) {
return cache[index]
}
const fn = (instance.type as any).staticRenderFns[index]
const ctx = instance.proxy
return (cache[index] = fn.call(ctx, null, ctx))
}
export function legacyCheckKeyCodes(
instance: ComponentInternalInstance,
eventKeyCode: number,
key: string,
builtInKeyCode?: number | number[],
eventKeyName?: string,
builtInKeyName?: string | string[]
) {
const config = instance.appContext.config as any
const configKeyCodes = config.keyCodes || {}
const mappedKeyCode = configKeyCodes[key] || builtInKeyCode
if (builtInKeyName && eventKeyName && !configKeyCodes[key]) {
return isKeyNotMatch(builtInKeyName, eventKeyName)
} else if (mappedKeyCode) {
return isKeyNotMatch(mappedKeyCode, eventKeyCode)
} else if (eventKeyName) {
return hyphenate(eventKeyName) !== key
}
}
function isKeyNotMatch<T>(expect: T | T[], actual: T): boolean {
if (isArray(expect)) {
return expect.indexOf(actual) === -1
} else {
return expect !== actual
}
}
export function legacyMarkOnce(tree: VNode) {
return tree
}
export function legacyBindDynamicKeys(props: any, values: any[]) {
for (let i = 0; i < values.length; i += 2) {
const key = values[i]
if (typeof key === 'string' && key) {
props[values[i]] = values[i + 1]
}
}
return props
}
export function legacyPrependModifier(value: any, symbol: string) {
return typeof value === 'string' ? symbol + value : value
}

View File

@ -0,0 +1,71 @@
import { ShapeFlags } from '@vue/shared'
import { ComponentInternalInstance, ComponentOptions } from '../component'
import { callWithErrorHandling, ErrorCodes } from '../errorHandling'
import { VNode } from '../vnode'
import { popWarningContext, pushWarningContext } from '../warning'
import {
DeprecationTypes,
warnDeprecation,
isCompatEnabled
} from './compatConfig'
export const compatModelEventPrefix = `onModelCompat:`
const warnedTypes = new WeakSet()
export function convertLegacyVModelProps(vnode: VNode) {
const { type, shapeFlag, props, dynamicProps } = vnode
if (shapeFlag & ShapeFlags.COMPONENT && props && 'modelValue' in props) {
if (
!isCompatEnabled(
DeprecationTypes.COMPONENT_V_MODEL,
// this is a special case where we want to use the vnode component's
// compat config instead of the current rendering instance (which is the
// parent of the component that exposes v-model)
{ type } as any
)
) {
return
}
if (__DEV__ && !warnedTypes.has(type as ComponentOptions)) {
pushWarningContext(vnode)
warnDeprecation(DeprecationTypes.COMPONENT_V_MODEL, { type } as any, type)
popWarningContext()
warnedTypes.add(type as ComponentOptions)
}
// v3 compiled model code -> v2 compat props
// modelValue -> value
// onUpdate:modelValue -> onModelCompat:input
const { prop = 'value', event = 'input' } = (type as any).model || {}
props[prop] = props.modelValue
delete props.modelValue
// important: update dynamic props
if (dynamicProps) {
dynamicProps[dynamicProps.indexOf('modelValue')] = prop
}
props[compatModelEventPrefix + event] = props['onUpdate:modelValue']
delete props['onUpdate:modelValue']
}
}
export function compatModelEmit(
instance: ComponentInternalInstance,
event: string,
args: any[]
) {
if (!isCompatEnabled(DeprecationTypes.COMPONENT_V_MODEL, instance)) {
return
}
const props = instance.vnode.props
const modelHandler = props && props[compatModelEventPrefix + event]
if (modelHandler) {
callWithErrorHandling(
modelHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}

View File

@ -47,13 +47,16 @@ import {
NO, NO,
makeMap, makeMap,
isPromise, isPromise,
ShapeFlags ShapeFlags,
extend
} from '@vue/shared' } from '@vue/shared'
import { SuspenseBoundary } from './components/Suspense' import { SuspenseBoundary } from './components/Suspense'
import { CompilerOptions } from '@vue/compiler-core' import { CompilerOptions } from '@vue/compiler-core'
import { markAttrsAccessed } from './componentRenderUtils' import { markAttrsAccessed } from './componentRenderUtils'
import { currentRenderingInstance } from './componentRenderContext' import { currentRenderingInstance } from './componentRenderContext'
import { startMeasure, endMeasure } from './profiling' import { startMeasure, endMeasure } from './profiling'
import { convertLegacyRenderFn } from './compat/renderFn'
import { globalCompatConfig, validateCompatConfig } from './compat/compatConfig'
export type Data = Record<string, unknown> export type Data = Record<string, unknown>
@ -93,6 +96,10 @@ export interface ComponentInternalOptions {
* @internal * @internal
*/ */
__hmrId?: string __hmrId?: string
/**
* Compat build only, for bailing out of certain compatibility behavior
*/
__isBuiltIn?: boolean
/** /**
* This one should be exposed so that devtools can make use of it * This one should be exposed so that devtools can make use of it
*/ */
@ -185,6 +192,10 @@ export type InternalRenderFunction = {
$options: ComponentInternalInstance['ctx'] $options: ComponentInternalInstance['ctx']
): VNodeChild ): VNodeChild
_rc?: boolean // isRuntimeCompiled _rc?: boolean // isRuntimeCompiled
// __COMPAT__ only
_compatChecked?: boolean // v3 and already checked for v2 compat
_compatWrapped?: boolean // is wrapped for v2 compat
} }
/** /**
@ -257,6 +268,11 @@ export interface ComponentInternalInstance {
* @internal * @internal
*/ */
directives: Record<string, Directive> | null directives: Record<string, Directive> | null
/**
* Resolved filters registry, v2 compat only
* @internal
*/
filters?: Record<string, Function>
/** /**
* resolved props options * resolved props options
* @internal * @internal
@ -562,6 +578,13 @@ function setupStatefulComponent(
validateDirectiveName(names[i]) validateDirectiveName(names[i])
} }
} }
if (Component.compilerOptions && isRuntimeOnly()) {
warn(
`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`
)
}
} }
// 0. create render proxy property access cache // 0. create render proxy property access cache
instance.accessCache = Object.create(null) instance.accessCache = Object.create(null)
@ -674,12 +697,21 @@ export function registerRuntimeCompiler(_compile: any) {
compile = _compile compile = _compile
} }
function finishComponentSetup( export function finishComponentSetup(
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
isSSR: boolean isSSR: boolean,
skipOptions?: boolean
) { ) {
const Component = instance.type as ComponentOptions const Component = instance.type as ComponentOptions
if (__COMPAT__) {
convertLegacyRenderFn(instance)
if (__DEV__ && Component.compatConfig) {
validateCompatConfig(Component.compatConfig)
}
}
// template / render function normalization // template / render function normalization
if (__NODE_JS__ && isSSR) { if (__NODE_JS__ && isSSR) {
// 1. the render function may already exist, returned by `setup` // 1. the render function may already exist, returned by `setup`
@ -692,18 +724,44 @@ function finishComponentSetup(
NOOP) as InternalRenderFunction NOOP) as InternalRenderFunction
} else if (!instance.render) { } else if (!instance.render) {
// could be set from setup() // could be set from setup()
if (compile && Component.template && !Component.render) { if (compile && !Component.render) {
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template
if (template) {
if (__DEV__) { if (__DEV__) {
startMeasure(instance, `compile`) startMeasure(instance, `compile`)
} }
Component.render = compile(Component.template, { const { isCustomElement, compilerOptions } = instance.appContext.config
isCustomElement: instance.appContext.config.isCustomElement, const {
delimiters: Component.delimiters delimiters,
}) compilerOptions: componentCompilerOptions
} = Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
if (__COMPAT__) {
// pass runtime compat config into the compiler
finalCompilerOptions.compatConfig = Object.create(globalCompatConfig)
if (Component.compatConfig) {
extend(finalCompilerOptions.compatConfig, Component.compatConfig)
}
}
Component.render = compile(template, finalCompilerOptions)
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `compile`) endMeasure(instance, `compile`)
} }
} }
}
instance.render = (Component.render || NOOP) as InternalRenderFunction instance.render = (Component.render || NOOP) as InternalRenderFunction
@ -719,7 +777,7 @@ function finishComponentSetup(
} }
// support for 2.x options // support for 2.x options
if (__FEATURE_OPTIONS_API__) { if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
currentInstance = instance currentInstance = instance
pauseTracking() pauseTracking()
applyOptions(instance, Component) applyOptions(instance, Component)

View File

@ -21,6 +21,8 @@ import { warn } from './warning'
import { UnionToIntersection } from './helpers/typeUtils' import { UnionToIntersection } from './helpers/typeUtils'
import { devtoolsComponentEmit } from './devtools' import { devtoolsComponentEmit } from './devtools'
import { AppContext } from './apiCreateApp' import { AppContext } from './apiCreateApp'
import { emit as compatInstanceEmit } from './compat/instanceEventEmitter'
import { compatModelEventPrefix, compatModelEmit } from './compat/vModel'
export type ObjectEmitsOptions = Record< export type ObjectEmitsOptions = Record<
string, string,
@ -56,7 +58,14 @@ export function emit(
propsOptions: [propsOptions] propsOptions: [propsOptions]
} = instance } = instance
if (emitsOptions) { if (emitsOptions) {
if (!(event in emitsOptions)) { if (
!(event in emitsOptions) &&
!(
__COMPAT__ &&
(event.startsWith('hook:') ||
event.startsWith(compatModelEventPrefix))
)
) {
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) { if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
warn( warn(
`Component emitted event "${event}" but it is neither declared in ` + `Component emitted event "${event}" but it is neither declared in ` +
@ -148,6 +157,11 @@ export function emit(
args args
) )
} }
if (__COMPAT__) {
compatModelEmit(instance, event, args)
return compatInstanceEmit(instance, event, args)
}
} }
export function normalizeEmitsOptions( export function normalizeEmitsOptions(
@ -205,6 +219,11 @@ export function isEmitListener(
if (!options || !isOn(key)) { if (!options || !isOn(key)) {
return false return false
} }
if (__COMPAT__ && key.startsWith(compatModelEventPrefix)) {
return true
}
key = key.slice(2).replace(/Once$/, '') key = key.slice(2).replace(/Once$/, '')
return ( return (
hasOwn(options, key[0].toLowerCase() + key.slice(1)) || hasOwn(options, key[0].toLowerCase() + key.slice(1)) ||

View File

@ -65,6 +65,19 @@ import { warn } from './warning'
import { VNodeChild } from './vnode' import { VNodeChild } from './vnode'
import { callWithAsyncErrorHandling } from './errorHandling' import { callWithAsyncErrorHandling } from './errorHandling'
import { UnionToIntersection } from './helpers/typeUtils' import { UnionToIntersection } from './helpers/typeUtils'
import { deepMergeData } from './compat/data'
import { DeprecationTypes } from './compat/compatConfig'
import {
CompatConfig,
isCompatEnabled,
softAssertCompatEnabled
} from './compat/compatConfig'
import {
AssetTypes,
COMPONENTS,
DIRECTIVES,
FILTERS
} from './helpers/resolveAssets'
/** /**
* Interface for declaring custom options. * Interface for declaring custom options.
@ -137,6 +150,9 @@ export interface ComponentOptionsBase<
expose?: string[] expose?: string[]
serverPrefetch?(): Promise<any> serverPrefetch?(): Promise<any>
// Runtime compiler only -----------------------------------------------------
compilerOptions?: RuntimeCompilerOptions
// Internal ------------------------------------------------------------------ // Internal ------------------------------------------------------------------
/** /**
@ -190,6 +206,16 @@ export interface ComponentOptionsBase<
__defaults?: Defaults __defaults?: Defaults
} }
/**
* Subset of compiler options that makes sense for the runtime.
*/
export interface RuntimeCompilerOptions {
isCustomElement?: (tag: string) => boolean
whitespace?: 'preserve' | 'condense'
comments?: boolean
delimiters?: [string, string]
}
export type ComponentOptionsWithoutProps< export type ComponentOptionsWithoutProps<
Props = {}, Props = {},
RawBindings = {}, RawBindings = {},
@ -347,10 +373,11 @@ export type ExtractComputedReturns<T extends any> = {
: T[key] extends (...args: any[]) => infer TReturn ? TReturn : never : T[key] extends (...args: any[]) => infer TReturn ? TReturn : never
} }
type WatchOptionItem = export type ObjectWatchOptionItem = {
| string handler: WatchCallback | string
| WatchCallback } & WatchOptions
| { handler: WatchCallback | string } & WatchOptions
type WatchOptionItem = string | WatchCallback | ObjectWatchOptionItem
type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[] type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
@ -371,6 +398,8 @@ interface LegacyOptions<
Mixin extends ComponentOptionsMixin, Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin Extends extends ComponentOptionsMixin
> { > {
compatConfig?: CompatConfig
// allow any custom options // allow any custom options
[key: string]: any [key: string]: any
@ -404,6 +433,9 @@ interface LegacyOptions<
provide?: Data | Function provide?: Data | Function
inject?: ComponentInjectOptions inject?: ComponentInjectOptions
// assets
filters?: Record<string, Function>
// composition // composition
mixins?: Mixin[] mixins?: Mixin[]
extends?: Extends extends?: Extends
@ -427,7 +459,10 @@ interface LegacyOptions<
renderTriggered?: DebuggerHook renderTriggered?: DebuggerHook
errorCaptured?: ErrorCapturedHook errorCaptured?: ErrorCapturedHook
// runtime compile only /**
* runtime compile only
* @deprecated use `compilerOptions.delimiters` instead.
*/
delimiters?: [string, string] delimiters?: [string, string]
/** /**
@ -490,6 +525,10 @@ export function applyOptions(
deferredProvide: (Data | Function)[] = [], deferredProvide: (Data | Function)[] = [],
asMixin: boolean = false asMixin: boolean = false
) { ) {
if (__COMPAT__ && isFunction(options)) {
options = options.options
}
const { const {
// composition // composition
mixins, mixins,
@ -501,9 +540,6 @@ export function applyOptions(
watch: watchOptions, watch: watchOptions,
provide: provideOptions, provide: provideOptions,
inject: injectOptions, inject: injectOptions,
// assets
components,
directives,
// lifecycle // lifecycle
beforeMount, beforeMount,
mounted, mounted,
@ -588,31 +624,7 @@ export function applyOptions(
// - watch (deferred since it relies on `this` access) // - watch (deferred since it relies on `this` access)
if (injectOptions) { if (injectOptions) {
if (isArray(injectOptions)) { resolveInjections(injectOptions, ctx, checkDuplicateProperties)
for (let i = 0; i < injectOptions.length; i++) {
const key = injectOptions[i]
ctx[key] = inject(key)
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
} else {
for (const key in injectOptions) {
const opt = injectOptions[key]
if (isObject(opt)) {
ctx[key] = inject(
opt.from || key,
opt.default,
true /* treat default function as factory */
)
} else {
ctx[key] = inject(opt)
}
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
}
} }
if (methods) { if (methods) {
@ -736,25 +748,10 @@ export function applyOptions(
// To reduce memory usage, only components with mixins or extends will have // To reduce memory usage, only components with mixins or extends will have
// resolved asset registry attached to instance. // resolved asset registry attached to instance.
if (asMixin) { if (asMixin) {
if (components) { resolveInstanceAssets(instance, options, COMPONENTS)
extend( resolveInstanceAssets(instance, options, DIRECTIVES)
instance.components || if (__COMPAT__ && isCompatEnabled(DeprecationTypes.FILTERS, instance)) {
(instance.components = extend( resolveInstanceAssets(instance, options, FILTERS)
{},
(instance.type as ComponentOptions).components
) as Record<string, ConcreteComponent>),
components
)
}
if (directives) {
extend(
instance.directives ||
(instance.directives = extend(
{},
(instance.type as ComponentOptions).directives
)),
directives
)
} }
} }
@ -795,19 +792,28 @@ export function applyOptions(
if (renderTriggered) { if (renderTriggered) {
onRenderTriggered(renderTriggered.bind(publicThis)) onRenderTriggered(renderTriggered.bind(publicThis))
} }
if (__DEV__ && beforeDestroy) {
warn(`\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`)
}
if (beforeUnmount) { if (beforeUnmount) {
onBeforeUnmount(beforeUnmount.bind(publicThis)) onBeforeUnmount(beforeUnmount.bind(publicThis))
} }
if (__DEV__ && destroyed) {
warn(`\`destroyed\` has been renamed to \`unmounted\`.`)
}
if (unmounted) { if (unmounted) {
onUnmounted(unmounted.bind(publicThis)) onUnmounted(unmounted.bind(publicThis))
} }
if (__COMPAT__) {
if (
beforeDestroy &&
softAssertCompatEnabled(DeprecationTypes.OPTIONS_BEFORE_DESTROY, instance)
) {
onBeforeUnmount(beforeDestroy.bind(publicThis))
}
if (
destroyed &&
softAssertCompatEnabled(DeprecationTypes.OPTIONS_DESTROYED, instance)
) {
onUnmounted(destroyed.bind(publicThis))
}
}
if (isArray(expose)) { if (isArray(expose)) {
if (!asMixin) { if (!asMixin) {
if (expose.length) { if (expose.length) {
@ -824,6 +830,55 @@ export function applyOptions(
} }
} }
function resolveInstanceAssets(
instance: ComponentInternalInstance,
mixin: ComponentOptions,
type: AssetTypes
) {
if (mixin[type]) {
extend(
instance[type] ||
(instance[type] = extend(
{},
(instance.type as ComponentOptions)[type]
) as any),
mixin[type]
)
}
}
export function resolveInjections(
injectOptions: ComponentInjectOptions,
ctx: any,
checkDuplicateProperties = NOOP as any
) {
if (isArray(injectOptions)) {
for (let i = 0; i < injectOptions.length; i++) {
const key = injectOptions[i]
ctx[key] = inject(key)
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
} else {
for (const key in injectOptions) {
const opt = injectOptions[key]
if (isObject(opt)) {
ctx[key] = inject(
opt.from || key,
opt.default,
true /* treat default function as factory */
)
} else {
ctx[key] = inject(opt)
}
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
}
}
function callSyncHook( function callSyncHook(
name: 'beforeCreate' | 'created', name: 'beforeCreate' | 'created',
type: LifecycleHooks, type: LifecycleHooks,
@ -854,7 +909,13 @@ function callHookWithMixinAndExtends(
} }
} }
if (selfHook) { if (selfHook) {
callWithAsyncErrorHandling(selfHook.bind(instance.proxy!), instance, type) callWithAsyncErrorHandling(
__COMPAT__ && isArray(selfHook)
? selfHook.map(h => h.bind(instance.proxy!))
: selfHook.bind(instance.proxy!),
instance,
type
)
} }
} }
@ -904,11 +965,18 @@ function resolveData(
instance.data = reactive(data) instance.data = reactive(data)
} else { } else {
// existing data: this is a mixin or extends. // existing data: this is a mixin or extends.
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.OPTIONS_DATA_MERGE, instance)
) {
deepMergeData(instance.data, data, instance)
} else {
extend(instance.data, data) extend(instance.data, data)
} }
} }
}
function createWatcher( export function createWatcher(
raw: ComponentWatchOptionItem, raw: ComponentWatchOptionItem,
ctx: Data, ctx: Data,
publicThis: ComponentPublicInstance, publicThis: ComponentPublicInstance,
@ -958,19 +1026,30 @@ export function resolveMergedOptions(
return (raw.__merged = options) return (raw.__merged = options)
} }
function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) { export function mergeOptions(
const strats = instance.appContext.config.optionMergeStrategies to: any,
from: any,
instance?: ComponentInternalInstance | null,
strats = instance && instance.appContext.config.optionMergeStrategies
) {
if (__COMPAT__ && isFunction(from)) {
from = from.options
}
const { mixins, extends: extendsOptions } = from const { mixins, extends: extendsOptions } = from
extendsOptions && mergeOptions(to, extendsOptions, instance) extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
mixins && mixins &&
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance)) mixins.forEach((m: ComponentOptionsMixin) =>
mergeOptions(to, m, instance, strats)
)
for (const key in from) { for (const key in from) {
if (strats && hasOwn(strats, key)) { if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance.proxy, key) to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
} else { } else {
to[key] = from[key] to[key] = from[key]
} }
} }
return to
} }

View File

@ -33,6 +33,10 @@ import {
import { isEmitListener } from './componentEmits' import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode' import { InternalObjectKey } from './vnode'
import { AppContext } from './apiCreateApp' import { AppContext } from './apiCreateApp'
import { createPropsDefaultThis } from './compat/props'
import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
import { shouldSkipAttr } from './compat/attrsFallthrough'
export type ComponentPropsOptions<P = Data> = export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P> | ComponentObjectPropsOptions<P>
@ -184,6 +188,7 @@ export function updateProps(
} = instance } = instance
const rawCurrentProps = toRaw(props) const rawCurrentProps = toRaw(props)
const [options] = instance.propsOptions const [options] = instance.propsOptions
let hasAttrsChanged = false
if ( if (
// always force full diff in dev // always force full diff in dev
@ -209,7 +214,10 @@ export function updateProps(
// attr / props separation was done on init and will be consistent // attr / props separation was done on init and will be consistent
// in this code path, so just check if attrs have it. // in this code path, so just check if attrs have it.
if (hasOwn(attrs, key)) { if (hasOwn(attrs, key)) {
if (value !== attrs[key]) {
attrs[key] = value attrs[key] = value
hasAttrsChanged = true
}
} else { } else {
const camelizedKey = camelize(key) const camelizedKey = camelize(key)
props[camelizedKey] = resolvePropValue( props[camelizedKey] = resolvePropValue(
@ -221,13 +229,21 @@ export function updateProps(
) )
} }
} else { } else {
if (__COMPAT__ && shouldSkipAttr(key, attrs[key], instance)) {
continue
}
if (value !== attrs[key]) {
attrs[key] = value attrs[key] = value
hasAttrsChanged = true
}
} }
} }
} }
} else { } else {
// full props update. // full props update.
setFullProps(instance, rawProps, props, attrs) if (setFullProps(instance, rawProps, props, attrs)) {
hasAttrsChanged = true
}
// in case of dynamic props, check if we need to delete keys from // in case of dynamic props, check if we need to delete keys from
// the props object // the props object
let kebabKey: string let kebabKey: string
@ -267,13 +283,16 @@ export function updateProps(
for (const key in attrs) { for (const key in attrs) {
if (!rawProps || !hasOwn(rawProps, key)) { if (!rawProps || !hasOwn(rawProps, key)) {
delete attrs[key] delete attrs[key]
hasAttrsChanged = true
} }
} }
} }
} }
// trigger updates for $attrs in case it's used in component slots // trigger updates for $attrs in case it's used in component slots
if (hasAttrsChanged) {
trigger(instance, TriggerOpTypes.SET, '$attrs') trigger(instance, TriggerOpTypes.SET, '$attrs')
}
if (__DEV__) { if (__DEV__) {
validateProps(rawProps || {}, props, instance) validateProps(rawProps || {}, props, instance)
@ -287,12 +306,27 @@ function setFullProps(
attrs: Data attrs: Data
) { ) {
const [options, needCastKeys] = instance.propsOptions const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
if (rawProps) { if (rawProps) {
for (const key in rawProps) { for (const key in rawProps) {
// key, ref are reserved and never passed down // key, ref are reserved and never passed down
if (isReservedProp(key)) { if (isReservedProp(key)) {
continue continue
} }
if (__COMPAT__) {
if (key.startsWith('onHook:')) {
softAssertCompatEnabled(
DeprecationTypes.INSTANCE_EVENT_HOOKS,
instance,
key.slice(2).toLowerCase()
)
}
if (key === 'inline-template') {
continue
}
}
const value = rawProps[key] const value = rawProps[key]
// prop option names are camelized during normalization, so to support // prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key. // kebab -> camel conversion here we need to camelize the key.
@ -303,7 +337,13 @@ function setFullProps(
// Any non-declared (either as a prop or an emitted event) props are put // Any non-declared (either as a prop or an emitted event) props are put
// into a separate `attrs` object for spreading. Make sure to preserve // into a separate `attrs` object for spreading. Make sure to preserve
// original key casing // original key casing
if (__COMPAT__ && shouldSkipAttr(key, attrs[key], instance)) {
continue
}
if (value !== attrs[key]) {
attrs[key] = value attrs[key] = value
hasAttrsChanged = true
}
} }
} }
} }
@ -321,6 +361,8 @@ function setFullProps(
) )
} }
} }
return hasAttrsChanged
} }
function resolvePropValue( function resolvePropValue(
@ -342,7 +384,13 @@ function resolvePropValue(
value = propsDefaults[key] value = propsDefaults[key]
} else { } else {
setCurrentInstance(instance) setCurrentInstance(instance)
value = propsDefaults[key] = defaultValue(props) value = propsDefaults[key] = defaultValue.call(
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
? createPropsDefaultThis(instance, props, key)
: null,
props
)
setCurrentInstance(null) setCurrentInstance(null)
} }
} else { } else {
@ -381,6 +429,9 @@ export function normalizePropsOptions(
let hasExtends = false let hasExtends = false
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) { if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => { const extendProps = (raw: ComponentOptions) => {
if (__COMPAT__ && isFunction(raw)) {
raw = raw.options
}
hasExtends = true hasExtends = true
const [props, keys] = normalizePropsOptions(raw, appContext, true) const [props, keys] = normalizePropsOptions(raw, appContext, true)
extend(normalized, props) extend(normalized, props)

View File

@ -11,7 +11,8 @@ import {
isGloballyWhitelisted, isGloballyWhitelisted,
NOOP, NOOP,
extend, extend,
isString isString,
isFunction
} from '@vue/shared' } from '@vue/shared'
import { import {
ReactiveEffect, ReactiveEffect,
@ -40,6 +41,7 @@ import { markAttrsAccessed } from './componentRenderUtils'
import { currentRenderingInstance } from './componentRenderContext' import { currentRenderingInstance } from './componentRenderContext'
import { warn } from './warning' import { warn } from './warning'
import { UnionToIntersection } from './helpers/typeUtils' import { UnionToIntersection } from './helpers/typeUtils'
import { installCompatInstanceProperties } from './compat/instance'
/** /**
* Custom properties added to component instances in any way and can be accessed through `this` * Custom properties added to component instances in any way and can be accessed through `this`
@ -201,7 +203,10 @@ export type ComponentPublicInstance<
M & M &
ComponentCustomProperties ComponentCustomProperties
type PublicPropertiesMap = Record<string, (i: ComponentInternalInstance) => any> export type PublicPropertiesMap = Record<
string,
(i: ComponentInternalInstance) => any
>
/** /**
* #2437 In Vue 3, functional components do not have a public instance proxy but * #2437 In Vue 3, functional components do not have a public instance proxy but
@ -233,6 +238,10 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap) } as PublicPropertiesMap)
if (__COMPAT__) {
installCompatInstanceProperties(publicPropertiesMap)
}
const enum AccessTypes { const enum AccessTypes {
SETUP, SETUP,
DATA, DATA,
@ -335,7 +344,17 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
((globalProperties = appContext.config.globalProperties), ((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key)) hasOwn(globalProperties, key))
) { ) {
if (__COMPAT__) {
const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
if (desc.get) {
return desc.get.call(instance.proxy)
} else {
const val = globalProperties[key]
return isFunction(val) ? val.bind(instance.proxy) : val
}
} else {
return globalProperties[key] return globalProperties[key]
}
} else if ( } else if (
__DEV__ && __DEV__ &&
currentRenderingInstance && currentRenderingInstance &&
@ -483,17 +502,6 @@ export function createRenderContext(instance: ComponentInternalInstance) {
}) })
}) })
// expose global properties
const { globalProperties } = instance.appContext.config
Object.keys(globalProperties).forEach(key => {
Object.defineProperty(target, key, {
configurable: true,
enumerable: false,
get: () => globalProperties[key],
set: NOOP
})
})
return target as ComponentRenderContext return target as ComponentRenderContext
} }

View File

@ -25,6 +25,10 @@ export function setCurrentRenderingInstance(
const prev = currentRenderingInstance const prev = currentRenderingInstance
currentRenderingInstance = instance currentRenderingInstance = instance
currentScopeId = (instance && instance.type.__scopeId) || null currentScopeId = (instance && instance.type.__scopeId) || null
// v2 pre-compiled components uses _scopeId instead of __scopeId
if (__COMPAT__ && !currentScopeId) {
currentScopeId = (instance && (instance.type as any)._scopeId) || null
}
return prev return prev
} }

View File

@ -1,7 +1,8 @@
import { import {
ComponentInternalInstance, ComponentInternalInstance,
FunctionalComponent, FunctionalComponent,
Data Data,
getComponentName
} from './component' } from './component'
import { import {
VNode, VNode,
@ -20,6 +21,11 @@ import { isHmrUpdating } from './hmr'
import { NormalizedProps } from './componentProps' import { NormalizedProps } from './componentProps'
import { isEmitListener } from './componentEmits' import { isEmitListener } from './componentEmits'
import { setCurrentRenderingInstance } from './componentRenderContext' import { setCurrentRenderingInstance } from './componentRenderContext'
import {
DeprecationTypes,
isCompatEnabled,
warnDeprecation
} from './compat/compatConfig'
/** /**
* dev only flag to track whether $attrs was used during render. * dev only flag to track whether $attrs was used during render.
@ -117,7 +123,7 @@ export function renderComponentRoot(
;[root, setRoot] = getChildRoot(result) ;[root, setRoot] = getChildRoot(result)
} }
if (Component.inheritAttrs !== false && fallthroughAttrs) { if (fallthroughAttrs && Component.inheritAttrs !== false) {
const keys = Object.keys(fallthroughAttrs) const keys = Object.keys(fallthroughAttrs)
const { shapeFlag } = root const { shapeFlag } = root
if (keys.length) { if (keys.length) {
@ -175,6 +181,29 @@ export function renderComponentRoot(
} }
} }
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE, instance) &&
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT &&
(root.shapeFlag & ShapeFlags.ELEMENT ||
root.shapeFlag & ShapeFlags.COMPONENT)
) {
const { class: cls, style } = vnode.props || {}
if (cls || style) {
if (__DEV__ && Component.inheritAttrs === false) {
warnDeprecation(
DeprecationTypes.INSTANCE_ATTRS_CLASS_STYLE,
instance,
getComponentName(instance.type)
)
}
root = cloneVNode(root, {
class: cls,
style: style
})
}
}
// inherit directives // inherit directives
if (vnode.dirs) { if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) { if (__DEV__ && !isElementRoot(root)) {

View File

@ -19,6 +19,7 @@ import { warn } from './warning'
import { isKeepAlive } from './components/KeepAlive' import { isKeepAlive } from './components/KeepAlive'
import { withCtx } from './componentRenderContext' import { withCtx } from './componentRenderContext'
import { isHmrUpdating } from './hmr' import { isHmrUpdating } from './hmr'
import { DeprecationTypes, isCompatEnabled } from './compat/compatConfig'
export type Slot = (...args: any[]) => VNode[] export type Slot = (...args: any[]) => VNode[]
@ -72,7 +73,11 @@ const normalizeSlot = (
return normalizeSlotValue(rawSlot(props)) return normalizeSlotValue(rawSlot(props))
}, ctx) as Slot }, ctx) as Slot
const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => { const normalizeObjectSlots = (
rawSlots: RawSlots,
slots: InternalSlots,
instance: ComponentInternalInstance
) => {
const ctx = rawSlots._ctx const ctx = rawSlots._ctx
for (const key in rawSlots) { for (const key in rawSlots) {
if (isInternalKey(key)) continue if (isInternalKey(key)) continue
@ -80,7 +85,13 @@ const normalizeObjectSlots = (rawSlots: RawSlots, slots: InternalSlots) => {
if (isFunction(value)) { if (isFunction(value)) {
slots[key] = normalizeSlot(key, value, ctx) slots[key] = normalizeSlot(key, value, ctx)
} else if (value != null) { } else if (value != null) {
if (__DEV__) { if (
__DEV__ &&
!(
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance)
)
) {
warn( warn(
`Non-function value encountered for slot "${key}". ` + `Non-function value encountered for slot "${key}". ` +
`Prefer function slots for better performance.` `Prefer function slots for better performance.`
@ -96,7 +107,11 @@ const normalizeVNodeSlots = (
instance: ComponentInternalInstance, instance: ComponentInternalInstance,
children: VNodeNormalizedChildren children: VNodeNormalizedChildren
) => { ) => {
if (__DEV__ && !isKeepAlive(instance.vnode)) { if (
__DEV__ &&
!isKeepAlive(instance.vnode) &&
!(__COMPAT__ && isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, instance))
) {
warn( warn(
`Non-function value encountered for default slot. ` + `Non-function value encountered for default slot. ` +
`Prefer function slots for better performance.` `Prefer function slots for better performance.`
@ -117,7 +132,11 @@ export const initSlots = (
// make compiler marker non-enumerable // make compiler marker non-enumerable
def(children as InternalSlots, '_', type) def(children as InternalSlots, '_', type)
} else { } else {
normalizeObjectSlots(children as RawSlots, (instance.slots = {})) normalizeObjectSlots(
children as RawSlots,
(instance.slots = {}),
instance
)
} }
} else { } else {
instance.slots = {} instance.slots = {}
@ -162,7 +181,7 @@ export const updateSlots = (
} }
} else { } else {
needDeletionCheck = !(children as RawSlots).$stable needDeletionCheck = !(children as RawSlots).$stable
normalizeObjectSlots(children as RawSlots, slots) normalizeObjectSlots(children as RawSlots, slots, instance)
} }
deletionComparisonTarget = children as RawSlots deletionComparisonTarget = children as RawSlots
} else if (children) { } else if (children) {

View File

@ -1,7 +1,8 @@
import { import {
getCurrentInstance, getCurrentInstance,
SetupContext, SetupContext,
ComponentInternalInstance ComponentInternalInstance,
ComponentOptions
} from '../component' } from '../component'
import { import {
cloneVNode, cloneVNode,
@ -110,7 +111,7 @@ export function useTransitionState(): TransitionState {
const TransitionHookValidator = [Function, Array] const TransitionHookValidator = [Function, Array]
const BaseTransitionImpl = { const BaseTransitionImpl: ComponentOptions = {
name: `BaseTransition`, name: `BaseTransition`,
props: { props: {
@ -250,6 +251,10 @@ const BaseTransitionImpl = {
} }
} }
if (__COMPAT__) {
BaseTransitionImpl.__isBuiltIn = true
}
// export the public type for h/tsx inference // export the public type for h/tsx inference
// also to avoid inline import() in generated d.ts files // also to avoid inline import() in generated d.ts files
export const BaseTransition = (BaseTransitionImpl as any) as { export const BaseTransition = (BaseTransitionImpl as any) as {

View File

@ -5,7 +5,8 @@ import {
ComponentInternalInstance, ComponentInternalInstance,
LifecycleHooks, LifecycleHooks,
currentInstance, currentInstance,
getComponentName getComponentName,
ComponentOptions
} from '../component' } from '../component'
import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode' import { VNode, cloneVNode, isVNode, VNodeProps } from '../vnode'
import { warn } from '../warning' import { warn } from '../warning'
@ -63,7 +64,7 @@ export interface KeepAliveContext extends ComponentRenderContext {
export const isKeepAlive = (vnode: VNode): boolean => export const isKeepAlive = (vnode: VNode): boolean =>
(vnode.type as any).__isKeepAlive (vnode.type as any).__isKeepAlive
const KeepAliveImpl = { const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`, name: `KeepAlive`,
// Marker for special handling inside the renderer. We are not using a === // Marker for special handling inside the renderer. We are not using a ===
@ -313,6 +314,10 @@ const KeepAliveImpl = {
} }
} }
if (__COMPAT__) {
KeepAliveImpl.__isBuildIn = true
}
// export the public type for h/tsx inference // export the public type for h/tsx inference
// also to avoid inline import() in generated d.ts files // also to avoid inline import() in generated d.ts files
export const KeepAlive = (KeepAliveImpl as any) as { export const KeepAlive = (KeepAliveImpl as any) as {

View File

@ -18,6 +18,7 @@ import { ComponentInternalInstance, Data } from './component'
import { currentRenderingInstance } from './componentRenderContext' import { currentRenderingInstance } from './componentRenderContext'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling' import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { mapCompatDirectiveHook } from './compat/customDirective'
export interface DirectiveBinding<V = any> { export interface DirectiveBinding<V = any> {
instance: ComponentPublicInstance | null instance: ComponentPublicInstance | null
@ -124,7 +125,10 @@ export function invokeDirectiveHook(
if (oldBindings) { if (oldBindings) {
binding.oldValue = oldBindings[i].value binding.oldValue = oldBindings[i].value
} }
const hook = binding.dir[name] as DirectiveHook | undefined let hook = binding.dir[name] as DirectiveHook | DirectiveHook[] | undefined
if (__COMPAT__ && !hook) {
hook = mapCompatDirectiveHook(name, binding.dir, instance)
}
if (hook) { if (hook) {
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [ callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
vnode.el, vnode.el,

View File

@ -10,8 +10,11 @@ import { camelize, capitalize, isString } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
import { VNodeTypes } from '../vnode' import { VNodeTypes } from '../vnode'
const COMPONENTS = 'components' export const COMPONENTS = 'components'
const DIRECTIVES = 'directives' export const DIRECTIVES = 'directives'
export const FILTERS = 'filters'
export type AssetTypes = typeof COMPONENTS | typeof DIRECTIVES | typeof FILTERS
/** /**
* @private * @private
@ -44,6 +47,14 @@ export function resolveDirective(name: string): Directive | undefined {
return resolveAsset(DIRECTIVES, name) return resolveAsset(DIRECTIVES, name)
} }
/**
* v2 compat only
* @internal
*/
export function resolveFilter(name: string): Function | undefined {
return resolveAsset(FILTERS, name)
}
/** /**
* @private * @private
* overload 1: components * overload 1: components
@ -60,8 +71,11 @@ function resolveAsset(
name: string name: string
): Directive | undefined ): Directive | undefined
// implementation // implementation
// overload 3: filters (compat only)
function resolveAsset(type: typeof FILTERS, name: string): Function | undefined
// implementation
function resolveAsset( function resolveAsset(
type: typeof COMPONENTS | typeof DIRECTIVES, type: AssetTypes,
name: string, name: string,
warnMissing = true, warnMissing = true,
maybeSelfReference = false maybeSelfReference = false

View File

@ -179,7 +179,8 @@ export {
ComponentOptionsBase, ComponentOptionsBase,
RenderFunction, RenderFunction,
MethodOptions, MethodOptions,
ComputedOptions ComputedOptions,
RuntimeCompilerOptions
} from './componentOptions' } from './componentOptions'
export { EmitsOptions, ObjectEmitsOptions } from './componentEmits' export { EmitsOptions, ObjectEmitsOptions } from './componentEmits'
export { export {
@ -279,3 +280,38 @@ const _ssrUtils = {
* @internal * @internal
*/ */
export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils export const ssrUtils = (__NODE_JS__ ? _ssrUtils : null) as typeof _ssrUtils
// 2.x COMPAT ------------------------------------------------------------------
export { DeprecationTypes } from './compat/compatConfig'
export { CompatVue } from './compat/global'
export { LegacyConfig } from './compat/globalConfig'
import { warnDeprecation } from './compat/compatConfig'
import { createCompatVue } from './compat/global'
import {
isCompatEnabled,
checkCompatEnabled,
softAssertCompatEnabled
} from './compat/compatConfig'
import { resolveFilter as _resolveFilter } from './helpers/resolveAssets'
/**
* @internal only exposed in compat builds
*/
export const resolveFilter = __COMPAT__ ? _resolveFilter : null
const _compatUtils = {
warnDeprecation,
createCompatVue,
isCompatEnabled,
checkCompatEnabled,
softAssertCompatEnabled
}
/**
* @internal only exposed in compat builds.
*/
export const compatUtils = (__COMPAT__
? _compatUtils
: null) as typeof _compatUtils

View File

@ -76,7 +76,6 @@ import {
import { createHydrationFunctions, RootHydrateFunction } from './hydration' import { createHydrationFunctions, RootHydrateFunction } from './hydration'
import { invokeDirectiveHook } from './directives' import { invokeDirectiveHook } from './directives'
import { startMeasure, endMeasure } from './profiling' import { startMeasure, endMeasure } from './profiling'
import { ComponentPublicInstance } from './componentPublicInstance'
import { import {
devtoolsComponentAdded, devtoolsComponentAdded,
devtoolsComponentRemoved, devtoolsComponentRemoved,
@ -85,6 +84,9 @@ import {
} from './devtools' } from './devtools'
import { initFeatureFlags } from './featureFlags' import { initFeatureFlags } from './featureFlags'
import { isAsyncWrapper } from './apiAsyncComponent' import { isAsyncWrapper } from './apiAsyncComponent'
import { isCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
import { registerLegacyRef } from './compat/ref'
export interface Renderer<HostElement = RendererElement> { export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement> render: RootRenderFunction<HostElement>
@ -307,7 +309,8 @@ export const setRef = (
rawRef: VNodeNormalizedRef, rawRef: VNodeNormalizedRef,
oldRawRef: VNodeNormalizedRef | null, oldRawRef: VNodeNormalizedRef | null,
parentSuspense: SuspenseBoundary | null, parentSuspense: SuspenseBoundary | null,
vnode: VNode | null vnode: VNode,
isUnmount = false
) => { ) => {
if (isArray(rawRef)) { if (isArray(rawRef)) {
rawRef.forEach((r, i) => rawRef.forEach((r, i) =>
@ -315,26 +318,25 @@ export const setRef = (
r, r,
oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
parentSuspense, parentSuspense,
vnode vnode,
isUnmount
) )
) )
return return
} }
let value: ComponentPublicInstance | RendererNode | Record<string, any> | null if (isAsyncWrapper(vnode) && !isUnmount) {
if (!vnode) {
// means unmount
value = null
} else if (isAsyncWrapper(vnode)) {
// when mounting async components, nothing needs to be done, // when mounting async components, nothing needs to be done,
// because the template ref is forwarded to inner component // because the template ref is forwarded to inner component
return return
} else if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
value = vnode.component!.exposed || vnode.component!.proxy
} else {
value = vnode.el
} }
const refValue =
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
? vnode.component!.exposed || vnode.component!.proxy
: vnode.el
const value = isUnmount ? null : refValue
const { i: owner, r: ref } = rawRef const { i: owner, r: ref } = rawRef
if (__DEV__ && !owner) { if (__DEV__ && !owner) {
warn( warn(
@ -347,7 +349,7 @@ export const setRef = (
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const setupState = owner.setupState const setupState = owner.setupState
// unset old ref // dynamic ref changed. unset old ref
if (oldRef != null && oldRef !== ref) { if (oldRef != null && oldRef !== ref) {
if (isString(oldRef)) { if (isString(oldRef)) {
refs[oldRef] = null refs[oldRef] = null
@ -361,7 +363,11 @@ export const setRef = (
if (isString(ref)) { if (isString(ref)) {
const doSet = () => { const doSet = () => {
if (__COMPAT__ && isCompatEnabled(DeprecationTypes.V_FOR_REF, owner)) {
registerLegacyRef(refs, ref, refValue, owner, rawRef.f, isUnmount)
} else {
refs[ref] = value refs[ref] = value
}
if (hasOwn(setupState, ref)) { if (hasOwn(setupState, ref)) {
setupState[ref] = value setupState[ref] = value
} }
@ -440,6 +446,9 @@ function baseCreateRenderer(
options: RendererOptions, options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions createHydrationFns?: typeof createHydrationFunctions
): any { ): any {
const isHookEventCompatEnabled =
__COMPAT__ && isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, null)
// compile-time feature flags check // compile-time feature flags check
if (__ESM_BUNDLER__ && !__TEST__) { if (__ESM_BUNDLER__ && !__TEST__) {
initFeatureFlags() initFeatureFlags()
@ -579,7 +588,7 @@ function baseCreateRenderer(
// set ref // set ref
if (ref != null && parentComponent) { if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2) setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
} }
} }
@ -1292,7 +1301,12 @@ function baseCreateRenderer(
isSVG, isSVG,
optimized optimized
) => { ) => {
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( // 2.x compat may pre-creaate the component instance before actually
// mounting
const compatMountInstance = __COMPAT__ && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode, initialVNode,
parentComponent, parentComponent,
parentSuspense parentSuspense
@ -1313,6 +1327,7 @@ function baseCreateRenderer(
} }
// resolve props and slots for setup context // resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) { if (__DEV__) {
startMeasure(instance, `init`) startMeasure(instance, `init`)
} }
@ -1320,6 +1335,7 @@ function baseCreateRenderer(
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `init`) endMeasure(instance, `init`)
} }
}
// setup() is async. This component relies on async logic to be resolved // setup() is async. This component relies on async logic to be resolved
// before proceeding // before proceeding
@ -1410,6 +1426,9 @@ function baseCreateRenderer(
if ((vnodeHook = props && props.onVnodeBeforeMount)) { if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode) invokeVNodeHook(vnodeHook, parent, initialVNode)
} }
if (__COMPAT__ && isHookEventCompatEnabled) {
instance.emit('hook:beforeMount')
}
// render // render
if (__DEV__) { if (__DEV__) {
@ -1460,19 +1479,29 @@ function baseCreateRenderer(
// onVnodeMounted // onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) { if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode const scopedInitialVNode = initialVNode
queuePostRenderEffect(() => { queuePostRenderEffect(
invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode) () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
}, parentSuspense) parentSuspense
)
} }
if (__COMPAT__ && isHookEventCompatEnabled) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
parentSuspense
)
}
// activated hook for keep-alive roots. // activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render // #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive // since the hook may be injected by a child keep-alive
const { a } = instance if (initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
if ( instance.a && queuePostRenderEffect(instance.a, parentSuspense)
a && if (__COMPAT__ && isHookEventCompatEnabled) {
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE queuePostRenderEffect(
) { () => instance.emit('hook:activated'),
queuePostRenderEffect(a, parentSuspense) parentSuspense
)
}
} }
instance.isMounted = true instance.isMounted = true
@ -1508,6 +1537,9 @@ function baseCreateRenderer(
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) { if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode) invokeVNodeHook(vnodeHook, parent, next, vnode)
} }
if (__COMPAT__ && isHookEventCompatEnabled) {
instance.emit('hook:beforeUpdate')
}
// render // render
if (__DEV__) { if (__DEV__) {
@ -1550,9 +1582,16 @@ function baseCreateRenderer(
} }
// onVnodeUpdated // onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) { if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(() => { queuePostRenderEffect(
invokeVNodeHook(vnodeHook!, parent, next!, vnode) () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
}, parentSuspense) parentSuspense
)
}
if (__COMPAT__ && isHookEventCompatEnabled) {
queuePostRenderEffect(
() => instance.emit('hook:updated'),
parentSuspense
)
} }
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
@ -1564,6 +1603,11 @@ function baseCreateRenderer(
} }
} }
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
if (__DEV__) {
// @ts-ignore
instance.update.ownerInstance = instance
}
} }
const updateComponentPreRender = ( const updateComponentPreRender = (
@ -2073,7 +2117,7 @@ function baseCreateRenderer(
} = vnode } = vnode
// unset ref // unset ref
if (ref != null) { if (ref != null) {
setRef(ref, null, parentSuspense, null) setRef(ref, null, parentSuspense, vnode, true)
} }
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
@ -2204,10 +2248,15 @@ function baseCreateRenderer(
} }
const { bum, effects, update, subTree, um } = instance const { bum, effects, update, subTree, um } = instance
// beforeUnmount hook // beforeUnmount hook
if (bum) { if (bum) {
invokeArrayFns(bum) invokeArrayFns(bum)
} }
if (__COMPAT__ && isHookEventCompatEnabled) {
instance.emit('hook:beforeDestroy')
}
if (effects) { if (effects) {
for (let i = 0; i < effects.length; i++) { for (let i = 0; i < effects.length; i++) {
stop(effects[i]) stop(effects[i])
@ -2223,6 +2272,12 @@ function baseCreateRenderer(
if (um) { if (um) {
queuePostRenderEffect(um, parentSuspense) queuePostRenderEffect(um, parentSuspense)
} }
if (__COMPAT__ && isHookEventCompatEnabled) {
queuePostRenderEffect(
() => instance.emit('hook:destroyed'),
parentSuspense
)
}
queuePostRenderEffect(() => { queuePostRenderEffect(() => {
instance.isUnmounted = true instance.isUnmounted = true
}, parentSuspense) }, parentSuspense)

View File

@ -1,6 +1,8 @@
import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { isArray } from '@vue/shared' import { isArray } from '@vue/shared'
import { ComponentPublicInstance } from './componentPublicInstance' import { ComponentPublicInstance } from './componentPublicInstance'
import { ComponentInternalInstance, getComponentName } from './component'
import { warn } from './warning'
export interface SchedulerJob { export interface SchedulerJob {
(): void (): void
@ -22,6 +24,7 @@ export interface SchedulerJob {
* stabilizes (#1727). * stabilizes (#1727).
*/ */
allowRecurse?: boolean allowRecurse?: boolean
ownerInstance?: ComponentInternalInstance
} }
export type SchedulerCb = Function & { id?: number } export type SchedulerCb = Function & { id?: number }
@ -164,8 +167,11 @@ export function flushPreFlushCbs(
preFlushIndex < activePreFlushCbs.length; preFlushIndex < activePreFlushCbs.length;
preFlushIndex++ preFlushIndex++
) { ) {
if (__DEV__) { if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex]) checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
) {
continue
} }
activePreFlushCbs[preFlushIndex]() activePreFlushCbs[preFlushIndex]()
} }
@ -200,8 +206,11 @@ export function flushPostFlushCbs(seen?: CountMap) {
postFlushIndex < activePostFlushCbs.length; postFlushIndex < activePostFlushCbs.length;
postFlushIndex++ postFlushIndex++
) { ) {
if (__DEV__) { if (
__DEV__ &&
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex]) checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
) {
continue
} }
activePostFlushCbs[postFlushIndex]() activePostFlushCbs[postFlushIndex]()
} }
@ -235,8 +244,8 @@ function flushJobs(seen?: CountMap) {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex] const job = queue[flushIndex]
if (job) { if (job) {
if (__DEV__) { if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
checkRecursiveUpdates(seen!, job) continue
} }
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
} }
@ -263,13 +272,18 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob | SchedulerCb) {
} else { } else {
const count = seen.get(fn)! const count = seen.get(fn)!
if (count > RECURSION_LIMIT) { if (count > RECURSION_LIMIT) {
throw new Error( const instance = (fn as SchedulerJob).ownerInstance
`Maximum recursive updates exceeded. ` + const componentName = instance && getComponentName(instance.type)
warn(
`Maximum recursive updates exceeded${
componentName ? ` in component <${componentName}>` : ``
}. ` +
`This means you have a reactive effect that is mutating its own ` + `This means you have a reactive effect that is mutating its own ` +
`dependencies and thus recursively triggering itself. Possible sources ` + `dependencies and thus recursively triggering itself. Possible sources ` +
`include component template, render function, updated hook or ` + `include component template, render function, updated hook or ` +
`watcher source function.` `watcher source function.`
) )
return true
} else { } else {
seen.set(fn, count + 1) seen.set(fn, count + 1)
} }

View File

@ -41,6 +41,10 @@ import { RendererNode, RendererElement } from './renderer'
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
import { hmrDirtyComponents } from './hmr' import { hmrDirtyComponents } from './hmr'
import { setCompiledSlotRendering } from './helpers/renderSlot' import { setCompiledSlotRendering } from './helpers/renderSlot'
import { convertLegacyComponent } from './compat/component'
import { convertLegacyVModelProps } from './compat/vModel'
import { defineLegacyVNodeProperties } from './compat/renderFn'
import { convertLegacyRefInFor } from './compat/ref'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true __isFragment: true
@ -71,6 +75,7 @@ export type VNodeRef =
export type VNodeNormalizedRefAtom = { export type VNodeNormalizedRefAtom = {
i: ComponentInternalInstance i: ComponentInternalInstance
r: VNodeRef r: VNodeRef
f?: boolean // v2 compat only, refInFor marker
} }
export type VNodeNormalizedRef = export type VNodeNormalizedRef =
@ -127,10 +132,12 @@ export interface VNode<
* @internal * @internal
*/ */
__v_isVNode: true __v_isVNode: true
/** /**
* @internal * @internal
*/ */
[ReactiveFlags.SKIP]: true [ReactiveFlags.SKIP]: true
type: VNodeTypes type: VNodeTypes
props: (VNodeProps & ExtraProps) | null props: (VNodeProps & ExtraProps) | null
key: string | number | null key: string | number | null
@ -358,6 +365,11 @@ function _createVNode(
type = type.__vccOpts type = type.__vccOpts
} }
// 2.x async/functional component compat
if (__COMPAT__) {
type = convertLegacyComponent(type, currentRenderingInstance)
}
// class & style normalization. // class & style normalization.
if (props) { if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation. // for reactive or proxy objects, we need to clone it to enable mutation.
@ -405,7 +417,7 @@ function _createVNode(
const vnode: VNode = { const vnode: VNode = {
__v_isVNode: true, __v_isVNode: true,
[ReactiveFlags.SKIP]: true, __v_skip: true,
type, type,
props, props,
key: props && normalizeKey(props), key: props && normalizeKey(props),
@ -463,6 +475,12 @@ function _createVNode(
currentBlock.push(vnode) currentBlock.push(vnode)
} }
if (__COMPAT__) {
convertLegacyVModelProps(vnode)
convertLegacyRefInFor(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode return vnode
} }
@ -475,9 +493,9 @@ export function cloneVNode<T, U>(
// key enumeration cost. // key enumeration cost.
const { props, ref, patchFlag, children } = vnode const { props, ref, patchFlag, children } = vnode
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
return { const cloned: VNode = {
__v_isVNode: true, __v_isVNode: true,
[ReactiveFlags.SKIP]: true, __v_skip: true,
type: vnode.type, type: vnode.type,
props: mergedProps, props: mergedProps,
key: mergedProps && normalizeKey(mergedProps), key: mergedProps && normalizeKey(mergedProps),
@ -529,6 +547,10 @@ export function cloneVNode<T, U>(
el: vnode.el, el: vnode.el,
anchor: vnode.anchor anchor: vnode.anchor
} }
if (__COMPAT__) {
defineLegacyVNodeProperties(cloned)
}
return cloned as any
} }
/** /**
@ -671,7 +693,7 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
const incoming = toMerge[key] const incoming = toMerge[key]
if (existing !== incoming) { if (existing !== incoming) {
ret[key] = existing ret[key] = existing
? [].concat(existing as any, toMerge[key] as any) ? [].concat(existing as any, incoming as any)
: incoming : incoming
} }
} else if (key !== '') { } else if (key !== '') {

View File

@ -71,7 +71,7 @@ export function warn(msg: string, ...args: any[]) {
resetTracking() resetTracking()
} }
function getComponentTrace(): ComponentTraceStack { export function getComponentTrace(): ComponentTraceStack {
let currentVNode: VNode | null = stack[stack.length - 1] let currentVNode: VNode | null = stack[stack.length - 1]
if (!currentVNode) { if (!currentVNode) {
return [] return []

View File

@ -3,7 +3,9 @@ import {
BaseTransitionProps, BaseTransitionProps,
h, h,
warn, warn,
FunctionalComponent FunctionalComponent,
compatUtils,
DeprecationTypes
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { isObject, toNumber, extend } from '@vue/shared' import { isObject, toNumber, extend } from '@vue/shared'
@ -44,6 +46,10 @@ export const Transition: FunctionalComponent<TransitionProps> = (
Transition.displayName = 'Transition' Transition.displayName = 'Transition'
if (__COMPAT__) {
Transition.__isBuiltIn = true
}
const DOMTransitionPropsValidators = { const DOMTransitionPropsValidators = {
name: String, name: String,
type: String, type: String,
@ -72,10 +78,20 @@ export const TransitionPropsValidators = (Transition.props = /*#__PURE__*/ exten
export function resolveTransitionProps( export function resolveTransitionProps(
rawProps: TransitionProps rawProps: TransitionProps
): BaseTransitionProps<Element> { ): BaseTransitionProps<Element> {
let { const baseProps: BaseTransitionProps<Element> = {}
for (const key in rawProps) {
if (!(key in DOMTransitionPropsValidators)) {
;(baseProps as any)[key] = (rawProps as any)[key]
}
}
if (rawProps.css === false) {
return baseProps
}
const {
name = 'v', name = 'v',
type, type,
css = true,
duration, duration,
enterFromClass = `${name}-enter-from`, enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`, enterActiveClass = `${name}-enter-active`,
@ -88,15 +104,24 @@ export function resolveTransitionProps(
leaveToClass = `${name}-leave-to` leaveToClass = `${name}-leave-to`
} = rawProps } = rawProps
const baseProps: BaseTransitionProps<Element> = {} // legacy transition class compat
for (const key in rawProps) { const legacyClassEnabled =
if (!(key in DOMTransitionPropsValidators)) { __COMPAT__ &&
;(baseProps as any)[key] = (rawProps as any)[key] compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
let legacyEnterFromClass: string
let legacyAppearFromClass: string
let legacyLeaveFromClass: string
if (__COMPAT__ && legacyClassEnabled) {
const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
if (!rawProps.enterFromClass) {
legacyEnterFromClass = toLegacyClass(enterFromClass)
} }
if (!rawProps.appearFromClass) {
legacyAppearFromClass = toLegacyClass(appearFromClass)
}
if (!rawProps.leaveFromClass) {
legacyLeaveFromClass = toLegacyClass(leaveFromClass)
} }
if (!css) {
return baseProps
} }
const durations = normalizeDuration(duration) const durations = normalizeDuration(duration)
@ -132,6 +157,12 @@ export function resolveTransitionProps(
hook && hook(el, resolve) hook && hook(el, resolve)
nextFrame(() => { nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass) removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(
el,
isAppear ? legacyAppearFromClass : legacyEnterFromClass
)
}
addTransitionClass(el, isAppear ? appearToClass : enterToClass) addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!(hook && hook.length > 1)) { if (!(hook && hook.length > 1)) {
whenTransitionEnds(el, type, enterDuration, resolve) whenTransitionEnds(el, type, enterDuration, resolve)
@ -144,11 +175,17 @@ export function resolveTransitionProps(
onBeforeEnter(el) { onBeforeEnter(el) {
onBeforeEnter && onBeforeEnter(el) onBeforeEnter && onBeforeEnter(el)
addTransitionClass(el, enterFromClass) addTransitionClass(el, enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyEnterFromClass)
}
addTransitionClass(el, enterActiveClass) addTransitionClass(el, enterActiveClass)
}, },
onBeforeAppear(el) { onBeforeAppear(el) {
onBeforeAppear && onBeforeAppear(el) onBeforeAppear && onBeforeAppear(el)
addTransitionClass(el, appearFromClass) addTransitionClass(el, appearFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyAppearFromClass)
}
addTransitionClass(el, appearActiveClass) addTransitionClass(el, appearActiveClass)
}, },
onEnter: makeEnterHook(false), onEnter: makeEnterHook(false),
@ -156,11 +193,17 @@ export function resolveTransitionProps(
onLeave(el, done) { onLeave(el, done) {
const resolve = () => finishLeave(el, done) const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass) addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyLeaveFromClass)
}
// force reflow so *-leave-from classes immediately take effect (#2593) // force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow() forceReflow()
addTransitionClass(el, leaveActiveClass) addTransitionClass(el, leaveActiveClass)
nextFrame(() => { nextFrame(() => {
removeTransitionClass(el, leaveFromClass) removeTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(el, legacyLeaveFromClass)
}
addTransitionClass(el, leaveToClass) addTransitionClass(el, leaveToClass)
if (!(onLeave && onLeave.length > 1)) { if (!(onLeave && onLeave.length > 1)) {
whenTransitionEnds(el, type, leaveDuration, resolve) whenTransitionEnds(el, type, leaveDuration, resolve)

View File

@ -20,7 +20,10 @@ import {
createVNode, createVNode,
onUpdated, onUpdated,
SetupContext, SetupContext,
toRaw toRaw,
compatUtils,
DeprecationTypes,
ComponentOptions
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { extend } from '@vue/shared' import { extend } from '@vue/shared'
@ -37,7 +40,7 @@ export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
moveClass?: string moveClass?: string
} }
const TransitionGroupImpl = { const TransitionGroupImpl: ComponentOptions = {
name: 'TransitionGroup', name: 'TransitionGroup',
props: /*#__PURE__*/ extend({}, TransitionPropsValidators, { props: /*#__PURE__*/ extend({}, TransitionPropsValidators, {
@ -99,7 +102,19 @@ const TransitionGroupImpl = {
return () => { return () => {
const rawProps = toRaw(props) const rawProps = toRaw(props)
const cssTransitionProps = resolveTransitionProps(rawProps) const cssTransitionProps = resolveTransitionProps(rawProps)
const tag = rawProps.tag || Fragment let tag = rawProps.tag || Fragment
if (
__COMPAT__ &&
!rawProps.tag &&
compatUtils.checkCompatEnabled(
DeprecationTypes.TRANSITION_GROUP_ROOT,
instance.parent
)
) {
tag = 'span'
}
prevChildren = children prevChildren = children
children = slots.default ? getTransitionRawChildren(slots.default()) : [] children = slots.default ? getTransitionRawChildren(slots.default()) : []
@ -131,6 +146,10 @@ const TransitionGroupImpl = {
} }
} }
if (__COMPAT__) {
TransitionGroupImpl.__isBuiltIn = true
}
/** /**
* TransitionGroup does not support "mode" so we need to remove it from the * TransitionGroup does not support "mode" so we need to remove it from the
* props declarations, but direct delete operation is considered a side effect * props declarations, but direct delete operation is considered a side effect

View File

@ -1,4 +1,11 @@
import { hyphenate } from '@vue/shared' import {
getCurrentInstance,
DeprecationTypes,
LegacyConfig,
compatUtils,
ComponentInternalInstance
} from '@vue/runtime-core'
import { hyphenate, isArray } from '@vue/shared'
const systemModifiers = ['ctrl', 'shift', 'alt', 'meta'] const systemModifiers = ['ctrl', 'shift', 'alt', 'meta']
@ -51,15 +58,60 @@ const keyNames: Record<string, string | string[]> = {
* @private * @private
*/ */
export const withKeys = (fn: Function, modifiers: string[]) => { export const withKeys = (fn: Function, modifiers: string[]) => {
return (event: KeyboardEvent) => { let globalKeyCodes: LegacyConfig['keyCodes']
if (!('key' in event)) return let instance: ComponentInternalInstance | null = null
const eventKey = hyphenate(event.key) if (__COMPAT__) {
instance = getCurrentInstance()
if ( if (
// None of the provided key modifiers match the current event key compatUtils.isCompatEnabled(DeprecationTypes.CONFIG_KEY_CODES, instance)
!modifiers.some(k => k === eventKey || keyNames[k] === eventKey)
) { ) {
if (instance) {
globalKeyCodes = ((instance.appContext.config as any) as LegacyConfig)
.keyCodes
}
}
if (__DEV__ && modifiers.some(m => /^\d+$/.test(m))) {
compatUtils.warnDeprecation(
DeprecationTypes.V_ON_KEYCODE_MODIFIER,
instance
)
}
}
return (event: KeyboardEvent) => {
if (!('key' in event)) {
return return
} }
const eventKey = hyphenate(event.key)
if (modifiers.some(k => k === eventKey || keyNames[k] === eventKey)) {
return fn(event)
}
if (__COMPAT__) {
const keyCode = String(event.keyCode)
if (
compatUtils.isCompatEnabled(
DeprecationTypes.V_ON_KEYCODE_MODIFIER,
instance
) &&
modifiers.some(mod => mod == keyCode)
) {
return fn(event)
}
if (globalKeyCodes) {
for (const mod of modifiers) {
const codes = globalKeyCodes[mod]
if (codes) {
const matches = isArray(codes)
? codes.some(code => String(code) === keyCode)
: String(codes) === keyCode
if (matches) {
return fn(event) return fn(event)
} }
} }
}
}
}
}
}

View File

@ -8,7 +8,9 @@ import {
HydrationRenderer, HydrationRenderer,
App, App,
RootHydrateFunction, RootHydrateFunction,
isRuntimeOnly isRuntimeOnly,
DeprecationTypes,
compatUtils
} from '@vue/runtime-core' } from '@vue/runtime-core'
import { nodeOps } from './nodeOps' import { nodeOps } from './nodeOps'
import { patchProp, forcePatchProp } from './patchProp' import { patchProp, forcePatchProp } from './patchProp'
@ -56,17 +58,36 @@ export const createApp = ((...args) => {
if (__DEV__) { if (__DEV__) {
injectNativeTagCheck(app) injectNativeTagCheck(app)
injectCustomElementCheck(app) injectCompilerOptionsCheck(app)
} }
const { mount } = app const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector) const container = normalizeContainer(containerOrSelector)
if (!container) return if (!container) return
const component = app._component const component = app._component
if (!isFunction(component) && !component.render && !component.template) { if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
component.template = container.innerHTML component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break
} }
}
}
}
// clear content before mounting // clear content before mounting
container.innerHTML = '' container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement) const proxy = mount(container, false, container instanceof SVGElement)
@ -85,7 +106,7 @@ export const createSSRApp = ((...args) => {
if (__DEV__) { if (__DEV__) {
injectNativeTagCheck(app) injectNativeTagCheck(app)
injectCustomElementCheck(app) injectCompilerOptionsCheck(app)
} }
const { mount } = app const { mount } = app
@ -109,21 +130,40 @@ function injectNativeTagCheck(app: App) {
} }
// dev only // dev only
function injectCustomElementCheck(app: App) { function injectCompilerOptionsCheck(app: App) {
if (isRuntimeOnly()) { if (isRuntimeOnly()) {
const value = app.config.isCustomElement const isCustomElement = app.config.isCustomElement
Object.defineProperty(app.config, 'isCustomElement', { Object.defineProperty(app.config, 'isCustomElement', {
get() { get() {
return value return isCustomElement
}, },
set() { set() {
warn( warn(
`The \`isCustomElement\` config option is only respected when using the runtime compiler.` + `The \`isCustomElement\` config option is deprecated. Use ` +
`If you are using the runtime-only build, \`isCustomElement\` must be passed to \`@vue/compiler-dom\` in the build setup instead` + `\`compilerOptions.isCustomElement\` instead.`
`- for example, via the \`compilerOptions\` option in vue-loader: https://vue-loader.vuejs.org/options.html#compileroptions.`
) )
} }
}) })
const compilerOptions = app.config.compilerOptions
const msg =
`The \`compilerOptions\` config option is only respected when using ` +
`a build of Vue.js that includes the runtime compiler (aka "full build"). ` +
`Since you are using the runtime-only build, \`compilerOptions\` ` +
`must be passed to \`@vue/compiler-dom\` in the build setup instead.\n` +
`- For vue-loader: pass it via vue-loader's \`compilerOptions\` loader option.\n` +
`- For vue-cli: see https://cli.vuejs.org/guide/webpack.html#modifying-options-of-a-loader\n` +
`- For vite: pass it via @vitejs/plugin-vue options. See https://github.com/vitejs/vite/tree/main/packages/plugin-vue#example-for-passing-options-to-vuecompiler-dom`
Object.defineProperty(app.config, 'compilerOptions', {
get() {
warn(msg)
return compilerOptions
},
set() {
warn(msg)
}
})
} }
} }

View File

@ -15,6 +15,10 @@ export function patchAttr(
el.setAttributeNS(xlinkNS, key, value) el.setAttributeNS(xlinkNS, key, value)
} }
} else { } else {
if (__COMPAT__ && compatCoerceAttr(el, key, value)) {
return
}
// note we are only checking boolean attributes that don't have a // note we are only checking boolean attributes that don't have a
// corresponding dom prop of the same name here. // corresponding dom prop of the same name here.
const isBoolean = isSpecialBooleanAttr(key) const isBoolean = isSpecialBooleanAttr(key)
@ -25,3 +29,51 @@ export function patchAttr(
} }
} }
} }
// 2.x compat
import { makeMap, NOOP } from '@vue/shared'
import { compatUtils, DeprecationTypes } from '@vue/runtime-core'
const isEnumeratedAttr = __COMPAT__
? /*#__PURE__*/ makeMap('contenteditable,draggable,spellcheck')
: NOOP
export function compatCoerceAttr(
el: Element,
key: string,
value: unknown
): boolean {
if (isEnumeratedAttr(key)) {
const v2CocercedValue =
value === null
? 'false'
: typeof value !== 'boolean' && value !== undefined
? 'true'
: null
if (
v2CocercedValue &&
compatUtils.softAssertCompatEnabled(
DeprecationTypes.ATTR_ENUMERATED_COERSION,
null,
key,
value,
v2CocercedValue
)
) {
el.setAttribute(key, v2CocercedValue)
return true
}
} else if (
value === false &&
!isSpecialBooleanAttr(key) &&
compatUtils.softAssertCompatEnabled(
DeprecationTypes.ATTR_FALSE_VALUE,
null,
key
)
) {
el.removeAttribute(key)
return true
}
return false
}

View File

@ -6,6 +6,7 @@
"formats": [ "formats": [
"global" "global"
], ],
"compat": true,
"env": "development", "env": "development",
"enableNonBrowserBranches": true "enableNonBrowserBranches": true
}, },

View File

@ -8,12 +8,13 @@ export const compilerOptions: CompilerOptions = reactive({
mode: 'module', mode: 'module',
filename: 'Foo.vue', filename: 'Foo.vue',
prefixIdentifiers: false, prefixIdentifiers: false,
optimizeImports: false,
hoistStatic: false, hoistStatic: false,
cacheHandlers: false, cacheHandlers: false,
scopeId: null, scopeId: null,
inline: false, inline: false,
ssrCssVars: `{ color }`, ssrCssVars: `{ color }`,
compatConfig: { MODE: 3 },
whitespace: 'condense',
bindingMetadata: { bindingMetadata: {
TestComponent: BindingTypes.SETUP_CONST, TestComponent: BindingTypes.SETUP_CONST,
setupRef: BindingTypes.SETUP_REF, setupRef: BindingTypes.SETUP_REF,
@ -83,6 +84,32 @@ const App = {
h('label', { for: 'mode-function' }, 'function') h('label', { for: 'mode-function' }, 'function')
]), ]),
// whitespace handling
h('li', { id: 'whitespace' }, [
h('span', { class: 'label' }, 'whitespace: '),
h('input', {
type: 'radio',
id: 'whitespace-condense',
name: 'whitespace',
checked: compilerOptions.whitespace === 'condense',
onChange() {
compilerOptions.whitespace = 'condense'
}
}),
h('label', { for: 'whitespace-condense' }, 'condense'),
' ',
h('input', {
type: 'radio',
id: 'whitespace-preserve',
name: 'whitespace',
checked: compilerOptions.whitespace === 'preserve',
onChange() {
compilerOptions.whitespace = 'preserve'
}
}),
h('label', { for: 'whitespace-preserve' }, 'preserve')
]),
// SSR // SSR
h('li', [ h('li', [
h('input', { h('input', {
@ -170,18 +197,20 @@ const App = {
h('label', { for: 'inline' }, 'inline') h('label', { for: 'inline' }, 'inline')
]), ]),
// toggle optimizeImports // compat mode
h('li', [ h('li', [
h('input', { h('input', {
type: 'checkbox', type: 'checkbox',
id: 'optimize-imports', id: 'compat',
disabled: !isModule || isSSR, checked: compilerOptions.compatConfig!.MODE === 2,
checked: isModule && !isSSR && compilerOptions.optimizeImports,
onChange(e: Event) { onChange(e: Event) {
compilerOptions.optimizeImports = (e.target as HTMLInputElement).checked compilerOptions.compatConfig!.MODE = (e.target as HTMLInputElement)
.checked
? 2
: 3
} }
}), }),
h('label', { for: 'optimize-imports' }, 'optimizeImports') h('label', { for: 'compat' }, 'v2 compat mode')
]) ])
]) ])
]) ])

View File

@ -0,0 +1 @@
# @vue/compat

View File

@ -0,0 +1,7 @@
{
"extends": "../../api-extractor.json",
"mainEntryPointFilePath": "./dist/packages/<unscopedPackageName>/src/index.d.ts",
"dtsRollup": {
"publicTrimmedFilePath": "./dist/<unscopedPackageName>.d.ts"
}
}

View File

@ -0,0 +1,7 @@
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/compat.cjs.prod.js')
} else {
module.exports = require('./dist/compat.cjs.js')
}

View File

@ -0,0 +1,44 @@
{
"name": "@vue/compat",
"version": "3.0.11",
"description": "Vue 3 compatibility build for Vue 2",
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts",
"unpkg": "dist/vue.global.js",
"jsdelivr": "dist/vue.global.js",
"files": [
"index.js",
"dist"
],
"buildOptions": {
"name": "Vue",
"filename": "vue",
"compat": true,
"formats": [
"esm-bundler",
"esm-bundler-runtime",
"cjs",
"global",
"global-runtime",
"esm-browser",
"esm-browser-runtime"
]
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue.git"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue/issues"
},
"homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-compat#readme",
"peerDependencies": {
"vue": "3.0.11"
}
}

View File

@ -0,0 +1,46 @@
// This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers.
import { initDev } from './dev'
import {
compatUtils,
createApp,
Transition,
TransitionGroup,
KeepAlive,
DeprecationTypes,
vShow,
vModelDynamic
} from '@vue/runtime-dom'
import { extend } from '@vue/shared'
if (__DEV__) {
initDev()
}
import * as runtimeDom from '@vue/runtime-dom'
function wrappedCreateApp(...args: any[]) {
// @ts-ignore
const app = createApp(...args)
if (compatUtils.isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, null)) {
// register built-in components so that they can be resolved via strings
// in the legacy h() call. The __compat__ prefix is to ensure that v3 h()
// doesn't get affected.
app.component('__compat__transition', Transition)
app.component('__compat__transition-group', TransitionGroup)
app.component('__compat__keep-alive', KeepAlive)
// built-in directives. No need for prefix since there's no render fn API
// for resolving directives via string in v3.
app._context.directives.show = vShow
app._context.directives.model = vModelDynamic
}
return app
}
export function createCompatVue() {
const Vue = compatUtils.createCompatVue(wrappedCreateApp)
extend(Vue, runtimeDom)
// @ts-ignore
Vue.createApp = wrappedCreateApp
return Vue
}

View File

@ -0,0 +1,14 @@
import { initCustomFormatter } from '@vue/runtime-dom'
export function initDev() {
if (__BROWSER__) {
if (!__ESM_BUNDLER__) {
console.info(
`You are running a development build of Vue.\n` +
`Make sure to use the production build (*.prod.js) when deploying for production.`
)
}
initCustomFormatter()
}
}

View File

@ -0,0 +1,3 @@
import Vue from './index'
export default Vue
export * from '@vue/runtime-dom'

View File

@ -0,0 +1,3 @@
import Vue from './runtime'
export default Vue
export * from '@vue/runtime-dom'

View File

@ -0,0 +1,97 @@
// This entry is the "full-build" that includes both the runtime
// and the compiler, and supports on-the-fly compilation of the template option.
import { createCompatVue } from './createCompatVue'
import { compile, CompilerError, CompilerOptions } from '@vue/compiler-dom'
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared'
import { InternalRenderFunction } from 'packages/runtime-core/src/component'
import * as runtimeDom from '@vue/runtime-dom'
import {
DeprecationTypes,
warnDeprecation
} from '../../runtime-core/src/compat/compatConfig'
const compileCache: Record<string, RenderFunction> = Object.create(null)
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
return NOOP
}
}
const key = template
const cached = compileCache[key]
if (cached) {
return cached
}
if (template[0] === '#') {
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's rendered
// by the server, the template should not contain any user data.
template = el ? el.innerHTML : ``
}
if (__DEV__ && !__TEST__ && (!options || !options.whitespace)) {
warnDeprecation(DeprecationTypes.CONFIG_WHITESPACE, null)
}
const { code } = compile(
template,
extend(
{
hoistStatic: true,
whitespace: 'preserve',
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP
} as CompilerOptions,
options
)
)
function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}
// The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise.
// In the global build we know `Vue` is available globally so we can avoid
// the wildcard object.
const render = (__GLOBAL__
? new Function(code)()
: new Function('Vue', code)(runtimeDom)) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (compileCache[key] = render)
}
registerRuntimeCompiler(compileToFunction)
const Vue = createCompatVue()
Vue.compile = compileToFunction
export default Vue

View File

@ -0,0 +1,23 @@
// This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers.
import { createCompatVue } from './createCompatVue'
import { warn } from '@vue/runtime-core'
const Vue = createCompatVue()
Vue.compile = (() => {
if (__DEV__) {
warn(
`Runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "@vue/compat/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
)
}
}) as any
export default Vue

View File

@ -0,0 +1,104 @@
import { createApp } from 'vue'
describe('config.compilerOptions', () => {
test('isCustomElement', () => {
const app = createApp({
template: `<foo/>`
})
app.config.compilerOptions.isCustomElement = (tag: string) => tag === 'foo'
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('<foo></foo>')
})
test('comments', () => {
const app = createApp({
template: `<div/><!--test--><div/>`
})
app.config.compilerOptions.comments = true
// the comments option is only relevant in production mode
__DEV__ = false
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('<div></div><!--test--><div></div>')
__DEV__ = true
})
test('whitespace', () => {
const app = createApp({
template: `<div><span/>\n <span/></div>`
})
app.config.compilerOptions.whitespace = 'preserve'
const root = document.createElement('div')
app.mount(root)
expect(root.firstChild!.childNodes.length).toBe(3)
expect(root.firstChild!.childNodes[1].nodeType).toBe(Node.TEXT_NODE)
})
test('delimiters', () => {
const app = createApp({
data: () => ({ foo: 'hi' }),
template: `[[ foo ]]`
})
app.config.compilerOptions.delimiters = [`[[`, `]]`]
const root = document.createElement('div')
app.mount(root)
expect(root.textContent).toBe('hi')
})
})
describe('per-component compilerOptions', () => {
test('isCustomElement', () => {
const app = createApp({
template: `<foo/>`,
compilerOptions: {
isCustomElement: (tag: string) => tag === 'foo'
}
})
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('<foo></foo>')
})
test('comments', () => {
const app = createApp({
template: `<div/><!--test--><div/>`,
compilerOptions: {
comments: true
}
})
app.config.compilerOptions.comments = false
// the comments option is only relevant in production mode
__DEV__ = false
const root = document.createElement('div')
app.mount(root)
expect(root.innerHTML).toBe('<div></div><!--test--><div></div>')
__DEV__ = true
})
test('whitespace', () => {
const app = createApp({
template: `<div><span/>\n <span/></div>`,
compilerOptions: {
whitespace: 'preserve'
}
})
const root = document.createElement('div')
app.mount(root)
expect(root.firstChild!.childNodes.length).toBe(3)
expect(root.firstChild!.childNodes[1].nodeType).toBe(Node.TEXT_NODE)
})
test('delimiters', () => {
const app = createApp({
data: () => ({ foo: 'hi' }),
template: `[[ foo ]]`,
compilerOptions: {
delimiters: [`[[`, `]]`]
}
})
const root = document.createElement('div')
app.mount(root)
expect(root.textContent).toBe('hi')
})
})

View File

@ -1,7 +1,7 @@
{ {
"name": "vue", "name": "vue",
"version": "3.0.11", "version": "3.0.11",
"description": "vue", "description": "The progressive JavaScript framework for buiding modern web UI.",
"main": "index.js", "main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js", "module": "dist/vue.runtime.esm-bundler.js",
"types": "dist/vue.d.ts", "types": "dist/vue.d.ts",

View File

@ -49,9 +49,17 @@ function compileToFunction(
extend( extend(
{ {
hoistStatic: true, hoistStatic: true,
onError(err: CompilerError) { onError: __DEV__ ? onError : undefined,
if (__DEV__) { onWarn: __DEV__ ? e => onError(e, true) : NOOP
const message = `Template compilation error: ${err.message}` } as CompilerOptions,
options
)
)
function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame = const codeFrame =
err.loc && err.loc &&
generateCodeFrame( generateCodeFrame(
@ -60,15 +68,7 @@ function compileToFunction(
err.loc.end.offset err.loc.end.offset
) )
warn(codeFrame ? `${message}\n${codeFrame}` : message) warn(codeFrame ? `${message}\n${codeFrame}` : message)
} else {
/* istanbul ignore next */
throw err
} }
}
},
options
)
)
// The wildcard import results in a huge object with every export // The wildcard import results in a huge object with every export
// with keys that cannot be mangled, and can be quite heavy size-wise. // with keys that cannot be mangled, and can be quite heavy size-wise.

View File

@ -1,3 +1,4 @@
// @ts-check
import path from 'path' import path from 'path'
import ts from 'rollup-plugin-typescript2' import ts from 'rollup-plugin-typescript2'
import replace from '@rollup/plugin-replace' import replace from '@rollup/plugin-replace'
@ -10,10 +11,10 @@ if (!process.env.TARGET) {
const masterVersion = require('./package.json').version const masterVersion = require('./package.json').version
const packagesDir = path.resolve(__dirname, 'packages') const packagesDir = path.resolve(__dirname, 'packages')
const packageDir = path.resolve(packagesDir, process.env.TARGET) const packageDir = path.resolve(packagesDir, process.env.TARGET)
const name = path.basename(packageDir)
const resolve = p => path.resolve(packageDir, p) const resolve = p => path.resolve(packageDir, p)
const pkg = require(resolve(`package.json`)) const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {} const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir)
// ensure TS checks only once for each build // ensure TS checks only once for each build
let hasTSChecked = false let hasTSChecked = false
@ -89,6 +90,7 @@ function createConfig(format, output, plugins = []) {
const isBrowserESMBuild = /esm-browser/.test(format) const isBrowserESMBuild = /esm-browser/.test(format)
const isNodeBuild = format === 'cjs' const isNodeBuild = format === 'cjs'
const isGlobalBuild = /global/.test(format) const isGlobalBuild = /global/.test(format)
const isCompatBuild = !!packageOptions.compat
if (isGlobalBuild) { if (isGlobalBuild) {
output.name = packageOptions.name output.name = packageOptions.name
@ -114,21 +116,34 @@ function createConfig(format, output, plugins = []) {
// during a single build. // during a single build.
hasTSChecked = true hasTSChecked = true
const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`
const external = // the compat build needs both default AND named exports. This will cause
isGlobalBuild || isBrowserESMBuild // Rollup to complain for non-ESM targets, so we use separate entries for
? packageOptions.enableNonBrowserBranches // esm vs. non-esm builds.
? [] if (isCompatBuild && (isBrowserESMBuild || isBundlerESMBuild)) {
: // normal browser builds - non-browser only imports are tree-shaken, entryFile = /runtime$/.test(format)
? `src/esm-runtime.ts`
: `src/esm-index.ts`
}
let external = []
if (isGlobalBuild || isBrowserESMBuild || isCompatBuild) {
if (!packageOptions.enableNonBrowserBranches) {
// normal browser builds - non-browser only imports are tree-shaken,
// they are only listed here to suppress warnings. // they are only listed here to suppress warnings.
['source-map', '@babel/parser', 'estree-walker'] external = ['source-map', '@babel/parser', 'estree-walker']
: // Node / esm-bundler builds. Externalize everything. }
[ } else {
// Node / esm-bundler builds.
// externalize all deps unless it's the compat build.
external = [
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}), ...Object.keys(pkg.peerDependencies || {}),
...['path', 'url', 'stream'] // for @vue/compiler-sfc / server-renderer ...['path', 'url', 'stream'] // for @vue/compiler-sfc / server-renderer
] ]
}
// the browser builds of @vue/compiler-sfc requires postcss to be available // the browser builds of @vue/compiler-sfc requires postcss to be available
// as a global (e.g. http://wzrd.in/standalone/postcss) // as a global (e.g. http://wzrd.in/standalone/postcss)
@ -139,9 +154,11 @@ function createConfig(format, output, plugins = []) {
const nodePlugins = const nodePlugins =
packageOptions.enableNonBrowserBranches && format !== 'cjs' packageOptions.enableNonBrowserBranches && format !== 'cjs'
? [ ? [
// @ts-ignore
require('@rollup/plugin-commonjs')({ require('@rollup/plugin-commonjs')({
sourceMap: false sourceMap: false
}), }),
// @ts-ignore
require('rollup-plugin-polyfill-node')(), require('rollup-plugin-polyfill-node')(),
require('@rollup/plugin-node-resolve').nodeResolve() require('@rollup/plugin-node-resolve').nodeResolve()
] ]
@ -165,7 +182,8 @@ function createConfig(format, output, plugins = []) {
(isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) && (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) &&
!packageOptions.enableNonBrowserBranches, !packageOptions.enableNonBrowserBranches,
isGlobalBuild, isGlobalBuild,
isNodeBuild isNodeBuild,
isCompatBuild
), ),
...nodePlugins, ...nodePlugins,
...plugins ...plugins
@ -188,7 +206,8 @@ function createReplacePlugin(
isBrowserESMBuild, isBrowserESMBuild,
isBrowserBuild, isBrowserBuild,
isGlobalBuild, isGlobalBuild,
isNodeBuild isNodeBuild,
isCompatBuild
) { ) {
const replacements = { const replacements = {
__COMMIT__: `"${process.env.COMMIT}"`, __COMMIT__: `"${process.env.COMMIT}"`,
@ -208,6 +227,9 @@ function createReplacePlugin(
// is targeting Node (SSR)? // is targeting Node (SSR)?
__NODE_JS__: isNodeBuild, __NODE_JS__: isNodeBuild,
// 2.x compat build
__COMPAT__: isCompatBuild,
// feature flags // feature flags
__FEATURE_SUSPENSE__: true, __FEATURE_SUSPENSE__: true,
__FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true, __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
@ -231,6 +253,7 @@ function createReplacePlugin(
} }
}) })
return replace({ return replace({
// @ts-ignore
values: replacements, values: replacements,
preventAssignment: true preventAssignment: true
}) })

View File

@ -169,6 +169,7 @@ function checkAllSizes(targets) {
function checkSize(target) { function checkSize(target) {
const pkgDir = path.resolve(`packages/${target}`) const pkgDir = path.resolve(`packages/${target}`)
checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`) checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`)
checkFileSize(`${pkgDir}/dist/${target}.runtime.global.prod.js`)
} }
function checkFileSize(filePath) { function checkFileSize(filePath) {

View File

@ -18,6 +18,7 @@
"types": ["jest", "puppeteer", "node"], "types": ["jest", "puppeteer", "node"],
"rootDir": ".", "rootDir": ".",
"paths": { "paths": {
"@vue/compat": ["packages/vue-compat/src"],
"@vue/*": ["packages/*/src"], "@vue/*": ["packages/*/src"],
"vue": ["packages/vue/src"] "vue": ["packages/vue/src"]
} }