]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
chore: Merge branch 'minor' into edison/feat/SuspenseVaporInterop
authordaiwei <daiwei521@126.com>
Wed, 16 Jul 2025 13:10:53 +0000 (21:10 +0800)
committerdaiwei <daiwei521@126.com>
Wed, 16 Jul 2025 13:10:53 +0000 (21:10 +0800)
1  2 
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-vapor/__tests__/_utils.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/vdomInterop.ts

index 31e8ee85df711c708d5dab48fb4b8033f95ebb42,a1409a7fe442e2247b357e40a5449075f95ec471..459d6ee93848d6ca574e4b077c2eb0a181c0cefd
@@@ -26,8 -26,8 +26,8 @@@ import type { InjectionKey } from './ap
  import { warn } from './warning'
  import type { VNode } from './vnode'
  import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
- import { NO, extend, isFunction, isObject } from '@vue/shared'
+ import { NO, extend, hasOwn, isFunction, isObject } from '@vue/shared'
 -import { version } from '.'
 +import { type SuspenseBoundary, version } from '.'
  import { installAppCompatProperties } from './compat/global'
  import type { NormalizedPropsOptions } from './componentProps'
  import type { ObjectEmitsOptions } from './componentEmits'
index eacee712a6d61a1b81aece3481a241b12665f835,243bde548c5cff327235a164abcb6aa4cd8e5a79..9d12d5115fd2f6ba0c531c26247ac022cda5bfd3
@@@ -557,7 -558,7 +558,11 @@@ export { startMeasure, endMeasure } fro
   * @internal
   */
  export { initFeatureFlags } from './featureFlags'
 +/**
 + * @internal
 + */
 +export { getComponentName } from './component'
+ /**
+  * @internal
+  */
+ export { createInternalObject } from './internalObject'
Simple merge
index 729d42de78c27ea602b9cdbbb72a988b9b92f224,d1ede2a6c9a49546f6ab313fdfd63c9a1b9c77e9..b0a3fa8deb4445bbc4e0f1aee3dcaaecfa2a99c6
@@@ -1,11 -1,7 +1,11 @@@
- import { createVaporApp } from '../src'
- import type { App } from '@vue/runtime-dom'
+ import { createVaporApp, vaporInteropPlugin } from '../src'
+ import { type App, type Component, createApp } from '@vue/runtime-dom'
  import type { VaporComponent, VaporComponentInstance } from '../src/component'
  import type { RawProps } from '../src/componentProps'
 +import { compileScript, parse } from '@vue/compiler-sfc'
 +import * as runtimeVapor from '../src'
 +import * as runtimeDom from '@vue/runtime-dom'
 +import * as VueServerRenderer from '@vue/server-renderer'
  
  export interface RenderContext {
    component: VaporComponent
@@@ -87,49 -83,55 +87,102 @@@ export function makeRender<C = VaporCom
    return define
  }
  
 +export { runtimeDom, runtimeVapor, VueServerRenderer }
 +export function compile(
 +  sfc: string,
 +  data: runtimeDom.Ref<any>,
 +  components: Record<string, any> = {},
 +  {
 +    vapor = true,
 +    ssr = false,
 +  }: {
 +    vapor?: boolean | undefined
 +    ssr?: boolean | undefined
 +  } = {},
 +): any {
 +  if (!sfc.includes(`<script`)) {
 +    sfc =
 +      `<script vapor>const data = _data; const components = _components;</script>` +
 +      sfc
 +  }
 +  const descriptor = parse(sfc).descriptor
 +
 +  const script = compileScript(descriptor, {
 +    id: 'x',
 +    isProd: true,
 +    inlineTemplate: true,
 +    genDefaultAs: '__sfc__',
 +    vapor,
 +    templateOptions: {
 +      ssr,
 +    },
 +  })
 +
 +  const code =
 +    script.content
 +      .replace(/\bimport {/g, 'const {')
 +      .replace(/ as _/g, ': _')
 +      .replace(/} from ['"]vue['"]/g, `} = Vue`)
 +      .replace(/} from "vue\/server-renderer"/g, '} = VueServerRenderer') +
 +    '\nreturn __sfc__'
 +
 +  return new Function('Vue', 'VueServerRenderer', '_data', '_components', code)(
 +    { ...runtimeDom, ...runtimeVapor },
 +    VueServerRenderer,
 +    data,
 +    components,
 +  )
 +}
++
+ export interface InteropRenderContext {
+   mount: (container?: string | ParentNode) => InteropRenderContext
+   render: (
+     props?: RawProps,
+     container?: string | ParentNode,
+   ) => InteropRenderContext
+   host: HTMLElement
+   html: () => string
+ }
+ export function makeInteropRender(): (comp: Component) => InteropRenderContext {
+   let host: HTMLElement
+   beforeEach(() => {
+     host = document.createElement('div')
+   })
+   afterEach(() => {
+     host.remove()
+   })
+   function define(comp: Component) {
+     let app: App
+     function render(
+       props: RawProps | undefined = undefined,
+       container: string | ParentNode = host,
+     ) {
+       app?.unmount()
+       app = createApp(comp, props)
+       app.use(vaporInteropPlugin)
+       return mount(container)
+     }
+     function mount(container: string | ParentNode = host) {
+       app.mount(container)
+       return res()
+     }
+     function html() {
+       return host.innerHTML
+     }
+     const res = () => ({
+       host,
+       mount,
+       render,
+       html,
+     })
+     return res()
+   }
+   return define
+ }
index b169f01260936bd454d958c09e8c4f91eb44efd3,da57882c49de648cd4abdda7c133af7cc390b262..e463f89fdb7bf7afcbebc8d09577066ed4e8d36f
@@@ -31,18 -30,11 +31,17 @@@ import 
    type ShallowRef,
    markRaw,
    onScopeDispose,
-   pauseTracking,
    proxyRefs,
-   resetTracking,
+   setActiveSub,
    unref,
  } from '@vue/reactivity'
 -import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
 +import {
 +  EMPTY_OBJ,
 +  invokeArrayFns,
 +  isFunction,
 +  isPromise,
 +  isString,
 +} from '@vue/shared'
  import {
    type DynamicPropsSource,
    type RawProps,
@@@ -66,8 -58,11 +65,12 @@@ import 
  } from './componentSlots'
  import { hmrReload, hmrRerender } from './hmr'
  import { isHydrating, locateHydrationNode } from './dom/hydration'
