diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
index 04e7d793b..fff7dd127 100644
--- a/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
@@ -26,7 +26,7 @@ export function render(_ctx) {
`;
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("
")
export function render(_ctx) {
@@ -38,9 +38,9 @@ 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
@@ -149,7 +149,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`;
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("{{ bar }}
")
const t1 = _template("
")
@@ -158,8 +158,8 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
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)
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
index 02235ddd9..cb14f56af 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
@@ -65,20 +65,20 @@ export function render(_ctx) {
`;
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(" ")
const t1 = _template("", 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
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index d7ec3ceed..ab3ade45b 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
@@ -36,14 +36,14 @@ export function render(_ctx) {
`;
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("", 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
}"
`;
diff --git a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
index a3fb18aa1..43077bf2e 100644
--- a/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
+++ b/packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts
@@ -132,10 +132,6 @@ describe('compiler: v-once', () => {
id: 0,
tag: 'Comp',
once: true,
- },
- {
- type: IRNodeTypes.INSERT_NODE,
- elements: [0],
parent: 1,
},
])
diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts
index 73e23150f..7c232db75 100644
--- a/packages/compiler-vapor/src/generators/component.ts
+++ b/packages/compiler-vapor/src/generators/component.ts
@@ -51,6 +51,7 @@ export function genCreateComponent(
const rawSlots = genRawSlots(slots, context)
const [ids, handlers] = processInlineHandlers(props, context)
const rawProps = context.withId(() => genRawProps(props, context), ids)
+
const inlineHandlers: CodeFragment[] = handlers.reduce(
(acc, { name, value }) => {
const handler = genEventHandler(context, value, undefined, false)
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
index 93eff0584..64b2a568e 100644
--- a/packages/compiler-vapor/src/generators/operation.ts
+++ b/packages/compiler-vapor/src/generators/operation.ts
@@ -1,4 +1,10 @@
-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'
@@ -14,6 +20,7 @@ import {
INDENT_START,
NEWLINE,
buildCodeFragment,
+ genCall,
} from './utils'
import { genCreateComponent } from './component'
import { genSlotOutlet } from './slotOutlet'
@@ -26,6 +33,9 @@ export function genOperations(
): 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
@@ -134,3 +144,21 @@ export function genEffect(
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}`,
+ ),
+ ]
+}
diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts
index 6616e35e9..d4beb1e3f 100644
--- a/packages/compiler-vapor/src/ir/index.ts
+++ b/packages/compiler-vapor/src/ir/index.ts
@@ -76,6 +76,8 @@ export interface IfIRNode extends BaseIRNode {
positive: BlockIRNode
negative?: BlockIRNode | IfIRNode
once?: boolean
+ parent?: number
+ anchor?: number
}
export interface IRFor {
@@ -93,6 +95,8 @@ export interface ForIRNode extends BaseIRNode, IRFor {
once: boolean
component: boolean
onlyChild: boolean
+ parent?: number
+ anchor?: number
}
export interface SetPropIRNode extends BaseIRNode {
@@ -158,6 +162,7 @@ export interface SetTemplateRefIRNode extends BaseIRNode {
effect: boolean
}
+// TODO remove, no longer needed
export interface CreateTextNodeIRNode extends BaseIRNode {
type: IRNodeTypes.CREATE_TEXT_NODE
id: number
@@ -198,6 +203,8 @@ export interface CreateComponentIRNode extends BaseIRNode {
root: boolean
once: boolean
dynamic?: SimpleExpressionNode
+ parent?: number
+ anchor?: number
}
export interface DeclareOldRefIRNode extends BaseIRNode {
@@ -211,6 +218,8 @@ export interface SlotOutletIRNode extends BaseIRNode {
name: SimpleExpressionNode
props: IRProps[]
fallback?: BlockIRNode
+ parent?: number
+ anchor?: number
}
export interface GetTextChildIRNode extends BaseIRNode {
@@ -288,3 +297,21 @@ export type VaporDirectiveNode = Overwrite<
arg: Exclude
}
>
+
+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
+ )
+}
diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts
index 9b76d86f3..8952036c0 100644
--- a/packages/compiler-vapor/src/transforms/transformChildren.ts
+++ b/packages/compiler-vapor/src/transforms/transformChildren.ts
@@ -4,7 +4,12 @@ import {
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 =
@@ -66,21 +71,11 @@ function processDynamicChildren(context: TransformContext) {
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 = []
}
@@ -89,10 +84,32 @@ function processDynamicChildren(context: TransformContext) {
}
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
+ }
+ }
+ }
}
}
diff --git a/packages/runtime-vapor/__tests__/hydration.spec.ts b/packages/runtime-vapor/__tests__/hydration.spec.ts
index 8abf1570d..3345debc3 100644
--- a/packages/runtime-vapor/__tests__/hydration.spec.ts
+++ b/packages/runtime-vapor/__tests__/hydration.spec.ts
@@ -1,11 +1,13 @@
// import { type SSRContext, renderToString } from '@vue/server-renderer'
import {
child,
+ createComponent,
createVaporSSRApp,
delegateEvents,
next,
renderEffect,
setClass,
+ setInsertionState,
setText,
template,
} from '../src'
@@ -144,6 +146,117 @@ describe('SSR hydration', () => {
)
})
+ 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('
', true)
+ const { container } = mountWithHydration(
+ 'foo
',
+ () => {
+ const n1 = t1() as Element
+ setInsertionState(n1)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(`foo
`)
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(`bar
`)
+ })
+
+ test('fragment component', async () => {
+ const t0 = template('
')
+ 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('
', true)
+ const { container } = mountWithHydration(
+ '',
+ () => {
+ const n1 = t2() as Element
+ setInsertionState(n1)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+ })
+
+ test('fragment component with prepend', async () => {
+ const t0 = template('
')
+ 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('
', true)
+ const { container } = mountWithHydration(
+ '',
+ () => {
+ const n1 = t2() as Element
+ setInsertionState(n1, 0)
+ createComponent(Comp)
+ return n1
+ },
+ )
+
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+
+ msg.value = 'bar'
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ ``,
+ )
+ })
+
// test('element with ref', () => {
// const el = ref()
// const { vnode, container } = mountWithHydration('', () =>
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 3716ac7ae..17cbc0c3b 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -58,6 +58,12 @@ import {
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'
@@ -136,6 +142,10 @@ export function createComponent(
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)
@@ -253,6 +263,11 @@ export function createComponent(
onScopeDispose(() => unmountComponent(instance), true)
+ if (!isHydrating && insertionParent) {
+ insert(instance.block, insertionParent, insertionAnchor)
+ resetInsertionState()
+ }
+
return instance
}
diff --git a/packages/runtime-vapor/src/dom/hydration.ts b/packages/runtime-vapor/src/dom/hydration.ts
index af6fe0ec1..db16e61b2 100644
--- a/packages/runtime-vapor/src/dom/hydration.ts
+++ b/packages/runtime-vapor/src/dom/hydration.ts
@@ -1,3 +1,10 @@
+import { warn } from '@vue/runtime-dom'
+import {
+ insertionAnchor,
+ insertionParent,
+ resetInsertionState,
+ setInsertionState,
+} from '../insertionState'
import { child, next } from './node'
export let isHydrating = false
@@ -10,31 +17,28 @@ export function setCurrentHydrationNode(node: Node | null): void {
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 =>
@@ -44,84 +48,82 @@ 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('