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( + '
foo
foo
', + () => { + const n1 = t2() as Element + setInsertionState(n1) + createComponent(Comp) + return n1 + }, + ) + + expect(container.innerHTML).toBe( + `
foo
foo
`, + ) + + msg.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `
bar
bar
`, + ) + }) + + 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( + '
foo
foo
', + () => { + const n1 = t2() as Element + setInsertionState(n1, 0) + createComponent(Comp) + return n1 + }, + ) + + expect(container.innerHTML).toBe( + `
foo
foo
`, + ) + + msg.value = 'bar' + await nextTick() + expect(container.innerHTML).toBe( + `
bar
bar
`, + ) + }) + // 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('