]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: Initial devtools support (#1125)
authorGuillaume Chau <guillaume.b.chau@gmail.com>
Thu, 16 Jul 2020 22:18:52 +0000 (00:18 +0200)
committerGitHub <noreply@github.com>
Thu, 16 Jul 2020 22:18:52 +0000 (18:18 -0400)
15 files changed:
packages/compiler-sfc/src/compileTemplate.ts
packages/runtime-core/__tests__/componentProps.spec.ts
packages/runtime-core/__tests__/helpers/renderSlot.spec.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/devtools.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/server-renderer/__tests__/renderToStream.spec.ts
packages/vue/src/dev.ts [new file with mode: 0644]
packages/vue/src/devCheck.ts [deleted file]
packages/vue/src/index.ts
packages/vue/src/runtime.ts
test-dts/defineComponent.test-d.tsx
test-dts/ref.test-d.ts

index f33c86bf10fde351847c09610fdb7ea6bce36190..f4a55fb32dc3df369224d8ea88f529c7ca5a36b2 100644 (file)
@@ -57,7 +57,7 @@ export interface SFCTemplateCompileOptions {
    */
   transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean
 }
-  
+
 interface PreProcessor {
   render(
     source: string,
index 83fc0bcc998ef9581021ba63be6fec60a76ecde8..ff30044f70baf367cdcf8f1ed0d35bb53be8802c 100644 (file)
@@ -45,7 +45,7 @@ describe('component props', () => {
     render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4, barBaz: 5 }), root)
     expect(proxy.fooBar).toBe(3)
     expect(proxy.barBaz).toBe(5)
-    expect(props).toEqual({ fooBar: 3,barBaz: 5 })
+    expect(props).toEqual({ fooBar: 3, barBaz: 5 })
     expect(attrs).toEqual({ bar: 3, baz: 4 })
 
     render(h(Comp, { qux: 5 }), root)
index 3f5e321ec8f7a6f4cbea43070c85e519dc51f25c..f07c777283e8bdb0339e9944325e4ca218cd1263 100644 (file)
@@ -19,7 +19,7 @@ describe('renderSlot', () => {
   })
 
   it('should warn render ssr slot', () => {
-    renderSlot({ default: (a, b, c) => [h('child')] }, 'default')
+    renderSlot({ default: (_a, _b, _c) => [h('child')] }, 'default')
     expect('SSR-optimized slot function detected').toHaveBeenWarned()
   })
 })
index 8fb61324121c5c84351a69bd0d854bb2045b3bdc..d63b2b25a10f925ff2a25b4414a95b0f3b586c5e 100644 (file)
@@ -13,6 +13,7 @@ import { isFunction, NO, isObject } from '@vue/shared'
 import { warn } from './warning'
 import { createVNode, cloneVNode, VNode } from './vnode'
 import { RootHydrateFunction } from './hydration'
