]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): component props (#40)
authorubugeeei <71201308+Ubugeeei@users.noreply.github.com>
Sat, 9 Dec 2023 17:33:18 +0000 (02:33 +0900)
committerGitHub <noreply@github.com>
Sat, 9 Dec 2023 17:33:18 +0000 (01:33 +0800)
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts [new file with mode: 0644]
packages/runtime-vapor/src/componentPublicInstance.ts [new file with mode: 0644]
packages/runtime-vapor/src/render.ts
playground/src/main.ts
playground/src/props.js [new file with mode: 0644]

index 26518598d4ff0e9812b586c87ee35a4138398b25..5ff3b86b8ff720f7f46912701c3ff5505afc7fc8 100644 (file)
@@ -1,13 +1,25 @@
-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
 }
@@ -17,13 +29,22 @@ export interface ComponentInternalInstance {
   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, ...
 }
 
@@ -51,14 +72,25 @@ export const createComponentInstance = (
     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
diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts
new file mode 100644 (file)
index 0000000..5cd0f1d
--- /dev/null
@@ -0,0 +1,267 @@
+// 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
+}
diff --git a/packages/runtime-vapor/src/componentPublicInstance.ts b/packages/runtime-vapor/src/componentPublicInstance.ts
new file mode 100644 (file)
index 0000000..8bfacf9
--- /dev/null
@@ -0,0 +1,22 @@
+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]
+    }
+  },
+}
index b03e56ae8e32426b04d60c44e853c462b5e86934..422d5e689e02c59d6c0a44682b872e0f6803d793 100644 (file)
@@ -1,14 +1,20 @@
-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[]
@@ -16,13 +22,13 @@ export type Fragment = { nodes: Block; anchor: 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 {
@@ -39,29 +45,34 @@ export function mountComponent(
 
   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) {
index 43565bc94c6584965054bfa68f60340091fdc2df..717629057a3e7988637c28b37aa4f8fcbe5c30dd 100644 (file)
@@ -1,6 +1,6 @@
 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'))
diff --git a/playground/src/props.js b/playground/src/props.js
new file mode 100644 (file)
index 0000000..b80768d
--- /dev/null
@@ -0,0 +1,104 @@
+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
+  }
+}