- import { insertionAnchor, insertionParent } from './insertionState'
+ import {
+   insertionAnchor,
+   insertionParent,
+   resetInsertionState,
+ } from './insertionState'
 +import { parentSuspense } from './components/Suspense'
  
  export { currentInstance } from '@vue/runtime-dom'
  
@@@ -188,9 -185,16 +193,17 @@@ export function createComponent
      rawProps as RawProps,
      rawSlots as RawSlots,
      appContext,
 +    parentSuspense,
    )
  
+   // HMR
+   if (__DEV__ && component.__hmrId) {
+     registerHMR(instance)
+     instance.isSingleRoot = isSingleRoot
+     instance.hmrRerender = hmrRerender.bind(null, instance)
+     instance.hmrReload = hmrReload.bind(null, instance)
+   }
    if (__DEV__) {
      pushWarningContext(instance)
      startMeasure(instance, `init`)
        ]) || EMPTY_OBJ
      : EMPTY_OBJ
  
 -  if (__DEV__ && !isBlock(setupResult)) {
 -    if (isFunction(component)) {
 -      warn(`Functional vapor component must return a block directly.`)
 -      instance.block = []
 -    } else if (!component.render) {
 +  const isAsyncSetup = isPromise(setupResult)
 +  if (isAsyncSetup) {
 +    if (__FEATURE_SUSPENSE__) {
 +      // async setup returned Promise.
 +      // bail here and wait for re-entry.
 +      instance.asyncDep = setupResult
 +      if (__DEV__ && !instance.suspense) {
 +        const name = getComponentName(component) ?? 'Anonymous'
 +        warn(
 +          `Component <${name}>: setup function returned a promise, but no ` +
 +            `<Suspense> boundary was found in the parent component tree. ` +
 +            `A component with async setup() must be nested in a <Suspense> ` +
 +            `in order to be rendered.`,
 +        )
 +      }
 +    } else if (__DEV__) {
        warn(
 -        `Vapor component setup() returned non-block value, and has no render function.`,
 +        `setup() returned a Promise, but the version of Vue you are using ` +
 +          `does not support it yet.`,
        )
 -      instance.block = []
 -    } else {
 -      instance.devtoolsRawSetupState = setupResult
 -      // TODO make the proxy warn non-existent property access during dev
 -      instance.setupState = proxyRefs(setupResult)
 -      devRender(instance)
      }
    } else {
 -    // component has a render function but no setup function
 -    // (typically components with only a template and no state)
 -    if (!setupFn && component.render) {
 -      instance.block = callWithErrorHandling(
 -        component.render,
 -        instance,
 -        ErrorCodes.RENDER_FUNCTION,
 -      )
 -    } else {
 -      // in prod result can only be block
 -      instance.block = setupResult as Block
 -    }
 -  }
 -
 -  // single root, inherit attrs
 -  if (
 -    instance.hasFallthrough &&
 -    component.inheritAttrs !== false &&
 -    Object.keys(instance.attrs).length
 -  ) {
 -    const el = getRootElement(instance)
 -    if (el) {
 -      renderEffect(() => {
 -        isApplyingFallthroughProps = true
 -        setDynamicProps(el, [instance.attrs])
 -        isApplyingFallthroughProps = false
 -      })
 -    }
 +    handleSetupResult(setupResult, component, instance, isSingleRoot, setupFn)
    }
  
-   resetTracking()
-   simpleSetCurrentInstance(prev, instance)
+   setActiveSub(prevSub)
+   setCurrentInstance(...prevInstance)
  
    if (__DEV__) {
      popWarningContext()
  
    onScopeDispose(() => unmountComponent(instance), true)
  
 -  if (!isHydrating && _insertionParent) {
 +  if (!isHydrating && _insertionParent && !isAsyncSetup) {
-     insert(instance.block, _insertionParent, _insertionAnchor)
+     mountComponent(instance, _insertionParent, _insertionAnchor)
    }
  
    return instance
@@@ -557,69 -579,17 +596,86 @@@ export function getExposed
    }
  }
  
-     instance.block instanceof Element &&
 +export function handleSetupResult(
 +  setupResult: any,
 +  component: VaporComponent,
 +  instance: VaporComponentInstance,
 +  isSingleRoot?: boolean,
 +  setupFn?: VaporSetupFn,
 +): void {
 +  if (__DEV__) {
 +    pushWarningContext(instance)
 +  }
 +  if (__DEV__ && !isBlock(setupResult)) {
 +    if (isFunction(component)) {
 +      warn(`Functional vapor component must return a block directly.`)
 +      instance.block = []
 +    } else if (!component.render) {
 +      warn(
 +        `Vapor component setup() returned non-block value, and has no render function.`,
 +      )
 +      instance.block = []
 +    } else {
 +      instance.devtoolsRawSetupState = setupResult
 +      // TODO make the proxy warn non-existent property access during dev
 +      instance.setupState = proxyRefs(setupResult)
 +      devRender(instance)
 +
 +      // HMR
 +      if (component.__hmrId) {
 +        registerHMR(instance)
 +        instance.isSingleRoot = isSingleRoot
 +        instance.hmrRerender = hmrRerender.bind(null, instance)
 +        instance.hmrReload = hmrReload.bind(null, instance)
 +      }
 +    }
 +  } else {
 +    // component has a render function but no setup function
 +    // (typically components with only a template and no state)
 +    if (!setupFn && component.render) {
 +      instance.block = callWithErrorHandling(
 +        component.render,
 +        instance,
 +        ErrorCodes.RENDER_FUNCTION,
 +      )
 +    } else {
 +      // in prod result can only be block
 +      instance.block = setupResult as Block
 +    }
 +  }
 +
 +  // single root, inherit attrs
 +  if (
 +    instance.hasFallthrough &&
 +    component.inheritAttrs !== false &&
-     renderEffect(() => {
-       isApplyingFallthroughProps = true
-       setDynamicProps(instance.block as Element, [instance.attrs])
-       isApplyingFallthroughProps = false
-     })
 +    Object.keys(instance.attrs).length
 +  ) {
++    const el = getRootElement(instance)
++    if (el) {
++      renderEffect(() => {
++        isApplyingFallthroughProps = true
++        setDynamicProps(el, [instance.attrs])
++        isApplyingFallthroughProps = false
++      })
++    }
 +  }
 +
 +  if (__DEV__) {
 +    popWarningContext()
 +  }
 +}
++
+ function getRootElement({
+   block,
+ }: VaporComponentInstance): Element | undefined {
+   if (block instanceof Element) {
+     return block
+   }
+   if (block instanceof DynamicFragment) {
+     const { nodes } = block
+     if (nodes instanceof Element && (nodes as any).$root) {
+       return nodes
+     }
+   }
+ }
index 36a4baa09cd4b4c69161655e6b88b8749a34907e,1573a306922aacd6813fc8cdfe8f7c7a84ec4f4a..0cea11ce5ac57fe9fd77900158f335e7a4013651
@@@ -33,8 -36,9 +36,10 @@@ import type { RawSlots, VaporSlot } fro
  import { renderEffect } from './renderEffect'
  import { createTextNode } from './dom/node'
  import { optimizePropertyLookup } from './dom/prop'
 +import { setParentSuspense } from './components/Suspense'
  
+ export const interopKey: unique symbol = Symbol(`interop`)
  // mounting vapor components and slots in vdom
  const vaporInteropImpl: Omit<
    VaporInteropInterface,
      const propsRef = shallowRef(vnode.props)
      const slotsRef = shallowRef(vnode.children)
  
 +    if (__FEATURE_SUSPENSE__) {
 +      setParentSuspense(parentSuspense)
 +    }
 +
 +    const component = vnode.type as any as VaporComponent
+     const dynamicPropSource: (() => any)[] & { [interopKey]?: boolean } = [
+       () => propsRef.value,
+     ]
+     // mark as interop props
+     dynamicPropSource[interopKey] = true
      // @ts-expect-error
      const instance = (vnode.component = createComponent(
 -      vnode.type as any as VaporComponent,
 +      component,
        {
-         $: [() => propsRef.value],
+         $: dynamicPropSource,
        } as RawProps,
        {
          _: slotsRef, // pass the slots ref