-import { type Ref, EffectScope, ref } from '@vue/reactivity'
-import type { Block } from './render'
-import type { DirectiveBinding } from './directive'
+import { EffectScope, Ref, ref } from '@vue/reactivity'
+
+import { EMPTY_OBJ } from '@vue/shared'
+import { Block } from './render'
+import { type DirectiveBinding } from './directive'
+import {
+ type ComponentPropsOptions,
+ type NormalizedPropsOptions,
+ normalizePropsOptions,
+} from './componentProps'
+
import type { Data } from '@vue/shared'
+export type Component = FunctionalComponent | ObjectComponent
+
export type SetupFn = (props: any, ctx: any) => Block | Data
export type FunctionalComponent = SetupFn & {
+ props: ComponentPropsOptions
render(ctx: any): Block
}
export interface ObjectComponent {
+ props: ComponentPropsOptions
setup: SetupFn
render(ctx: any): Block
}
container: ParentNode
block: Block | null
scope: EffectScope
-
component: FunctionalComponent | ObjectComponent
- get isMounted(): boolean
- isMountedRef: Ref<boolean>
+ propsOptions: NormalizedPropsOptions
+
+ // TODO: type
+ proxy: Data | null
+
+ // state
+ props: Data
+ setupState: Data
/** directives */
dirs: Map<Node, DirectiveBinding[]>
+
+ // lifecycle
+ get isMounted(): boolean
+ isMountedRef: Ref<boolean>
// TODO: registory of provides, appContext, lifecycles, ...
}
block: null,
container: null!, // set on mount
scope: new EffectScope(true /* detached */)!,
-
component,
+
+ // resolved props and emits options
+ propsOptions: normalizePropsOptions(component),
+ // emitsOptions: normalizeEmitsOptions(type, appContext), // TODO:
+
+ proxy: null,
+
+ // state
+ props: EMPTY_OBJ,
+ setupState: EMPTY_OBJ,
+
+ dirs: new Map(),
+
+ // lifecycle
get isMounted() {
return isMountedRef.value
},
isMountedRef,
-
- dirs: new Map(),
// TODO: registory of provides, appContext, lifecycles, ...
}
return instance
--- /dev/null
+// NOTE: runtime-core/src/componentProps.ts
+
+import {
+ Data,
+ EMPTY_ARR,
+ EMPTY_OBJ,
+ camelize,
+ extend,
+ hasOwn,
+ hyphenate,
+ isArray,
+ isFunction,
+ isReservedProp,
+} from '@vue/shared'
+import { shallowReactive, toRaw } from '@vue/reactivity'
+import { type ComponentInternalInstance, type Component } from './component'
+
+export type ComponentPropsOptions<P = Data> =
+ | ComponentObjectPropsOptions<P>
+ | string[]
+
+export type ComponentObjectPropsOptions<P = Data> = {
+ [K in keyof P]: Prop<P[K]> | null
+}
+
+export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
+
+type DefaultFactory<T> = (props: Data) => T | null | undefined
+
+export interface PropOptions<T = any, D = T> {
+ type?: PropType<T> | true | null
+ required?: boolean
+ default?: D | DefaultFactory<D> | null | undefined | object
+ validator?(value: unknown): boolean
+ /**
+ * @internal
+ */
+ skipFactory?: boolean
+}
+
+export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
+
+type PropConstructor<T = any> =
+ | { new (...args: any[]): T & {} }
+ | { (): T }
+ | PropMethod<T>
+
+type PropMethod<T, TConstructor = any> = [T] extends [
+ ((...args: any) => any) | undefined,
+] // if is function with args, allowing non-required functions
+ ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
+ : never
+
+enum BooleanFlags {
+ shouldCast,
+ shouldCastTrue,
+}
+
+type NormalizedProp =
+ | null
+ | (PropOptions & {
+ [BooleanFlags.shouldCast]?: boolean
+ [BooleanFlags.shouldCastTrue]?: boolean
+ })
+
+export type NormalizedProps = Record<string, NormalizedProp>
+export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
+
+export function initProps(
+ instance: ComponentInternalInstance,
+ rawProps: Data | null,
+) {
+ const props: Data = {}
+
+ const [options, needCastKeys] = instance.propsOptions
+ let rawCastValues: Data | undefined
+ if (rawProps) {
+ for (let key in rawProps) {
+ // key, ref are reserved and never passed down
+ if (isReservedProp(key)) {
+ continue
+ }
+
+ const valueGetter = () => rawProps[key]
+ let camelKey
+ if (options && hasOwn(options, (camelKey = camelize(key)))) {
+ if (!needCastKeys || !needCastKeys.includes(camelKey)) {
+ // NOTE: must getter
+ // props[camelKey] = value
+ Object.defineProperty(props, camelKey, {
+ get() {
+ return valueGetter()
+ },
+ })
+ } else {
+ // NOTE: must getter
+ // ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
+ rawCastValues || (rawCastValues = {})
+ Object.defineProperty(rawCastValues, camelKey, {
+ get() {
+ return valueGetter()
+ },
+ })
+ }
+ } else {
+ // TODO:
+ }
+ }
+ }
+
+ if (needCastKeys) {
+ const rawCurrentProps = toRaw(props)
+ const castValues = rawCastValues || EMPTY_OBJ
+ for (let i = 0; i < needCastKeys.length; i++) {
+ const key = needCastKeys[i]
+
+ // NOTE: must getter
+ // props[key] = resolvePropValue(
+ // options!,
+ // rawCurrentProps,
+ // key,
+ // castValues[key],
+ // instance,
+ // !hasOwn(castValues, key),
+ // )
+ Object.defineProperty(props, key, {
+ get() {
+ return resolvePropValue(
+ options!,
+ rawCurrentProps,
+ key,
+ castValues[key],
+ instance,
+ !hasOwn(castValues, key),
+ )
+ },
+ })
+ }
+ }
+
+ instance.props = shallowReactive(props)
+}
+
+function resolvePropValue(
+ options: NormalizedProps,
+ props: Data,
+ key: string,
+ value: unknown,
+ instance: ComponentInternalInstance,
+ isAbsent: boolean,
+) {
+ const opt = options[key]
+ if (opt != null) {
+ const hasDefault = hasOwn(opt, 'default')
+ // default values
+ if (hasDefault && value === undefined) {
+ const defaultValue = opt.default
+ if (
+ opt.type !== Function &&
+ !opt.skipFactory &&
+ isFunction(defaultValue)
+ ) {
+ // TODO: caching?
+ // const { propsDefaults } = instance
+ // if (key in propsDefaults) {
+ // value = propsDefaults[key]
+ // } else {
+ // setCurrentInstance(instance)
+ // value = propsDefaults[key] = defaultValue.call(
+ // __COMPAT__ &&
+ // isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
+ // ? createPropsDefaultThis(instance, props, key)
+ // : null,
+ // props,
+ // )
+ // unsetCurrentInstance()
+ // }
+ } else {
+ value = defaultValue
+ }
+ }
+ // boolean casting
+ if (opt[BooleanFlags.shouldCast]) {
+ if (isAbsent && !hasDefault) {
+ value = false
+ } else if (
+ opt[BooleanFlags.shouldCastTrue] &&
+ (value === '' || value === hyphenate(key))
+ ) {
+ value = true
+ }
+ }
+ }
+ return value
+}
+
+export function normalizePropsOptions(comp: Component): NormalizedPropsOptions {
+ // TODO: cahching?
+
+ const raw = comp.props as any
+ const normalized: NormalizedPropsOptions[0] = {}
+ const needCastKeys: NormalizedPropsOptions[1] = []
+
+ if (!raw) {
+ return EMPTY_ARR as any
+ }
+
+ if (isArray(raw)) {
+ for (let i = 0; i < raw.length; i++) {
+ const normalizedKey = camelize(raw[i])
+ if (validatePropName(normalizedKey)) {
+ normalized[normalizedKey] = EMPTY_OBJ
+ }
+ }
+ } else if (raw) {
+ for (const key in raw) {
+ const normalizedKey = camelize(key)
+ if (validatePropName(normalizedKey)) {
+ const opt = raw[key]
+ const prop: NormalizedProp = (normalized[normalizedKey] =
+ isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
+ if (prop) {
+ const booleanIndex = getTypeIndex(Boolean, prop.type)
+ const stringIndex = getTypeIndex(String, prop.type)
+ prop[BooleanFlags.shouldCast] = booleanIndex > -1
+ prop[BooleanFlags.shouldCastTrue] =
+ stringIndex < 0 || booleanIndex < stringIndex
+ // if the prop needs boolean casting or default value
+ if (booleanIndex > -1 || hasOwn(prop, 'default')) {
+ needCastKeys.push(normalizedKey)
+ }
+ }
+ }
+ }
+ }
+
+ const res: NormalizedPropsOptions = [normalized, needCastKeys]
+ return res
+}
+
+function validatePropName(key: string) {
+ if (key[0] !== '$') {
+ return true
+ }
+ return false
+}
+
+function getType(ctor: Prop<any>): string {
+ const match = ctor && ctor.toString().match(/^\s*(function|class) (\w+)/)
+ return match ? match[2] : ctor === null ? 'null' : ''
+}
+
+function isSameType(a: Prop<any>, b: Prop<any>): boolean {
+ return getType(a) === getType(b)
+}
+
+function getTypeIndex(
+ type: Prop<any>,
+ expectedTypes: PropType<any> | void | null | true,
+): number {
+ if (isArray(expectedTypes)) {
+ return expectedTypes.findIndex((t) => isSameType(t, type))
+ } else if (isFunction(expectedTypes)) {
+ return isSameType(expectedTypes, type) ? 0 : -1
+ }
+ return -1
+}
--- /dev/null
+import { hasOwn } from '@vue/shared'
+import { type ComponentInternalInstance } from './component'
+
+export interface ComponentRenderContext {
+ [key: string]: any
+ _: ComponentInternalInstance
+}
+
+export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
+ get({ _: instance }: ComponentRenderContext, key: string) {
+ let normalizedProps
+ const { setupState, props } = instance
+ if (hasOwn(setupState, key)) {
+ return setupState[key]
+ } else if (
+ (normalizedProps = instance.propsOptions[0]) &&
+ hasOwn(normalizedProps, key)
+ ) {
+ return props![key]
+ }
+ },
+}
-import { reactive } from '@vue/reactivity'
+import { markRaw, proxyRefs } from '@vue/reactivity'
+import { type Data } from '@vue/shared'
+
import {
+ type Component,
type ComponentInternalInstance,
- type FunctionalComponent,
- type ObjectComponent,
createComponentInstance,
setCurrentInstance,
unsetCurrentInstance,
} from './component'
+
+import { initProps } from './componentProps'
+
import { invokeDirectiveHook } from './directive'
+
import { insert, remove } from './dom'
+import { PublicInstanceProxyHandlers } from './componentPublicInstance'
export type Block = Node | Fragment | Block[]
export type ParentBlock = ParentNode | Node[]
export type BlockFn = (props: any, ctx: any) => Block
export function render(
- comp: ObjectComponent | FunctionalComponent,
+ comp: Component,
+ props: Data,
container: string | ParentNode,
): ComponentInternalInstance {
const instance = createComponentInstance(comp)
- setCurrentInstance(instance)
- mountComponent(instance, (container = normalizeContainer(container)))
- return instance
+ initProps(instance, props)
+ return mountComponent(instance, (container = normalizeContainer(container)))
}
export function normalizeContainer(container: string | ParentNode): ParentNode {
setCurrentInstance(instance)
const block = instance.scope.run(() => {
- const { component } = instance
- const props = {}
+ const { component, props } = instance
const ctx = { expose: () => {} }
const setupFn =
typeof component === 'function' ? component : component.setup
const state = setupFn(props, ctx)
+ instance.proxy = markRaw(
+ new Proxy({ _: instance }, PublicInstanceProxyHandlers),
+ )
if (state && '__isScriptSetup' in state) {
- return (instance.block = component.render(reactive(state)))
+ instance.setupState = proxyRefs(state)
+ return (instance.block = component.render(instance.proxy))
} else {
return (instance.block = state as Block)
}
})!
-
invokeDirectiveHook(instance, 'beforeMount')
insert(block, instance.container)
instance.isMountedRef.value = true
invokeDirectiveHook(instance, 'mounted')
+ unsetCurrentInstance()
// TODO: lifecycle hooks (mounted, ...)
// const { m } = instance
// m && invoke(m)
+
+ return instance
}
export function unmountComponent(instance: ComponentInternalInstance) {
import { render } from 'vue/vapor'
-const modules = import.meta.glob<any>('./*.vue')
+const modules = import.meta.glob<any>('./*.(vue|js)')
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
-mod.then(({ default: mod }) => render(mod, '#app'))
+mod.then(({ default: mod }) => render(mod, {}, '#app'))
--- /dev/null
+import { watch } from 'vue'
+import {
+ children,
+ on,
+ ref,
+ template,
+ effect,
+ setText,
+ render as renderComponent // TODO:
+} from '@vue/vapor'
+
+export default {
+ props: undefined,
+
+ setup(_, {}) {
+ const count = ref(1)
+ const handleClick = () => {
+ count.value++
+ }
+
+ const __returned__ = { count, handleClick }
+
+ Object.defineProperty(__returned__, '__isScriptSetup', {
+ enumerable: false,
+ value: true
+ })
+
+ return __returned__
+ },
+
+ render(_ctx) {
+ const t0 = template('<button></button>')
+ const n0 = t0()
+ const {
+ 0: [n1]
+ } = children(n0)
+ on(n1, 'click', _ctx.handleClick)
+ effect(() => {
+ setText(n1, void 0, _ctx.count)
+ })
+
+ // TODO: create component fn?
+ // const c0 = createComponent(...)
+ // insert(n0, c0)
+ renderComponent(
+ child,
+
+ // TODO: proxy??
+ {
+ /* <Comp :count="count" /> */
+ get count() {
+ return _ctx.count
+ },
+
+ /* <Comp :inline-double="count * 2" /> */
+ get inlineDouble() {
+ return _ctx.count * 2
+ }
+ },
+ n0
+ )
+
+ return n0
+ }
+}
+
+const child = {
+ props: {
+ count: { type: Number, default: 1 },
+ inlineDouble: { type: Number, default: 2 }
+ },
+
+ setup(props) {
+ watch(
+ () => props.count,
+ v => console.log('count changed', v)
+ )
+ watch(
+ () => props.inlineDouble,
+ v => console.log('inlineDouble changed', v)
+ )
+
+ const __returned__ = {}
+
+ Object.defineProperty(__returned__, '__isScriptSetup', {
+ enumerable: false,
+ value: true
+ })
+
+ return __returned__
+ },
+
+ render(_ctx) {
+ const t0 = template('<p></p>')
+ const n0 = t0()
+ const {
+ 0: [n1]
+ } = children(n0)
+ effect(() => {
+ setText(n1, void 0, _ctx.count + ' * 2 = ' + _ctx.inlineDouble)
+ })
+ return n0
+ }
+}