Evan You 54727f9874 feat: provide ability to overwrite feature flags in esm-bundler builds
e.g. by replacing `__VUE_OPTIONS_API__` to `false` using webpack's
`DefinePlugin`, the final bundle will drop all code supporting the
options API.

This does not break existing usage, but requires the user to explicitly
configure the feature flags via bundlers to properly tree-shake the
disabled branches. As a result, users will see a console warning if
the flags have not been properly configured.
2020-07-20 21:51:30 -04:00

741 lines
18 KiB
TypeScript

import { VNode, VNodeChild, isVNode } from './vnode'
import {
reactive,
ReactiveEffect,
pauseTracking,
resetTracking,
shallowReadonly
} from '@vue/reactivity'
import {
CreateComponentPublicInstance,
ComponentPublicInstance,
PublicInstanceProxyHandlers,
RuntimeCompiledPublicInstanceProxyHandlers,
createRenderContext,
exposePropsOnRenderContext,
exposeSetupStateOnRenderContext
} from './componentProxy'
import {
ComponentPropsOptions,
NormalizedPropsOptions,
initProps
} from './componentProps'
import { Slots, initSlots, InternalSlots } from './componentSlots'
import { warn } from './warning'
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
import { Directive, validateDirectiveName } from './directives'
import { applyOptions, ComponentOptions } from './componentOptions'
import {
EmitsOptions,
ObjectEmitsOptions,
EmitFn,
emit
} from './componentEmits'
import {
EMPTY_OBJ,
isFunction,
NOOP,
isObject,
NO,
makeMap,
isPromise,
ShapeFlags
} from '@vue/shared'
import { SuspenseBoundary } from './components/Suspense'
import { CompilerOptions } from '@vue/compiler-core'
import {
currentRenderingInstance,
markAttrsAccessed
} from './componentRenderUtils'
import { startMeasure, endMeasure } from './profiling'
import { devtoolsComponentAdded } from './devtools'
export type Data = Record<string, unknown>
/**
* For extending allowed non-declared props on components in TSX
*/
export interface ComponentCustomProps {}
/**
* Default allowed non-declared props on ocmponent in TSX
*/
export interface AllowedComponentProps {
class?: unknown
style?: unknown
}
// Note: can't mark this whole interface internal because some public interfaces
// extend it.
export interface ComponentInternalOptions {
/**
* @internal
*/
__props?: NormalizedPropsOptions | []
/**
* @internal
*/
__emits?: ObjectEmitsOptions
/**
* @internal
*/
__scopeId?: string
/**
* @internal
*/
__cssModules?: Data
/**
* @internal
*/
__hmrId?: string
/**
* This one should be exposed so that devtools can make use of it
*/
__file?: string
}
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: SetupContext<E>): any
props?: ComponentPropsOptions<P>
emits?: E | (keyof E)[]
inheritAttrs?: boolean
displayName?: string
}
export interface ClassComponent {
new (...args: any[]): ComponentPublicInstance<any, any, any, any, any>
__vccOpts: ComponentOptions
}
export type Component = ComponentOptions | FunctionalComponent<any>
// A type used in public APIs where a component type is expected.
// The constructor type is an artificial type returned by defineComponent().
export type PublicAPIComponent =
| Component
| {
new (...args: any[]): CreateComponentPublicInstance<
any,
any,
any,
any,
any
>
}
export { ComponentOptions }
type LifecycleHook = Function[] | null
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec'
}
export interface SetupContext<E = EmitsOptions> {
attrs: Data
slots: Slots
emit: EmitFn<E>
}
/**
* @internal
*/
export type InternalRenderFunction = {
(
ctx: ComponentPublicInstance,
cache: ComponentInternalInstance['renderCache'],
// for compiler-optimized bindings
$props: ComponentInternalInstance['props'],
$setup: ComponentInternalInstance['setupState'],
$data: ComponentInternalInstance['data'],
$options: ComponentInternalInstance['ctx']
): VNodeChild
_rc?: boolean // isRuntimeCompiled
}
/**
* We expose a subset of properties on the internal instance as they are
* useful for advanced external libraries and tools.
*/
export interface ComponentInternalInstance {
uid: number
type: Component
parent: ComponentInternalInstance | null
root: ComponentInternalInstance
appContext: AppContext
/**
* Vnode representing this component in its parent's vdom tree
*/
vnode: VNode
/**
* The pending new vnode from parent updates
* @internal
*/
next: VNode | null
/**
* Root vnode of this component's own vdom tree
*/
subTree: VNode
/**
* The reactive effect for rendering and patching the component. Callable.
*/
update: ReactiveEffect
/**
* The render function that returns vdom tree.
* @internal
*/
render: InternalRenderFunction | null
/**
* Object containing values this component provides for its descendents
* @internal
*/
provides: Data
/**
* Tracking reactive effects (e.g. watchers) associated with this component
* so that they can be automatically stopped on component unmount
* @internal
*/
effects: ReactiveEffect[] | null
/**
* cache for proxy access type to avoid hasOwnProperty calls
* @internal
*/
accessCache: Data | null
/**
* cache for render function values that rely on _ctx but won't need updates
* after initialized (e.g. inline handlers)
* @internal
*/
renderCache: (Function | VNode)[]
/**
* Asset hashes that prototypally inherits app-level asset hashes for fast
* resolution
* @internal
*/
components: Record<string, Component>
/**
* @internal
*/
directives: Record<string, Directive>
// the rest are only for stateful components ---------------------------------
// main proxy that serves as the public instance (`this`)
proxy: ComponentPublicInstance | null
/**
* alternative proxy used only for runtime-compiled render functions using
* `with` block
* @internal
*/
withProxy: ComponentPublicInstance | null
/**
* This is the target for the public instance proxy. It also holds properties
* injected by user options (computed, methods etc.) and user-attached
* custom properties (via `this.x = ...`)
* @internal
*/
ctx: Data
// internal state
data: Data
props: Data
attrs: Data
slots: InternalSlots
refs: Data
emit: EmitFn
// used for keeping track of .once event handlers on components
emitted: Record<string, boolean> | null
/**
* setup related
* @internal
*/
setupState: Data
/**
* @internal
*/
setupContext: SetupContext | null
/**
* suspense related
* @internal
*/
suspense: SuspenseBoundary | null
/**
* @internal
*/
asyncDep: Promise<any> | null
/**
* @internal
*/
asyncResolved: boolean
// lifecycle
isMounted: boolean
isUnmounted: boolean
isDeactivated: boolean
/**
* @internal
*/
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.CREATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.MOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UPDATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.UNMOUNTED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.DEACTIVATED]: LifecycleHook
/**
* @internal
*/
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
}
const emptyAppContext = createAppContext()
let uid = 0
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
uid: uid++,
vnode,
parent,
appContext,
type: vnode.type as Component,
root: null!, // to be immediately set
next: null,
subTree: null!, // will be set synchronously right after creation
update: null!, // will be set synchronously right after creation
render: null,
proxy: null,
withProxy: null,
effects: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null!,
renderCache: [],
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
// per-instance asset storage (mutable during options resolution)
components: Object.create(appContext.components),
directives: Object.create(appContext.directives),
// suspense related
suspense,
asyncDep: null,
asyncResolved: false,
// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
emit: null as any, // to be set immediately
emitted: null
}
if (__DEV__) {
instance.ctx = createRenderContext(instance)
} else {
instance.ctx = { _: instance }
}
instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
return instance
}
export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
export const setCurrentInstance = (
instance: ComponentInternalInstance | null
) => {
currentInstance = instance
}
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
export function validateComponentName(name: string, config: AppConfig) {
const appIsNativeTag = config.isNativeTag || NO
if (isBuiltInTag(name) || appIsNativeTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component id: ' + name
)
}
}
export let isInSSRComponentSetup = false
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children, shapeFlag } = instance.vnode
const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config)
}
}
if (Component.directives) {
const names = Object.keys(Component.directives)
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i])
}
}
}
// 0. create render proxy property access cache
instance.accessCache = {}
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
if (isPromise(setupResult)) {
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup returned an inline render function
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
if (__DEV__ && isVNode(setupResult)) {
warn(
`setup() should not return VNodes directly - ` +
`return a render function instead.`
)
}
// setup returned bindings.
// assuming a render function compiled from template is present.
instance.setupState = reactive(setupResult)
if (__DEV__) {
exposeSetupStateOnRenderContext(instance)
}
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? 'null' : typeof setupResult
}`
)
}
finishComponentSetup(instance, isSSR)
}
type CompileFunction = (
template: string | object,
options?: CompilerOptions
) => InternalRenderFunction
let compile: CompileFunction | undefined
/**
* For runtime-dom to register the compiler.
* Note the exported method uses any to avoid d.ts relying on the compiler types.
*/
export function registerRuntimeCompiler(_compile: any) {
compile = _compile
}
function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// template / render function normalization
if (__NODE_JS__ && isSSR) {
if (Component.render) {
instance.render = Component.render as InternalRenderFunction
}
} else if (!instance.render) {
if (compile && Component.template && !Component.render) {
if (__DEV__) {
startMeasure(instance, `compile`)
}
Component.render = compile(Component.template, {
isCustomElement: instance.appContext.config.isCustomElement || NO
})
if (__DEV__) {
endMeasure(instance, `compile`)
}
// mark the function as runtime compiled
;(Component.render as InternalRenderFunction)._rc = true
}
if (__DEV__ && !Component.render) {
/* istanbul ignore if */
if (!compile && Component.template) {
warn(
`Component provided template option but ` +
`runtime compilation is not supported in this build of Vue.` +
(__ESM_BUNDLER__
? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
: __ESM_BROWSER__
? ` Use "vue.esm-browser.js" instead.`
: __GLOBAL__
? ` Use "vue.global.js" instead.`
: ``) /* should not happen */
)
} else {
warn(`Component is missing template or render function.`)
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
}
// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance
applyOptions(instance, Component)
currentInstance = null
}
}
const attrHandlers: ProxyHandler<Data> = {
get: (target, key: string) => {
if (__DEV__) {
markAttrsAccessed()
}
return target[key]
},
set: () => {
warn(`setupContext.attrs is readonly.`)
return false
},
deleteProperty: () => {
warn(`setupContext.attrs is readonly.`)
return false
}
}
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
get attrs() {
return new Proxy(instance.attrs, attrHandlers)
},
get slots() {
return shallowReadonly(instance.slots)
},
get emit() {
return (event: string, ...args: any[]) => instance.emit(event, ...args)
}
})
} else {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit
}
}
}
// record effects created during a component's setup() so that they can be
// stopped when the component unmounts
export function recordInstanceBoundEffect(effect: ReactiveEffect) {
if (currentInstance) {
;(currentInstance.effects || (currentInstance.effects = [])).push(effect)
}
}
const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string =>
str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
/* istanbul ignore next */
export function formatComponentName(
instance: ComponentInternalInstance | null,
Component: Component,
isRoot = false
): string {
let name = isFunction(Component)
? Component.displayName || Component.name
: Component.name
if (!name && Component.__file) {
const match = Component.__file.match(/([^/\\]+)\.vue$/)
if (match) {
name = match[1]
}
}
if (!name && instance && instance.parent) {
// try to infer the name based on local resolution
const registry = instance.parent.components
for (const key in registry) {
if (registry[key] === Component) {
name = key
break
}
}
}
return name ? classify(name) : isRoot ? `App` : `Anonymous`
}