unmountComponent,
} from './component'
import {
+ type App,
type AppMountFn,
type AppUnmountFn,
type CreateAppFunction,
import type { RawProps } from './componentProps'
import { getGlobalThis } from '@vue/shared'
import { optimizePropertyLookup } from './dom/prop'
+import { withHydration } from './dom/hydrate'
let _createApp: CreateAppFunction<ParentNode, VaporComponent>
// clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) {
+ if (__DEV__ && container.childNodes.length) {
+ warn('mount target container is not empty and will be cleared.')
+ }
container.textContent = ''
}
false,
app._context,
)
-
mountComponent(instance, container)
flushOnAppMount()
- return instance
+ return instance!
+}
+
+let _hydrateApp: CreateAppFunction<ParentNode, VaporComponent>
+
+const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
+ optimizePropertyLookup()
+
+ let instance: VaporComponentInstance
+ withHydration(container, () => {
+ instance = createComponent(
+ app._component,
+ app._props as RawProps,
+ null,
+ false,
+ app._context,
+ )
+ mountComponent(instance, container)
+ flushOnAppMount()
+ })
+
+ return instance!
}
const unmountApp: AppUnmountFn = app => {
unmountComponent(app._instance as VaporComponentInstance, app._container)
}
-export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
- comp,
- props,
-) => {
+function prepareApp() {
// compile-time feature flags check
if (__ESM_BUNDLER__ && !__TEST__) {
initFeatureFlags()
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target)
}
+}
- if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
- const app = _createApp(comp, props)
-
+function postPrepareApp(app: App) {
if (__DEV__) {
app.config.globalProperties = new Proxy(
{},
container = normalizeContainer(container) as ParentNode
return mount(container, ...args)
}
+}
+
+export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
+ comp,
+ props,
+) => {
+ prepareApp()
+ if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed)
+ const app = _createApp(comp, props)
+ postPrepareApp(app)
+ return app
+}
+
+export const createVaporSSRApp: CreateAppFunction<
+ ParentNode,
+ VaporComponent
+> = (comp, props) => {
+ prepareApp()
+ if (!_hydrateApp)
+ _hydrateApp = createAppAPI(hydrateApp, unmountApp, getExposed)
+ const app = _hydrateApp(comp, props)
+ postPrepareApp(app)
return app
}
} from './component'
import { createComment, createTextNode } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
+import { isHydrating } from './dom/hydrate'
export type Block =
| Node
): void {
anchor = anchor === 0 ? parent.firstChild : anchor
if (block instanceof Node) {
- parent.insertBefore(block, anchor)
+ if (!isHydrating) {
+ parent.insertBefore(block, anchor)
+ }
} else if (isVaporComponent(block)) {
- mountComponent(block, parent, anchor)
+ if (block.isMounted) {
+ insert(block.block!, parent, anchor)
+ } else {
+ mountComponent(block, parent, anchor)
+ }
} else if (isArray(block)) {
- for (let i = 0; i < block.length; i++) {
- insert(block[i], parent, anchor)
+ for (const b of block) {
+ insert(b, parent, anchor)
}
} else {
// fragment
if (block.insert) {
+ // TODO handle hydration for vdom interop
block.insert(parent, anchor)
} else {
insert(block.nodes, parent, anchor)
}
}
+export type InsertFn = typeof insert
+
export function prepend(parent: ParentNode, ...blocks: Block[]): void {
let i = blocks.length
while (i--) insert(blocks[i], parent, 0)
if (__DEV__) {
startMeasure(instance, `mount`)
}
- if (!instance.isMounted) {
- if (instance.bm) invokeArrayFns(instance.bm)
- insert(instance.block, parent, anchor)
- if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
- instance.isMounted = true
- } else {
- insert(instance.block, parent, anchor)
- }
+ if (instance.bm) invokeArrayFns(instance.bm)
+ insert(instance.block, parent, anchor)
+ if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
+ instance.isMounted = true
if (__DEV__) {
endMeasure(instance, `mount`)
}
--- /dev/null
+import { child, next } from './node'
+
+export let isHydrating = false
+export let currentHydrationNode: Node | null = null
+
+export function setCurrentHydrationNode(node: Node | null): void {
+ currentHydrationNode = node
+}
+
+export function withHydration(container: ParentNode, fn: () => void): void {
+ adoptHydrationNode = adoptHydrationNodeImpl
+ isHydrating = true
+ currentHydrationNode = child(container)
+ const res = fn()
+ isHydrating = false
+ currentHydrationNode = null
+ return res
+}
+
+export let adoptHydrationNode: (
+ node: Node | null,
+ template?: string,
+) => Node | null
+
+type Anchor = Comment & {
+ // previous open anchor
+ $p?: Anchor
+ // matching end anchor
+ $e?: Anchor
+}
+
+const isComment = (node: Node, data: string): node is Anchor =>
+ node.nodeType === 8 && (node as Comment).data === data
+
+/**
+ * Locate the first non-fragment-comment node and locate the next node
+ * while handling potential fragments.
+ */
+function adoptHydrationNodeImpl(
+ node: Node | null,
+ template?: string,
+): Node | null {
+ if (!isHydrating || !node) {
+ return node
+ }
+
+ let adopted: Node | undefined
+ let end: Node | undefined | null
+
+ if (template) {
+ while (node.nodeType === 8) node = next(node)
+ adopted = end = node
+ } else if (isComment(node, '[')) {
+ // fragment
+ let start = node
+ let cur: Node = node
+ let fragmentDepth = 1
+ // previously recorded fragment end
+ if (!end && node.$e) {
+ end = node.$e
+ }
+ while (true) {
+ cur = next(cur)
+ if (isComment(cur, '[')) {
+ // previously recorded fragment end
+ if (!end && node.$e) {
+ end = node.$e
+ }
+ fragmentDepth++
+ cur.$p = start
+ start = cur
+ } else if (isComment(cur, ']')) {
+ fragmentDepth--
+ // record fragment end on start node for later traversal
+ start.$e = cur
+ start = start.$p!
+ if (!fragmentDepth) {
+ // fragment end
+ end = cur
+ break
+ }
+ } else if (!adopted) {
+ adopted = cur
+ if (end) {
+ break
+ }
+ }
+ }
+ if (!adopted) {
+ throw new Error('hydration mismatch')
+ }
+ } else {
+ adopted = end = node
+ }
+
+ if (__DEV__ && template) {
+ const type = adopted.nodeType
+ if (
+ type === 8 ||
+ (type === 1 &&
+ !template.startsWith(
+ `<` + (adopted as Element).tagName.toLowerCase(),
+ )) ||
+ (type === 3 && !template.startsWith((adopted as Text).data))
+ ) {
+ // TODO recover
+ throw new Error('hydration mismatch!')
+ }
+ }
+
+ currentHydrationNode = next(end!)
+ return adopted
+}
+import {
+ adoptHydrationNode,
+ currentHydrationNode,
+ isHydrating,
+} from './hydrate'
+import { child } from './node'
+
+let t: HTMLTemplateElement
+
/*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) {
- let node: ChildNode
- const create = () => {
- const t = document.createElement('template')
- t.innerHTML = html
- return t.content.firstChild!
- }
+ let node: Node
return (): Node & { $root?: true } => {
- const ret = (node || (node = create())).cloneNode(true)
+ if (isHydrating) {
+ if (__DEV__ && !currentHydrationNode) {
+ // TODO this should not happen
+ throw new Error('No current hydration node')
+ }
+ return adoptHydrationNode(currentHydrationNode, html)!
+ }
+ if (!node) {
+ t = t || document.createElement('template')
+ t.innerHTML = html
+ node = child(t.content)
+ }
+ const ret = node.cloneNode(true)
if (root) (ret as any).$root = true
return ret
}
// public APIs
-export { createVaporApp } from './apiCreateApp'
+export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
export { defineVaporComponent } from './apiDefineComponent'
export { vaporInteropPlugin } from './vdomInterop'
export type { VaporDirective } from './directives/custom'