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,
|
||||
getGlobalThis,
|
||||
isArray,
|
||||
isBuiltInTag,
|
||||
isFunction,
|
||||
isObject,
|
||||
isPromise,
|
||||
makeMap,
|
||||
} from '@vue/shared'
|
||||
import type { Data } from '@vue/runtime-shared'
|
||||
import type { SuspenseBoundary } from './components/Suspense'
|
||||
|
@ -761,8 +761,6 @@ export const unsetCurrentInstance = () => {
|
|||
internalSetCurrentInstance(null)
|
||||
}
|
||||
|
||||
const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
||||
|
||||
export function validateComponentName(
|
||||
name: string,
|
||||
{ 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 {
|
||||
type Component,
|
||||
type ComponentInternalInstance,
|
||||
createComponentInstance,
|
||||
validateComponentName,
|
||||
} from './component'
|
||||
import { warn } from './warning'
|
||||
import { version } from '.'
|
||||
import { type Directive, version } from '.'
|
||||
import { render, setupComponent, unmountComponent } from './apiRender'
|
||||
import type { InjectionKey } from './apiInject'
|
||||
import type { RawProps } from './componentProps'
|
||||
import { validateDirectiveName } from './directives'
|
||||
|
||||
export function createVaporApp(
|
||||
rootComponent: Component,
|
||||
|
@ -60,6 +62,35 @@ export function createVaporApp(
|
|||
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 {
|
||||
if (!instance) {
|
||||
instance = createComponentInstance(
|
||||
|
@ -119,11 +150,14 @@ export function createAppContext(): AppContext {
|
|||
return {
|
||||
app: null as any,
|
||||
config: {
|
||||
isNativeTag: NO,
|
||||
errorHandler: undefined,
|
||||
warnHandler: undefined,
|
||||
globalProperties: {},
|
||||
},
|
||||
provides: Object.create(null),
|
||||
components: {},
|
||||
directives: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,6 +185,11 @@ export interface App {
|
|||
): 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(
|
||||
rootContainer: ParentNode | string,
|
||||
isHydrate?: boolean,
|
||||
|
@ -163,6 +202,9 @@ export interface App {
|
|||
}
|
||||
|
||||
export interface AppConfig {
|
||||
// @private
|
||||
readonly isNativeTag: (tag: string) => boolean
|
||||
|
||||
errorHandler?: (
|
||||
err: unknown,
|
||||
instance: ComponentInternalInstance | null,
|
||||
|
@ -180,6 +222,17 @@ export interface AppContext {
|
|||
app: App // for devtools
|
||||
config: AppConfig
|
||||
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 { 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 ComponentPropsOptions,
|
||||
|
@ -24,7 +30,11 @@ import {
|
|||
} from './componentSlots'
|
||||
import { VaporLifecycleHooks } from './apiLifecycle'
|
||||
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 { BlockEffectScope } from './blockEffectScope'
|
||||
|
||||
|
@ -233,7 +243,6 @@ export interface ComponentInternalInstance {
|
|||
// [VaporLifecycleHooks.SERVER_PREFETCH]: LifecycleHook<() => Promise<unknown>>
|
||||
}
|
||||
|
||||
// TODO
|
||||
export let currentInstance: ComponentInternalInstance | null = null
|
||||
|
||||
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
|
||||
|
@ -256,7 +265,7 @@ const emptyAppContext = createAppContext()
|
|||
|
||||
let uid = 0
|
||||
export function createComponentInstance(
|
||||
component: ObjectComponent | FunctionalComponent,
|
||||
component: Component,
|
||||
rawProps: RawProps | null,
|
||||
slots: Slots | null,
|
||||
dynamicSlots: DynamicSlots | null,
|
||||
|
@ -367,6 +376,17 @@ export function isVaporComponent(
|
|||
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 {
|
||||
return (
|
||||
instance.attrsProxy ||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { invokeArrayFns, isFunction } from '@vue/shared'
|
||||
import { invokeArrayFns, isBuiltInDirective, isFunction } from '@vue/shared'
|
||||
import {
|
||||
type ComponentInternalInstance,
|
||||
currentInstance,
|
||||
|
@ -72,6 +72,12 @@ export type Directive<T = any, V = any, M extends string = string> =
|
|||
| ObjectDirective<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<
|
||||
| [Directive | undefined]
|
||||
| [Directive | undefined, () => any]
|
||||
|
|
|
@ -1,7 +1,95 @@
|
|||
export function resolveComponent() {
|
||||
// TODO
|
||||
import { camelize, capitalize } from '@vue/shared'
|
||||
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() {
|
||||
// TODO
|
||||
export function resolveDirective(name: string) {
|
||||
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',
|
||||
)
|
||||
|
||||
export const isBuiltInTag = /*#__PURE__*/ makeMap('slot,component')
|
||||
|
||||
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
|
||||
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue