Repository: vuejs/composition-api (ts)

Issue:
Alias `vm.$root` with `this.root` to match with Vue 3
https://github.com/vuejs/vue-apollo/pull/1040#issuecomment-685866246 by @frandiox

Pull request:
feat(getCurrentInstance): Aligning with vue3
fix #499 

BREAKING CHANGE: The internal vm can be accessed with `getCurrentInstance().proxy`

```js
const vm = getCurrentInstance()

// becomes

const vm = getCurrentInstance().proxy
```

Patch:
diff --git a/src/apis/computed.ts b/src/apis/computed.ts
index 3aab9f7..d2042de 100644
--- a/src/apis/computed.ts
+++ b/src/apis/computed.ts
@@ -26,7 +26,7 @@ export function computed<T>(options: Option<T>): WritableComputedRef<T>
 export function computed<T>(
   options: Option<T>['get'] | Option<T>
 ): ComputedRef<T> | WritableComputedRef<T> {
-  const vm = getCurrentInstance()
+  const vm = getCurrentInstance()?.proxy
   let get: Option<T>['get'], set: Option<T>['set'] | undefined
   if (typeof options === 'function') {
     get = options
diff --git a/src/apis/createElement.ts b/src/apis/createElement.ts
index 8480836..07ae325 100644
--- a/src/apis/createElement.ts
+++ b/src/apis/createElement.ts
@@ -8,7 +8,7 @@ type CreateElement = Vue['$createElement']
 let fallbackCreateElement: CreateElement
 
 export const createElement = (function createElement(...args: any) {
-  const instance = getCurrentInstance()
+  const instance = getCurrentInstance()?.proxy
   if (!instance) {
     warn('`createElement()` has been called outside of render function.')
     if (!fallbackCreateElement) {
diff --git a/src/apis/inject.ts b/src/apis/inject.ts
index dd71aaa..b7bd112 100644
--- a/src/apis/inject.ts
+++ b/src/apis/inject.ts
@@ -52,7 +52,7 @@ export function inject(
     return defaultValue
   }
 
-  const vm = getCurrentInstance()
+  const vm = getCurrentInstance()?.proxy
   if (!vm) {
     warn(`inject() can only be used inside setup() or functional components.`)
     return
diff --git a/src/apis/lifecycle.ts b/src/apis/lifecycle.ts
index ac6adaa..32b05f3 100644
--- a/src/apis/lifecycle.ts
+++ b/src/apis/lifecycle.ts
@@ -30,12 +30,12 @@ function injectHookOption(
 
 function wrapHookCall(vm: ComponentInstance, fn: Function) {
   return (...args: any) => {
-    let preVm = getCurrentInstance()
+    let preVm = getCurrentInstance()?.proxy
     setCurrentInstance(vm)
     try {
       return fn(...args)
     } finally {
-      setCurrentInstance(preVm)
+      setCurrentInstance(preVm!)
     }
   }
 }
diff --git a/src/apis/warn.ts b/src/apis/warn.ts
index 2bb5236..242704e 100644
--- a/src/apis/warn.ts
+++ b/src/apis/warn.ts
@@ -8,5 +8,5 @@ import { warn as vueWarn } from '../utils'
  * @param message warning message to be displayed
  */
 export function warn(message: string) {
-  vueWarn(message, getCurrentInstance())
+  vueWarn(message, getCurrentInstance()?.proxy)
 }
diff --git a/src/apis/watch.ts b/src/apis/watch.ts
index 19df3e2..f10f9bd 100644
--- a/src/apis/watch.ts
+++ b/src/apis/watch.ts
@@ -101,7 +101,7 @@ function getWatchEffectOption(options?: Partial<WatchOptions>): WatchOptions {
 }
 
 function getWatcherVM() {
-  let vm = getCurrentInstance()
+  let vm = getCurrentInstance()?.proxy
   if (!vm) {
     if (!fallbackVM) {
       fallbackVM = defineComponentInstance(getVueConstructor())
diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts
index 0d3e837..557dfe9 100644
--- a/src/runtimeContext.ts
+++ b/src/runtimeContext.ts
@@ -1,6 +1,6 @@
-import type { VueConstructor } from 'vue'
-import { ComponentInstance } from './component'
-import { assert, hasOwn, warn } from './utils'
+import type { VueConstructor, VNode } from 'vue'
+import { ComponentInstance, Data } from './component'
+import { assert, hasOwn, warn, proxy, UnionToIntersection } from './utils'
 
 let vueDependency: VueConstructor | undefined = undefined
 
@@ -71,10 +71,168 @@ export function setVueConstructor(Vue: VueConstructor) {
   })
 }
 
-export function getCurrentInstance(): ComponentInstance | null {
+export function setCurrentInstance(vm: ComponentInstance | null) {
+  // currentInstance?.$scopedSlots
+  currentInstance = vm
+}
+
+export type Slot = (...args: any[]) => VNode[]
+
+export type InternalSlots = {
+  [name: string]: Slot | undefined
+}
+
+export type ObjectEmitsOptions = Record<
+  string,
+  ((...args: any[]) => any) | null
+>
+export type EmitsOptions = ObjectEmitsOptions | string[]
+
+export type EmitFn<
+  Options = ObjectEmitsOptions,
+  Event extends keyof Options = keyof Options
+> = Options extends Array<infer V>
+  ? (event: V, ...args: any[]) => void
+  : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function
+  ? (event: string, ...args: any[]) => void
+  : UnionToIntersection<
+      {
+        [key in Event]: Options[key] extends (...args: infer Args) => any
+          ? (event: key, ...args: Args) => void
+          : (event: key, ...args: any[]) => void
+      }[Event]
+    >
+
+/**
+ * We expose a subset of properties on the internal instance as they are
+ * useful for advanced external libraries and tools.
+ */
+export declare interface ComponentInternalInstance {
+  uid: number
+  // type: ConcreteComponent
+  parent: ComponentInternalInstance | null
+  root: ComponentInternalInstance
+
+  //appContext: AppContext
+
+  /**
+   * Vnode representing this component in its parent's vdom tree
+   */
+  vnode: VNode
+  /**
+   * Root vnode of this component's own vdom tree
+   */
+  // subTree: VNode // does not exist in Vue 2
+
+  /**
+   * The reactive effect for rendering and patching the component. Callable.
+   */
+  update: Function
+
+  data: Data
+  props: Data
+  attrs: Data
+  refs: Data
+  emit: EmitFn
+
+  slots: InternalSlots
+  emitted: Record<string, boolean> | null
+
+  proxy: ComponentInstance
+
+  isMounted: boolean
+  isUnmounted: boolean
+  isDeactivated: boolean
+}
+
+export function getCurrentVu2Instance() {
   return currentInstance
 }
 
-export function setCurrentInstance(vm: ComponentInstance | null) {
-  currentInstance = vm
+export function getCurrentInstance() {
+  if (currentInstance) {
+    return toVue3ComponentInstance(currentInstance)
+  }
+  return null
+}
+
+const instanceMapCache = new WeakMap<
+  ComponentInstance,
+  ComponentInternalInstance
+>()
+
+function toVue3ComponentInstance(
+  vue2Instance: ComponentInstance
+): ComponentInternalInstance {
+  if (instanceMapCache.has(vue2Instance)) {
+    return instanceMapCache.get(vue2Instance)!
+  }
+
+  const instance: ComponentInternalInstance = ({
+    proxy: vue2Instance,
+    update: vue2Instance.$forceUpdate,
+    uid: vue2Instance._uid,
+
+    parent: null,
+    root: null as any,
+  } as unknown) as ComponentInternalInstance
+
+  // map vm.$props =
+  const instanceProps = [
+    'data',
+    'props',
+    'attrs',
+    'refs',
+    'emit',
+    'vnode',
+    'slots',
+  ] as const
+
+  instanceProps.forEach((prop) => {
+    proxy(instance, prop, {
+      get() {
+        return (vue2Instance as any)[`$${prop}`]
+      },
+    })
+  })
+
+  proxy(instance, 'isMounted', {
+    get() {
+      // @ts-expect-error private api
+      return vue2Instance._isMounted
+    },
+  })
+
+  proxy(instance, 'isUnmounted', {
+    get() {
+      // @ts-expect-error private api
+      return vue2Instance._isDestroyed
+    },
+  })
+
+  proxy(instance, 'isDeactivated', {
+    get() {
+      // @ts-expect-error private api
+      return vue2Instance._inactive
+    },
+  })
+
+  proxy(instance, 'emitted', {
+    get() {
+      // @ts-expect-error private api
+      return vue2Instance._events
+    },
+  })
+
+  instanceMapCache.set(vue2Instance, instance)
+
+  if (vue2Instance.$parent) {
+    instance.parent = toVue3ComponentInstance(vue2Instance.$parent)
+  }
+
+  if (vue2Instance.$root) {
+    instance.root = toVue3ComponentInstance(vue2Instance.$root)
+  }
+
+  return instance
 }
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index c79f31e..b72ce79 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -3,7 +3,7 @@ import { ComponentInstance } from '../component'
 import { getCurrentInstance, getVueConstructor } from '../runtimeContext'
 import { warn } from './utils'
 
-export function currentVMInFn(hook: string): ComponentInstance | null {
+export function currentVMInFn(hook: string): ComponentInstance | undefined {
   const vm = getCurrentInstance()
   if (__DEV__ && !vm) {
     warn(
@@ -12,7 +12,7 @@ export function currentVMInFn(hook: string): ComponentInstance | null {
         `Lifecycle injection APIs can only be used during execution of setup().`
     )
   }
-  return vm
+  return vm?.proxy
 }
 
 export function defineComponentInstance<V extends Vue = Vue>(
diff --git a/src/utils/index.ts b/src/utils/index.ts
index f7c9deb..856c3f8 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,2 +1,3 @@
 export * from './utils'
 export * from './helper'
+export * from './typeutils'
diff --git a/src/utils/instance.ts b/src/utils/instance.ts
index 688a380..e2d242d 100644
--- a/src/utils/instance.ts
+++ b/src/utils/instance.ts
@@ -1,6 +1,6 @@
 import { ComponentInstance } from '../component'
 import vmStateManager from './vmStateManager'
-import { setCurrentInstance, getCurrentInstance } from '../runtimeContext'
+import { setCurrentInstance, getCurrentVu2Instance } from '../runtimeContext'
 import { Ref, isRef } from '../apis'
 import { hasOwn, proxy, warn } from './utils'
 import { createSlotProxy, resolveSlots } from './helper'
@@ -112,7 +112,7 @@ export function activateCurrentInstance(
   fn: (vm_: ComponentInstance) => any,
   onError?: (err: Error) => void
 ) {
-  let preVm = getCurrentInstance()
+  let preVm = getCurrentVu2Instance()
   setCurrentInstance(vm)
   try {
     return fn(vm)
diff --git a/src/utils/typeutils.ts b/src/utils/typeutils.ts
new file mode 100644
index 0000000..724acc9
--- /dev/null
+++ b/src/utils/typeutils.ts
@@ -0,0 +1,5 @@
+export type UnionToIntersection<U> = (
+  U extends any ? (k: U) => void : never
+) extends (k: infer I) => void
+  ? I
+  : never


Test patch:
diff --git a/test/setup.spec.js b/test/setup.spec.js
index 9bd1c7f..3532984 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -12,7 +12,7 @@ const {
   nextTick,
   isReactive,
   defineComponent,
-  onMounted
+  onMounted,
 } = require('../src')
 const { sleep } = require('./helpers/utils')
 
@@ -998,8 +998,7 @@ describe('setup', () => {
   //   await nextTick()
   //   expect(vm.$el.textContent).toBe('1')
   // })
-  
-  
+
   // #448
   it('should not cause infinite loop', async () => {
     const A = defineComponent({
diff --git a/test/v3/runtime-core/apiLifecycle.spec.ts b/test/v3/runtime-core/apiLifecycle.spec.ts
index 7a89d2c..ecdba05 100644
--- a/test/v3/runtime-core/apiLifecycle.spec.ts
+++ b/test/v3/runtime-core/apiLifecycle.spec.ts
@@ -1,5 +1,3 @@
-import { VueConstructor } from 'vue'
-const Vue = require('vue/dist/vue.common.js') as VueConstructor
 import {
   onBeforeMount,
   onMounted,
@@ -10,6 +8,7 @@ import {
   onBeforeUnmount,
   onUnmounted,
   nextTick,
+  createApp,
 } from '../../../src'
 
 // reference: https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks
@@ -28,7 +27,7 @@ describe('api: lifecycle hooks', () => {
         return () => h('div')
       },
     }
-    new Vue(Comp).$mount(root)
+    createApp(Comp).mount(root)
     //render(h(Comp), root);
     expect(fn).toHaveBeenCalledTimes(1)
   })
@@ -46,7 +45,7 @@ describe('api: lifecycle hooks', () => {
         return () => h('div')
       },
     }
-    new Vue(Comp).$mount(root)
+    createApp(Comp).mount(root)
     //render(h(Comp), root);
     expect(fn).toHaveBeenCalledTimes(1)
   })
@@ -65,7 +64,7 @@ describe('api: lifecycle hooks', () => {
         return () => h('div', (count.value as unknown) as string)
       },
     }
-    const vm = new Vue(Comp).$mount()
+    const vm = createApp(Comp).mount()
     //render(h(Comp), root);
 
     count.value = 1
@@ -87,7 +86,7 @@ describe('api: lifecycle hooks', () => {
         return () => h('div', (count.value as unknown) as string)
       },
     }
-    const vm = new Vue(Comp).$mount()
+    const vm = createApp(Comp).mount()
     //render(h(Comp), root);
 
     count.value++
@@ -116,7 +115,7 @@ describe('api: lifecycle hooks', () => {
   //     },
   //   };
 
-  //   new Vue(Comp).$mount(root);
+  //   createApp(Comp).mount(root);
   //   //render(h(Comp), root);
 
   //   toggle.value = false;
@@ -145,7 +144,7 @@ describe('api: lifecycle hooks', () => {
   //     },
   //   };
 
-  //   new Vue(Comp).$mount(root);
+  //   createApp(Comp).mount(root);
   //   //render(h(Comp), root);
 
   //   toggle.value = false;
@@ -176,7 +175,7 @@ describe('api: lifecycle hooks', () => {
       },
     }
 
-    new Vue(Comp).$mount(root)
+    createApp(Comp).mount(root)
     //render(h(Comp), root);
 
     toggle.value = false
@@ -231,8 +230,8 @@ describe('api: lifecycle hooks', () => {
 
     // mount
     // render(h(Root), root);
-    const vm = new Vue(Root)
-    vm.$mount()
+    const vm = createApp(Root)
+    vm.mount()
     expect(calls).toEqual([
       'root onBeforeMount',
       'mid onBeforeMount',
@@ -262,7 +261,7 @@ describe('api: lifecycle hooks', () => {
 
     // unmount
     // render(null, root);
-    vm.$destroy()
+    vm.unmount()
     expect(calls).toEqual([
       'root onBeforeUnmount',
       'mid onBeforeUnmount',
diff --git a/test/v3/runtime-core/componentProxy.spec.ts b/test/v3/runtime-core/componentProxy.spec.ts
new file mode 100644
index 0000000..a05d82b
--- /dev/null
+++ b/test/v3/runtime-core/componentProxy.spec.ts
@@ -0,0 +1,226 @@
+import { createApp, getCurrentInstance, nextTick } from '../../../src'
+import { ComponentInternalInstance } from '../../../src/runtimeContext'
+import { mockWarn } from '../../helpers'
+
+describe('component: proxy', () => {
+  mockWarn(true)
+  test('data', () => {
+    let instance: ComponentInternalInstance
+    let instanceProxy: any
+    const Comp = {
+      data() {
+        return {
+          foo: 1,
+        }
+      },
+      setup() {
+        instance = getCurrentInstance()!
+      },
+      mounted() {
+        instanceProxy = this
+      },
+    }
+    createApp(Comp).mount(document.createElement('div'))
+
+    // render(h(Comp), nodeOps.createElement('div'))
+    expect(instanceProxy.foo).toBe(1)
+    instanceProxy.foo = 2
+    expect(instance!.data.foo).toBe(2)
+  })
+
+  test('public properties', async () => {
+    let instance: ComponentInternalInstance = undefined!
+    let instanceProxy: any = undefined!
+
+    const Comp = {
+      setup() {
+        instance = getCurrentInstance()!
+      },
+      mounted() {
+        instanceProxy = this
+      },
+      template: '<div></div>',
+    }
+    const Parent = {
+      components: {
+        Comp,
+      },
+      template: '<Comp/>',
+    }
+    const app = createApp(Parent).mount(document.createElement('div'))
+
+    expect(instanceProxy).toBeDefined()
+    expect(instanceProxy).toBe(instance!.proxy)
+    expect(instanceProxy.$data).toBe(instance!.data)
+    expect(instanceProxy.$props).toBe(instance!.props)
+    expect(instanceProxy.$attrs).toBe(instance!.attrs)
+    expect(instanceProxy.$slots).toBe(instance!.slots)
+    expect(instanceProxy.$refs).toBe(instance!.refs)
+    expect(instanceProxy.$parent).toBe(
+      instance!.parent && instance!.parent.proxy
+    )
+    expect(instanceProxy.$root).toBe(instance!.root.proxy)
+    expect(instanceProxy.$emit).toBe(instance!.emit)
+    expect(instance.isMounted).toBe(true)
+    expect(instance.isUnmounted).toBe(false)
+
+    // expect(instanceProxy.$el).toBe(instance!.vnode.el)
+    // expect(instanceProxy.$options).toBe(instance!.type)
+
+    instanceProxy.$data = {}
+    expect(
+      `[Vue warn]: Avoid replacing instance root $data. Use nested data properties instead.`
+    ).toHaveBeenWarned()
+
+    // const nextTickThis = await instanceProxy.$nextTick(function(this: any) {
+    //   return this
+    // })
+    // expect(nextTickThis).toBe(instanceProxy)
+
+    app.$destroy()
+
+    await nextTick()
+
+    expect(instance.isUnmounted).toBe(true)
+  })
+
+  // test('user attached properties', async () => {
+  //   let instance: ComponentInternalInstance
+  //   let instanceProxy: any
+  //   const Comp = {
+  //     setup() {
+  //       return () => null
+  //     },
+  //     mounted() {
+  //       instance = getCurrentInstance()!
+  //       instanceProxy = this
+  //     }
+  //   }
+  //   render(h(Comp), nodeOps.createElement('div'))
+  //   instanceProxy.foo = 1
+  //   expect(instanceProxy.foo).toBe(1)
+  //   expect(instance!.ctx.foo).toBe(1)
+
+  //   // should also allow properties that start with $
+  //   const obj = (instanceProxy.$store = {})
+  //   expect(instanceProxy.$store).toBe(obj)
+  //   expect(instance!.ctx.$store).toBe(obj)
+  // })
+
+  // test('globalProperties', () => {
+  //   let instance: ComponentInternalInstance
+  //   let instanceProxy: any
+  //   const Comp = {
+  //     setup() {
+  //       return () => null
+  //     },
+  //     mounted() {
+  //       instance = getCurrentInstance()!
+  //       instanceProxy = this
+  //     }
+  //   }
+
+  //   const app = createApp(Comp)
+  //   app.config.globalProperties.foo = 1
+  //   app.mount(nodeOps.createElement('div'))
+
+  //   expect(instanceProxy.foo).toBe(1)
+
+  //   // set should overwrite globalProperties with local
+  //   instanceProxy.foo = 2
+  //   // expect(instanceProxy.foo).toBe(2)
+  //   expect(instance!.ctx.foo).toBe(2)
+  //   // should not affect global
+  //   expect(app.config.globalProperties.foo).toBe(1)
+  // })
+
+  // test('has check', () => {
+  //   let instanceProxy: any
+  //   const Comp = {
+  //     render() {},
+  //     props: {
+  //       msg: String
+  //     },
+  //     data() {
+  //       return {
+  //         foo: 0
+  //       }
+  //     },
+  //     setup() {
+  //       return {
+  //         bar: 1
+  //       }
+  //     },
+  //     mounted() {
+  //       instanceProxy = this
+  //     }
+  //   }
+
+  //   const app = createApp(Comp, { msg: 'hello' })
+  //   app.config.globalProperties.global = 1
+
+  //   app.mount(nodeOps.createElement('div'))
+
+  //   // props
+  //   expect('msg' in instanceProxy).toBe(true)
+  //   // data
+  //   expect('foo' in instanceProxy).toBe(true)
+  //   // ctx
+  //   expect('bar' in instanceProxy).toBe(true)
+  //   // public properties
+  //   expect('$el' in instanceProxy).toBe(true)
+  //   // global properties
+  //   expect('global' in instanceProxy).toBe(true)
+
+  //   // non-existent
+  //   expect('$foobar' in instanceProxy).toBe(false)
+  //   expect('baz' in instanceProxy).toBe(false)
+
+  //   // set non-existent (goes into proxyTarget sink)
+  //   instanceProxy.baz = 1
+  //   expect('baz' in instanceProxy).toBe(true)
+
+  //   // dev mode ownKeys check for console inspection
+  //   // should only expose own keys
+  //   expect(Object.keys(instanceProxy)).toMatchObject([
+  //     'msg',
+  //     'bar',
+  //     'foo',
+  //     'baz'
+  //   ])
+  // })
+
+  // // #864
+  // test('should not warn declared but absent props', () => {
+  //   const Comp = {
+  //     props: ['test'],
+  //     render(this: any) {
+  //       return this.test
+  //     }
+  //   }
+  //   render(h(Comp), nodeOps.createElement('div'))
+  //   expect(
+  //     `was accessed during render but is not defined`
+  //   ).not.toHaveBeenWarned()
+  // })
+
+  // test('should allow symbol to access on render', () => {
+  //   const Comp = {
+  //     render() {
+  //       if ((this as any)[Symbol.unscopables]) {
+  //         return '1'
+  //       }
+  //       return '2'
+  //     }
+  //   }
+
+  //   const app = createApp(Comp)
+  //   app.mount(nodeOps.createElement('div'))
+
+  //   expect(
+  //     `Property ${JSON.stringify(
+  //       Symbol.unscopables
+  //     )} was accessed during render ` + `but is not defined on instance.`
+  //   ).toHaveBeenWarned()
+  // })
+})