diff --git a/packages/compiler-sfc/src/compileTemplate.ts b/packages/compiler-sfc/src/compileTemplate.ts index f33c86bf..f4a55fb3 100644 --- a/packages/compiler-sfc/src/compileTemplate.ts +++ b/packages/compiler-sfc/src/compileTemplate.ts @@ -57,7 +57,7 @@ export interface SFCTemplateCompileOptions { */ transformAssetUrls?: AssetURLOptions | AssetURLTagConfig | boolean } - + interface PreProcessor { render( source: string, diff --git a/packages/runtime-core/__tests__/componentProps.spec.ts b/packages/runtime-core/__tests__/componentProps.spec.ts index 83fc0bcc..ff30044f 100644 --- a/packages/runtime-core/__tests__/componentProps.spec.ts +++ b/packages/runtime-core/__tests__/componentProps.spec.ts @@ -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) diff --git a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts index 3f5e321e..f07c7772 100644 --- a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts +++ b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts @@ -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() }) }) diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 8fb61324..d63b2b25 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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 { @@ -31,7 +32,7 @@ export interface App { unmount(rootContainer: HostElement | string): void provide(key: InjectionKey | 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 provides: Record reload?: () => void // HMR only + + // internal for devtools + __app?: App } type PluginInstallFunction = (app: App, ...options: any[]) => any @@ -226,6 +230,9 @@ export function createAppAPI( } isMounted = true app._container = rootContainer + + __DEV__ && initApp(app, version) + return vnode.component!.proxy } else if (__DEV__) { warn( @@ -240,6 +247,8 @@ export function createAppAPI( 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( } } + context.__app = app + return app } } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index a3a11276..c621d0f1 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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 index 00000000..cd1e6fec --- /dev/null +++ b/packages/runtime-core/src/devtools.ts @@ -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 + ) +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index c8f52ada..c1066ac7 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 895bf064..8400ad53 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -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 { render: RootRenderFunction @@ -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 = ( diff --git a/packages/server-renderer/__tests__/renderToStream.spec.ts b/packages/server-renderer/__tests__/renderToStream.spec.ts index 8d8e4540..a679e961 100644 --- a/packages/server-renderer/__tests__/renderToStream.spec.ts +++ b/packages/server-renderer/__tests__/renderToStream.spec.ts @@ -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(`${msg}`) }, // important to avoid slots being normalized diff --git a/packages/vue/src/dev.ts b/packages/vue/src/dev.ts new file mode 100644 index 00000000..f24c0187 --- /dev/null +++ b/packages/vue/src/dev.ts @@ -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 index 07aaab4a..00000000 --- a/packages/vue/src/devCheck.ts +++ /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.` - ) -} diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index d6a472a2..a9ccf894 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -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 = Object.create(null) function compileToFunction( diff --git a/packages/vue/src/runtime.ts b/packages/vue/src/runtime.ts index fdbdd153..b78d60c9 100644 --- a/packages/vue/src/runtime.ts +++ b/packages/vue/src/runtime.ts @@ -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 = () => { diff --git a/test-dts/defineComponent.test-d.tsx b/test-dts/defineComponent.test-d.tsx index 9063ef5d..6bfd968a 100644 --- a/test-dts/defineComponent.test-d.tsx +++ b/test-dts/defineComponent.test-d.tsx @@ -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) diff --git a/test-dts/ref.test-d.ts b/test-dts/ref.test-d.ts index 4fb519a4..cafe2143 100644 --- a/test-dts/ref.test-d.ts +++ b/test-dts/ref.test-d.ts @@ -76,11 +76,13 @@ function bailType(arg: HTMLElement | Ref) { expectType(unref(arg)) // ref inner type should be unwrapped + // eslint-disable-next-line no-restricted-globals const nestedRef = ref({ foo: ref(document.createElement('DIV')) }) expectType>(nestedRef) expectType<{ foo: HTMLElement }>(nestedRef.value) } +// eslint-disable-next-line no-restricted-globals const el = document.createElement('DIV') bailType(el)