+import { initApp, appUnmounted } from './devtools'
 import { version } from '.'
 
 export interface App<HostElement = any> {
@@ -31,7 +32,7 @@ export interface App<HostElement = any> {
   unmount(rootContainer: HostElement | string): void
   provide<T>(key: InjectionKey<T> | string, value: T): this
 
-  // internal. We need to expose these for the server-renderer
+  // internal. We need to expose these for the server-renderer and devtools
   _component: Component
   _props: Data | null
   _container: HostElement | null
@@ -73,6 +74,9 @@ export interface AppContext {
   directives: Record<string, Directive>
   provides: Record<string | symbol, any>
   reload?: () => void // HMR only
+
+  // internal for devtools
+  __app?: App
 }
 
 type PluginInstallFunction = (app: App, ...options: any[]) => any
@@ -226,6 +230,9 @@ export function createAppAPI<HostElement>(
           }
           isMounted = true
           app._container = rootContainer
+
+          __DEV__ && initApp(app, version)
+
           return vnode.component!.proxy
         } else if (__DEV__) {
           warn(
@@ -240,6 +247,8 @@ export function createAppAPI<HostElement>(
       unmount() {
         if (isMounted) {
           render(null, app._container)
+
+          __DEV__ && appUnmounted(app)
         } else if (__DEV__) {
           warn(`Cannot unmount an app that is not mounted.`)
         }
@@ -260,6 +269,8 @@ export function createAppAPI<HostElement>(
       }
     }
 
+    context.__app = app
+
     return app
   }
 }
index a3a1127691146d34b893bd415fe81128d3d57343..c621d0f161fcae52a10b9be7dc076f258c6e66c5 100644 (file)
@@ -49,6 +49,7 @@ import {
   markAttrsAccessed
 } from './componentRenderUtils'
 import { startMeasure, endMeasure } from './profiling'
+import { componentAdded } from './devtools'
 
 export type Data = { [key: string]: unknown }
 
@@ -408,6 +409,9 @@ export function createComponentInstance(
   }
   instance.root = parent ? parent.root : instance
   instance.emit = emit.bind(null, instance)
+
+  __DEV__ && componentAdded(instance)
+
   return instance
 }
 
diff --git a/packages/runtime-core/src/devtools.ts b/packages/runtime-core/src/devtools.ts
new file mode 100644 (file)
index 0000000..cd1e6fe
--- /dev/null
@@ -0,0 +1,78 @@
+import { App } from './apiCreateApp'
+import { Fragment, Text, Comment, Static } from './vnode'
+import { ComponentInternalInstance } from './component'
+
+export interface AppRecord {
+  id: number
+  app: App
+  version: string
+  types: { [key: string]: string | Symbol }
+}
+
+enum DevtoolsHooks {
+  APP_INIT = 'app:init',
+  APP_UNMOUNT = 'app:unmount',
+  COMPONENT_UPDATED = 'component:updated',
+  COMPONENT_ADDED = 'component:added',
+  COMPONENT_REMOVED = 'component:removed'
+}
+
+export interface DevtoolsHook {
+  emit: (event: string, ...payload: any[]) => void
+  on: (event: string, handler: Function) => void
+  once: (event: string, handler: Function) => void
+  off: (event: string, handler: Function) => void
+  appRecords: AppRecord[]
+}
+
+export let devtools: DevtoolsHook
+
+export function setDevtoolsHook(hook: DevtoolsHook) {
+  devtools = hook
+}
+
+export function initApp(app: App, version: string) {
+  // TODO queue if devtools is undefined
+  if (!devtools) return
+  devtools.emit(DevtoolsHooks.APP_INIT, app, version, {
+    Fragment: Fragment,
+    Text: Text,
+    Comment: Comment,
+    Static: Static
+  })
+}
+
+export function appUnmounted(app: App) {
+  if (!devtools) return
+  devtools.emit(DevtoolsHooks.APP_UNMOUNT, app)
+}
+
+export function componentAdded(component: ComponentInternalInstance) {
+  if (!devtools || !component.appContext.__app) return
+  devtools.emit(
+    DevtoolsHooks.COMPONENT_ADDED,
+    component.appContext.__app,
+    component.uid,
+    component.parent ? component.parent.uid : undefined
+  )
+}
+
+export function componentUpdated(component: ComponentInternalInstance) {
+  if (!devtools || !component.appContext.__app) return
+  devtools.emit(
+    DevtoolsHooks.COMPONENT_UPDATED,
+    component.appContext.__app,
+    component.uid,
+    component.parent ? component.parent.uid : undefined
+  )
+}
+
+export function componentRemoved(component: ComponentInternalInstance) {
+  if (!devtools || !component.appContext.__app) return
+  devtools.emit(
+    DevtoolsHooks.COMPONENT_REMOVED,
+    component.appContext.__app,
+    component.uid,
+    component.parent ? component.parent.uid : undefined
+  )
+}
index c8f52ada7a06ab788c5ec5b92d2481fe23944536..c1066ac7cb1cf48d3913b3841493ee5b7c831024 100644 (file)
@@ -93,7 +93,10 @@ export {
   getTransitionRawChildren
 } from './components/BaseTransition'
 
-// Types -----------------------------------------------------------------------
+// For devtools
+export { devtools, setDevtoolsHook } from './devtools'
+
+// Types -------------------------------------------------------------------------
 
 import { VNode } from './vnode'
 import { ComponentInternalInstance } from './component'
index 895bf064afa5d02579bede79077406f5d5c7ab9c..8400ad538768131d5125c237a7deee7909559b70 100644 (file)
@@ -65,6 +65,7 @@ import { createHydrationFunctions, RootHydrateFunction } from './hydration'
 import { invokeDirectiveHook } from './directives'
 import { startMeasure, endMeasure } from './profiling'
 import { ComponentPublicInstance } from './componentProxy'
+import { componentRemoved, componentUpdated } from './devtools'
 
 export interface Renderer<HostElement = RendererElement> {
   render: RootRenderFunction<HostElement>
@@ -1417,6 +1418,7 @@ function baseCreateRenderer(
         }
         if (__DEV__) {
           popWarningContext()
+          componentUpdated(instance)
         }
       }
     }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
