Merge branch 'vapor' into edison/feat/vaporTransition

This commit is contained in:
edison 2025-03-08 09:59:09 +08:00 committed by GitHub
commit 7c9bd7c093
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 2680 additions and 138 deletions

View File

@ -149,17 +149,17 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`; `;
exports[`compile > directives > v-pre > should not affect siblings after it 1`] = ` exports[`compile > directives > v-pre > should not affect siblings after it 1`] = `
"import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, createTextNode as _createTextNode, insert as _insert, toDisplayString as _toDisplayString, setText as _setText, setProp as _setProp, renderEffect as _renderEffect, template as _template } from 'vue'; "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';
const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>") const t0 = _template("<div :id=\\"foo\\"><Comp></Comp>{{ bar }}</div>")
const t1 = _template("<div></div>") const t1 = _template("<div> </div>")
export function render(_ctx, $props, $emit, $attrs, $slots) { export function render(_ctx, $props, $emit, $attrs, $slots) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n0 = t0() const n0 = t0()
const n3 = t1() const n3 = t1()
const n2 = _child(n3)
const n1 = _createComponentWithFallback(_component_Comp) const n1 = _createComponentWithFallback(_component_Comp)
const n2 = _createTextNode() _prepend(n3, n1)
_insert([n1, n2], n3)
_renderEffect(() => { _renderEffect(() => {
_setText(n2, _toDisplayString(_ctx.bar)) _setText(n2, _toDisplayString(_ctx.bar))
_setProp(n3, "id", _ctx.foo) _setProp(n3, "id", _ctx.foo)
@ -169,10 +169,12 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
`; `;
exports[`compile > dynamic root 1`] = ` exports[`compile > dynamic root 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const n0 = _createTextNode(_toDisplayString(1) + _toDisplayString(2)) const n0 = t0()
_setText(n0, _toDisplayString(1) + _toDisplayString(2))
return n0 return n0
}" }"
`; `;
@ -197,7 +199,7 @@ export function render(_ctx) {
exports[`compile > expression parsing > interpolation 1`] = ` exports[`compile > expression parsing > interpolation 1`] = `
" "
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(a + b.value))) _renderEffect(() => _setText(n0, _toDisplayString(a + b.value)))
return n0 return n0
" "
@ -229,10 +231,12 @@ export function render(_ctx) {
`; `;
exports[`compile > static + dynamic root 1`] = ` exports[`compile > static + dynamic root 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const n0 = _createTextNode(_toDisplayString(1) + _toDisplayString(2) + "3" + _toDisplayString(4) + _toDisplayString(5) + "6" + _toDisplayString(7) + _toDisplayString(8) + "9" + 'A' + 'B') const n0 = t0()
_setText(n0, _toDisplayString(1) + _toDisplayString(2) + "3" + _toDisplayString(4) + _toDisplayString(5) + "6" + _toDisplayString(7) + _toDisplayString(8) + "9" + 'A' + 'B')
return n0 return n0
}" }"
`; `;

View File

@ -1,30 +1,33 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: expression > basic 1`] = ` exports[`compiler: expression > basic 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_ctx.a))) _renderEffect(() => _setText(n0, _toDisplayString(_ctx.a)))
return n0 return n0
}" }"
`; `;
exports[`compiler: expression > props 1`] = ` exports[`compiler: expression > props 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx, $props, $emit, $attrs, $slots) { export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString($props.foo))) _renderEffect(() => _setText(n0, _toDisplayString($props.foo)))
return n0 return n0
}" }"
`; `;
exports[`compiler: expression > props aliased 1`] = ` exports[`compiler: expression > props aliased 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx, $props, $emit, $attrs, $slots) { export function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString($props['bar']))) _renderEffect(() => _setText(n0, _toDisplayString($props['bar'])))
return n0 return n0
}" }"

View File

@ -1,24 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: children transform > children & sibling references 1`] = ` exports[`compiler: children transform > children & sibling references 1`] = `
"import { child as _child, nthChild as _nthChild, next as _next, createTextNode as _createTextNode, insert as _insert, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue'; "import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<div><p> </p> <!><p> </p></div>", true) const t0 = _template("<div><p> </p> <p> </p></div>", true)
export function render(_ctx) { export function render(_ctx) {
const n4 = t0() const n3 = t0()
const n0 = _child(n4) const n0 = _child(n3)
const n3 = _nthChild(n4, 2) const n1 = _next(n0)
const n2 = _next(n3) const n2 = _next(n1)
const x0 = _child(n0) const x0 = _child(n0)
const n1 = _createTextNode()
const x2 = _child(n2) const x2 = _child(n2)
_insert(n1, n4, n3)
_renderEffect(() => { _renderEffect(() => {
_setText(x0, _toDisplayString(_ctx.first)) _setText(x0, _toDisplayString(_ctx.first))
_setText(n1, _toDisplayString(_ctx.second) + " " + _toDisplayString(_ctx.third) + " ") _setText(n1, " " + _toDisplayString(_ctx.second) + " " + _toDisplayString(_ctx.third) + " ")
_setText(x2, _toDisplayString(_ctx.forth)) _setText(x2, _toDisplayString(_ctx.forth))
}) })
return n4 return n3
}" }"
`; `;

View File

@ -1,20 +1,23 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: text transform > consecutive text 1`] = ` exports[`compiler: text transform > consecutive text 1`] = `
"import { createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect } from 'vue'; "import { toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_ctx.msg))) _renderEffect(() => _setText(n0, _toDisplayString(_ctx.msg)))
return n0 return n0
}" }"
`; `;
exports[`compiler: text transform > no consecutive text 1`] = ` exports[`compiler: text transform > no consecutive text 1`] = `
"import { createTextNode as _createTextNode } from 'vue'; "import { setText as _setText, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const n0 = _createTextNode("hello world") const n0 = t0()
_setText(n0, "hello world")
return n0 return n0
}" }"
`; `;

View File

@ -12,15 +12,15 @@ export function render(_ctx) {
`; `;
exports[`compiler: v-once > basic 1`] = ` exports[`compiler: v-once > basic 1`] = `
"import { child as _child, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setClass as _setClass, prepend as _prepend, template as _template } from 'vue'; "import { child as _child, next as _next, toDisplayString as _toDisplayString, setText as _setText, setClass as _setClass, template as _template } from 'vue';
const t0 = _template("<div><span></span></div>", true) const t0 = _template("<div> <span></span></div>", true)
export function render(_ctx, $props, $emit, $attrs, $slots) { export function render(_ctx, $props, $emit, $attrs, $slots) {
const n2 = t0() const n2 = t0()
const n1 = _child(n2) const n0 = _child(n2)
const n0 = _createTextNode(_toDisplayString(_ctx.msg) + " ") const n1 = _next(n0)
_setText(n0, _toDisplayString(_ctx.msg) + " ")
_setClass(n1, _ctx.clz) _setClass(n1, _ctx.clz)
_prepend(n2, n0)
return n2 return n2
}" }"
`; `;

View File

@ -22,7 +22,8 @@ export function render(_ctx) {
`; `;
exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = ` exports[`compiler: transform slot > dynamic slots name w/ v-for 1`] = `
"import { resolveComponent as _resolveComponent, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createForSlots as _createForSlots, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
@ -31,7 +32,7 @@ export function render(_ctx) {
() => (_createForSlots(_ctx.list, (item) => ({ () => (_createForSlots(_ctx.list, (item) => ({
name: item, name: item,
fn: (_slotProps0) => { fn: (_slotProps0) => {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"]))) _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"])))
return n0 return n0
} }
@ -158,7 +159,7 @@ export function render(_ctx) {
`; `;
exports[`compiler: transform slot > nested slots scoping 1`] = ` exports[`compiler: transform slot > nested slots scoping 1`] = `
"import { resolveComponent as _resolveComponent, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; "import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" ") const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
@ -166,17 +167,16 @@ export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n5 = _createComponentWithFallback(_component_Comp, null, { const n5 = _createComponentWithFallback(_component_Comp, null, {
"default": (_slotProps0) => { "default": (_slotProps0) => {
const n2 = t0() const n3 = t0()
const n1 = _createComponentWithFallback(_component_Inner, null, { const n1 = _createComponentWithFallback(_component_Inner, null, {
"default": (_slotProps1) => { "default": (_slotProps1) => {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz))) _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz)))
return n0 return n0
} }
}) })
const n3 = _createTextNode() _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz)))
_renderEffect(() => _setText(n3, _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz))) return [n1, n3]
return [n1, n2, n3]
} }
}, true) }, true)
return n5 return n5
@ -184,7 +184,8 @@ export function render(_ctx) {
`; `;
exports[`compiler: transform slot > on component dynamically named slot 1`] = ` exports[`compiler: transform slot > on component dynamically named slot 1`] = `
"import { resolveComponent as _resolveComponent, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
@ -193,7 +194,7 @@ export function render(_ctx) {
() => ({ () => ({
name: _ctx.named, name: _ctx.named,
fn: (_slotProps0) => { fn: (_slotProps0) => {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
return n0 return n0
} }
@ -205,13 +206,14 @@ export function render(_ctx) {
`; `;
exports[`compiler: transform slot > on component named slot 1`] = ` exports[`compiler: transform slot > on component named slot 1`] = `
"import { resolveComponent as _resolveComponent, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n1 = _createComponentWithFallback(_component_Comp, null, { const n1 = _createComponentWithFallback(_component_Comp, null, {
"named": (_slotProps0) => { "named": (_slotProps0) => {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
return n0 return n0
} }
@ -221,13 +223,14 @@ export function render(_ctx) {
`; `;
exports[`compiler: transform slot > on-component default slot 1`] = ` exports[`compiler: transform slot > on-component default slot 1`] = `
"import { resolveComponent as _resolveComponent, createTextNode as _createTextNode, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback } from 'vue'; "import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
const t0 = _template(" ")
export function render(_ctx) { export function render(_ctx) {
const _component_Comp = _resolveComponent("Comp") const _component_Comp = _resolveComponent("Comp")
const n1 = _createComponentWithFallback(_component_Comp, null, { const n1 = _createComponentWithFallback(_component_Comp, null, {
"default": (_slotProps0) => { "default": (_slotProps0) => {
const n0 = _createTextNode() const n0 = t0()
_renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar))) _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
return n0 return n0
} }

View File

@ -27,10 +27,11 @@ describe('compiler: children transform', () => {
) )
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(Array.from(helpers)).containSubset([ expect(Array.from(helpers)).containSubset([
'child',
'toDisplayString',
'renderEffect',
'next', 'next',
'setText', 'setText',
'createTextNode',
'insert',
'template', 'template',
]) ])
}) })

View File

@ -1,4 +1,5 @@
// TODO: add tests for this transform // TODO: add tests for this transform
import { NodeTypes } from '@vue/compiler-dom'
import { import {
IRNodeTypes, IRNodeTypes,
transformChildren, transformChildren,
@ -24,14 +25,14 @@ describe('compiler: text transform', () => {
'{{ "hello world" }}', '{{ "hello world" }}',
) )
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(helpers).contains.all.keys('createTextNode') expect(helpers).contains.all.keys('setText', 'template')
expect(ir.block.operation).toMatchObject([ expect(ir.block.operation).toMatchObject([
{ {
type: IRNodeTypes.CREATE_TEXT_NODE, type: IRNodeTypes.SET_TEXT,
id: 0, element: 0,
values: [ values: [
{ {
type: IRNodeTypes.SET_TEXT, type: NodeTypes.SIMPLE_EXPRESSION,
content: '"hello world"', content: '"hello world"',
isStatic: false, isStatic: false,
}, },
@ -43,14 +44,8 @@ describe('compiler: text transform', () => {
it('consecutive text', () => { it('consecutive text', () => {
const { code, ir, helpers } = compileWithTextTransform('{{ msg }}') const { code, ir, helpers } = compileWithTextTransform('{{ msg }}')
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
expect(helpers).contains.all.keys('createTextNode') expect(helpers).contains.all.keys('setText', 'template')
expect(ir.block.operation).toMatchObject([ expect(ir.block.operation).toMatchObject([])
{
type: IRNodeTypes.CREATE_TEXT_NODE,
id: 0,
values: undefined,
},
])
expect(ir.block.effect.length).toBe(1) expect(ir.block.effect.length).toBe(1)
}) })
}) })

View File

@ -28,8 +28,8 @@ describe('compiler: v-once', () => {
expect(ir.block.effect).lengthOf(0) expect(ir.block.effect).lengthOf(0)
expect(ir.block.operation).toMatchObject([ expect(ir.block.operation).toMatchObject([
{ {
type: IRNodeTypes.CREATE_TEXT_NODE, type: IRNodeTypes.SET_TEXT,
id: 0, element: 0,
values: [ values: [
{ {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
@ -61,11 +61,6 @@ describe('compiler: v-once', () => {
], ],
}, },
}, },
{
type: IRNodeTypes.PREPEND_NODE,
elements: [0],
parent: 2,
},
]) ])
}) })

View File

@ -66,11 +66,10 @@ export function genChildren(
push(NEWLINE, `const ${variable} = `) push(NEWLINE, `const ${variable} = `)
if (prev) { if (prev) {
const offset = elementIndex - prev[1] if (elementIndex - prev[1] === 1) {
if (offset === 1) {
push(...genCall(helper('next'), prev[0])) push(...genCall(helper('next'), prev[0]))
} else { } else {
push(...genCall(helper('nthChild'), from, String(offset))) push(...genCall(helper('nthChild'), from, String(elementIndex)))
} }
} else { } else {
if (newPath.length === 1 && newPath[0] === 0) { if (newPath.length === 1 && newPath[0] === 0) {

View File

@ -30,32 +30,71 @@ export const transformText: NodeTransform = (node, context) => {
return return
} }
const isFragment =
node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.TEMPLATE ||
node.tagType === ElementTypes.COMPONENT))
if ( if (
node.type === NodeTypes.ELEMENT && (isFragment ||
node.tagType === ElementTypes.ELEMENT && (node.type === NodeTypes.ELEMENT &&
isAllTextLike(node.children) node.tagType === ElementTypes.ELEMENT)) &&
node.children.length
) { ) {
processTextLikeContainer( let hasInterp = false
node.children, let isAllTextLike = true
for (const c of node.children) {
if (c.type === NodeTypes.INTERPOLATION) {
hasInterp = true
} else if (c.type !== NodeTypes.TEXT) {
isAllTextLike = false
}
}
// all text like with interpolation
if (!isFragment && isAllTextLike && hasInterp) {
processTextContainer(
node.children as TextLike[],
context as TransformContext<ElementNode>, context as TransformContext<ElementNode>,
) )
} else if (hasInterp) {
// check if there's any text before interpolation, it needs to be merged
for (let i = 0; i < node.children.length; i++) {
const c = node.children[i]
const prev = node.children[i - 1]
if (
c.type === NodeTypes.INTERPOLATION &&
prev &&
prev.type === NodeTypes.TEXT
) {
// mark leading text node for skipping
seen.get(context.root)!.add(prev)
}
}
}
} else if (node.type === NodeTypes.INTERPOLATION) { } else if (node.type === NodeTypes.INTERPOLATION) {
processTextLike(context as TransformContext<InterpolationNode>) processInterpolation(context as TransformContext<InterpolationNode>)
} else if (node.type === NodeTypes.TEXT) { } else if (node.type === NodeTypes.TEXT) {
context.template += node.content context.template += node.content
} }
} }
function processTextLike(context: TransformContext<InterpolationNode>) { function processInterpolation(context: TransformContext<InterpolationNode>) {
const nexts = context.parent!.node.children.slice(context.index) const children = context.parent!.node.children
const nexts = children.slice(context.index)
const idx = nexts.findIndex(n => !isTextLike(n)) const idx = nexts.findIndex(n => !isTextLike(n))
const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike> const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike>
// merge leading text
const prev = children[context.index - 1]
if (prev && prev.type === NodeTypes.TEXT) {
nodes.unshift(prev)
}
context.template += ' '
const id = context.reference() const id = context.reference()
const values = nodes.map(node => createTextLikeExpression(node, context)) const values = nodes.map(node => createTextLikeExpression(node, context))
context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
const nonConstantExps = values.filter(v => !isConstantExpression(v)) const nonConstantExps = values.filter(v => !isConstantExpression(v))
const isStatic = const isStatic =
!nonConstantExps.length || !nonConstantExps.length ||
@ -64,12 +103,13 @@ function processTextLike(context: TransformContext<InterpolationNode>) {
) || ) ||
context.inVOnce context.inVOnce
if (isStatic) {
context.registerOperation({ context.registerOperation({
type: IRNodeTypes.CREATE_TEXT_NODE, type: IRNodeTypes.SET_TEXT,
id, element: id,
values: isStatic ? values : undefined, values,
}) })
if (!isStatic) { } else {
context.registerEffect(values, { context.registerEffect(values, {
type: IRNodeTypes.SET_TEXT, type: IRNodeTypes.SET_TEXT,
element: id, element: id,
@ -78,7 +118,7 @@ function processTextLike(context: TransformContext<InterpolationNode>) {
} }
} }
function processTextLikeContainer( function processTextContainer(
children: TextLike[], children: TextLike[],
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
) { ) {
@ -111,15 +151,6 @@ function createTextLikeExpression(node: TextLike, context: TransformContext) {
} }
} }
function isAllTextLike(children: TemplateChildNode[]): children is TextLike[] {
return (
!!children.length &&
children.every(isTextLike) &&
// at least one an interpolation
children.some(n => n.type === NodeTypes.INTERPOLATION)
)
}
function isTextLike(node: TemplateChildNode): node is TextLike { function isTextLike(node: TemplateChildNode): node is TextLike {
return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT return node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
} }

View File

@ -35,8 +35,10 @@ describe('api: createVaporApp', () => {
const root1 = document.createElement('div') const root1 = document.createElement('div')
createVaporApp(Comp).mount(root1) createVaporApp(Comp).mount(root1)
expect(root1.innerHTML).toBe(`0`) expect(root1.innerHTML).toBe(`0`)
//#5571 mount multiple apps to the same host element //#5571 mount multiple apps to the same host element
createVaporApp(Comp).mount(root1) createVaporApp(Comp).mount(root1)
expect(`mount target container is not empty`).toHaveBeenWarned()
expect( expect(
`There is already an app instance mounted on the host container`, `There is already an app instance mounted on the host container`,
).toHaveBeenWarned() ).toHaveBeenWarned()

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import {
unmountComponent, unmountComponent,
} from './component' } from './component'
import { import {
type App,
type AppMountFn, type AppMountFn,
type AppUnmountFn, type AppUnmountFn,
type CreateAppFunction, type CreateAppFunction,
@ -20,6 +21,7 @@ import {
import type { RawProps } from './componentProps' import type { RawProps } from './componentProps'
import { getGlobalThis } from '@vue/shared' import { getGlobalThis } from '@vue/shared'
import { optimizePropertyLookup } from './dom/prop' import { optimizePropertyLookup } from './dom/prop'
import { withHydration } from './dom/hydration'
let _createApp: CreateAppFunction<ParentNode, VaporComponent> let _createApp: CreateAppFunction<ParentNode, VaporComponent>
@ -28,6 +30,9 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
// clear content before mounting // clear content before mounting
if (container.nodeType === 1 /* Node.ELEMENT_NODE */) { 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 = '' container.textContent = ''
} }
@ -38,21 +43,38 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
false, false,
app._context, app._context,
) )
mountComponent(instance, container) mountComponent(instance, container)
flushOnAppMount() 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 => { const unmountApp: AppUnmountFn = app => {
unmountComponent(app._instance as VaporComponentInstance, app._container) unmountComponent(app._instance as VaporComponentInstance, app._container)
} }
export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = ( function prepareApp() {
comp,
props,
) => {
// compile-time feature flags check // compile-time feature flags check
if (__ESM_BUNDLER__ && !__TEST__) { if (__ESM_BUNDLER__ && !__TEST__) {
initFeatureFlags() initFeatureFlags()
@ -63,10 +85,9 @@ export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target) setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__, target)
} }
}
if (!_createApp) _createApp = createAppAPI(mountApp, unmountApp, getExposed) function postPrepareApp(app: App) {
const app = _createApp(comp, props)
if (__DEV__) { if (__DEV__) {
app.config.globalProperties = new Proxy( app.config.globalProperties = new Proxy(
{}, {},
@ -84,5 +105,27 @@ export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = (
container = normalizeContainer(container) as ParentNode container = normalizeContainer(container) as ParentNode
return mount(container, ...args) 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 return app
} }

View File

@ -7,6 +7,7 @@ import {
} from './component' } from './component'
import { createComment, createTextNode } from './dom/node' import { createComment, createTextNode } from './dom/node'
import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity' import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
import { isHydrating } from './dom/hydration'
import { import {
type TransitionHooks, type TransitionHooks,
type TransitionProps, type TransitionProps,
@ -154,6 +155,7 @@ export function insert(
): void { ): void {
anchor = anchor === 0 ? parent.firstChild : anchor anchor = anchor === 0 ? parent.firstChild : anchor
if (block instanceof Node) { if (block instanceof Node) {
if (!isHydrating) {
// don't apply transition on text or comment nodes // don't apply transition on text or comment nodes
if ((block as TransitionBlock).$transition && block instanceof Element) { if ((block as TransitionBlock).$transition && block instanceof Element) {
performTransitionEnter( performTransitionEnter(
@ -165,15 +167,21 @@ export function insert(
} else { } else {
parent.insertBefore(block, anchor) parent.insertBefore(block, anchor)
} }
}
} else if (isVaporComponent(block)) { } else if (isVaporComponent(block)) {
if (block.isMounted) {
insert(block.block!, parent, anchor)
} else {
mountComponent(block, parent, anchor) mountComponent(block, parent, anchor)
}
} else if (isArray(block)) { } else if (isArray(block)) {
for (let i = 0; i < block.length; i++) { for (const b of block) {
insert(block[i], parent, anchor) insert(b, parent, anchor)
} }
} else { } else {
// fragment // fragment
if (block.insert) { if (block.insert) {
// TODO handle hydration for vdom interop
block.insert(parent, anchor, (block as TransitionBlock).$transition) block.insert(parent, anchor, (block as TransitionBlock).$transition)
} else { } else {
insert(block.nodes, parent, anchor, parentSuspense) insert(block.nodes, parent, anchor, parentSuspense)
@ -182,6 +190,8 @@ export function insert(
} }
} }
export type InsertFn = typeof insert
export function prepend(parent: ParentNode, ...blocks: Block[]): void { export function prepend(parent: ParentNode, ...blocks: Block[]): void {
let i = blocks.length let i = blocks.length
while (i--) insert(blocks[i], parent, 0) while (i--) insert(blocks[i], parent, 0)

View File

@ -480,14 +480,10 @@ export function mountComponent(
if (__DEV__) { if (__DEV__) {
startMeasure(instance, `mount`) startMeasure(instance, `mount`)
} }
if (!instance.isMounted) {
if (instance.bm) invokeArrayFns(instance.bm) if (instance.bm) invokeArrayFns(instance.bm)
insert(instance.block, parent, anchor) insert(instance.block, parent, anchor)
if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!)) if (instance.m) queuePostFlushCb(() => invokeArrayFns(instance.m!))
instance.isMounted = true instance.isMounted = true
} else {
insert(instance.block, parent, anchor)
}
if (__DEV__) { if (__DEV__) {
endMeasure(instance, `mount`) endMeasure(instance, `mount`)
} }

View File

@ -0,0 +1,125 @@
import { child, next } from './node'
export let isHydrating = false
export let currentHydrationNode: Node | null = null
export function setCurrentHydrationNode(node: Node | null): void {
currentHydrationNode = node
}
let isOptimized = false
export function withHydration(container: ParentNode, fn: () => void): void {
adoptHydrationNode = adoptHydrationNodeImpl
if (!isOptimized) {
// optimize anchor cache lookup
const proto = Comment.prototype as any
proto.$p = proto.$e = undefined
isOptimized = true
}
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) {
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 (
(type === 8 && !template.startsWith('<!')) ||
(type === 1 &&
!template.startsWith(
`<` + (adopted as Element).tagName.toLowerCase(),
)) ||
(type === 3 &&
template.trim() &&
!template.startsWith((adopted as Text).data))
) {
// TODO recover and provide more info
throw new Error('hydration mismatch!')
}
}
currentHydrationNode = next(end!)
return adopted
}

View File

@ -1,13 +1,33 @@
import {
adoptHydrationNode,
currentHydrationNode,
isHydrating,
} from './hydration'
import { child, createTextNode } from './node'
let t: HTMLTemplateElement
/*! #__NO_SIDE_EFFECTS__ */ /*! #__NO_SIDE_EFFECTS__ */
export function template(html: string, root?: boolean) { export function template(html: string, root?: boolean) {
let node: ChildNode let node: Node
const create = () => {
const t = document.createElement('template')
t.innerHTML = html
return t.content.firstChild!
}
return (): Node & { $root?: true } => { 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)!
}
// fast path for text nodes
if (html[0] !== '<') {
return createTextNode(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 if (root) (ret as any).$root = true
return ret return ret
} }

View File

@ -1,5 +1,5 @@
// public APIs // public APIs
export { createVaporApp } from './apiCreateApp' export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
export { defineVaporComponent } from './apiDefineComponent' export { defineVaporComponent } from './apiDefineComponent'
export { vaporInteropPlugin } from './vdomInterop' export { vaporInteropPlugin } from './vdomInterop'
export type { VaporDirective } from './directives/custom' export type { VaporDirective } from './directives/custom'