observed.a = 2
expect(observed.a).toBe(1)
expect(dummy).toBe(1)
+ expect(`target is readonly`).toHaveBeenWarned()
})
it('should trigger effects when unlocked', () => {
observed[0].a = 2
expect(observed[0].a).toBe(1)
expect(dummy).toBe(1)
+ expect(`target is readonly`).toHaveBeenWarnedTimes(1)
observed[0] = { a: 2 }
expect(observed[0].a).toBe(1)
expect(dummy).toBe(1)
+ expect(`target is readonly`).toHaveBeenWarnedTimes(2)
})
it('should trigger effects when unlocked', () => {
--- /dev/null
+import {
+ createApp,
+ h,
+ nodeOps,
+ serializeInner,
+ mockWarn,
+ provide,
+ inject,
+ resolveComponent,
+ resolveDirective,
+ applyDirectives,
+ Plugin,
+ ref
+} from '@vue/runtime-test'
+
+describe('api: createApp', () => {
+ mockWarn()
+
+ test('mount', () => {
+ const Comp = {
+ props: {
+ count: {
+ default: 0
+ }
+ },
+ render() {
+ return this.count
+ }
+ }
+
+ const root1 = nodeOps.createElement('div')
+ createApp().mount(Comp, root1)
+ expect(serializeInner(root1)).toBe(`0`)
+
+ // mount with props
+ const root2 = nodeOps.createElement('div')
+ const app2 = createApp()
+ app2.mount(Comp, root2, { count: 1 })
+ expect(serializeInner(root2)).toBe(`1`)
+
+ // remount warning
+ const root3 = nodeOps.createElement('div')
+ app2.mount(Comp, root3)
+ expect(serializeInner(root3)).toBe(``)
+ expect(`already been mounted`).toHaveBeenWarned()
+ })
+
+ test('provide', () => {
+ const app = createApp()
+ app.provide('foo', 1)
+ app.provide('bar', 2)
+
+ const Root = {
+ setup() {
+ // test override
+ provide('foo', 3)
+ return () => h(Child)
+ }
+ }
+
+ const Child = {
+ setup() {
+ const foo = inject('foo')
+ const bar = inject('bar')
+ return () => `${foo},${bar}`
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ app.mount(Root, root)
+ expect(serializeInner(root)).toBe(`3,2`)
+ })
+
+ test('component', () => {
+ const app = createApp()
+ app.component('FooBar', () => 'foobar!')
+ app.component('BarBaz', () => 'barbaz!')
+
+ const Root = {
+ // local override
+ components: {
+ BarBaz: () => 'barbaz-local!'
+ },
+ setup() {
+ // resolve in setup
+ const FooBar = resolveComponent('foo-bar') as any
+ return () => {
+ // resolve in render
+ const BarBaz = resolveComponent('bar-baz') as any
+ return h('div', [h(FooBar), h(BarBaz)])
+ }
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ app.mount(Root, root)
+ expect(serializeInner(root)).toBe(`<div>foobar!barbaz-local!</div>`)
+ })
+
+ test('directive', () => {
+ const app = createApp()
+
+ const spy1 = jest.fn()
+ const spy2 = jest.fn()
+ const spy3 = jest.fn()
+ app.directive('FooBar', {
+ mounted: spy1
+ })
+ app.directive('BarBaz', {
+ mounted: spy2
+ })
+
+ const Root = {
+ // local override
+ directives: {
+ BarBaz: { mounted: spy3 }
+ },
+ setup() {
+ // resolve in setup
+ const FooBar = resolveDirective('foo-bar') as any
+ return () => {
+ // resolve in render
+ const BarBaz = resolveDirective('bar-baz') as any
+ return applyDirectives(h('div'), [[FooBar], [BarBaz]])
+ }
+ }
+ }
+
+ const root = nodeOps.createElement('div')
+ app.mount(Root, root)
+ expect(spy1).toHaveBeenCalled()
+ expect(spy2).not.toHaveBeenCalled()
+ expect(spy3).toHaveBeenCalled()
+ })
+
+ test('use', () => {
+ const PluginA: Plugin = app => app.provide('foo', 1)
+ const PluginB: Plugin = {
+ install: app => app.provide('bar', 2)
+ }
+
+ const app = createApp()
+ app.use(PluginA)
+ app.use(PluginB)
+
+ const Root = {
+ setup() {
+ const foo = inject('foo')
+ const bar = inject('bar')
+ return () => `${foo},${bar}`
+ }
+ }
+ const root = nodeOps.createElement('div')
+ app.mount(Root, root)
+ expect(serializeInner(root)).toBe(`1,2`)
+ })
+
+ test('config.errorHandler', () => {
+ const app = createApp()
+
+ const error = new Error()
+ const count = ref(0)
+
+ const handler = (app.config.errorHandler = jest.fn(
+ (err, instance, info) => {
+ expect(err).toBe(error)
+ expect((instance as any).count).toBe(count.value)
+ expect(info).toBe(`render function`)
+ }
+ ))
+
+ const Root = {
+ setup() {
+ const count = ref(0)
+ return {
+ count
+ }
+ },
+ render() {
+ throw error
+ }
+ }
+
+ app.mount(Root, nodeOps.createElement('div'))
+ expect(handler).toHaveBeenCalled()
+ })
+
+ test('config.warnHandler', () => {
+ const app = createApp()
+
+ const handler = (app.config.warnHandler = jest.fn(
+ (msg, instance, trace) => {}
+ ))
+
+ const Root = {
+ setup() {}
+ }
+
+ app.mount(Root, nodeOps.createElement('div'))
+ expect(handler).toHaveBeenCalled()
+ })
+
+ test.todo('mixin')
+
+ test.todo('config.optionsMergeStrategies')
+})
+++ /dev/null
-describe('api: createApp', () => {
- test('mount', () => {})
-
- test('provide', () => {})
-
- test('component', () => {})
-
- test('directive', () => {})
-
- test('use', () => {})
-
- test.todo('mixin')
-
- test('config.errorHandler', () => {})
-
- test('config.warnHandler', () => {})
-
- test.todo('config.optionsMergeStrategies')
-})
render() {
_prevVnode = _vnode
_vnode = applyDirectives(h('div', count.value), [
- {
- beforeMount,
- mounted,
- beforeUpdate,
- updated,
- beforeUnmount,
- unmounted
- },
- // value
- count.value,
- // argument
- 'foo',
- // modifiers
- { ok: true }
+ [
+ {
+ beforeMount,
+ mounted,
+ beforeUpdate,
+ updated,
+ beforeUnmount,
+ unmounted
+ },
+ // value
+ count.value,
+ // argument
+ 'foo',
+ // modifiers
+ { ok: true }
+ ]
])
return _vnode
}
performance: boolean
errorHandler?: (
err: Error,
- instance: ComponentRenderProxy,
+ instance: ComponentRenderProxy | null,
info: string
) => void
warnHandler?: (
msg: string,
- instance: ComponentRenderProxy,
+ instance: ComponentRenderProxy | null,
trace: string
) => void
}
type PluginInstallFunction = (app: App) => any
-type Plugin =
+export type Plugin =
| PluginInstallFunction
| {
install: PluginInstallFunction
return function createApp(): App {
const context = createAppContext()
+ let isMounted = false
+
const app: App = {
get config() {
return context.config
}
},
- mount(rootComponent, rootContainer, rootProps?: Data) {
- const vnode = createVNode(rootComponent, rootProps)
- // store app context on the root VNode.
- // this will be set on the root instance on initial mount.
- vnode.appContext = context
- render(vnode, rootContainer)
- return (vnode.component as ComponentInstance)
- .renderProxy as ComponentRenderProxy
+ mount(rootComponent, rootContainer, rootProps?: Data): any {
+ if (!isMounted) {
+ const vnode = createVNode(rootComponent, rootProps)
+ // store app context on the root VNode.
+ // this will be set on the root instance on initial mount.
+ vnode.appContext = context
+ render(vnode, rootContainer)
+ isMounted = true
+ return (vnode.component as ComponentInstance).renderProxy
+ } else if (__DEV__) {
+ warn(
+ `App has already been mounted. Create a new app instance instead.`
+ )
+ }
},
provide(key, value) {
if (instance) {
let camelized
let capitalized
+ let res
const local = (instance.type as any)[type]
- const global = instance.appContext[type]
- const res =
- local[name] ||
- local[(camelized = camelize(name))] ||
- local[(capitalized = capitalize(name))] ||
- global[name] ||
- global[camelized] ||
- global[capitalized]
+ if (local) {
+ res =
+ local[name] ||
+ local[(camelized = camelize(name))] ||
+ local[(capitalized = capitalize(camelized))]
+ }
+ if (!res) {
+ const global = instance.appContext[type]
+ res =
+ global[name] ||
+ global[camelized || (camelized = camelize(name))] ||
+ global[capitalized || capitalize(camelized)]
+ }
if (__DEV__ && !res) {
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
}
export function provide<T>(key: InjectionKey<T> | string, value: T) {
if (!currentInstance) {
if (__DEV__) {
- warn(`provide() is used without an active component instance.`)
+ warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
export function inject(key: InjectionKey<any> | string, defaultValue?: any) {
- if (!currentInstance) {
- // TODO warn
- } else {
- // TODO should also check for app-level provides
- const provides = currentInstance.parent && currentInstance.provides
- if (provides && key in provides) {
+ if (currentInstance) {
+ const provides = currentInstance.provides
+ if (key in provides) {
return provides[key as any] as any
} else if (defaultValue !== undefined) {
return defaultValue
} else if (__DEV__) {
warn(`injection "${key}" not found.`)
}
+ } else if (__DEV__) {
+ warn(`inject() can only be used inside setup().`)
}
}
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
-import { AppContext, createAppContext, resolveAsset } from './apiCreateApp'
+import { AppContext, createAppContext, resolveAsset } from './apiApp'
+import { Directive } from './directives'
export type Data = { [key: string]: unknown }
} & P &
S
-type SetupFunction<Props, RawBindings> = (
- props: Props,
- ctx: SetupContext
-) => RawBindings | (() => VNodeChild)
-
type RenderFunction<Props = {}, RawBindings = {}> = <
Bindings extends UnwrapRef<RawBindings>
>(
this: ComponentRenderProxy<Props, Bindings>
) => VNodeChild
-interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
- props?: undefined
- setup?: SetupFunction<Props, RawBindings>
+interface ComponentOptionsBase<Props, RawBindings> {
+ setup?: (
+ props: Props,
+ ctx: SetupContext
+ ) => RawBindings | (() => VNodeChild) | void
render?: RenderFunction<Props, RawBindings>
+ components?: Record<string, Component>
+ directives?: Record<string, Directive>
+ // TODO full 2.x options compat
+}
+
+interface ComponentOptionsWithoutProps<Props = {}, RawBindings = {}>
+ extends ComponentOptionsBase<Props, RawBindings> {
+ props?: undefined
}
interface ComponentOptionsWithArrayProps<
PropNames extends string = string,
- RawBindings = Data,
+ RawBindings = {},
Props = { [key in PropNames]?: unknown }
-> {
+> extends ComponentOptionsBase<Props, RawBindings> {
props: PropNames[]
- setup?: SetupFunction<Props, RawBindings>
- render?: RenderFunction<Props, RawBindings>
}
interface ComponentOptionsWithProps<
PropsOptions = ComponentPropsOptions,
- RawBindings = Data,
+ RawBindings = {},
Props = ExtractPropTypes<PropsOptions>
-> {
+> extends ComponentOptionsBase<Props, RawBindings> {
props: PropsOptions
- setup?: SetupFunction<Props, RawBindings>
- render?: RenderFunction<Props, RawBindings>
}
export type ComponentOptions =
emit: ((event: string, ...args: unknown[]) => void)
}
-export type ComponentInstance<P = Data, S = Data> = {
+export type ComponentInstance<P = {}, S = {}> = {
type: FunctionalComponent | ComponentOptions
parent: ComponentInstance | null
appContext: AppContext
vnode: VNode,
parent: ComponentInstance | null
): ComponentInstance {
+ // inherit parent app context - or - if root, adopt from root vnode
+ const appContext =
+ (parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance = {
vnode,
parent,
- // inherit parent app context - or - if root, adopt from root vnode
- appContext:
- (parent ? parent.appContext : vnode.appContext) || emptyAppContext,
+ appContext,
type: vnode.type as any,
root: null as any, // set later so it can point to itself
next: null,
propsProxy: null,
setupContext: null,
effects: null,
- provides: parent ? parent.provides : {},
+ provides: parent ? parent.provides : Object.create(appContext.provides),
// setup context properties
data: EMPTY_OBJ,
const foo = resolveDirective('foo')
const bar = resolveDirective('bar')
-return applyDirectives(
- h(comp),
+return applyDirectives(h(comp), [
[foo, this.x],
[bar, this.y]
-)
+])
*/
import { VNode, cloneVNode } from './vnode'
} from './component'
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
import { HostNode } from './createRenderer'
-import { resolveAsset } from './apiCreateApp'
+import { resolveAsset } from './apiApp'
export interface DirectiveBinding {
instance: ComponentRenderProxy | null
| [Directive, any, string, DirectiveModifiers]
>
-export function applyDirectives(
- vnode: VNode,
- ...directives: DirectiveArguments
-) {
+export function applyDirectives(vnode: VNode, directives: DirectiveArguments) {
const instance = currentRenderingInstance
if (instance !== null) {
vnode = cloneVNode(vnode)
NATIVE_EVENT_HANDLER,
COMPONENT_EVENT_HANDLER,
DIRECTIVE_HOOK,
+ APP_ERROR_HANDLER,
+ APP_WARN_HANDLER,
SCHEDULER
}
[ErrorTypes.NATIVE_EVENT_HANDLER]: 'native event handler',
[ErrorTypes.COMPONENT_EVENT_HANDLER]: 'component event handler',
[ErrorTypes.DIRECTIVE_HOOK]: 'directive hook',
+ [ErrorTypes.APP_ERROR_HANDLER]: 'app errorHandler',
+ [ErrorTypes.APP_WARN_HANDLER]: 'app warnHandler',
[ErrorTypes.SCHEDULER]:
'scheduler flush. This may be a Vue internals bug. ' +
'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue'
type: AllErrorTypes
) {
const contextVNode = instance ? instance.vnode : null
- let cur: ComponentInstance | null = instance && instance.parent
- while (cur) {
- const errorCapturedHooks = cur.ec
- if (errorCapturedHooks !== null) {
- for (let i = 0; i < errorCapturedHooks.length; i++) {
- if (
- errorCapturedHooks[i](
- err,
- instance && instance.renderProxy,
- // in production the hook receives only the error code
- __DEV__ ? ErrorTypeStrings[type] : type
- )
- ) {
- return
+ if (instance) {
+ let cur: ComponentInstance | null = instance.parent
+ // the exposed instance is the render proxy to keep it consistent with 2.x
+ const exposedInstance = instance.renderProxy
+ // in production the hook receives only the error code
+ const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
+ while (cur) {
+ const errorCapturedHooks = cur.ec
+ if (errorCapturedHooks !== null) {
+ for (let i = 0; i < errorCapturedHooks.length; i++) {
+ if (errorCapturedHooks[i](err, exposedInstance, errorInfo)) {
+ return
+ }
}
}
+ cur = cur.parent
+ }
+ // app-level handling
+ const appErrorHandler = instance.appContext.config.errorHandler
+ if (appErrorHandler) {
+ callWithErrorHandling(
+ appErrorHandler,
+ null,
+ ErrorTypes.APP_ERROR_HANDLER,
+ [err, exposedInstance, errorInfo]
+ )
+ return
}
- cur = cur.parent
}
logError(err, type, contextVNode)
}
export { getCurrentInstance } from './component'
// For custom renderers
-export { createAppAPI } from './apiCreateApp'
+export { createAppAPI } from './apiApp'
export { createRenderer } from './createRenderer'
export {
handleError,
// Types -----------------------------------------------------------------------
-export { App } from './apiCreateApp'
-export { VNode } from './vnode'
+export { App, AppConfig, AppContext, Plugin } from './apiApp'
+export { VNode, VNodeTypes } from './vnode'
export { FunctionalComponent, ComponentInstance } from './component'
export { RendererOptions } from './createRenderer'
export { Slot, Slots } from './componentSlots'
import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags'
import { isReactive } from '@vue/reactivity'
-import { AppContext } from './apiCreateApp'
+import { AppContext } from './apiApp'
export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text')
}
export function warn(msg: string, ...args: any[]) {
- // TODO app level warn handler
+ const instance = stack.length ? stack[stack.length - 1].component : null
+ const appWarnHandler = instance && instance.appContext.config.warnHandler
+ const trace = getComponentTrace()
+
+ if (appWarnHandler) {
+ appWarnHandler(
+ msg + args.join(''),
+ instance && instance.renderProxy,
+ formatTrace(trace).join('')
+ )
+ return
+ }
+
console.warn(`[Vue warn]: ${msg}`, ...args)
// avoid spamming console during tests
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
return
}
- const trace = getComponentTrace()
if (!trace.length) {
return
}
console.log(...logs)
console.groupEnd()
} else {
- const logs: string[] = []
- trace.forEach((entry, i) => {
- const formatted = formatTraceEntry(entry, i)
- if (i === 0) {
- logs.push('at', ...formatted)
- } else {
- logs.push('\n', ...formatted)
- }
- })
- console.log(...logs)
+ console.log(...formatTrace(trace))
}
}
return normlaizedStack
}
+function formatTrace(trace: ComponentTraceStack): string[] {
+ const logs: string[] = []
+ trace.forEach((entry, i) => {
+ const formatted = formatTraceEntry(entry, i)
+ if (i === 0) {
+ logs.push('at', ...formatted)
+ } else {
+ logs.push('\n', ...formatted)
+ }
+ })
+ return logs
+}
+
function formatTraceEntry(
{ vnode, recurseCount }: TraceEntry,
depth: number = 0