feat: Initial devtools support (#1125)

This commit is contained in:
Guillaume Chau 2020-07-17 00:18:52 +02:00 committed by GitHub
parent 5ed73cd874
commit 568b6db12b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 132 additions and 17 deletions

View File

@ -45,7 +45,7 @@ describe('component props', () => {
render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4, barBaz: 5 }), root) render(h(Comp, { 'foo-bar': 3, bar: 3, baz: 4, barBaz: 5 }), root)
expect(proxy.fooBar).toBe(3) expect(proxy.fooBar).toBe(3)
expect(proxy.barBaz).toBe(5) 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 }) expect(attrs).toEqual({ bar: 3, baz: 4 })
render(h(Comp, { qux: 5 }), root) render(h(Comp, { qux: 5 }), root)

View File

@ -19,7 +19,7 @@ describe('renderSlot', () => {
}) })
it('should warn render ssr slot', () => { 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() expect('SSR-optimized slot function detected').toHaveBeenWarned()
}) })
}) })

View File

@ -13,6 +13,7 @@ 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 { initApp, appUnmounted } from './devtools'
import { version } from '.' import { version } from '.'
export interface App<HostElement = any> { export interface App<HostElement = any> {
@ -31,7 +32,7 @@ export interface App<HostElement = any> {
unmount(rootContainer: HostElement | string): void unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this 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 _component: Component
_props: Data | null _props: Data | null
_container: HostElement | null _container: HostElement | null
@ -73,6 +74,9 @@ export interface AppContext {
directives: Record<string, Directive> directives: Record<string, Directive>
provides: Record<string | symbol, any> provides: Record<string | symbol, any>
reload?: () => void // HMR only reload?: () => void // HMR only
// internal for devtools
__app?: App
} }
type PluginInstallFunction = (app: App, ...options: any[]) => any type PluginInstallFunction = (app: App, ...options: any[]) => any
@ -226,6 +230,9 @@ export function createAppAPI<HostElement>(
} }
isMounted = true isMounted = true
app._container = rootContainer app._container = rootContainer
__DEV__ && initApp(app, version)
return vnode.component!.proxy return vnode.component!.proxy
} else if (__DEV__) { } else if (__DEV__) {
warn( warn(
@ -240,6 +247,8 @@ export function createAppAPI<HostElement>(
unmount() { unmount() {
if (isMounted) { if (isMounted) {
render(null, app._container) render(null, app._container)
__DEV__ && appUnmounted(app)
} else if (__DEV__) { } else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`) warn(`Cannot unmount an app that is not mounted.`)
} }
@ -260,6 +269,8 @@ export function createAppAPI<HostElement>(
} }
} }
context.__app = app
return app return app
} }
} }

View File

@ -49,6 +49,7 @@ import {
markAttrsAccessed markAttrsAccessed
} from './componentRenderUtils' } from './componentRenderUtils'
import { startMeasure, endMeasure } from './profiling' import { startMeasure, endMeasure } from './profiling'
import { componentAdded } from './devtools'
export type Data = { [key: string]: unknown } export type Data = { [key: string]: unknown }
@ -408,6 +409,9 @@ export function createComponentInstance(
} }
instance.root = parent ? parent.root : instance instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance) instance.emit = emit.bind(null, instance)
__DEV__ && componentAdded(instance)
return instance return instance
} }

View File

@ -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
)
}

View File

@ -93,7 +93,10 @@ export {
getTransitionRawChildren getTransitionRawChildren
} from './components/BaseTransition' } from './components/BaseTransition'
// Types ----------------------------------------------------------------------- // For devtools
export { devtools, setDevtoolsHook } from './devtools'
// Types -------------------------------------------------------------------------
import { VNode } from './vnode' import { VNode } from './vnode'
import { ComponentInternalInstance } from './component' import { ComponentInternalInstance } from './component'

View File

@ -65,6 +65,7 @@ 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 './componentProxy' import { ComponentPublicInstance } from './componentProxy'
import { componentRemoved, componentUpdated } from './devtools'
export interface Renderer<HostElement = RendererElement> { export interface Renderer<HostElement = RendererElement> {
render: RootRenderFunction<HostElement> render: RootRenderFunction<HostElement>
@ -1417,6 +1418,7 @@ function baseCreateRenderer(
} }
if (__DEV__) { if (__DEV__) {
popWarningContext() popWarningContext()
componentUpdated(instance)
} }
} }
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions) }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
@ -2068,6 +2070,8 @@ function baseCreateRenderer(
parentSuspense.resolve() parentSuspense.resolve()
} }
} }
__DEV__ && componentRemoved(instance)
} }
const unmountChildren: UnmountChildrenFn = ( const unmountChildren: UnmountChildrenFn = (

View File

@ -84,7 +84,7 @@ describe('ssr: renderToStream', () => {
expect( expect(
await renderToStream( await renderToStream(
createApp( createApp(
defineComponent((props: {}) => { defineComponent(() => {
const msg = ref('hello') const msg = ref('hello')
return () => h('div', msg.value) return () => h('div', msg.value)
}) })
@ -266,7 +266,7 @@ describe('ssr: renderToStream', () => {
{ msg: 'hello' }, { msg: 'hello' },
{ {
// optimized slot using string push // optimized slot using string push
default: ({ msg }: any, push: any, p: any) => { default: ({ msg }: any, push: any) => {
push(`<span>${msg}</span>`) push(`<span>${msg}</span>`)
}, },
// important to avoid slots being normalized // important to avoid slots being normalized

16
packages/vue/src/dev.ts Normal file
View File

@ -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.`
)
}
}

View File

@ -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.`
)
}

View File

@ -1,11 +1,13 @@
// This entry is the "full-build" that includes both the runtime // This entry is the "full-build" that includes both the runtime
// and the compiler, and supports on-the-fly compilation of the template option. // 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 { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom' import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom' import * as runtimeDom from '@vue/runtime-dom'
import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared' import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared'
__DEV__ && initDev()
const compileCache: Record<string, RenderFunction> = Object.create(null) const compileCache: Record<string, RenderFunction> = Object.create(null)
function compileToFunction( function compileToFunction(

View File

@ -1,8 +1,10 @@
// This entry exports the runtime only, and is built as // This entry exports the runtime only, and is built as
// `dist/vue.esm-bundler.js` which is used by default for bundlers. // `dist/vue.esm-bundler.js` which is used by default for bundlers.
import './devCheck' import { initDev } from './dev'
import { warn } from '@vue/runtime-dom' import { warn } from '@vue/runtime-dom'
__DEV__ && initDev()
export * from '@vue/runtime-dom' export * from '@vue/runtime-dom'
export const compile = () => { export const compile = () => {

View File

@ -622,7 +622,7 @@ describe('emits', () => {
defineComponent({ defineComponent({
emits: { emits: {
click: (n: number) => typeof n === 'number', click: (n: number) => typeof n === 'number',
input: (b: string) => null input: (b: string) => b.length > 1
}, },
setup(props, { emit }) { setup(props, { emit }) {
emit('click', 1) emit('click', 1)

View File

@ -76,11 +76,13 @@ function bailType(arg: HTMLElement | Ref<HTMLElement>) {
expectType<HTMLElement>(unref(arg)) expectType<HTMLElement>(unref(arg))
// ref inner type should be unwrapped // ref inner type should be unwrapped
// eslint-disable-next-line no-restricted-globals
const nestedRef = ref({ foo: ref(document.createElement('DIV')) }) const nestedRef = ref({ foo: ref(document.createElement('DIV')) })
expectType<Ref<{ foo: HTMLElement }>>(nestedRef) expectType<Ref<{ foo: HTMLElement }>>(nestedRef)
expectType<{ foo: HTMLElement }>(nestedRef.value) expectType<{ foo: HTMLElement }>(nestedRef.value)
} }
// eslint-disable-next-line no-restricted-globals
const el = document.createElement('DIV') const el = document.createElement('DIV')
bailType(el) bailType(el)