test: test for app-level APIs

This commit is contained in:
Evan You
2019-09-03 18:11:04 -04:00
parent 1e4535dc78
commit 98d1406214
12 changed files with 353 additions and 120 deletions

View File

@@ -36,12 +36,12 @@ export interface AppConfig {
performance: boolean
errorHandler?: (
err: Error,
instance: ComponentRenderProxy,
instance: ComponentRenderProxy | null,
info: string
) => void
warnHandler?: (
msg: string,
instance: ComponentRenderProxy,
instance: ComponentRenderProxy | null,
trace: string
) => void
}
@@ -56,7 +56,7 @@ export interface AppContext {
type PluginInstallFunction = (app: App) => any
type Plugin =
export type Plugin =
| PluginInstallFunction
| {
install: PluginInstallFunction
@@ -82,6 +82,8 @@ export function createAppAPI(render: RootRenderFunction): () => App {
return function createApp(): App {
const context = createAppContext()
let isMounted = false
const app: App = {
get config() {
return context.config
@@ -134,14 +136,20 @@ export function createAppAPI(render: RootRenderFunction): () => App {
}
},
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) {
@@ -164,15 +172,21 @@ export function resolveAsset(type: 'components' | 'directives', name: string) {
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}`)
}

View File

@@ -6,7 +6,7 @@ export interface InjectionKey<T> extends Symbol {}
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
@@ -27,17 +27,16 @@ export function provide<T>(key: InjectionKey<T> | string, value: T) {
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().`)
}
}

View File

@@ -13,7 +13,8 @@ import {
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 }
@@ -31,41 +32,42 @@ export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
} & 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 =
@@ -105,7 +107,7 @@ interface SetupContext {
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
@@ -193,12 +195,13 @@ export function createComponentInstance(
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,
@@ -209,7 +212,7 @@ export function createComponentInstance(
propsProxy: null,
setupContext: null,
effects: null,
provides: parent ? parent.provides : {},
provides: parent ? parent.provides : Object.create(appContext.provides),
// setup context properties
data: EMPTY_OBJ,

View File

@@ -5,11 +5,10 @@ const comp = resolveComponent('comp')
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'
@@ -22,7 +21,7 @@ import {
} 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
@@ -103,10 +102,7 @@ type DirectiveArguments = Array<
| [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)

View File

@@ -13,6 +13,8 @@ export const enum ErrorTypes {
NATIVE_EVENT_HANDLER,
COMPONENT_EVENT_HANDLER,
DIRECTIVE_HOOK,
APP_ERROR_HANDLER,
APP_WARN_HANDLER,
SCHEDULER
}
@@ -38,6 +40,8 @@ export const ErrorTypeStrings: Record<number | string, string> = {
[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'
@@ -81,24 +85,34 @@ export function handleError(
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)
}

View File

@@ -28,7 +28,7 @@ export { PublicShapeFlags as ShapeFlags } from './shapeFlags'
export { getCurrentInstance } from './component'
// For custom renderers
export { createAppAPI } from './apiCreateApp'
export { createAppAPI } from './apiApp'
export { createRenderer } from './createRenderer'
export {
handleError,
@@ -42,8 +42,8 @@ export { applyDirectives, resolveDirective } from './directives'
// 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'

View File

@@ -12,7 +12,7 @@ import { RawSlots } 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')

View File

@@ -21,13 +21,24 @@ export function popWarningContext() {
}
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
}
@@ -41,16 +52,7 @@ export function warn(msg: string, ...args: any[]) {
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))
}
}
@@ -83,6 +85,19 @@ function getComponentTrace(): ComponentTraceStack {
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