From: daiwei Date: Wed, 16 Jul 2025 13:10:53 +0000 (+0800) Subject: chore: Merge branch 'minor' into edison/feat/SuspenseVaporInterop X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=35aba7c51502c2f7fba28fdd040f3d974c893f76;p=thirdparty%2Fvuejs%2Fcore.git chore: Merge branch 'minor' into edison/feat/SuspenseVaporInterop --- 35aba7c51502c2f7fba28fdd040f3d974c893f76 diff --cc packages/runtime-core/src/apiCreateApp.ts index 31e8ee85df,a1409a7fe4..459d6ee938 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@@ -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' diff --cc packages/runtime-core/src/index.ts index eacee712a6,243bde548c..9d12d5115f --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@@ -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' diff --cc packages/runtime-vapor/__tests__/_utils.ts index 729d42de78,d1ede2a6c9..b0a3fa8deb --- a/packages/runtime-vapor/__tests__/_utils.ts +++ b/packages/runtime-vapor/__tests__/_utils.ts @@@ -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, + components: Record = {}, + { + vapor = true, + ssr = false, + }: { + vapor?: boolean | undefined + ssr?: boolean | undefined + } = {}, +): any { + if (!sfc.includes(`const data = _data; const components = _components;` + + 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 + } diff --cc packages/runtime-vapor/src/component.ts index b169f01260,da57882c49..e463f89fdb --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@@ -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`) @@@ -216,33 -219,54 +228,33 @@@ ]) || 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 ` + + ` boundary was found in the parent component tree. ` + + `A component with async setup() must be nested in a ` + + `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() @@@ -251,8 -275,8 +263,8 @@@ 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 } } +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 && - instance.block instanceof Element && + Object.keys(instance.attrs).length + ) { - renderEffect(() => { - isApplyingFallthroughProps = true - setDynamicProps(instance.block as Element, [instance.attrs]) - isApplyingFallthroughProps = false - }) ++ 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 + } + } + } diff --cc packages/runtime-vapor/src/vdomInterop.ts index 36a4baa09c,1573a30692..0cea11ce5a --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@@ -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, @@@ -49,16 -53,16 +54,21 @@@ 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