diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 5af5babd..28eb0847 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -15,8 +15,7 @@ import { RootHydrateFunction } from './hydration' import { devtoolsInitApp, devtoolsUnmountApp } from './devtools' import { isFunction, NO, isObject } from '@vue/shared' import { version } from '.' -import { installCompatMount } from './compat/globalMount' -import { installLegacyConfigTraps } from './compat/globalConfig' +import { installCompatMount, installLegacyConfigTraps } from './compat/global' export interface App { version: string @@ -307,7 +306,7 @@ export function createAppAPI( if (__COMPAT__) { installCompatMount(app, context, render, hydrate) - installLegacyConfigTraps(app.config) + if (__DEV__) installLegacyConfigTraps(app.config) } return app diff --git a/packages/runtime-core/src/compat/__tests__/compatGlobal.spec.ts b/packages/runtime-core/src/compat/__tests__/compatGlobal.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/runtime-core/src/compat/deprecations.ts b/packages/runtime-core/src/compat/deprecations.ts index ae5a39b7..749c04b8 100644 --- a/packages/runtime-core/src/compat/deprecations.ts +++ b/packages/runtime-core/src/compat/deprecations.ts @@ -1,15 +1,22 @@ import { isRuntimeOnly } from '../component' export const enum DeprecationTypes { - DOM_TEMPLATE_MOUNT, - $MOUNT, - $DESTROY, - CONFIG_SILENT, CONFIG_DEVTOOLS, CONFIG_KEY_CODES, CONFIG_PRODUCTION_TIP, - CONFIG_IGNORED_ELEMENTS + CONFIG_IGNORED_ELEMENTS, + + GLOBAL_PROTOTYPE, + GLOBAL_SET, + GLOBAL_DELETE, + GLOBAL_OBSERVABLE, + GLOBAL_DOM_TEMPLATE_MOUNT, + + INSTANCE_SET, + INSTANCE_DELETE, + INSTANCE_MOUNT, + INSTANCE_DESTROY } type DeprecationData = { @@ -18,26 +25,6 @@ type DeprecationData = { } const deprecations: Record = { - [DeprecationTypes.DOM_TEMPLATE_MOUNT]: { - 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.$MOUNT]: { - message: - `vm.$mount() has been removed. ` + - `Use createApp(RootComponent).mount() instead.`, - link: `https://v3.vuejs.org/guide/migration/global-api.html#mounting-app-instance` - }, - - [DeprecationTypes.$DESTROY]: { - message: `vm.$destroy() has been removed. Use app.unmount() instead.`, - link: `https://v3.vuejs.org/api/application-api.html#unmount` - }, - [DeprecationTypes.CONFIG_SILENT]: { message: `config.silent has been removed because it is not good practice to ` + @@ -75,6 +62,64 @@ const deprecations: Record = { return msg }, link: `https://v3.vuejs.org/guide/migration/global-api.html#config-ignoredelements-is-now-config-iscustomelement` + }, + + [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_DOM_TEMPLATE_MOUNT]: { + 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.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_MOUNT]: { + message: + `The global app boostrapping 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.INSTANCE_DESTROY]: { + message: `vm.$destroy() has been removed. Use app.unmount() instead.`, + link: `https://v3.vuejs.org/api/application-api.html#unmount` } } diff --git a/packages/runtime-core/src/compat/global.ts b/packages/runtime-core/src/compat/global.ts index 5088ddf9..dac2719c 100644 --- a/packages/runtime-core/src/compat/global.ts +++ b/packages/runtime-core/src/compat/global.ts @@ -1,13 +1,32 @@ import { reactive } from '@vue/reactivity' -import { extend } from '@vue/shared' -import { createApp } from '../../../runtime-dom/src' -import { App, AppConfig, Plugin } from '../apiCreateApp' +import { isFunction } 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 { defineComponent } from '../apiDefineComponent' -import { Component, ComponentOptions, isRuntimeOnly } from '../component' +import { + Component, + ComponentOptions, + createComponentInstance, + finishComponentSetup, + isRuntimeOnly, + setupComponent +} from '../component' import { RenderFunction } from '../componentOptions' import { ComponentPublicInstance } from '../componentPublicInstance' +import { devtoolsInitApp } from '../devtools' import { Directive } from '../directives' import { nextTick } from '../scheduler' +import { warnDeprecation, DeprecationTypes } from './deprecations' +import { version } from '..' /** * @deprecated the default `Vue` export has been removed in Vue 3. The type for @@ -21,7 +40,7 @@ export type CompatVue = Pick & { new (options?: ComponentOptions): ComponentPublicInstance version: string - config: AppConfig + config: AppConfig & LegacyConfig extend: typeof defineComponent nextTick: typeof nextTick @@ -54,10 +73,40 @@ export type CompatVue = Pick & { filter(name: string, arg: any): null } +// 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 + /** + * @deprecated + * https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed + */ + productionTip?: boolean +} + export let isCopyingConfig = false // Legacy global Vue constructor -export function createCompatVue(): CompatVue { +export function createCompatVue( + createApp: CreateAppFunction +): CompatVue { if (!__COMPAT__) { // @ts-ignore this function will never be called in non-compat mode return @@ -86,9 +135,16 @@ export function createCompatVue(): CompatVue { isCopyingConfig = false // copy prototype augmentations as config.globalProperties + let hasPrototypeAugmentations = false for (const key in Ctor.prototype) { + if (key !== 'constructor') { + hasPrototypeAugmentations = true + } app.config.globalProperties[key] = Ctor.prototype[key] } + if (hasPrototypeAugmentations) { + __DEV__ && warnDeprecation(DeprecationTypes.GLOBAL_PROTOTYPE) + } const vm = app._createRoot!(options) if (options.el) { @@ -124,15 +180,21 @@ export function createCompatVue(): CompatVue { Vue.nextTick = nextTick Vue.set = (target, key, value) => { - // TODO deprecation warnings + __DEV__ && warnDeprecation(DeprecationTypes.GLOBAL_SET) target[key] = value } + Vue.delete = (target, key) => { - // TODO deprecation warnings + __DEV__ && warnDeprecation(DeprecationTypes.GLOBAL_DELETE) delete target[key] } - // TODO wrap with deprecation warning - Vue.observable = reactive + + Vue.observable = __DEV__ + ? (target: any) => { + warnDeprecation(DeprecationTypes.GLOBAL_OBSERVABLE) + return reactive(target) + } + : reactive Vue.use = (p, ...options) => { singletonApp.use(p, ...options) @@ -169,3 +231,156 @@ export function createCompatVue(): CompatVue { 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, __NODE_JS__) + 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 { + if (!selectorOrEl) { + __DEV__ && + warn( + `Failed to mount root instance: invalid mount target ${selectorOrEl}.` + ) + return + } + container = selectorOrEl + } + + 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_DOM_TEMPLATE_MOUNT) + break + } + } + } + instance.render = null + ;(component as ComponentOptions).template = container.innerHTML + finishComponentSetup(instance, __NODE_JS__, 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! + } +} + +// dev only +export function installLegacyConfigTraps(config: AppConfig) { + const legacyConfigOptions: Record = { + 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]) + } + val = newVal + } + }) + }) +} diff --git a/packages/runtime-core/src/compat/globalConfig.ts b/packages/runtime-core/src/compat/globalConfig.ts deleted file mode 100644 index e8eafae0..00000000 --- a/packages/runtime-core/src/compat/globalConfig.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { AppConfig } from '../apiCreateApp' -import { DeprecationTypes, warnDeprecation } from './deprecations' -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 - /** - * @deprecated - * https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed - */ - productionTip?: boolean -} - -export function installLegacyConfigTraps(config: AppConfig) { - const legacyConfigOptions: Record = { - 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]) - } - val = newVal - } - }) - }) -} diff --git a/packages/runtime-core/src/compat/globalMount.ts b/packages/runtime-core/src/compat/globalMount.ts deleted file mode 100644 index 0e30d6c5..00000000 --- a/packages/runtime-core/src/compat/globalMount.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { isFunction } from '@vue/shared' -import { App, AppContext } from '../apiCreateApp' -import { - ComponentOptions, - createComponentInstance, - finishComponentSetup, - setupComponent -} from '../component' -import { devtoolsInitApp } from '../devtools' -import { RootHydrateFunction } from '../hydration' -import { RootRenderFunction } from '../renderer' -import { cloneVNode, createVNode } from '../vnode' -import { warn } from '../warning' -import { version } from '..' -import { DeprecationTypes, warnDeprecation } from './deprecations' - -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, __NODE_JS__) - 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 { - if (!selectorOrEl) { - __DEV__ && - warn( - `Failed to mount root instance: invalid mount target ${selectorOrEl}.` - ) - return - } - container = selectorOrEl - } - - 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.DOM_TEMPLATE_MOUNT) - break - } - } - } - instance.render = null - ;(component as ComponentOptions).template = container.innerHTML - finishComponentSetup(instance, __NODE_JS__, 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! - } -} diff --git a/packages/runtime-core/src/compat/instance.ts b/packages/runtime-core/src/compat/instance.ts new file mode 100644 index 00000000..b92a1ebe --- /dev/null +++ b/packages/runtime-core/src/compat/instance.ts @@ -0,0 +1,34 @@ +import { extend, NOOP } from '@vue/shared' +import { PublicPropertiesMap } from '../componentPublicInstance' +import { DeprecationTypes, warnDeprecation } from './deprecations' + +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: () => { + __DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_SET) + return set + }, + $delete: () => { + __DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_DELETE) + return del + }, + $mount: i => { + __DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_MOUNT) + // root mount override from ./global.ts in installCompatMount + return i.ctx._compat_mount || NOOP + }, + $destroy: i => { + __DEV__ && warnDeprecation(DeprecationTypes.INSTANCE_DESTROY) + // root destroy override from ./global.ts in installCompatMount + return i.ctx._compat_destroy || NOOP + } + } as PublicPropertiesMap) +} diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7572c430..f9d95aac 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -41,7 +41,8 @@ import { markAttrsAccessed } from './componentRenderUtils' import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' import { UnionToIntersection } from './helpers/typeUtils' -import { warnDeprecation, DeprecationTypes } from './compat/deprecations' +import { installCompatInstanceProperties } from './compat/instance' + /** * Custom properties added to component instances in any way and can be accessed through `this` * @@ -202,7 +203,10 @@ export type ComponentPublicInstance< M & ComponentCustomProperties -type PublicPropertiesMap = Record any> +export type PublicPropertiesMap = Record< + string, + (i: ComponentInternalInstance) => any +> /** * #2437 In Vue 3, functional components do not have a public instance proxy but @@ -235,22 +239,7 @@ const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { } as PublicPropertiesMap) if (__COMPAT__) { - extend(publicPropertiesMap, { - $mount: i => { - if (__DEV__) { - warnDeprecation(DeprecationTypes.$MOUNT) - } - // root mount override from apiCreateApp.ts - return i.ctx._compat_mount || NOOP - }, - $destroy: i => { - if (__DEV__) { - warnDeprecation(DeprecationTypes.$DESTROY) - } - // root destroy override from apiCreateApp.ts - return i.ctx._compat_destroy || NOOP - } - } as PublicPropertiesMap) + installCompatInstanceProperties(publicPropertiesMap) } const enum AccessTypes { diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index fe019a44..77adc6aa 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -78,7 +78,7 @@ export const createApp = ((...args) => { 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.DOM_TEMPLATE_MOUNT) + warnDeprecation(DeprecationTypes.GLOBAL_DOM_TEMPLATE_MOUNT) break } } diff --git a/packages/vue-compat/src/index.ts b/packages/vue-compat/src/index.ts index a1aa615f..d766d248 100644 --- a/packages/vue-compat/src/index.ts +++ b/packages/vue-compat/src/index.ts @@ -6,6 +6,7 @@ import { registerRuntimeCompiler, RenderFunction, warn, + createApp, createCompatVue } from '@vue/runtime-dom' import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared' @@ -91,7 +92,7 @@ function compileToFunction( registerRuntimeCompiler(compileToFunction) -const Vue = createCompatVue() +const Vue = createCompatVue(createApp) Vue.compile = compileToFunction extend(Vue, runtimeDom)