mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): resolve assets of components & directives (#214)
This commit is contained in:
parent
4ed4fb633f
commit
107569b922
|
@ -67,10 +67,10 @@ import {
|
||||||
extend,
|
extend,
|
||||||
getGlobalThis,
|
getGlobalThis,
|
||||||
isArray,
|
isArray,
|
||||||
|
isBuiltInTag,
|
||||||
isFunction,
|
isFunction,
|
||||||
isObject,
|
isObject,
|
||||||
isPromise,
|
isPromise,
|
||||||
makeMap,
|
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import type { Data } from '@vue/runtime-shared'
|
import type { Data } from '@vue/runtime-shared'
|
||||||
import type { SuspenseBoundary } from './components/Suspense'
|
import type { SuspenseBoundary } from './components/Suspense'
|
||||||
|
@ -761,8 +761,6 @@ export const unsetCurrentInstance = () => {
|
||||||
internalSetCurrentInstance(null)
|
internalSetCurrentInstance(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
|
||||||
|
|
||||||
export function validateComponentName(
|
export function validateComponentName(
|
||||||
name: string,
|
name: string,
|
||||||
{ isNativeTag }: AppConfig,
|
{ isNativeTag }: AppConfig,
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import {
|
||||||
|
type Component,
|
||||||
|
type Directive,
|
||||||
|
createVaporApp,
|
||||||
|
resolveComponent,
|
||||||
|
resolveDirective,
|
||||||
|
} from '@vue/runtime-vapor'
|
||||||
|
import { makeRender } from '../_utils'
|
||||||
|
|
||||||
|
const define = makeRender()
|
||||||
|
|
||||||
|
describe('resolveAssets', () => {
|
||||||
|
test('todo', () => {
|
||||||
|
expect(true).toBeTruthy()
|
||||||
|
})
|
||||||
|
test('should work', () => {
|
||||||
|
const FooBar = () => []
|
||||||
|
const BarBaz = { mounted: () => null }
|
||||||
|
let component1: Component | string
|
||||||
|
let component2: Component | string
|
||||||
|
let component3: Component | string
|
||||||
|
let component4: Component | string
|
||||||
|
let directive1: Directive
|
||||||
|
let directive2: Directive
|
||||||
|
let directive3: Directive
|
||||||
|
let directive4: Directive
|
||||||
|
const Root = define({
|
||||||
|
render() {
|
||||||
|
component1 = resolveComponent('FooBar')!
|
||||||
|
directive1 = resolveDirective('BarBaz')!
|
||||||
|
// camelize
|
||||||
|
component2 = resolveComponent('Foo-bar')!
|
||||||
|
directive2 = resolveDirective('Bar-baz')!
|
||||||
|
// capitalize
|
||||||
|
component3 = resolveComponent('fooBar')!
|
||||||
|
directive3 = resolveDirective('barBaz')!
|
||||||
|
// camelize and capitalize
|
||||||
|
component4 = resolveComponent('foo-bar')!
|
||||||
|
directive4 = resolveDirective('bar-baz')!
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = createVaporApp(Root.component)
|
||||||
|
app.component('FooBar', FooBar)
|
||||||
|
app.directive('BarBaz', BarBaz)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
app.mount(root)
|
||||||
|
expect(component1!).toBe(FooBar)
|
||||||
|
expect(component2!).toBe(FooBar)
|
||||||
|
expect(component3!).toBe(FooBar)
|
||||||
|
expect(component4!).toBe(FooBar)
|
||||||
|
expect(directive1!).toBe(BarBaz)
|
||||||
|
expect(directive2!).toBe(BarBaz)
|
||||||
|
expect(directive3!).toBe(BarBaz)
|
||||||
|
expect(directive4!).toBe(BarBaz)
|
||||||
|
})
|
||||||
|
test('maybeSelfReference', async () => {
|
||||||
|
let component1: Component | string
|
||||||
|
let component2: Component | string
|
||||||
|
let component3: Component | string
|
||||||
|
const Foo = () => []
|
||||||
|
const Root = define({
|
||||||
|
name: 'Root',
|
||||||
|
render() {
|
||||||
|
component1 = resolveComponent('Root', true)
|
||||||
|
component2 = resolveComponent('Foo', true)
|
||||||
|
component3 = resolveComponent('Bar', true)
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = createVaporApp(Root.component)
|
||||||
|
app.component('Foo', Foo)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
app.mount(root)
|
||||||
|
expect(component1!).toMatchObject(Root.component) // explicit self name reference
|
||||||
|
expect(component2!).toBe(Foo) // successful resolve take higher priority
|
||||||
|
expect(component3!).toMatchObject(Root.component) // fallback when resolve fails
|
||||||
|
})
|
||||||
|
describe('warning', () => {
|
||||||
|
test('used outside render() or setup()', () => {
|
||||||
|
resolveComponent('foo')
|
||||||
|
expect(
|
||||||
|
'[Vue warn]: resolveComponent can only be used in render() or setup().',
|
||||||
|
).toHaveBeenWarned()
|
||||||
|
resolveDirective('foo')
|
||||||
|
expect(
|
||||||
|
'[Vue warn]: resolveDirective can only be used in render() or setup().',
|
||||||
|
).toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
test('not exist', () => {
|
||||||
|
const Root = define({
|
||||||
|
setup() {
|
||||||
|
resolveComponent('foo')
|
||||||
|
resolveDirective('bar')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const app = createVaporApp(Root.component)
|
||||||
|
const root = document.createElement('div')
|
||||||
|
app.mount(root)
|
||||||
|
expect('Failed to resolve component: foo').toHaveBeenWarned()
|
||||||
|
expect('Failed to resolve directive: bar').toHaveBeenWarned()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,14 +1,16 @@
|
||||||
import { isFunction, isObject } from '@vue/shared'
|
import { NO, isFunction, isObject } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
|
validateComponentName,
|
||||||
} from './component'
|
} from './component'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { version } from '.'
|
import { type Directive, version } from '.'
|
||||||
import { render, setupComponent, unmountComponent } from './apiRender'
|
import { render, setupComponent, unmountComponent } from './apiRender'
|
||||||
import type { InjectionKey } from './apiInject'
|
import type { InjectionKey } from './apiInject'
|
||||||
import type { RawProps } from './componentProps'
|
import type { RawProps } from './componentProps'
|
||||||
|
import { validateDirectiveName } from './directives'
|
||||||
|
|
||||||
export function createVaporApp(
|
export function createVaporApp(
|
||||||
rootComponent: Component,
|
rootComponent: Component,
|
||||||
|
@ -60,6 +62,35 @@ export function createVaporApp(
|
||||||
return app
|
return app
|
||||||
},
|
},
|
||||||
|
|
||||||
|
component(name: string, component?: Component): any {
|
||||||
|
if (__DEV__) {
|
||||||
|
validateComponentName(name, context.config)
|
||||||
|
}
|
||||||
|
if (!component) {
|
||||||
|
return context.components[name]
|
||||||
|
}
|
||||||
|
if (__DEV__ && context.components[name]) {
|
||||||
|
warn(`Component "${name}" has already been registered in target app.`)
|
||||||
|
}
|
||||||
|
context.components[name] = component
|
||||||
|
return app
|
||||||
|
},
|
||||||
|
|
||||||
|
directive(name: string, directive?: Directive) {
|
||||||
|
if (__DEV__) {
|
||||||
|
validateDirectiveName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!directive) {
|
||||||
|
return context.directives[name] as any
|
||||||
|
}
|
||||||
|
if (__DEV__ && context.directives[name]) {
|
||||||
|
warn(`Directive "${name}" has already been registered in target app.`)
|
||||||
|
}
|
||||||
|
context.directives[name] = directive
|
||||||
|
return app
|
||||||
|
},
|
||||||
|
|
||||||
mount(rootContainer): any {
|
mount(rootContainer): any {
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = createComponentInstance(
|
instance = createComponentInstance(
|
||||||
|
@ -119,11 +150,14 @@ export function createAppContext(): AppContext {
|
||||||
return {
|
return {
|
||||||
app: null as any,
|
app: null as any,
|
||||||
config: {
|
config: {
|
||||||
|
isNativeTag: NO,
|
||||||
errorHandler: undefined,
|
errorHandler: undefined,
|
||||||
warnHandler: undefined,
|
warnHandler: undefined,
|
||||||
globalProperties: {},
|
globalProperties: {},
|
||||||
},
|
},
|
||||||
provides: Object.create(null),
|
provides: Object.create(null),
|
||||||
|
components: {},
|
||||||
|
directives: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +185,11 @@ export interface App {
|
||||||
): this
|
): this
|
||||||
use<Options>(plugin: Plugin<Options>, options: Options): this
|
use<Options>(plugin: Plugin<Options>, options: Options): this
|
||||||
|
|
||||||
|
component(name: string): Component | undefined
|
||||||
|
component<T extends Component>(name: string, component: T): this
|
||||||
|
directive<T = any, V = any>(name: string): Directive<T, V> | undefined
|
||||||
|
directive<T = any, V = any>(name: string, directive: Directive<T, V>): this
|
||||||
|
|
||||||
mount(
|
mount(
|
||||||
rootContainer: ParentNode | string,
|
rootContainer: ParentNode | string,
|
||||||
isHydrate?: boolean,
|
isHydrate?: boolean,
|
||||||
|
@ -163,6 +202,9 @@ export interface App {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
// @private
|
||||||
|
readonly isNativeTag: (tag: string) => boolean
|
||||||
|
|
||||||
errorHandler?: (
|
errorHandler?: (
|
||||||
err: unknown,
|
err: unknown,
|
||||||
instance: ComponentInternalInstance | null,
|
instance: ComponentInternalInstance | null,
|
||||||
|
@ -180,6 +222,17 @@ export interface AppContext {
|
||||||
app: App // for devtools
|
app: App // for devtools
|
||||||
config: AppConfig
|
config: AppConfig
|
||||||
provides: Record<string | symbol, any>
|
provides: Record<string | symbol, any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved component registry, only for components with mixins or extends
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
components: Record<string, Component>
|
||||||
|
/**
|
||||||
|
* Resolved directive registry, only for components with mixins or extends
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
directives: Record<string, Directive>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { isRef } from '@vue/reactivity'
|
import { isRef } from '@vue/reactivity'
|
||||||
import { EMPTY_OBJ, hasOwn, isArray, isFunction } from '@vue/shared'
|
import {
|
||||||
|
EMPTY_OBJ,
|
||||||
|
hasOwn,
|
||||||
|
isArray,
|
||||||
|
isBuiltInTag,
|
||||||
|
isFunction,
|
||||||
|
} from '@vue/shared'
|
||||||
import type { Block } from './apiRender'
|
import type { Block } from './apiRender'
|
||||||
import {
|
import {
|
||||||
type ComponentPropsOptions,
|
type ComponentPropsOptions,
|
||||||
|
@ -24,7 +30,11 @@ import {
|
||||||
} from './componentSlots'
|
} from './componentSlots'
|
||||||
import { VaporLifecycleHooks } from './apiLifecycle'
|
import { VaporLifecycleHooks } from './apiLifecycle'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { type AppContext, createAppContext } from './apiCreateVaporApp'
|
import {
|
||||||
|
type AppConfig,
|
||||||
|
type AppContext,
|
||||||
|
createAppContext,
|
||||||
|
} from './apiCreateVaporApp'
|
||||||
import type { Data } from '@vue/runtime-shared'
|
import type { Data } from '@vue/runtime-shared'
|
||||||
import { BlockEffectScope } from './blockEffectScope'
|
import { BlockEffectScope } from './blockEffectScope'
|
||||||
|
|
||||||
|
@ -233,7 +243,6 @@ export interface ComponentInternalInstance {
|
||||||
// [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
|
// [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
|
||||||
export let currentInstance: ComponentInternalInstance | null = null
|
export let currentInstance: ComponentInternalInstance | null = null
|
||||||
|
|
||||||
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||||
|
@ -256,7 +265,7 @@ const emptyAppContext = createAppContext()
|
||||||
|
|
||||||
let uid = 0
|
let uid = 0
|
||||||
export function createComponentInstance(
|
export function createComponentInstance(
|
||||||
component: ObjectComponent | FunctionalComponent,
|
component: Component,
|
||||||
rawProps: RawProps | null,
|
rawProps: RawProps | null,
|
||||||
slots: Slots | null,
|
slots: Slots | null,
|
||||||
dynamicSlots: DynamicSlots | null,
|
dynamicSlots: DynamicSlots | null,
|
||||||
|
@ -367,6 +376,17 @@ export function isVaporComponent(
|
||||||
return !!val && hasOwn(val, componentKey)
|
return !!val && hasOwn(val, componentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateComponentName(
|
||||||
|
name: string,
|
||||||
|
{ isNativeTag }: AppConfig,
|
||||||
|
) {
|
||||||
|
if (isBuiltInTag(name) || isNativeTag(name)) {
|
||||||
|
warn(
|
||||||
|
'Do not use built-in or reserved HTML elements as component id: ' + name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getAttrsProxy(instance: ComponentInternalInstance): Data {
|
function getAttrsProxy(instance: ComponentInternalInstance): Data {
|
||||||
return (
|
return (
|
||||||
instance.attrsProxy ||
|
instance.attrsProxy ||
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { invokeArrayFns, isFunction } from '@vue/shared'
|
import { invokeArrayFns, isBuiltInDirective, isFunction } from '@vue/shared'
|
||||||
import {
|
import {
|
||||||
type ComponentInternalInstance,
|
type ComponentInternalInstance,
|
||||||
currentInstance,
|
currentInstance,
|
||||||
|
@ -72,6 +72,12 @@ export type Directive<T = any, V = any, M extends string = string> =
|
||||||
| ObjectDirective<T, V, M>
|
| ObjectDirective<T, V, M>
|
||||||
| FunctionDirective<T, V, M>
|
| FunctionDirective<T, V, M>
|
||||||
|
|
||||||
|
export function validateDirectiveName(name: string) {
|
||||||
|
if (isBuiltInDirective(name)) {
|
||||||
|
warn('Do not use built-in directive ids as custom directive id: ' + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type DirectiveArguments = Array<
|
export type DirectiveArguments = Array<
|
||||||
| [Directive | undefined]
|
| [Directive | undefined]
|
||||||
| [Directive | undefined, () => any]
|
| [Directive | undefined, () => any]
|
||||||
|
|
|
@ -1,7 +1,95 @@
|
||||||
export function resolveComponent() {
|
import { camelize, capitalize } from '@vue/shared'
|
||||||
// TODO
|
import { type Directive, warn } from '..'
|
||||||
|
import { type Component, currentInstance } from '../component'
|
||||||
|
import { getComponentName } from '../warning'
|
||||||
|
|
||||||
|
export const COMPONENTS = 'components'
|
||||||
|
export const DIRECTIVES = 'directives'
|
||||||
|
|
||||||
|
export type AssetTypes = typeof COMPONENTS | typeof DIRECTIVES
|
||||||
|
|
||||||
|
export function resolveComponent(name: string, maybeSelfReference?: boolean) {
|
||||||
|
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDirective() {
|
export function resolveDirective(name: string) {
|
||||||
// TODO
|
return resolveAsset(DIRECTIVES, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* overload 1: components
|
||||||
|
*/
|
||||||
|
function resolveAsset(
|
||||||
|
type: typeof COMPONENTS,
|
||||||
|
name: string,
|
||||||
|
warnMissing?: boolean,
|
||||||
|
maybeSelfReference?: boolean,
|
||||||
|
): Component | undefined
|
||||||
|
// overload 2: directives
|
||||||
|
function resolveAsset(
|
||||||
|
type: typeof DIRECTIVES,
|
||||||
|
name: string,
|
||||||
|
): Directive | undefined
|
||||||
|
// implementation
|
||||||
|
function resolveAsset(
|
||||||
|
type: AssetTypes,
|
||||||
|
name: string,
|
||||||
|
warnMissing = true,
|
||||||
|
maybeSelfReference = false,
|
||||||
|
) {
|
||||||
|
const instance = currentInstance
|
||||||
|
if (instance) {
|
||||||
|
const Component = instance.component
|
||||||
|
|
||||||
|
// explicit self name has highest priority
|
||||||
|
if (type === COMPONENTS) {
|
||||||
|
const selfName = getComponentName(
|
||||||
|
Component,
|
||||||
|
false /* do not include inferred name to avoid breaking existing code */,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
selfName &&
|
||||||
|
(selfName === name ||
|
||||||
|
selfName === camelize(name) ||
|
||||||
|
selfName === capitalize(camelize(name)))
|
||||||
|
) {
|
||||||
|
return Component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res =
|
||||||
|
// global registration
|
||||||
|
resolve(instance.appContext[type], name)
|
||||||
|
|
||||||
|
if (!res && maybeSelfReference) {
|
||||||
|
// fallback to implicit self-reference
|
||||||
|
return Component
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__DEV__ && warnMissing && !res) {
|
||||||
|
const extra =
|
||||||
|
type === COMPONENTS
|
||||||
|
? `\nIf this is a native custom element, make sure to exclude it from ` +
|
||||||
|
`component resolution via compilerOptions.isCustomElement.`
|
||||||
|
: ``
|
||||||
|
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn(
|
||||||
|
`resolve${capitalize(type.slice(0, -1))} ` +
|
||||||
|
`can only be used in render() or setup().`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(registry: Record<string, any> | undefined, name: string) {
|
||||||
|
return (
|
||||||
|
registry &&
|
||||||
|
(registry[name] ||
|
||||||
|
registry[camelize(name)] ||
|
||||||
|
registry[capitalize(camelize(name))])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,8 @@ export const isReservedProp = /*#__PURE__*/ makeMap(
|
||||||
'onVnodeBeforeUnmount,onVnodeUnmounted',
|
'onVnodeBeforeUnmount,onVnodeUnmounted',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
||||||
|
|
||||||
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
||||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
|
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue