]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: custom directlve
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Sun, 3 Dec 2023 10:36:01 +0000 (18:36 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Sun, 3 Dec 2023 10:36:54 +0000 (18:36 +0800)
closes #19

packages/compiler-vapor/package.json
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/directives.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/render.ts
playground/src/directive.vue [new file with mode: 0644]
pnpm-lock.yaml

index 2f57f3777fc497a7e0037661d52e333157359ef1..adc2681c84b2bc7b5004916cdc28e2b0fa3d22fc 100644 (file)
@@ -37,8 +37,8 @@
   },
   "homepage": "https://github.com/vuejs/core-vapor/tree/main/packages/compiler-vapor#readme",
   "dependencies": {
-    "@vue/compiler-dom": "3.3.8",
-    "@vue/shared": "3.3.8",
+    "@vue/compiler-dom": "workspace:*",
+    "@vue/shared": "workspace:*",
     "source-map-js": "^1.0.2"
   }
 }
index 07011c9737b91e52dd4b2e95841374410ff0fd43..0b84f863878a2b23255751608fdc3e7a9babc26b 100644 (file)
@@ -9,6 +9,7 @@ import {
   NodeTypes,
   BindingTypes,
   isSimpleIdentifier,
+  createSimpleExpression,
 } from '@vue/compiler-dom'
 import {
   type IRDynamicChildren,
@@ -20,7 +21,7 @@ import {
   SetEventIRNode,
 } from './ir'
 import { SourceMapGenerator } from 'source-map-js'
-import { isString } from '@vue/shared'
+import { camelize, capitalize, isString } from '@vue/shared'
 
 // remove when stable
 // @ts-expect-error
@@ -361,6 +362,23 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
       )
       return
     }
+    case IRNodeTypes.WITH_DIRECTIVE: {
+      // TODO merge directive for the same node
+      pushWithNewline(`${vaporHelper('withDirectives')}(n${oper.element}, [[`)
+
+      // TODO resolve directive
+      const directiveReference = camelize(`v-${oper.name}`)
+      if (context.bindingMetadata[directiveReference]) {
+        genExpression(createSimpleExpression(directiveReference), context)
+      }
+
+      if (oper.binding) {
+        push(', ')
+        genExpression(oper.binding, context)
+      }
+      push(']])')
+      return
+    }
     default:
       return checkNever(oper)
   }
@@ -406,6 +424,7 @@ function genExpression(
     vaporHelper,
     push,
   }: CodegenContext,
+  { unref = true }: { unref?: boolean } = {},
 ) {
   if (isString(exp)) return push(exp)
 
@@ -418,14 +437,15 @@ function genExpression(
   if (exp.isStatic) {
     content = JSON.stringify(content)
   } else {
-    switch (bindingMetadata[content]) {
-      case BindingTypes.SETUP_REF:
-        content += '.value'
-        break
-      case BindingTypes.SETUP_MAYBE_REF:
-        content = `${vaporHelper('unref')}(${content})`
-        break
-    }
+    if (unref)
+      switch (bindingMetadata[content]) {
+        case BindingTypes.SETUP_REF:
+          content += '.value'
+          break
+        case BindingTypes.SETUP_MAYBE_REF:
+          content = `${vaporHelper('unref')}(${content})`
+          break
+      }
     if (prefixIdentifiers && !inline) {
       if (isSimpleIdentifier(content)) name = content
       content = `_ctx.${content}`
index 4e4c06e52bacf8edfb864e1387347c1cac705c07..0ed0ebfb3f1db7f19c5306b4a75bbb138b721432 100644 (file)
@@ -20,6 +20,8 @@ export enum IRNodeTypes {
   PREPEND_NODE,
   APPEND_NODE,
   CREATE_TEXT_NODE,
+
+  WITH_DIRECTIVE,
 }
 
 export interface BaseIRNode {
@@ -110,6 +112,13 @@ export interface AppendNodeIRNode extends BaseIRNode {
   parent: number
 }
 
+export interface WithDirectiveIRNode extends BaseIRNode {
+  type: IRNodeTypes.WITH_DIRECTIVE
+  element: number
+  name: string
+  binding: IRExpression | undefined
+}
+
 export type IRNode =
   | OperationNode
   | RootIRNode
@@ -124,6 +133,7 @@ export type OperationNode =
   | InsertNodeIRNode
   | PrependNodeIRNode
   | AppendNodeIRNode
+  | WithDirectiveIRNode
 
 export interface IRDynamicInfo {
   id: number | null
index 9a144ac8f1a49bd3beb62274d1de3386fdc6092b..9ac60cd9ba67799f8d02e249dc7d39312db178b5 100644 (file)
@@ -7,7 +7,7 @@ import {
   createCompilerError,
   ElementTypes,
 } from '@vue/compiler-dom'
-import { isVoidTag } from '@vue/shared'
+import { isBuiltInDirective, isVoidTag } from '@vue/shared'
 import { NodeTransform, TransformContext } from '../transform'
 import { IRNodeTypes } from '../ir'
 
@@ -74,6 +74,14 @@ function transformProp(
   const directiveTransform = context.options.directiveTransforms[name]
   if (directiveTransform) {
     directiveTransform(prop, node, context)
+  } else if (!isBuiltInDirective(name)) {
+    context.registerOperation({
+      type: IRNodeTypes.WITH_DIRECTIVE,
+      element: context.reference(),
+      name,
+      binding: prop.exp,
+      loc: prop.loc,
+    })
   }
 
   switch (name) {
index 05310689eda163463114c6d562e5b895120df26b..c6195f36ef239f84dd3af1f9d9f349ed5ca66f19 100644 (file)
@@ -1,6 +1,6 @@
 import { EffectScope } from '@vue/reactivity'
-
 import { Block, BlockFn } from './render'
+import { DirectiveBinding } from './directives'
 
 export interface ComponentInternalInstance {
   uid: number
@@ -11,9 +11,29 @@ export interface ComponentInternalInstance {
   component: BlockFn
   isMounted: boolean
 
+  /** directives */
+  dirs: Map<Node, DirectiveBinding[]>
   // TODO: registory of provides, appContext, lifecycles, ...
 }
 
+// TODO
+export let currentInstance: ComponentInternalInstance | null = null
+
+export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
+  currentInstance
+
+export const setCurrentInstance = (instance: ComponentInternalInstance) => {
+  currentInstance = instance
+  instance.scope.on()
+}
+
+export const unsetCurrentInstance = () => {
+  currentInstance && currentInstance.scope.off()
+  currentInstance = null
+}
+
+export interface ComponentPublicInstance {}
+
 let uid = 0
 export const createComponentInstance = (
   component: BlockFn,
@@ -26,6 +46,8 @@ export const createComponentInstance = (
 
     component,
     isMounted: false,
+
+    dirs: new Map(),
     // TODO: registory of provides, appContext, lifecycles, ...
   }
   return instance
diff --git a/packages/runtime-vapor/src/directives.ts b/packages/runtime-vapor/src/directives.ts
new file mode 100644 (file)
index 0000000..03d8c62
--- /dev/null
@@ -0,0 +1,81 @@
+import { isFunction } from '@vue/shared'
+import { currentInstance, type ComponentPublicInstance } from './component'
+
+export interface DirectiveBinding<V = any> {
+  instance: ComponentPublicInstance | null
+  value: V
+  oldValue: V | null
+  arg?: string
+  // TODO: should we support modifiers for custom directives?
+  // modifiers: DirectiveModifiers
+  dir: ObjectDirective<any, V>
+}
+
+export type DirectiveHook<T = any | null, V = any> = (
+  node: T,
+  binding: DirectiveBinding<V>,
+) => void
+
+// create node -> `created` -> node operation -> `beforeMount` -> node mounted -> `mounted`
+// effect update -> `beforeUpdate` -> node updated -> `updated`
+// `beforeUnmount`-> node unmount -> `unmounted`
+export interface ObjectDirective<T = any, V = any> {
+  created?: DirectiveHook<T, V>
+  // beforeMount?: DirectiveHook<T, V>
+  // mounted?: DirectiveHook<T, V>
+  // beforeUpdate?: DirectiveHook<T, V>
+  // updated?: DirectiveHook<T, V>
+  // beforeUnmount?: DirectiveHook<T, V>
+  // unmounted?: DirectiveHook<T, V>
+  // getSSRProps?: SSRDirectiveHook
+  deep?: boolean
+}
+
+export type FunctionDirective<T = any, V = any> = DirectiveHook<T, V>
+export type Directive<T = any, V = any> =
+  | ObjectDirective<T, V>
+  | FunctionDirective<T, V>
+
+export type DirectiveArguments = Array<
+  | [Directive | undefined]
+  | [Directive | undefined, value: any]
+  | [Directive | undefined, value: any, argument: string]
+>
+
+export function withDirectives<T extends Node>(
+  node: T,
+  directives: DirectiveArguments,
+): T {
+  if (!currentInstance) {
+    // TODO warning
+    return node
+  }
+
+  if (!currentInstance.dirs.has(node)) currentInstance.dirs.set(node, [])
+  const bindings = currentInstance.dirs.get(node)!
+
+  // TODO public instance
+  const instance = currentInstance as any
+  for (const directive of directives) {
+    let [dir, value, arg] = directive
+    if (!dir) continue
+    if (isFunction(dir)) {
+      // TODO function directive
+      dir = {
+        created: dir,
+      } satisfies ObjectDirective
+    }
+
+    const binding: DirectiveBinding = {
+      dir,
+      instance,
+      value,
+      oldValue: void 0,
+      arg,
+    }
+    if (dir.created) dir.created(node, binding)
+    bindings.push(binding)
+  }
+
+  return node
+}
index cf6be7c44dd2f2f4e3d34449be73a119d243ac2c..5533dfc826c81d405b37a4ad8dae9d67e6b788a5 100644 (file)
@@ -35,9 +35,10 @@ export {
   getCurrentScope,
   onScopeDispose,
 } from '@vue/reactivity'
-export { effect } from './scheduler'
+export { withModifiers, withKeys } from '@vue/runtime-dom'
+
 export * from './on'
 export * from './render'
 export * from './template'
 export * from './scheduler'
-export { withModifiers, withKeys } from '@vue/runtime-dom'
+export * from './directives'
index 49c30e6e10594d66aa02dd5f52a713036a02c07f..82a3bfde7e3d9251f6d14cf0c2cd1320e5fc0464 100644 (file)
@@ -5,7 +5,11 @@ import {
   toDisplayString,
 } from '@vue/shared'
 
-import { ComponentInternalInstance, createComponentInstance } from './component'
+import {
+  ComponentInternalInstance,
+  createComponentInstance,
+  setCurrentInstance,
+} from './component'
 
 export type Block = Node | Fragment | Block[]
 export type ParentBlock = ParentNode | Node[]
@@ -17,6 +21,7 @@ export function render(
   container: string | ParentNode,
 ): ComponentInternalInstance {
   const instance = createComponentInstance(comp)
+  setCurrentInstance(instance)
   mountComponent(instance, (container = normalizeContainer(container)))
   return instance
 }
diff --git a/playground/src/directive.vue b/playground/src/directive.vue
new file mode 100644 (file)
index 0000000..74bb387
--- /dev/null
@@ -0,0 +1,12 @@
+<script setup lang="ts">
+import { FunctionDirective } from '@vue/vapor'
+
+const vDirective: FunctionDirective<HTMLDivElement, undefined> = node => {
+  node.textContent = 'hello world'
+  node.style.color = 'red'
+}
+</script>
+
+<template>
+  <div v-directive />
+</template>
index 82ca550ff8c1f5f7ddb5f822f62368a3f0c04991..4b45499bde9e36ba5b9ffeb85d7fb2f09151e35d 100644 (file)
@@ -269,11 +269,11 @@ importers:
   packages/compiler-vapor:
     dependencies:
       '@vue/compiler-dom':
-        specifier: 3.3.8
-        version: 3.3.8
+        specifier: workspace:*
+        version: link:../compiler-dom
       '@vue/shared':
-        specifier: 3.3.8
-        version: 3.3.8
+        specifier: workspace:*
+        version: link:../shared
       source-map-js:
         specifier: ^1.0.2
         version: 1.0.2
@@ -1825,22 +1825,6 @@ packages:
       pretty-format: 29.7.0
     dev: true
 
-  /@vue/compiler-core@3.3.8:
-    resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==}
-    dependencies:
-      '@babel/parser': 7.23.5
-      '@vue/shared': 3.3.8
-      estree-walker: 2.0.2
-      source-map-js: 1.0.2
-    dev: false
-
-  /@vue/compiler-dom@3.3.8:
-    resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==}
-    dependencies:
-      '@vue/compiler-core': 3.3.8
-      '@vue/shared': 3.3.8
-    dev: false
-
   /@vue/consolidate@0.17.3:
     resolution: {integrity: sha512-nl0SWcTMzaaTnJ5G6V8VlMDA1CVVrNnaQKF1aBZU3kXtjgU9jtHMsEAsgjoRUx+T0EVJk9TgbmxGhK3pOk22zw==}
     engines: {node: '>= 0.12.0'}
@@ -1850,10 +1834,6 @@ packages:
     resolution: {integrity: sha512-zzyb+tVvzmOePv8Gp4sefP/7CKidx4WiJDfKPP698b9bN5jSFtmSOg4nvPoJEE1ICKeAEgdRKVneYJ8Mp7C/WA==}
     dev: false
 
-  /@vue/shared@3.3.8:
-    resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==}
-    dev: false
-
   /@zeit/schemas@2.29.0:
     resolution: {integrity: sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==}
     dev: true