`;
exports[`compile > custom directive > component 1`] = `
-"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, insert as _insert, createIf as _createIf, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, resolveDirective as _resolveDirective, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, withVaporDirectives as _withVaporDirectives, createIf as _createIf, template as _template } from 'vue';
const t0 = _template("<div></div>")
export function render(_ctx) {
"default": () => {
const n0 = _createIf(() => (true), () => {
const n3 = t0()
+ _setInsertionState(n3)
const n2 = _createComponentWithFallback(_component_Bar)
_withVaporDirectives(n2, [[_directive_hello, void 0, void 0, { world: true }]])
- _insert(n2, n3)
return n3
})
return n0
`;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
-"import { resolveComponent as _resolveComponent, child as _child, createComponentWithFallback as _createComponentWithFallback, prepend as _prepend, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, child as _child, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
const t1 = _template("<div> </div>")
const n0 = t0()
const n3 = t1()
const n2 = _child(n3)
+ _setInsertionState(n3, 0)
const n1 = _createComponentWithFallback(_component_Comp)
- _prepend(n3, n1)
_renderEffect(() => {
_setText(n2, _toDisplayString(_ctx.bar))
_setProp(n3, "id", _ctx.foo)
`;
exports[`compiler: v-for > nested v-for 1`] = `
-"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, insert as _insert, template as _template } from 'vue';
+"import { setInsertionState as _setInsertionState, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<span> </span>")
const t1 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_for_item0) => {
const n5 = t1()
+ _setInsertionState(n5)
const n2 = _createFor(() => (_for_item0.value), (_for_item1) => {
const n4 = t0()
const x4 = _child(n4)
_renderEffect(() => _setText(x4, _toDisplayString(_for_item1.value+_for_item0.value)))
return n4
}, null, 1)
- _insert(n2, n5)
return n5
})
return n0
`;
exports[`compiler: v-once > on component 1`] = `
-"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, insert as _insert, template as _template } from 'vue';
+"import { resolveComponent as _resolveComponent, setInsertionState as _setInsertionState, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp")
const n1 = t0()
+ _setInsertionState(n1)
const n0 = _createComponentWithFallback(_component_Comp, { id: () => (_ctx.foo) }, null, null, true)
- _insert(n0, n1)
return n1
}"
`;
id: 0,
tag: 'Comp',
once: true,
- },
- {
- type: IRNodeTypes.INSERT_NODE,
- elements: [0],
parent: 1,
},
])
const rawSlots = genRawSlots(slots, context)
const [ids, handlers] = processInlineHandlers(props, context)
const rawProps = context.withId(() => genRawProps(props, context), ids)
+
const inlineHandlers: CodeFragment[] = handlers.reduce<CodeFragment[]>(
(acc, { name, value }) => {
const handler = genEventHandler(context, value, undefined, false)
-import { type IREffect, IRNodeTypes, type OperationNode } from '../ir'
+import {
+ type IREffect,
+ IRNodeTypes,
+ type InsertionStateTypes,
+ type OperationNode,
+ isTypeThatNeedsInsertionState,
+} from '../ir'
import type { CodegenContext } from '../generate'
import { genInsertNode, genPrependNode } from './dom'
import { genSetDynamicEvents, genSetEvent } from './event'
INDENT_START,
NEWLINE,
buildCodeFragment,
+ genCall,
} from './utils'
import { genCreateComponent } from './component'
import { genSlotOutlet } from './slotOutlet'
): CodeFragment[] {
const [frag, push] = buildCodeFragment()
for (const operation of opers) {
+ if (isTypeThatNeedsInsertionState(operation) && operation.parent) {
+ push(...genInsertionstate(operation, context))
+ }
push(...genOperation(operation, context))
}
return frag
return frag
}
+
+function genInsertionstate(
+ operation: InsertionStateTypes,
+ context: CodegenContext,
+): CodeFragment[] {
+ return [
+ NEWLINE,
+ ...genCall(
+ context.helper('setInsertionState'),
+ `n${operation.parent}`,
+ operation.anchor == null
+ ? undefined
+ : operation.anchor === -1 // -1 indicates prepend
+ ? `0` // runtime anchor value for prepend
+ : `n${operation.anchor}`,
+ ),
+ ]
+}
positive: BlockIRNode
negative?: BlockIRNode | IfIRNode
once?: boolean
+ parent?: number
+ anchor?: number
}
export interface IRFor {
once: boolean
component: boolean
onlyChild: boolean
+ parent?: number
+ anchor?: number
}
export interface SetPropIRNode extends BaseIRNode {
effect: boolean
}
+// TODO remove, no longer needed
export interface CreateTextNodeIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_TEXT_NODE
id: number
root: boolean
once: boolean
dynamic?: SimpleExpressionNode
+ parent?: number
+ anchor?: number
}
export interface DeclareOldRefIRNode extends BaseIRNode {
name: SimpleExpressionNode
props: IRProps[]
fallback?: BlockIRNode
+ parent?: number
+ anchor?: number
}
export interface GetTextChildIRNode extends BaseIRNode {
arg: Exclude<DirectiveNode['arg'], CompoundExpressionNode>
}
>
+
+export type InsertionStateTypes =
+ | IfIRNode
+ | ForIRNode
+ | SlotOutletIRNode
+ | CreateComponentIRNode
+
+export function isTypeThatNeedsInsertionState(
+ op: OperationNode,
+): op is InsertionStateTypes {
+ const type = op.type
+ return (
+ type === IRNodeTypes.CREATE_COMPONENT_NODE ||
+ type === IRNodeTypes.SLOT_OUTLET_NODE ||
+ type === IRNodeTypes.IF ||
+ type === IRNodeTypes.FOR
+ )
+}
type TransformContext,
transformNode,
} from '../transform'
-import { DynamicFlag, type IRDynamicInfo, IRNodeTypes } from '../ir'
+import {
+ DynamicFlag,
+ type IRDynamicInfo,
+ IRNodeTypes,
+ isTypeThatNeedsInsertionState as isBlockOperation,
+} from '../ir'
export const transformChildren: NodeTransform = (node, context) => {
const isFragment =
if (prevDynamics.length) {
if (hasStaticTemplate) {
context.childrenTemplate[index - prevDynamics.length] = `<!>`
-
prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
const anchor = (prevDynamics[0].anchor = context.increaseId())
- context.registerOperation({
- type: IRNodeTypes.INSERT_NODE,
- elements: prevDynamics.map(child => child.id!),
- parent: context.reference(),
- anchor,
- })
+ registerInsertion(prevDynamics, context, anchor)
} else {
- context.registerOperation({
- type: IRNodeTypes.PREPEND_NODE,
- elements: prevDynamics.map(child => child.id!),
- parent: context.reference(),
- })
+ registerInsertion(prevDynamics, context, -1 /* prepend */)
}
prevDynamics = []
}
}
if (prevDynamics.length) {
- context.registerOperation({
- type: IRNodeTypes.INSERT_NODE,
- elements: prevDynamics.map(child => child.id!),
- parent: context.reference(),
- })
+ registerInsertion(prevDynamics, context)
+ }
+}
+
+function registerInsertion(
+ dynamics: IRDynamicInfo[],
+ context: TransformContext,
+ anchor?: number,
+) {
+ for (const child of dynamics) {
+ if (child.template != null) {
+ // template node due to invalid nesting - generate actual insertion
+ context.registerOperation({
+ type: IRNodeTypes.INSERT_NODE,
+ elements: dynamics.map(child => child.id!),
+ parent: context.reference(),
+ anchor,
+ })
+ } else {
+ // block types
+ for (const op of context.block.operation) {
+ if (isBlockOperation(op) && op.id === child.id) {
+ op.parent = context.reference()
+ op.anchor = anchor
+ }
+ }
+ }
}
}
// import { type SSRContext, renderToString } from '@vue/server-renderer'
import {
child,
+ createComponent,
createVaporSSRApp,
delegateEvents,
next,
renderEffect,
setClass,
+ setInsertionState,
setText,
template,
} from '../src'
)
})
+ test('basic component', async () => {
+ const t0 = template(' ')
+ const msg = ref('foo')
+ const Comp = {
+ setup() {
+ const n0 = t0() as Text
+ renderEffect(() => setText(n0, toDisplayString(msg.value)))
+ return n0
+ },
+ }
+
+ const t1 = template('<div><span></span></div>', true)
+ const { container } = mountWithHydration(
+ '<div><span></span>foo</div>',
+ () => {
+ const n1 = t1() as Element
+ setInsertionState(n1)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(`<div><span></span>foo</div>`)
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(`<div><span></span>bar</div>`)
+ })
+
+ test('fragment component', async () => {
+ const t0 = template('<div> </div>')
+ const t1 = template(' ')
+ const msg = ref('foo')
+ const Comp = {
+ setup() {
+ const n0 = t0() as Element
+ const n1 = t1() as Text
+ const x0 = child(n0) as Text
+ renderEffect(() => {
+ const _msg = msg.value
+
+ setText(x0, toDisplayString(_msg))
+ setText(n1, toDisplayString(_msg))
+ })
+ return [n0, n1]
+ },
+ }
+
+ const t2 = template('<div><span></span></div>', true)
+ const { container } = mountWithHydration(
+ '<div><span></span><!--[--><div>foo</div>foo<!--]--></div>',
+ () => {
+ const n1 = t2() as Element
+ setInsertionState(n1)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<div><span></span><!--[--><div>foo</div>foo<!--]--></div>`,
+ )
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div><span></span><!--[--><div>bar</div>bar<!--]--></div>`,
+ )
+ })
+
+ test('fragment component with prepend', async () => {
+ const t0 = template('<div> </div>')
+ const t1 = template(' ')
+ const msg = ref('foo')
+ const Comp = {
+ setup() {
+ const n0 = t0() as Element
+ const n1 = t1() as Text
+ const x0 = child(n0) as Text
+ renderEffect(() => {
+ const _msg = msg.value
+
+ setText(x0, toDisplayString(_msg))
+ setText(n1, toDisplayString(_msg))
+ })
+ return [n0, n1]
+ },
+ }
+
+ const t2 = template('<div><span></span></div>', true)
+ const { container } = mountWithHydration(
+ '<div><!--[--><div>foo</div>foo<!--]--><span></span></div>',
+ () => {
+ const n1 = t2() as Element
+ setInsertionState(n1, 0)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ `<div><!--[--><div>foo</div>foo<!--]--><span></span></div>`,
+ )
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ `<div><!--[--><div>bar</div>bar<!--]--><span></span></div>`,
+ )
+ })
+
// test('element with ref', () => {
// const el = ref()
// const { vnode, container } = mountWithHydration('<div></div>', () =>
getSlot,
} from './componentSlots'
import { hmrReload, hmrRerender } from './hmr'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+} from './insertionState'
export { currentInstance } from '@vue/runtime-dom'
currentInstance.appContext) ||
emptyContext,
): VaporComponentInstance {
+ if (isHydrating) {
+ locateHydrationNode()
+ }
+
// vdom interop enabled and component is not an explicit vapor component
if (appContext.vapor && !component.__vapor) {
return appContext.vapor.vdomMount(component as any, rawProps, rawSlots)
onScopeDispose(() => unmountComponent(instance), true)
+ if (!isHydrating && insertionParent) {
+ insert(instance.block, insertionParent, insertionAnchor)
+ resetInsertionState()
+ }
+
return instance
}
+import { warn } from '@vue/runtime-dom'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+ setInsertionState,
+} from '../insertionState'
import { child, next } from './node'
export let isHydrating = false
let isOptimized = false
export function withHydration(container: ParentNode, fn: () => void): void {
- adoptHydrationNode = adoptHydrationNodeImpl
+ adoptTemplate = adoptTemplateImpl
+ locateHydrationNode = locateHydrationNodeImpl
if (!isOptimized) {
// optimize anchor cache lookup
- const proto = Comment.prototype as any
- proto.$p = proto.$e = undefined
+ ;(Comment.prototype as any).$fs = undefined
isOptimized = true
}
isHydrating = true
- currentHydrationNode = child(container)
+ setInsertionState(container, 0)
const res = fn()
+ resetInsertionState()
isHydrating = false
- currentHydrationNode = null
return res
}
-export let adoptHydrationNode: (
- node: Node | null,
- template?: string,
-) => Node | null
+export let adoptTemplate: (node: Node, template: string) => Node | null
+export let locateHydrationNode: () => void
type Anchor = Comment & {
- // previous open anchor
- $p?: Anchor
- // matching end anchor
- $e?: Anchor
+ // cached matching fragment start to avoid repeated traversal
+ // on nested fragments
+ $fs?: Anchor
}
const isComment = (node: Node, data: string): node is Anchor =>
* 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
+function adoptTemplateImpl(node: Node, template: string): Node | null {
+ if (!(template[0] === '<' && template[1] === '!')) {
+ while (node.nodeType === 8) node = next(node)
}
- let adopted: Node | undefined
- let end: Node | undefined | null
-
- if (template) {
- if (template[0] !== '<' && template[1] !== '!') {
- 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 (__DEV__) {
+ const type = node.nodeType
if (
(type === 8 && !template.startsWith('<!')) ||
(type === 1 &&
- !template.startsWith(
- `<` + (adopted as Element).tagName.toLowerCase(),
- )) ||
+ !template.startsWith(`<` + (node as Element).tagName.toLowerCase())) ||
(type === 3 &&
template.trim() &&
- !template.startsWith((adopted as Text).data))
+ !template.startsWith((node as Text).data))
) {
// TODO recover and provide more info
- console.error(`adopted: `, adopted)
- console.error(`template: ${template}`)
- throw new Error('hydration mismatch!')
+ warn(`adopted: `, node)
+ warn(`template: ${template}`)
+ warn('hydration mismatch!')
}
}
- currentHydrationNode = next(end!)
- return adopted
+ currentHydrationNode = next(node)
+ return node
+}
+
+function locateHydrationNodeImpl() {
+ if (__DEV__ && !insertionParent) {
+ warn('Hydration error: missing insertion state.')
+ }
+
+ let node: Node | null
+
+ // prepend / firstChild
+ if (insertionAnchor === 0) {
+ node = child(insertionParent!)
+ } else {
+ node = insertionAnchor
+ ? insertionAnchor.previousSibling
+ : insertionParent!.lastChild
+
+ if (node && isComment(node, ']')) {
+ // fragment backward search
+ if (node.$fs) {
+ // already cached matching fragment start
+ node = node.$fs
+ } else {
+ let cur: Node | null = node
+ let curFragEnd = node
+ let fragDepth = 0
+ node = null
+ while (cur) {
+ cur = cur.previousSibling
+ if (cur) {
+ if (isComment(cur, '[')) {
+ curFragEnd.$fs = cur
+ if (!fragDepth) {
+ node = cur
+ break
+ } else {
+ fragDepth--
+ }
+ } else if (isComment(cur, ']')) {
+ curFragEnd = cur
+ fragDepth++
+ }
+ }
+ }
+ }
+ }
+ }
+
+ currentHydrationNode = node
+
+ if (__DEV__ && !currentHydrationNode) {
+ // TODO more info
+ warn('Hydration mismatch in ', insertionParent)
+ }
}
-import {
- adoptHydrationNode,
- currentHydrationNode,
- isHydrating,
-} from './hydration'
+import { adoptTemplate, currentHydrationNode, isHydrating } from './hydration'
import { child, createTextNode } from './node'
let t: HTMLTemplateElement
// TODO this should not happen
throw new Error('No current hydration node')
}
- return adoptHydrationNode(currentHydrationNode, html)!
+ return adoptTemplate(currentHydrationNode!, html)!
}
// fast path for text nodes
if (html[0] !== '<') {
// compiler-use only
export { insert, prepend, remove, isFragment, VaporFragment } from './block'
+export { setInsertionState } from './insertionState'
export { createComponent, createComponentWithFallback } from './component'
export { renderEffect } from './renderEffect'
export { createSlot } from './componentSlots'
--- /dev/null
+import { setCurrentHydrationNode } from './dom/hydration'
+
+export let insertionParent: ParentNode | undefined
+export let insertionAnchor: Node | 0 | undefined
+
+/**
+ * This function is called before a block type that requires insertion
+ * (component, slot outlet, if, for) is created. The state is used for actual
+ * insertion on client-side render, and used for node adoption during hydration.
+ */
+export function setInsertionState(parent: ParentNode, anchor?: Node | 0): void {
+ insertionParent = parent
+ insertionAnchor = anchor
+}
+
+export function resetInsertionState(): void {
+ insertionParent = insertionAnchor = undefined
+ setCurrentHydrationNode(null)
+}