"format-check": "prettier --check --cache .",
"test": "vitest",
"test-unit": "vitest --project unit --project unit-jsdom",
- "test-e2e": "node scripts/build.js vue -f global -d && vitest --project e2e",
+ "test-e2e": "node scripts/build.js vue -f global+esm-browser-vapor -d && vitest --project e2e",
"test-e2e-vapor": "pnpm run prepare-e2e-vapor && vitest --project e2e-vapor",
"prepare-e2e-vapor": "node scripts/build.js -f cjs+esm-bundler+esm-bundler-runtime && pnpm run -C packages-private/vapor-e2e-test build",
"test-dts": "run-s build-dts test-dts-only",
type ComponentInternalInstance,
type ComponentOptions,
type ConcreteComponent,
+ type GenericComponent,
type GenericComponentInstance,
currentInstance,
getComponentName,
__asyncLoader: load,
__asyncHydrate(el, instance, hydrate) {
- let patched = false
- ;(instance.bu || (instance.bu = [])).push(() => (patched = true))
- const performHydrate = () => {
- // skip hydration if the component has been patched
- if (patched) {
- if (__DEV__) {
- const resolvedComp = getResolvedComp()!
- warn(
- `Skipping lazy hydration for component '${getComponentName(resolvedComp!) || resolvedComp!.__file}': ` +
- `it was updated before lazy hydration performed.`,
- )
- }
- return
- }
- hydrate()
- }
- const doHydrate = hydrateStrategy
- ? () => {
- const teardown = hydrateStrategy(performHydrate, cb =>
- forEachElement(el, cb),
- )
- if (teardown) {
- ;(instance.bum || (instance.bum = [])).push(teardown)
- }
- }
- : performHydrate
- if (getResolvedComp()) {
- doHydrate()
- } else {
- load().then(() => !instance.isUnmounted && doHydrate())
- }
+ performAsyncHydrate(
+ el,
+ instance,
+ hydrate,
+ getResolvedComp,
+ load,
+ hydrateStrategy,
+ )
},
get __asyncResolved() {
return { loaded, error, delayed }
}
+
+/**
+ * shared between core and vapor
+ * @internal
+ */
+export function performAsyncHydrate(
+ el: Element,
+ instance: GenericComponentInstance,
+ hydrate: () => void,
+ getResolvedComp: () => GenericComponent | undefined,
+ load: () => Promise<GenericComponent>,
+ hydrateStrategy: HydrationStrategy | undefined,
+): void {
+ let patched = false
+ ;(instance.bu || (instance.bu = [])).push(() => (patched = true))
+ const performHydrate = () => {
+ // skip hydration if the component has been patched
+ if (patched) {
+ if (__DEV__) {
+ const resolvedComp = getResolvedComp()! as GenericComponent
+ warn(
+ `Skipping lazy hydration for component '${getComponentName(resolvedComp) || resolvedComp.__file}': ` +
+ `it was updated before lazy hydration performed.`,
+ )
+ }
+ return
+ }
+ hydrate()
+ }
+ const doHydrate = hydrateStrategy
+ ? () => {
+ const teardown = hydrateStrategy(performHydrate, cb =>
+ forEachElement(el, cb),
+ )
+ if (teardown) {
+ ;(instance.bum || (instance.bum = [])).push(teardown)
+ }
+ }
+ : performHydrate
+ if (getResolvedComp()) {
+ doHydrate()
+ } else {
+ load().then(() => !instance.isUnmounted && doHydrate())
+ }
+}
__name?: string
}
+export interface AsyncComponentInternalOptions<
+ R = ConcreteComponent,
+ I = ComponentInternalInstance,
+> {
+ /**
+ * marker for AsyncComponentWrapper
+ * @internal
+ */
+ __asyncLoader?: () => Promise<R>
+ /**
+ * the inner component resolved by the AsyncComponentWrapper
+ * @internal
+ */
+ __asyncResolved?: R
+ /**
+ * Exposed for lazy hydration
+ * @internal
+ */
+ __asyncHydrate?: (el: Element, instance: I, hydrate: () => void) => void
+}
+
export interface FunctionalComponent<
P = {},
E extends EmitsOptions | Record<string, any[]> = {},
import {
+ type AsyncComponentInternalOptions,
type Component,
type ComponentInternalInstance,
type ComponentInternalOptions,
- type ConcreteComponent,
type Data,
type InternalRenderFunction,
type SetupContext,
Provide extends ComponentProvideOptions = ComponentProvideOptions,
> extends LegacyOptions<Props, D, C, M, Mixin, Extends, I, II, Provide>,
ComponentInternalOptions,
+ AsyncComponentInternalOptions,
ComponentCustomOptions {
setup?: (
this: void,
*/
__ssrInlineRender?: boolean
- /**
- * marker for AsyncComponentWrapper
- * @internal
- */
- __asyncLoader?: () => Promise<ConcreteComponent>
- /**
- * the inner component resolved by the AsyncComponentWrapper
- * @internal
- */
- __asyncResolved?: ConcreteComponent
- /**
- * Exposed for lazy hydration
- * @internal
- */
- __asyncHydrate?: (
- el: Element,
- instance: ComponentInternalInstance,
- hydrate: () => void,
- ) => void
-
// Type differentiators ------------------------------------------------------
// Note these are internal but need to be exposed in d.ts for type inference
hasHydrated = true
teardown()
hydrate()
- // replay event
- e.target!.dispatchEvent(new (e.constructor as any)(e.type, e))
+ // replay event if the event is not delegated
+ if (!(`$evt${e.type}` in e.target!)) {
+ e.target!.dispatchEvent(new (e.constructor as any)(e.type, e))
+ }
}
}
const teardown = () => {
GlobalDirectives,
ComponentInstance,
ComponentCustomElementInterface,
+ AsyncComponentInternalOptions,
} from './component'
export type {
DefineComponent,
createAsyncComponentContext,
useAsyncComponentState,
isAsyncWrapper,
+ performAsyncHydrate,
} from './apiAsyncComponent'
/**
* @internal
-import { createVaporSSRApp, delegateEvents } from '../src'
-import { nextTick, reactive, ref } from '@vue/runtime-dom'
+import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ delegateEvents,
+} from '../src'
+import { defineAsyncComponent, nextTick, reactive, ref } from '@vue/runtime-dom'
import { compileScript, parse } from '@vue/compiler-sfc'
import * as runtimeVapor from '../src'
import * as runtimeDom from '@vue/runtime-dom'
})
})
+ describe('async component', async () => {
+ test('async component', async () => {
+ const data = ref({
+ spy: vi.fn(),
+ })
+
+ const compCode = `<button @click="data.spy">hello!</button>`
+ const SSRComp = compileVaporComponent(compCode, data, undefined, true)
+ let serverResolve: any
+ // use defineAsyncComponent in SSR
+ let AsyncComp = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ serverResolve = r
+ }),
+ )
+ const appCode = `hello<components.AsyncComp/>world`
+ const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
+
+ // server render
+ const htmlPromise = VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ )
+ serverResolve(SSRComp)
+ const html = await htmlPromise
+ expect(html).toMatchInlineSnapshot(
+ `"<!--[-->hello<button>hello!</button>world<!--]-->"`,
+ )
+
+ // hydration
+ let clientResolve: any
+ AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ clientResolve = r
+ }),
+ ) as any
+
+ const Comp = compileVaporComponent(compCode, data)
+ const App = compileVaporComponent(appCode, data, { AsyncComp })
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ document.body.appendChild(container)
+ createVaporSSRApp(App).mount(container)
+
+ // hydration not complete yet
+ triggerEvent('click', container.querySelector('button')!)
+ expect(data.value.spy).not.toHaveBeenCalled()
+
+ // resolve
+ clientResolve(Comp)
+ await new Promise(r => setTimeout(r))
+
+ // should be hydrated now
+ triggerEvent('click', container.querySelector('button')!)
+ expect(data.value.spy).toHaveBeenCalled()
+ })
+
+ // No longer needed, parent component updates in vapor mode no longer
+ // cause child components to re-render
+ // test.todo('update async wrapper before resolve', async () => {})
+
+ test('update async component after parent mount before async component resolve', async () => {
+ const data = ref({
+ toggle: true,
+ })
+ const compCode = `
+ <script vapor>
+ defineProps(['toggle'])
+ </script>
+ <template>
+ <h1>{{ toggle ? 'Async component' : 'Updated async component' }}</h1>
+ </template>
+ `
+ const SSRComp = compileVaporComponent(
+ compCode,
+ undefined,
+ undefined,
+ true,
+ )
+ let serverResolve: any
+ // use defineAsyncComponent in SSR
+ let AsyncComp = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ serverResolve = r
+ }),
+ )
+ const appCode = `<components.AsyncComp :toggle="data.toggle"/>`
+ const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
+
+ // server render
+ const htmlPromise = VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ )
+ serverResolve(SSRComp)
+ const html = await htmlPromise
+ expect(html).toMatchInlineSnapshot(`"<h1>Async component</h1>"`)
+
+ // hydration
+ let clientResolve: any
+ AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ clientResolve = r
+ }),
+ ) as any
+
+ const Comp = compileVaporComponent(compCode)
+ const App = compileVaporComponent(appCode, data, { AsyncComp })
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ document.body.appendChild(container)
+ createVaporSSRApp(App).mount(container)
+
+ // update before resolve
+ data.value.toggle = false
+ await nextTick()
+
+ // resolve
+ clientResolve(Comp)
+ await new Promise(r => setTimeout(r))
+
+ // prevent lazy hydration since the component has been patched
+ expect('Skipping lazy hydration for component').toHaveBeenWarned()
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<h1>Updated async component</h1><!--async component-->"`,
+ )
+ })
+
+ test('update async component (fragment root) after parent mount before async component resolve', async () => {
+ const data = ref({
+ toggle: true,
+ })
+ const compCode = `
+ <script vapor>
+ defineProps(['toggle'])
+ </script>
+ <template>
+ <h1>{{ toggle ? 'Async component' : 'Updated async component' }}</h1>
+ <h2>fragment root</h2>
+ </template>
+ `
+ const SSRComp = compileVaporComponent(
+ compCode,
+ undefined,
+ undefined,
+ true,
+ )
+ let serverResolve: any
+ // use defineAsyncComponent in SSR
+ let AsyncComp = defineAsyncComponent(
+ () =>
+ new Promise(r => {
+ serverResolve = r
+ }),
+ )
+ const appCode = `<components.AsyncComp :toggle="data.toggle"/>`
+ const SSRApp = compileVaporComponent(appCode, data, { AsyncComp }, true)
+
+ // server render
+ const htmlPromise = VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ )
+ serverResolve(SSRComp)
+ const html = await htmlPromise
+ expect(html).toMatchInlineSnapshot(
+ `"<!--[--><h1>Async component</h1><h2>fragment root</h2><!--]-->"`,
+ )
+
+ // hydration
+ let clientResolve: any
+ AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ clientResolve = r
+ }),
+ ) as any
+
+ const Comp = compileVaporComponent(compCode)
+ const App = compileVaporComponent(appCode, data, { AsyncComp })
+
+ const container = document.createElement('div')
+ container.innerHTML = html
+ document.body.appendChild(container)
+ createVaporSSRApp(App).mount(container)
+
+ // update before resolve
+ data.value.toggle = false
+ await nextTick()
+
+ // resolve
+ clientResolve(Comp)
+ await new Promise(r => setTimeout(r))
+
+ // prevent lazy hydration since the component has been patched
+ expect('Skipping lazy hydration for component').toHaveBeenWarned()
+ expect(`Hydration node mismatch`).not.toHaveBeenWarned()
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"<!--[--><h1>Updated async component</h1><h2>fragment root</h2><!--async component--><!--]-->"`,
+ )
+ })
+
+ // required vapor Suspense
+ test.todo(
+ 'hydrate safely when property used by async setup changed before render',
+ async () => {},
+ )
+
+ // required vapor Suspense
+ test.todo(
+ 'hydrate safely when property used by deep nested async setup changed before render',
+ async () => {},
+ )
+
+ test('unmount async wrapper before load', async () => {
+ const data = ref({
+ toggle: true,
+ })
+ const compCode = `<div>async</div>`
+ const appCode = `
+ <div>
+ <components.AsyncComp v-if="data.toggle"/>
+ <div v-else>hi</div>
+ </div>
+ `
+
+ // hydration
+ let clientResolve: any
+ const AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ clientResolve = r
+ }),
+ )
+
+ const Comp = compileVaporComponent(compCode)
+ const App = compileVaporComponent(appCode, data, {
+ AsyncComp,
+ })
+
+ const container = document.createElement('div')
+ container.innerHTML = '<div><div>async</div></div>'
+ createVaporSSRApp(App).mount(container)
+
+ // unmount before resolve
+ data.value.toggle = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
+
+ // resolve
+ clientResolve(Comp)
+ await new Promise(r => setTimeout(r))
+ // should remain unmounted
+ expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
+ })
+
+ test('unmount async wrapper before load (fragment)', async () => {
+ const data = ref({
+ toggle: true,
+ })
+ const compCode = `<div>async</div><div>fragment</div>`
+ const appCode = `
+ <div>
+ <components.AsyncComp v-if="data.toggle"/>
+ <div v-else>hi</div>
+ </div>
+ `
+
+ // hydration
+ let clientResolve: any
+ const AsyncComp = defineVaporAsyncComponent(
+ () =>
+ new Promise(r => {
+ clientResolve = r
+ }),
+ )
+
+ const Comp = compileVaporComponent(compCode)
+ const App = compileVaporComponent(appCode, data, {
+ AsyncComp,
+ })
+
+ const container = document.createElement('div')
+ container.innerHTML =
+ '<div><!--[--><div>async</div><div>fragment</div><!--]--></div>'
+ createVaporSSRApp(App).mount(container)
+
+ // unmount before resolve
+ data.value.toggle = false
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
+
+ // resolve
+ clientResolve(Comp)
+ await new Promise(r => setTimeout(r))
+ // should remain unmounted
+ expect(container.innerHTML).toBe(`<div><div>hi</div><!--if--></div>`)
+ })
+
+ test('nested async wrapper', async () => {
+ const toggleCode = `
+ <script vapor>
+ import { onMounted, ref, nextTick } from 'vue'
+ const show = ref(false)
+ onMounted(() => {
+ nextTick(() => {
+ show.value = true
+ })
+ })
+ </script>
+ <template>
+ <div v-show="show">
+ <slot />
+ </div>
+ </template>
+ `
+
+ const SSRToggle = compileVaporComponent(
+ toggleCode,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const wrapperCode = `<slot/>`
+ const SSRWrapper = compileVaporComponent(
+ wrapperCode,
+ undefined,
+ undefined,
+ true,
+ )
+
+ const data = ref({
+ count: 0,
+ fn: vi.fn(),
+ })
+
+ const childCode = `
+ <script vapor>
+ import { onMounted } from 'vue'
+ const data = _data; const components = _components;
+ onMounted(() => {
+ data.value.fn()
+ data.value.count++
+ })
+ </script>
+ <template>
+ <div>{{data.count}}</div>
+ </template>
+ `
+
+ const SSRChild = compileVaporComponent(childCode, data, undefined, true)
+
+ const appCode = `
+ <components.Toggle>
+ <components.Wrapper>
+ <components.Wrapper>
+ <components.Child/>
+ </components.Wrapper>
+ </components.Wrapper>
+ </components.Toggle>
+ `
+
+ const SSRApp = compileVaporComponent(
+ appCode,
+ undefined,
+ {
+ Toggle: SSRToggle,
+ Wrapper: SSRWrapper,
+ Child: SSRChild,
+ },
+ true,
+ )
+
+ const root = document.createElement('div')
+
+ // server render
+ root.innerHTML = await VueServerRenderer.renderToString(
+ runtimeDom.createSSRApp(SSRApp),
+ )
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `"<div style="display:none;"><!--[--><!--[--><!--[--><div>0</div><!--]--><!--]--><!--]--></div>"`,
+ )
+
+ const Toggle = compileVaporComponent(toggleCode)
+ const Wrapper = compileVaporComponent(wrapperCode)
+ const Child = compileVaporComponent(childCode, data)
+
+ const App = compileVaporComponent(appCode, undefined, {
+ Toggle,
+ Wrapper,
+ Child,
+ })
+
+ // hydration
+ createVaporSSRApp(App).mount(root)
+ await nextTick()
+ await nextTick()
+ expect(root.innerHTML).toMatchInlineSnapshot(
+ `"<div style=""><!--[--><!--[--><!--[--><div>1</div><!--]--><!--]--><!--]--></div>"`,
+ )
+ expect(data.value.fn).toBeCalledTimes(1)
+ })
+ })
+
describe.todo('Suspense')
describe('force hydrate prop', async () => {
currentInstance,
handleError,
markAsyncBoundary,
+ performAsyncHydrate,
useAsyncComponentState,
+ watch,
} from '@vue/runtime-dom'
import { defineVaporComponent } from './apiDefineComponent'
import {
} from './component'
import { renderEffect } from './renderEffect'
import { DynamicFragment } from './fragment'
-
-/*! #__NO_SIDE_EFFECTS__ */
+import {
+ hydrateNode,
+ isComment,
+ isHydrating,
+ locateEndAnchor,
+ removeFragmentNodes,
+} from './dom/hydration'
+import { invokeArrayFns } from '@vue/shared'
+import { insert, remove } from './block'
+import { parentNode } from './dom/node'
+
+/*@ __NO_SIDE_EFFECTS__ */
export function defineVaporAsyncComponent<T extends VaporComponent>(
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>,
): T {
loadingComponent,
errorComponent,
delay,
- // hydrate: hydrateStrategy,
+ hydrate: hydrateStrategy,
timeout,
- // suspensible = true,
+ suspensible = true,
},
} = createAsyncComponentContext<T, VaporComponent>(source)
__asyncLoader: load,
- // __asyncHydrate(el, instance, hydrate) {
- // // TODO async hydrate
- // },
+ __asyncHydrate(
+ el: Element,
+ instance: VaporComponentInstance,
+ // Note: this hydrate function essentially calls the setup method of the component
+ // not the actual hydrate function
+ hydrate: () => void,
+ ) {
+ // if async component needs to be updated before hydration, hydration is no longer needed.
+ let isHydrated = false
+ watch(
+ () => instance.attrs,
+ () => {
+ // early return if already hydrated
+ if (isHydrated) return
+
+ // call the beforeUpdate hook to avoid calling hydrate in performAsyncHydrate
+ instance.bu && invokeArrayFns(instance.bu)
+
+ // mount the inner component and remove the placeholder
+ const parent = parentNode(el)!
+ load().then(() => {
+ if (instance.isUnmounted) return
+ hydrate()
+ if (isComment(el, '[')) {
+ const endAnchor = locateEndAnchor(el)!
+ removeFragmentNodes(el, endAnchor)
+ insert(instance.block, parent, endAnchor)
+ } else {
+ insert(instance.block, parent, el)
+ remove(el, parent)
+ }
+ })
+ },
+ { deep: true, once: true },
+ )
+
+ performAsyncHydrate(
+ el,
+ instance,
+ () => {
+ hydrateNode(el, () => {
+ hydrate()
+ insert(instance.block, parentNode(el)!, el)
+ isHydrated = true
+ })
+ },
+ getResolvedComp,
+ load,
+ hydrateStrategy,
+ )
+ },
get __asyncResolved() {
return getResolvedComp()
const instance = currentInstance as VaporComponentInstance
markAsyncBoundary(instance)
- const frag = __DEV__
- ? new DynamicFragment('async component')
- : new DynamicFragment()
+ const frag =
+ __DEV__ || isHydrating
+ ? new DynamicFragment('async component')
+ : new DynamicFragment()
// already resolved
let resolvedComp = getResolvedComp()
if (resolvedComp) {
- frag.update(() => createInnerComp(resolvedComp!, instance))
+ frag!.update(() => createInnerComp(resolvedComp!, instance))
return frag
}
)
}
- // TODO suspense-controlled or SSR.
+ // TODO suspense-controlled
+ if (__FEATURE_SUSPENSE__ && suspensible && instance.suspense) {
+ }
const { loaded, error, delayed } = useAsyncComponentState(
delay,
} else if (loadingComponent && !delayed.value) {
render = () => createComponent(loadingComponent)
}
- frag.update(render)
+ frag!.update(render)
})
return frag
import {
+ type AsyncComponentInternalOptions,
type ComponentInternalOptions,
type ComponentPropsOptions,
EffectScope,
currentInstance,
endMeasure,
expose,
+ isAsyncWrapper,
isKeepAlive,
nextUid,
popWarningContext,
adoptTemplate,
advanceHydrationNode,
currentHydrationNode,
+ isComment,
isHydrating,
+ locateEndAnchor,
locateHydrationNode,
locateNextNode,
setCurrentHydrationNode,
} from './dom/hydration'
-import { createElement } from './dom/node'
+import { _next, createElement } from './dom/node'
import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
import type { KeepAliveInstance } from './components/KeepAlive'
import {
export interface ObjectVaporComponent
extends ComponentInternalOptions,
+ AsyncComponentInternalOptions<ObjectVaporComponent, VaporComponentInstance>,
SharedInternalOptions {
setup?: VaporSetupFn
inheritAttrs?: boolean
instance.emitsOptions = normalizeEmitsOptions(component)
}
+ // hydrating async component
+ if (
+ isHydrating &&
+ isAsyncWrapper(instance) &&
+ component.__asyncHydrate &&
+ !component.__asyncResolved
+ ) {
+ // it may get unmounted before its inner component is loaded,
+ // so we need to give it a placeholder block that matches its
+ // adopted DOM
+ const el = currentHydrationNode!
+ if (isComment(el, '[')) {
+ const end = _next(locateEndAnchor(el)!)
+ const block = (instance.block = [el as Node])
+ let cur = el as Node
+ while (true) {
+ let n = _next(cur)
+ if (n && n !== end) {
+ block.push((cur = n))
+ } else {
+ break
+ }
+ }
+ } else {
+ instance.block = el
+ }
+ // also mark it as mounted to ensure it can be unmounted before
+ // its inner component is resolved
+ instance.isMounted = true
+
+ // advance current hydration node to the nextSibling
+ setCurrentHydrationNode(
+ isComment(el, '[') ? locateEndAnchor(el)! : el.nextSibling,
+ )
+ component.__asyncHydrate(el as Element, instance, () =>
+ setupComponent(instance, component),
+ )
+ } else {
+ setupComponent(instance, component)
+ }
+
+ onScopeDispose(() => unmountComponent(instance), true)
+
+ if (_insertionParent || isHydrating) {
+ mountComponent(instance, _insertionParent!, _insertionAnchor)
+ }
+
+ if (isHydrating && _insertionAnchor !== undefined) {
+ advanceHydrationNode(_insertionParent!)
+ }
+ return instance
+}
+
+export function setupComponent(
+ instance: VaporComponentInstance,
+ component: VaporComponent,
+): void {
const prevInstance = setCurrentInstance(instance)
const prevSub = setActiveSub()
}
}
+ // TODO: scopeid
+
setActiveSub(prevSub)
setCurrentInstance(...prevInstance)
popWarningContext()
endMeasure(instance, 'init')
}
-
- onScopeDispose(() => unmountComponent(instance), true)
-
- if (_insertionParent) {
- mountComponent(instance, _insertionParent, _insertionAnchor)
- }
-
- if (isHydrating && _isLastInsertion) {
- advanceHydrationNode(_insertionParent!)
- }
-
- return instance
}
export let isApplyingFallthroughProps = false
startMeasure(instance, `mount`)
}
if (instance.bm) invokeArrayFns(instance.bm)
- insert(instance.block, parent, anchor)
+ if (!isHydrating) {
+ insert(instance.block, parent, anchor)
+ }
if (instance.m) queuePostFlushCb(instance.m!)
if (
instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE &&
--- /dev/null
+<div><span id="custom-trigger">click here to hydrate</span></div>
+<div id="app"><button>0</button></div>
+
+<script type="module">
+ import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ ref,
+ onMounted,
+ delegateEvents,
+ template,
+ createIf,
+ createComponent,
+ child,
+ renderEffect,
+ setText,
+ } from '../../dist/vue.runtime-with-vapor.esm-browser.js'
+
+ delegateEvents('click')
+
+ window.isHydrated = false
+ const Comp = {
+ setup() {
+ const count = ref(0)
+ onMounted(() => {
+ console.log('hydrated')
+ window.isHydrated = true
+ })
+
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ return n0
+ },
+ }
+
+ const AsyncComp = defineVaporAsyncComponent({
+ loader: () => Promise.resolve(Comp),
+ hydrate: (hydrate, el) => {
+ const triggerEl = document.getElementById('custom-trigger')
+ triggerEl.addEventListener('click', hydrate, { once: true })
+ return () => {
+ window.teardownCalled = true
+ triggerEl.removeEventListener('click', hydrate)
+ }
+ },
+ })
+
+ const show = (window.show = ref(true))
+ createVaporSSRApp({
+ setup() {
+ onMounted(() => {
+ window.isRootMounted = true
+ })
+
+ const n0 = createIf(
+ () => show.value,
+ () => {
+ return createComponent(AsyncComp)
+ },
+ () => {
+ return template('off')()
+ },
+ )
+ return n0
+ },
+ }).mount('#app')
+</script>
--- /dev/null
+<div id="app"><button>0</button></div>
+
+<script type="module">
+ import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ ref,
+ onMounted,
+ hydrateOnIdle,
+ delegateEvents,
+ template,
+ createComponent,
+ child,
+ renderEffect,
+ setText,
+ } from '../../dist/vue.runtime-with-vapor.esm-browser.js'
+
+ delegateEvents('click')
+
+ window.isHydrated = false
+ const Comp = {
+ setup() {
+ const count = ref(0)
+ onMounted(() => {
+ console.log('hydrated')
+ window.isHydrated = true
+ })
+
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ return n0
+ },
+ }
+
+ const AsyncComp = defineVaporAsyncComponent({
+ loader: () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ console.log('resolve')
+ resolve(Comp)
+ requestIdleCallback(() => {
+ console.log('busy')
+ })
+ }, 10)
+ }),
+ hydrate: hydrateOnIdle(),
+ })
+
+ createVaporSSRApp({
+ setup() {
+ return createComponent(AsyncComp)
+ },
+ }).mount('#app')
+</script>
--- /dev/null
+<div>click to hydrate</div>
+<div id="app"><button>0</button></div>
+<style>
+ body {
+ margin: 0;
+ }
+</style>
+
+<script type="module">
+ import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ ref,
+ onMounted,
+ hydrateOnInteraction,
+ delegateEvents,
+ template,
+ createComponent,
+ child,
+ renderEffect,
+ setText,
+ } from '../../dist/vue.runtime-with-vapor.esm-browser.js'
+
+ delegateEvents('click')
+
+ const isFragment = location.search.includes('?fragment')
+ if (isFragment) {
+ document.getElementById('app').innerHTML =
+ `<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
+ }
+
+ window.isHydrated = false
+
+ const Comp = {
+ setup() {
+ const count = ref(0)
+ onMounted(() => {
+ console.log('hydrated')
+ window.isHydrated = true
+ })
+
+ if (isFragment) {
+ const n1 = template('<span>one</span>')()
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ const n2 = template('<span>two</span>')()
+ return [n1, n0, n2]
+ } else {
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ return n0
+ }
+ },
+ }
+
+ const AsyncComp = defineVaporAsyncComponent({
+ loader: () => Promise.resolve(Comp),
+ hydrate: hydrateOnInteraction(['click', 'wheel']),
+ })
+
+ createVaporSSRApp({
+ setup() {
+ onMounted(() => {
+ window.isRootMounted = true
+ })
+ return createComponent(AsyncComp)
+ },
+ }).mount('#app')
+</script>
--- /dev/null
+<div>resize the window width to < 500px to hydrate</div>
+<div id="app"><button>0</button></div>
+
+<script type="module">
+ import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ ref,
+ onMounted,
+ hydrateOnMediaQuery,
+ delegateEvents,
+ template,
+ createComponent,
+ child,
+ renderEffect,
+ setText,
+ } from '../../dist/vue.runtime-with-vapor.esm-browser.js'
+
+ delegateEvents('click')
+
+ window.isHydrated = false
+ const Comp = {
+ props: {
+ value: Boolean,
+ },
+ setup(props) {
+ const count = ref(0)
+ onMounted(() => {
+ console.log('hydrated')
+ window.isHydrated = true
+ })
+
+ props.value
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ return n0
+ },
+ }
+
+ const AsyncComp = defineVaporAsyncComponent({
+ loader: () => Promise.resolve(Comp),
+ hydrate: hydrateOnMediaQuery('(max-width:500px)'),
+ })
+
+ createVaporSSRApp({
+ setup() {
+ onMounted(() => {
+ window.isRootMounted = true
+ })
+
+ const show = (window.show = ref(true))
+ return createComponent(AsyncComp, { value: () => show.value })
+ },
+ }).mount('#app')
+</script>
--- /dev/null
+<script src="../../dist/vue.global.js"></script>
+
+<div style="height: 1000px">scroll to the bottom to hydrate</div>
+<div id="app"><button>0</button></div>
+<style>
+ body {
+ margin: 0;
+ }
+</style>
+
+<script type="module">
+ import {
+ createVaporSSRApp,
+ defineVaporAsyncComponent,
+ ref,
+ onMounted,
+ hydrateOnVisible,
+ delegateEvents,
+ template,
+ createComponent,
+ child,
+ renderEffect,
+ setText,
+ } from '../../dist/vue.runtime-with-vapor.esm-browser.js'
+
+ delegateEvents('click')
+
+ const rootMargin = location.search.match(/rootMargin=(\d+)/)?.[1] ?? 0
+ const isFragment = location.search.includes('?fragment')
+ const isVIf = location.search.includes('?v-if')
+ if (isFragment) {
+ document.getElementById('app').innerHTML =
+ `<!--[--><!--[--><span>one</span><!--]--><button>0</button><span>two</span><!--]-->`
+ } else if (isVIf) {
+ document.getElementById('app').innerHTML = `<!---->`
+ }
+
+ window.isHydrated = false
+
+ const Comp = {
+ setup() {
+ const count = ref(0)
+ onMounted(() => {
+ console.log('hydrated')
+ window.isHydrated = true
+ })
+
+ if (isVIf) {
+ return template('<!--v-if-->')()
+ } else if (isFragment) {
+ const n1 = template('<span>one</span>')()
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ const n2 = template('<span>two</span>')()
+ return [n1, n0, n2]
+ } else {
+ const n0 = template('<button> </button>', true)()
+ const x0 = child(n0)
+ n0.$evtclick = () => count.value++
+ renderEffect(() => setText(x0, count.value))
+ return n0
+ }
+ },
+ }
+
+ const AsyncComp = defineVaporAsyncComponent({
+ loader: () => Promise.resolve(Comp),
+ hydrate: hydrateOnVisible({ rootMargin: rootMargin + 'px' }),
+ })
+
+ createVaporSSRApp({
+ setup() {
+ onMounted(() => {
+ window.isRootMounted = true
+ })
+ return createComponent(AsyncComp)
+ },
+ }).mount('#app')
+</script>
}
describe('async component hydration strategies', () => {
- const { page, click, text, count } = setupPuppeteer(['--window-size=800,600'])
+ const { page, click, text, count } = setupPuppeteer([
+ '--window-size=800,600',
+ '--disable-web-security',
+ ])
- async function goToCase(name: string, query = '') {
- const file = `file://${path.resolve(__dirname, `./hydration-strat-${name}.html${query}`)}`
+ async function goToCase(name: string, query = '', vapor = false) {
+ const file = `file://${path.resolve(__dirname, `./hydration-strat-${name}${vapor ? '-vapor' : ''}.html${query}`)}`
await page().goto(file)
}
expect(await text('button')).toBe(n)
}
- test('idle', async () => {
- const messages: string[] = []
- page().on('console', e => messages.push(e.text()))
-
- await goToCase('idle')
- // not hydrated yet
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- // wait for hydration
- await page().waitForFunction(() => window.isHydrated)
- // assert message order: hyration should happen after already queued main thread work
- expect(messages.slice(1)).toMatchObject(['resolve', 'busy', 'hydrated'])
- await assertHydrationSuccess()
+ describe('vdom', () => {
+ runSharedTests(false)
})
- test('visible', async () => {
- await goToCase('visible')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- // scroll down
- await page().evaluate(() => window.scrollTo({ top: 1000 }))
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess()
+ describe('vapor', () => {
+ runSharedTests(true)
})
- test('visible (with rootMargin)', async () => {
- await goToCase('visible', '?rootMargin=1000')
- await page().waitForFunction(() => window.isRootMounted)
- // should hydrate without needing to scroll
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess()
- })
-
- test('visible (fragment)', async () => {
- await goToCase('visible', '?fragment')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- expect(await count('span')).toBe(2)
- // scroll down
- await page().evaluate(() => window.scrollTo({ top: 1000 }))
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess()
- })
-
- test('visible (root v-if) should not throw error', async () => {
- const spy = vi.fn()
- const currentPage = page()
- currentPage.on('pageerror', spy)
- await goToCase('visible', '?v-if')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- expect(spy).toBeCalledTimes(0)
- currentPage.off('pageerror', spy)
- })
-
- test('media query', async () => {
- await goToCase('media')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- // resize
- await page().setViewport({ width: 400, height: 600 })
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess()
- })
-
- // #13255
- test('media query (patched before hydration)', async () => {
- const spy = vi.fn()
- const currentPage = page()
- currentPage.on('pageerror', spy)
-
- const warn: any[] = []
- currentPage.on('console', e => warn.push(e.text()))
-
- await goToCase('media')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
-
- // patch
- await page().evaluate(() => (window.show.value = false))
- await click('button')
- expect(await text('button')).toBe('1')
-
- // resize
- await page().setViewport({ width: 400, height: 600 })
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess('2')
-
- expect(spy).toBeCalledTimes(0)
- currentPage.off('pageerror', spy)
- expect(
- warn.some(w => w.includes('Skipping lazy hydration for component')),
- ).toBe(true)
- })
-
- test('interaction', async () => {
- await goToCase('interaction')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- await click('button')
- await page().waitForFunction(() => window.isHydrated)
- // should replay event
- expect(await text('button')).toBe('1')
- await assertHydrationSuccess('2')
- })
-
- test('interaction (fragment)', async () => {
- await goToCase('interaction', '?fragment')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- await click('button')
- await page().waitForFunction(() => window.isHydrated)
- // should replay event
- expect(await text('button')).toBe('1')
- await assertHydrationSuccess('2')
- })
-
- test('custom', async () => {
- await goToCase('custom')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- await click('#custom-trigger')
- await page().waitForFunction(() => window.isHydrated)
- await assertHydrationSuccess()
- })
-
- test('custom teardown', async () => {
- await goToCase('custom')
- await page().waitForFunction(() => window.isRootMounted)
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- await page().evaluate(() => (window.show.value = false))
- expect(await text('#app')).toBe('off')
- expect(await page().evaluate(() => window.isHydrated)).toBe(false)
- expect(await page().evaluate(() => window.teardownCalled)).toBe(true)
- })
+ function runSharedTests(vapor: boolean) {
+ test('idle', async () => {
+ const messages: string[] = []
+ page().on('console', e => messages.push(e.text()))
+
+ await goToCase('idle', '', vapor)
+ // not hydrated yet
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ // wait for hydration
+ await page().waitForFunction(() => window.isHydrated)
+ // assert message order: hyration should happen after already queued main thread work
+ expect(messages.slice(1)).toMatchObject(['resolve', 'busy', 'hydrated'])
+ await assertHydrationSuccess()
+ })
+
+ test('visible', async () => {
+ await goToCase('visible', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ // scroll down
+ await page().evaluate(() => window.scrollTo({ top: 1000 }))
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess()
+ })
+
+ test('visible (with rootMargin)', async () => {
+ await goToCase('visible', '?rootMargin=1000', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ // should hydrate without needing to scroll
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess()
+ })
+
+ test('visible (fragment)', async () => {
+ await goToCase('visible', '?fragment', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ expect(await count('span')).toBe(2)
+ // scroll down
+ await page().evaluate(() => window.scrollTo({ top: 1000 }))
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess()
+ })
+
+ test('visible (root v-if) should not throw error', async () => {
+ const spy = vi.fn()
+ const currentPage = page()
+ currentPage.on('pageerror', spy)
+ await goToCase('visible', '?v-if', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ expect(spy).toBeCalledTimes(0)
+ currentPage.off('pageerror', spy)
+ })
+
+ test('media query', async () => {
+ await goToCase('media', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ // resize
+ await page().setViewport({ width: 400, height: 600 })
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess()
+ })
+
+ // #13255
+ test('media query (patched before hydration)', async () => {
+ const spy = vi.fn()
+ const currentPage = page()
+ currentPage.on('pageerror', spy)
+
+ const warn: any[] = []
+ currentPage.on('console', e => warn.push(e.text()))
+
+ await goToCase('media', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+
+ // patch
+ await page().evaluate(() => (window.show.value = false))
+ await click('button')
+ expect(await text('button')).toBe('1')
+
+ // resize
+ await page().setViewport({ width: 400, height: 600 })
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess('2')
+
+ expect(spy).toBeCalledTimes(0)
+ currentPage.off('pageerror', spy)
+ expect(
+ warn.some(w => w.includes('Skipping lazy hydration for component')),
+ ).toBe(true)
+ })
+
+ test('interaction', async () => {
+ await goToCase('interaction', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ await click('button')
+ await page().waitForFunction(() => window.isHydrated)
+ // should replay event
+ expect(await text('button')).toBe('1')
+ await assertHydrationSuccess('2')
+ })
+
+ test('interaction (fragment)', async () => {
+ await goToCase('interaction', '?fragment', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ await click('button')
+ await page().waitForFunction(() => window.isHydrated)
+ // should replay event
+ expect(await text('button')).toBe('1')
+ await assertHydrationSuccess('2')
+ })
+
+ test('custom', async () => {
+ await goToCase('custom', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ await click('#custom-trigger')
+ await page().waitForFunction(() => window.isHydrated)
+ await assertHydrationSuccess()
+ })
+
+ test('custom teardown', async () => {
+ await goToCase('custom', '', vapor)
+ await page().waitForFunction(() => window.isRootMounted)
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ await page().evaluate(() => (window.show.value = false))
+ expect(await text('#app')).toBe('off')
+ expect(await page().evaluate(() => window.isHydrated)).toBe(false)
+ expect(await page().evaluate(() => window.teardownCalled)).toBe(true)
+ })
+ }
})
// for type generation only
-export * from './index'
+export * from './indexBase'
export * from '@vue/runtime-vapor'
-// This entry is the "full-build" that includes both the runtime
-// and the compiler, and supports on-the-fly compilation of the template option.
-import { initDev } from './dev'
-import {
- type CompilerError,
- type CompilerOptions,
- compile,
-} from '@vue/compiler-dom'
-import {
- type RenderFunction,
- registerRuntimeCompiler,
- warn,
-} from '@vue/runtime-dom'
-import * as runtimeDom from '@vue/runtime-dom'
-import {
- NOOP,
- extend,
- genCacheKey,
- generateCodeFrame,
- isString,
-} from '@vue/shared'
-import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
-
-if (__DEV__) {
- initDev()
-}
-
-const compileCache: Record<string, RenderFunction> = Object.create(null)
-
-function compileToFunction(
- template: string | HTMLElement,
- options?: CompilerOptions,
-): RenderFunction {
- if (!isString(template)) {
- if (template.nodeType) {
- template = template.innerHTML
- } else {
- __DEV__ && warn(`invalid template option: `, template)
- return NOOP
- }
- }
-
- const key = genCacheKey(template, options)
- const cached = compileCache[key]
- if (cached) {
- return cached
- }
-
- if (template[0] === '#') {
- const el = document.querySelector(template)
- if (__DEV__ && !el) {
- warn(`Template element not found or is empty: ${template}`)
- }
- // __UNSAFE__
- // Reason: potential execution of JS expressions in in-DOM template.
- // The user must make sure the in-DOM template is trusted. If it's rendered
- // by the server, the template should not contain any user data.
- template = el ? el.innerHTML : ``
- }
-
- const opts = extend(
- {
- hoistStatic: true,
- onError: __DEV__ ? onError : undefined,
- onWarn: __DEV__ ? e => onError(e, true) : NOOP,
- } as CompilerOptions,
- options,
- )
-
- if (!opts.isCustomElement && typeof customElements !== 'undefined') {
- opts.isCustomElement = tag => !!customElements.get(tag)
- }
-
- const { code } = compile(template, opts)
-
- function onError(err: CompilerError, asWarning = false) {
- const message = asWarning
- ? err.message
- : `Template compilation error: ${err.message}`
- const codeFrame =
- err.loc &&
- generateCodeFrame(
- template as string,
- err.loc.start.offset,
- err.loc.end.offset,
- )
- warn(codeFrame ? `${message}\n${codeFrame}` : message)
- }
-
- // The wildcard import results in a huge object with every export
- // with keys that cannot be mangled, and can be quite heavy size-wise.
- // In the global build we know `Vue` is available globally so we can avoid
- // the wildcard object.
- const render = (
- __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
- ) as RenderFunction
-
- // mark the function as runtime compiled
- ;(render as InternalRenderFunction)._rc = true
-
- return (compileCache[key] = render)
-}
-
-registerRuntimeCompiler(compileToFunction)
-
-export { compileToFunction as compile }
-export * from '@vue/runtime-dom'
+export * from './indexBase'
+export * from './vaporAliases'
--- /dev/null
+// This entry is the "full-build" that includes both the runtime
+// and the compiler, and supports on-the-fly compilation of the template option.
+import { initDev } from './dev'
+import {
+ type CompilerError,
+ type CompilerOptions,
+ compile,
+} from '@vue/compiler-dom'
+import {
+ type RenderFunction,
+ registerRuntimeCompiler,
+ warn,
+} from '@vue/runtime-dom'
+import * as runtimeDom from '@vue/runtime-dom'
+import {
+ NOOP,
+ extend,
+ genCacheKey,
+ generateCodeFrame,
+ isString,
+} from '@vue/shared'
+import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
+
+if (__DEV__) {
+ initDev()
+}
+
+const compileCache: Record<string, RenderFunction> = Object.create(null)
+
+function compileToFunction(
+ template: string | HTMLElement,
+ options?: CompilerOptions,
+): RenderFunction {
+ if (!isString(template)) {
+ if (template.nodeType) {
+ template = template.innerHTML
+ } else {
+ __DEV__ && warn(`invalid template option: `, template)
+ return NOOP
+ }
+ }
+
+ const key = genCacheKey(template, options)
+ const cached = compileCache[key]
+ if (cached) {
+ return cached
+ }
+
+ if (template[0] === '#') {
+ const el = document.querySelector(template)
+ if (__DEV__ && !el) {
+ warn(`Template element not found or is empty: ${template}`)
+ }
+ // __UNSAFE__
+ // Reason: potential execution of JS expressions in in-DOM template.
+ // The user must make sure the in-DOM template is trusted. If it's rendered
+ // by the server, the template should not contain any user data.
+ template = el ? el.innerHTML : ``
+ }
+
+ const opts = extend(
+ {
+ hoistStatic: true,
+ onError: __DEV__ ? onError : undefined,
+ onWarn: __DEV__ ? e => onError(e, true) : NOOP,
+ } as CompilerOptions,
+ options,
+ )
+
+ if (!opts.isCustomElement && typeof customElements !== 'undefined') {
+ opts.isCustomElement = tag => !!customElements.get(tag)
+ }
+
+ const { code } = compile(template, opts)
+
+ function onError(err: CompilerError, asWarning = false) {
+ const message = asWarning
+ ? err.message
+ : `Template compilation error: ${err.message}`
+ const codeFrame =
+ err.loc &&
+ generateCodeFrame(
+ template as string,
+ err.loc.start.offset,
+ err.loc.end.offset,
+ )
+ warn(codeFrame ? `${message}\n${codeFrame}` : message)
+ }
+
+ // The wildcard import results in a huge object with every export
+ // with keys that cannot be mangled, and can be quite heavy size-wise.
+ // In the global build we know `Vue` is available globally so we can avoid
+ // the wildcard object.
+ const render = (
+ __GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
+ ) as RenderFunction
+
+ // mark the function as runtime compiled
+ ;(render as InternalRenderFunction)._rc = true
+
+ return (compileCache[key] = render)
+}
+
+registerRuntimeCompiler(compileToFunction)
+
+export { compileToFunction as compile }
+export * from '@vue/runtime-dom'
-export * from './runtime'
+export * from './runtimeBase'
export * from '@vue/runtime-vapor'
-// This entry exports the runtime only, and is built as
-// `dist/vue.esm-bundler.js` which is used by default for bundlers.
-import { NOOP } from '@vue/shared'
-import { initDev } from './dev'
-import { type RenderFunction, warn } from '@vue/runtime-dom'
-
-if (__DEV__) {
- initDev()
-}
-
-export * from '@vue/runtime-dom'
-
-export const compile = (_template: string): RenderFunction => {
- if (__DEV__) {
- warn(
- `Runtime compilation is not supported in this build of Vue.` +
- (__ESM_BUNDLER__
- ? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
- : __ESM_BROWSER__
- ? ` Use "vue.esm-browser.js" instead.`
- : __GLOBAL__
- ? ` Use "vue.global.js" instead.`
- : ``) /* should not happen */,
- )
- }
- return NOOP
-}
+export * from './runtimeBase'
+export * from './vaporAliases'
--- /dev/null
+// This entry exports the runtime only, and is built as
+// `dist/vue.esm-bundler.js` which is used by default for bundlers.
+import { NOOP } from '@vue/shared'
+import { initDev } from './dev'
+import { type RenderFunction, warn } from '@vue/runtime-dom'
+
+if (__DEV__) {
+ initDev()
+}
+
+export * from '@vue/runtime-dom'
+
+export const compile = (_template: string): RenderFunction => {
+ if (__DEV__) {
+ warn(
+ `Runtime compilation is not supported in this build of Vue.` +
+ (__ESM_BUNDLER__
+ ? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
+ : __ESM_BROWSER__
+ ? ` Use "vue.esm-browser.js" instead.`
+ : __GLOBAL__
+ ? ` Use "vue.global.js" instead.`
+ : ``) /* should not happen */,
+ )
+ }
+ return NOOP
+}
--- /dev/null
+// Vapor-only APIs do not exist in the standard build, yet SSR executes
+// the standard entry. We alias them to the core implementations so SSR
+// keeps working without the Vapor runtime.
+export {
+ defineAsyncComponent as defineVaporAsyncComponent,
+ defineComponent as defineVaporComponent,
+} from '@vue/runtime-core'