feat(runtime-vapor): resolve assets of components & directives (#214)

This commit is contained in:
Doctor Wu 2024-05-29 01:43:47 +08:00 committed by GitHub
parent 4ed4fb633f
commit 107569b922
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 285 additions and 14 deletions

View File

@ -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,

View File

@ -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()
})
})
})

View File

@ -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>
}
/**

View File

@ -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 ||

View File

@ -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]

View File

@ -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))])
)
}

View File

@ -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',
)