@@ -2068,6 +2070,8 @@ function baseCreateRenderer(
         parentSuspense.resolve()
       }
     }
+
+    __DEV__ && componentRemoved(instance)
   }
 
   const unmountChildren: UnmountChildrenFn = (
index 8d8e45409babced03ac56e1069b70d9baf0f9f9c..a679e9615345a8eac717e404016af4f0009bdf91 100644 (file)
@@ -84,7 +84,7 @@ describe('ssr: renderToStream', () => {
       expect(
         await renderToStream(
           createApp(
-            defineComponent((props: {}) => {
+            defineComponent(() => {
               const msg = ref('hello')
               return () => h('div', msg.value)
             })
@@ -266,7 +266,7 @@ describe('ssr: renderToStream', () => {
                   { msg: 'hello' },
                   {
                     // optimized slot using string push
-                    default: ({ msg }: any, push: any, p: any) => {
+                    default: ({ msg }: any, push: any) => {
                       push(`<span>${msg}</span>`)
                     },
                     // important to avoid slots being normalized
diff --git a/packages/vue/src/dev.ts b/packages/vue/src/dev.ts
new file mode 100644 (file)
index 0000000..f24c018
--- /dev/null
@@ -0,0 +1,16 @@
+import { version, setDevtoolsHook } from '@vue/runtime-dom'
+
+export function initDev() {
+  const target: any = __BROWSER__ ? window : global
+
+  target.__VUE__ = version
+  setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__)
+
+  if (__BROWSER__) {
+    // @ts-ignore `console.info` cannot be null error
+    console[console.info ? 'info' : 'log'](
+      `You are running a development build of Vue.\n` +
+        `Make sure to use the production build (*.prod.js) when deploying for production.`
+    )
+  }
+}
diff --git a/packages/vue/src/devCheck.ts b/packages/vue/src/devCheck.ts
deleted file mode 100644 (file)
index 07aaab4..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-if (__BROWSER__ && __DEV__) {
-  // @ts-ignore `console.info` cannot be null error
-  console[console.info ? 'info' : 'log'](
-    `You are running a development build of Vue.\n` +
-      `Make sure to use the production build (*.prod.js) when deploying for production.`
-  )
-}
index d6a472a2046ee08e7eb891922028302b9c44ee5d..a9ccf894ef9f398a9f1468209c5d35848d0774fe 100644 (file)
@@ -1,11 +1,13 @@
 // 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 './devCheck'
+import { initDev } from './dev'
 import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
 import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
 import * as runtimeDom from '@vue/runtime-dom'
 import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared'
 
+__DEV__ && initDev()
+
 const compileCache: Record<string, RenderFunction> = Object.create(null)
 
 function compileToFunction(
index fdbdd153d27be03cf09afd47602dd26336519ee8..b78d60c99357a9efde1b22ce71ff79f6a9ad048d 100644 (file)
@@ -1,8 +1,10 @@
 // This entry exports the runtime only, and is built as
 // `dist/vue.esm-bundler.js` which is used by default for bundlers.
-import './devCheck'
+import { initDev } from './dev'
 import { warn } from '@vue/runtime-dom'
 
+__DEV__ && initDev()
+
 export * from '@vue/runtime-dom'
 
 export const compile = () => {
index 9063ef5dab3fdff6d6cc9fb23125ed493eade5b2..6bfd968a7f99afc381fdd9e469cc89357d7badb4 100644 (file)
@@ -622,7 +622,7 @@ describe('emits', () => {
   defineComponent({
     emits: {
       click: (n: number) => typeof n === 'number',
-      input: (b: string) => null
+      input: (b: string) => b.length > 1
     },
     setup(props, { emit }) {
       emit('click', 1)
index 4fb519a423b443ac654c5ea81aa2fbd388b1b8b8..cafe21439c9adacdd24b3aadbd573768b8afdc91 100644 (file)
@@ -76,11 +76,13 @@ function bailType(arg: HTMLElement | Ref<HTMLElement>) {
   expectType<HTMLElement>(unref(arg))
 
   // ref inner type should be unwrapped
+  // eslint-disable-next-line no-restricted-globals
   const nestedRef = ref({ foo: ref(document.createElement('DIV')) })
 
   expectType<Ref<{ foo: HTMLElement }>>(nestedRef)
   expectType<{ foo: HTMLElement }>(nestedRef.value)
 }
+// eslint-disable-next-line no-restricted-globals
 const el = document.createElement('DIV')
 bailType(el)