diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSkip.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSkip.spec.ts.snap
new file mode 100644
index 00000000000..79ddaa94d2c
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSkip.spec.ts.snap
@@ -0,0 +1,407 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`compiler: v-skip > transform > basic 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_ctx.ok)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("div", { key: 1 }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > nested v-skip 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, openBlock: _openBlock, createElementBlock: _createElementBlock, Fragment: _Fragment } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+ (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("span", { key: 1 }))
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createElementBlock("div", { key: 1 }, [
+ (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("span", { key: 1 }))
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on Teleport 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, createCommentVNode: _createCommentVNode, Teleport: _Teleport, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ return (_ctx.ok)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createBlock(_Teleport, {
+ key: 1,
+ to: "target"
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with default slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, resolveComponent: _resolveComponent, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, ["foo"], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createBlock(_component_Comp, { key: 1 }, {
+ default: _withCtx(() => ["foo"]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with dynamic slot + default slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, resolveComponent: _resolveComponent, resolveSkipComponent: _resolveSkipComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _component_Comp), null, {
+ [_ctx.foo]: _withCtx(() => ["foo"]),
+ default: _withCtx(() => ["default"]),
+ _: 2 /* DYNAMIC */
+ }, 1024 /* DYNAMIC_SLOTS */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with dynamic slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, resolveSkipComponent: _resolveSkipComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _component_Comp), null, {
+ [_ctx.foo]: _withCtx(() => [_toDisplayString(_ctx.foo)]),
+ _: 2 /* DYNAMIC */
+ }, 1024 /* DYNAMIC_SLOTS */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with implicit default slot + v-if 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent, Fragment: _Fragment, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+ (_ctx.yes)
+ ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.default), 1 /* TEXT */))
+ : _createCommentVNode("v-if", true)
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createBlock(_component_Comp, { key: 1 }, {
+ default: _withCtx(() => [
+ (_ctx.yes)
+ ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.default), 1 /* TEXT */))
+ : _createCommentVNode("v-if", true)
+ ]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with multiple implicit default slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+ _createElementVNode("span"),
+ _createElementVNode("div")
+ ], 64 /* STABLE_FRAGMENT */))
+ : (_openBlock(), _createBlock(_component_Comp, { key: 1 }, {
+ foo: _withCtx(() => [_toDisplayString(_ctx.foo)]),
+ default: _withCtx(() => [
+ _createElementVNode("span"),
+ _createElementVNode("div")
+ ]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with multiple named slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, resolveComponent: _resolveComponent, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, ["default"], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createBlock(_component_Comp, { key: 1 }, {
+ default: _withCtx(() => ["default"]),
+ foo: _withCtx(() => ["foo"]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with name default slot + v-if 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, createSlots: _createSlots, toDisplayString: _toDisplayString, resolveComponent: _resolveComponent, resolveSkipComponent: _resolveSkipComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _component_Comp), null, _createSlots({ _: 2 /* DYNAMIC */ }, [
+ (_ctx.yes)
+ ? {
+ name: "default",
+ fn: _withCtx(() => [_toDisplayString(_ctx.default)]),
+ key: "0"
+ }
+ : undefined
+ ]), 1024 /* DYNAMIC_SLOTS */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component without slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createBlock(_component_Comp, { key: 1 }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on dynamic component with default slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment, createElementBlock: _createElementBlock } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, ["foo"], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createBlock(_resolveDynamicComponent(_ctx.Comp), { key: 1 }, {
+ default: _withCtx(() => ["foo"]),
+ _: 1 /* STABLE */
+ }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on dynamic component with dynamic slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock, resolveSkipComponent: _resolveSkipComponent } = _Vue
+
+ return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _resolveDynamicComponent(_ctx.Comp)), null, {
+ [_ctx.foo]: _withCtx(() => ["foo"]),
+ _: 2 /* DYNAMIC */
+ }, 1024 /* DYNAMIC_SLOTS */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > v-else + v-skip 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock("div", { key: 0 }))
+ : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
+ (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("div", { key: 1 }))
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > v-else-if + v-skip 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock("div", { key: 0 }))
+ : (_ctx.yes)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
+ (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("div", { key: 1 }))
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : _createCommentVNode("v-if", true)
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > v-if + v-skip 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, openBlock: _openBlock, createElementBlock: _createElementBlock, Fragment: _Fragment } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+ (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("div", { key: 1 }))
+ ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : _createCommentVNode("v-if", true)
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > v-skip with key 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createCommentVNode: _createCommentVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_ctx.nested)
+ ? _createCommentVNode("v-skip", true)
+ : (_openBlock(), _createElementBlock("div", { key: "foo" }))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > with component children 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { resolveComponent: _resolveComponent, openBlock: _openBlock, createBlock: _createBlock, createElementBlock: _createElementBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createBlock(_component_Comp, { key: 0 }))
+ : (_openBlock(), _createElementBlock("div", { key: 1 }, [
+ (_openBlock(), _createBlock(_component_Comp, { key: 0 }))
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > with element children 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock("span", { key: 0 }))
+ : (_openBlock(), _createElementBlock("div", { key: 1 }, [
+ (_openBlock(), _createElementBlock("span", { key: 0 }))
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > with multiple children 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ const _component_Comp = _resolveComponent("Comp")
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+ _createElementVNode("span"),
+ _createVNode(_component_Comp)
+ ], 64 /* STABLE_FRAGMENT */))
+ : (_openBlock(), _createElementBlock("div", { key: 1 }, [
+ _createElementVNode("span"),
+ _createVNode(_component_Comp)
+ ]))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > with text children 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [_toDisplayString(_ctx.foo)], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
+ : (_openBlock(), _createElementBlock("div", { key: 1 }, _toDisplayString(_ctx.foo), 1 /* TEXT */))
+ }
+}"
+`;
+
+exports[`compiler: v-skip > transform > with v-memo 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+ with (_ctx) {
+ const { openBlock: _openBlock, createElementBlock: _createElementBlock, withMemo: _withMemo } = _Vue
+
+ return (_ctx.ok)
+ ? (_openBlock(), _createElementBlock("span", { key: 0 }))
+ : _withMemo([1], () => (_openBlock(), _createElementBlock("div", { key: 1 }, [
+ (_openBlock(), _createElementBlock("span", { key: 0 }))
+ ])), _cache, 0)
+ }
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/vSkip.spec.ts b/packages/compiler-core/__tests__/transforms/vSkip.spec.ts
new file mode 100644
index 00000000000..b352ecf3f46
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/vSkip.spec.ts
@@ -0,0 +1,552 @@
+import {
+ type CallExpression,
+ type CompilerOptions,
+ type ComponentNode,
+ type ElementNode,
+ ElementTypes,
+ ErrorCodes,
+ type IfBranchNode,
+ type IfNode,
+ NodeTypes,
+ RESOLVE_SKIP_COMPONENT,
+ type RootNode,
+ type SimpleExpressionNode,
+ type SkipNode,
+ type VNodeCall,
+ WITH_MEMO,
+ generate,
+ baseParse as parse,
+ transform,
+ transformElement,
+ transformExpression,
+} from '@vue/compiler-core'
+import { transformIf } from '../../src/transforms/vIf'
+import { transformFor } from '../../src/transforms/vFor'
+import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet'
+import { transformSkip } from '../../src/transforms/vSkip'
+import { transformMemo } from '../../src/transforms/vMemo'
+
+export function parseWithSkipTransform(
+ template: string,
+ options: CompilerOptions = { prefixIdentifiers: true },
+): {
+ root: RootNode
+ node: SkipNode | ComponentNode
+} {
+ const ast = parse(template, options)
+ transform(ast, {
+ nodeTransforms: [
+ transformIf,
+ transformSkip,
+ transformMemo,
+ transformFor,
+ transformExpression,
+ transformSlotOutlet,
+ transformElement,
+ ],
+ ...options,
+ })
+ return {
+ root: ast,
+ node: ast.children[0] as SkipNode | ComponentNode,
+ }
+}
+
+describe('compiler: v-skip', () => {
+ describe('transform', () => {
+ test('basic', () => {
+ const { root, node } = parseWithSkipTransform(`
`) as {
+ root: RootNode
+ node: SkipNode
+ }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(node.consequent.type === NodeTypes.JS_CALL_EXPRESSION).toBe(true)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with text children', () => {
+ const { root, node } = parseWithSkipTransform(
+ `{{foo}}
`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.INTERPOLATION,
+ )
+ expect(
+ (
+ ((node.consequent as IfBranchNode).children[0] as any)
+ .content as SimpleExpressionNode
+ ).content,
+ ).toBe(`_ctx.foo`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with element children', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as ElementNode).tag,
+ ).toBe(`span`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with component children', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as ElementNode).tag,
+ ).toBe(`Comp`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with multiple children', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(2)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as ElementNode).tag,
+ ).toBe(`span`)
+ expect((node.consequent as IfBranchNode).children[1].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[1] as ElementNode).tag,
+ ).toBe(`Comp`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('nested v-skip', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.SKIP,
+ )
+ expect(
+ (
+ ((node.consequent as IfBranchNode).children[0] as SkipNode)
+ .test as SimpleExpressionNode
+ ).content,
+ ).toBe(`_ctx.nested`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ const nestedNode = (node.consequent as IfBranchNode)
+ .children[0] as SkipNode
+ expect(nestedNode.type).toBe(NodeTypes.SKIP)
+ expect((nestedNode.test as SimpleExpressionNode).content).toBe(
+ `_ctx.nested`,
+ )
+ expect(nestedNode.consequent.type === NodeTypes.JS_CALL_EXPRESSION).toBe(
+ true,
+ )
+ expect(nestedNode.alternate.children.length).toBe(1)
+ expect(nestedNode.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((nestedNode.alternate.children[0] as ElementNode).tag).toBe(`span`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('v-if + v-skip', () => {
+ const { root, node } = parseWithSkipTransform(
+ ``,
+ )
+ expect(node.type).toBe(NodeTypes.IF)
+ const ifNode = node as unknown as IfNode
+ const branch = ifNode.branches[0]
+ expect((branch.condition as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(branch.children.length).toBe(1)
+ // skipNode
+ expect(branch.children[0].type).toBe(NodeTypes.SKIP)
+ expect(
+ ((branch.children[0] as SkipNode).test as SimpleExpressionNode).content,
+ ).toBe(`_ctx.nested`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('v-skip with key', () => {
+ const { root, node } = parseWithSkipTransform(
+ ``,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.nested`)
+ expect(node.consequent.type === NodeTypes.JS_CALL_EXPRESSION).toBe(true)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ expect(
+ (node.alternate.children[0] as ElementNode).props[0],
+ ).toMatchObject({
+ name: 'key',
+ type: NodeTypes.ATTRIBUTE,
+ value: {
+ content: 'foo',
+ },
+ })
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('v-else + v-skip', () => {
+ const { root, node } = parseWithSkipTransform(
+ ``,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.IF)
+ const elseNode = node as unknown as IfNode
+ const branch = elseNode.branches[1]
+ expect(branch.children.length).toBe(1)
+ // skipNode
+ expect(branch.children[0].type).toBe(NodeTypes.SKIP)
+ expect(
+ ((branch.children[0] as SkipNode).test as SimpleExpressionNode).content,
+ ).toBe(`_ctx.nested`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('v-else-if + v-skip', () => {
+ const { root, node } = parseWithSkipTransform(
+ ``,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.IF)
+ const elseIfNode = node as unknown as IfNode
+ const branch = elseIfNode.branches[1]
+ expect((branch.condition as SimpleExpressionNode).content).toBe(
+ `_ctx.yes`,
+ )
+ expect(branch.children.length).toBe(1)
+ // skipNode
+ expect(branch.children[0].type).toBe(NodeTypes.SKIP)
+ expect(
+ ((branch.children[0] as SkipNode).test as SimpleExpressionNode).content,
+ ).toBe(`_ctx.nested`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('with v-memo', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
`,
+ ) as {
+ root: RootNode
+ node: SkipNode
+ }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`div`)
+ const codegenNode = (node.alternate.children[0] as ElementNode)
+ .codegenNode!
+ expect(codegenNode.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
+ expect((codegenNode as any).callee).toBe(WITH_MEMO)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component without slot', () => {
+ // equivalent to
+ const { root, node } = parseWithSkipTransform(``) as {
+ root: RootNode
+ node: SkipNode
+ }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(node.consequent.type === NodeTypes.JS_CALL_EXPRESSION).toBe(true)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`Comp`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with default slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `foo`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.TEXT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as any).content,
+ ).toBe(`foo`)
+ expect(node.alternate.children.length).toBe(1)
+ expect((node.alternate.children[0] as ElementNode).tagType).toBe(
+ ElementTypes.COMPONENT,
+ )
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`Comp`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with multiple named slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ default
+ foo
+ `,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.TEXT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as any).content,
+ ).toBe(`default`)
+ expect(node.alternate.children.length).toBe(1)
+ expect((node.alternate.children[0] as ElementNode).tagType).toBe(
+ ElementTypes.COMPONENT,
+ )
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`Comp`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with multiple implicit default slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+
+ {{foo}}
+
+ `,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(2)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as ElementNode).tag,
+ ).toBe(`span`)
+ expect((node.consequent as IfBranchNode).children[1].type).toBe(
+ NodeTypes.ELEMENT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[1] as ElementNode).tag,
+ ).toBe(`div`)
+ expect(node.alternate.children.length).toBe(1)
+ expect((node.alternate.children[0] as ElementNode).tagType).toBe(
+ ElementTypes.COMPONENT,
+ )
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`Comp`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with name default slot + v-if', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ {{default}}
+ `,
+ ) as { root: RootNode; node: ComponentNode }
+ expect(node.type).toBe(NodeTypes.ELEMENT)
+ expect(node.tagType).toBe(ElementTypes.COMPONENT)
+ const codegenNode = node.codegenNode! as VNodeCall
+ expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
+ const vnodeTag = codegenNode.tag as CallExpression
+ expect(vnodeTag.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
+ expect(vnodeTag.callee).toBe(RESOLVE_SKIP_COMPONENT)
+ expect((vnodeTag.arguments[0] as SimpleExpressionNode).content).toBe(
+ `_ctx.ok`,
+ )
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with implicit default slot + v-if', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ {{default}}
+ `,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with dynamic slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ {{foo}}
+ `,
+ ) as { root: RootNode; node: ComponentNode }
+ expect(node.type).toBe(NodeTypes.ELEMENT)
+ expect(node.tagType).toBe(ElementTypes.COMPONENT)
+ const codegenNode = node.codegenNode! as VNodeCall
+ expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
+ const vnodeTag = codegenNode.tag as CallExpression
+ expect(vnodeTag.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
+ expect(vnodeTag.callee).toBe(RESOLVE_SKIP_COMPONENT)
+ expect((vnodeTag.arguments[0] as SimpleExpressionNode).content).toBe(
+ `_ctx.ok`,
+ )
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on component with dynamic slot + default slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ foo
+ default
+ `,
+ ) as { root: RootNode; node: ComponentNode }
+ expect(node.type).toBe(NodeTypes.ELEMENT)
+ expect(node.tagType).toBe(ElementTypes.COMPONENT)
+ const codegenNode = node.codegenNode! as VNodeCall
+ expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
+ const vnodeTag = codegenNode.tag as CallExpression
+ expect(vnodeTag.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
+ expect(vnodeTag.callee).toBe(RESOLVE_SKIP_COMPONENT)
+ expect((vnodeTag.arguments[0] as SimpleExpressionNode).content).toBe(
+ `_ctx.ok`,
+ )
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on dynamic component with default slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `foo`,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect((node.consequent as IfBranchNode).children.length).toBe(1)
+ expect((node.consequent as IfBranchNode).children[0].type).toBe(
+ NodeTypes.TEXT,
+ )
+ expect(
+ ((node.consequent as IfBranchNode).children[0] as any).content,
+ ).toBe(`foo`)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`component`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on dynamic component with dynamic slot', () => {
+ const { root, node } = parseWithSkipTransform(
+ `
+ foo
+ `,
+ ) as { root: RootNode; node: ComponentNode }
+ expect(node.type).toBe(NodeTypes.ELEMENT)
+ expect(node.tagType).toBe(ElementTypes.COMPONENT)
+ const codegenNode = node.codegenNode! as VNodeCall
+ expect(codegenNode.type).toBe(NodeTypes.VNODE_CALL)
+ const vnodeTag = codegenNode.tag as CallExpression
+ expect(vnodeTag.type).toBe(NodeTypes.JS_CALL_EXPRESSION)
+ expect(vnodeTag.callee).toBe(RESOLVE_SKIP_COMPONENT)
+ expect((vnodeTag.arguments[0] as SimpleExpressionNode).content).toBe(
+ `_ctx.ok`,
+ )
+ expect(generate(root).code).toMatchSnapshot()
+ })
+
+ test('on Teleport', () => {
+ const { root, node } = parseWithSkipTransform(
+ ``,
+ ) as { root: RootNode; node: SkipNode }
+ expect(node.type).toBe(NodeTypes.SKIP)
+ expect((node.test as SimpleExpressionNode).content).toBe(`_ctx.ok`)
+ expect(node.consequent.type === NodeTypes.JS_CALL_EXPRESSION).toBe(true)
+ expect(node.alternate.children.length).toBe(1)
+ expect(node.alternate.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect((node.alternate.children[0] as ElementNode).tag).toBe(`teleport`)
+ expect(generate(root).code).toMatchSnapshot()
+ })
+ })
+
+ describe('errors', () => {
+ test('no expression', () => {
+ const onError = vi.fn()
+ const { node } = parseWithSkipTransform(``, { onError })
+ expect(onError.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_SKIP_NO_EXPRESSION,
+ loc: node.loc,
+ },
+ ])
+ })
+
+ test('on ', () => {
+ const onError = vi.fn()
+ parseWithSkipTransform(``, { onError })
+ expect(onError.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_SKIP_MISPLACED,
+ },
+ ])
+ })
+
+ test('on ', () => {
+ const onError = vi.fn()
+ parseWithSkipTransform(``, { onError })
+ expect(onError.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_SKIP_MISPLACED,
+ },
+ ])
+ })
+
+ test('on component without default slot', () => {
+ const onError = vi.fn()
+ parseWithSkipTransform(
+ `foo`,
+ { onError },
+ )
+ expect(onError.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT,
+ },
+ ])
+ })
+
+ test('with v-for', () => {
+ const onError = vi.fn()
+ parseWithSkipTransform(``, {
+ onError,
+ })
+ expect(onError.mock.calls[0]).toMatchObject([
+ {
+ code: ErrorCodes.X_V_SKIP_WITH_V_FOR,
+ },
+ ])
+ })
+ })
+})
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index 2d6df9d9010..3d09dc9691e 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -41,6 +41,7 @@ export enum NodeTypes {
IF_BRANCH,
FOR,
TEXT_CALL,
+ SKIP,
// codegen
VNODE_CALL,
JS_CALL_EXPRESSION,
@@ -100,6 +101,7 @@ export type TemplateChildNode =
| IfBranchNode
| ForNode
| TextCallNode
+ | SkipNode
export interface RootNode extends Node {
type: NodeTypes.ROOT
@@ -144,12 +146,15 @@ export interface PlainElementNode extends BaseElementNode {
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
+ | ConditionalExpression
| undefined
ssrCodegenNode?: TemplateLiteral
}
export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT
+ slots: SlotsExpression
+ hasDynamicSlots: boolean
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
@@ -405,6 +410,15 @@ export interface FunctionExpression extends Node {
isNonScopedSlot?: boolean
}
+export interface SkipNode extends Node {
+ type: NodeTypes.SKIP
+ test: ExpressionNode
+ consequent: IfBranchNode | CallExpression
+ alternate: IfBranchNode
+ newline: boolean
+ codegenNode: ConditionalExpression | undefined
+}
+
export interface ConditionalExpression extends Node {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION
test: JSChildNode
@@ -454,7 +468,7 @@ export interface TemplateLiteral extends Node {
export interface IfStatement extends Node {
type: NodeTypes.JS_IF_STATEMENT
test: ExpressionNode
- consequent: BlockStatement
+ consequent: BlockStatement | CallExpression
alternate: IfStatement | BlockStatement | ReturnStatement | undefined
}
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 70116cfb61a..83fd61ee24e 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -656,10 +656,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
+ case NodeTypes.SKIP:
__DEV__ &&
assert(
node.codegenNode != null,
- `Codegen node is missing for element/if/for node. ` +
+ `Codegen node is missing for element/if/for/skip node. ` +
`Apply appropriate transforms first.`,
)
genNode(node.codegenNode!, context)
diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts
index a697c9d22e6..637d31c5191 100644
--- a/packages/compiler-core/src/compile.ts
+++ b/packages/compiler-core/src/compile.ts
@@ -22,6 +22,7 @@ import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
import { transformMemo } from './transforms/vMemo'
+import { transformSkip } from './transforms/vSkip'
export type TransformPreset = [
NodeTransform[],
@@ -35,6 +36,7 @@ export function getBaseTransformPreset(
[
transformOnce,
transformIf,
+ transformSkip,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 58e113ab19e..8bd6df968e3 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -90,6 +90,10 @@ export enum ErrorCodes {
X_V_MODEL_ON_PROPS,
X_INVALID_EXPRESSION,
X_KEEP_ALIVE_INVALID_CHILDREN,
+ X_V_SKIP_NO_EXPRESSION,
+ X_V_SKIP_MISPLACED,
+ X_V_SKIP_UNEXPECTED_SLOT,
+ X_V_SKIP_WITH_V_FOR,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
@@ -179,6 +183,10 @@ export const errorMessages: Record = {
[ErrorCodes.X_INVALID_EXPRESSION]: `Error parsing JavaScript expression: `,
[ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: ` expects exactly one child component.`,
[ErrorCodes.X_VNODE_HOOKS]: `@vnode-* hooks in templates are no longer supported. Use the vue: prefix instead. For example, @vnode-mounted should be changed to @vue:mounted. @vnode-* hooks support has been removed in 3.4.`,
+ [ErrorCodes.X_V_SKIP_NO_EXPRESSION]: `v-skip is missing expression.`,
+ [ErrorCodes.X_V_SKIP_MISPLACED]: `v-skip can only be used on elements or components.`,
+ [ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT]: `v-skip requires the component to have a default slot without slot props`,
+ [ErrorCodes.X_V_SKIP_WITH_V_FOR]: `v-skip with v-for is not supported.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts
index 29e5f681300..0ad4f070ebb 100644
--- a/packages/compiler-core/src/index.ts
+++ b/packages/compiler-core/src/index.ts
@@ -48,6 +48,7 @@ export { transformBind } from './transforms/vBind'
export { noopDirectiveTransform } from './transforms/noopDirectiveTransform'
export { processIf } from './transforms/vIf'
export { processFor, createForLoopParams } from './transforms/vFor'
+export { processSkip } from './transforms/vSkip'
export {
transformExpression,
processExpression,
diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts
index 7cf3757b249..46b36f45054 100644
--- a/packages/compiler-core/src/runtimeHelpers.ts
+++ b/packages/compiler-core/src/runtimeHelpers.ts
@@ -26,6 +26,9 @@ export const CREATE_STATIC: unique symbol = Symbol(
export const RESOLVE_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveComponent` : ``,
)
+export const RESOLVE_SKIP_COMPONENT: unique symbol = Symbol(
+ __DEV__ ? `resolveSkipComponent` : ``,
+)
export const RESOLVE_DYNAMIC_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveDynamicComponent` : ``,
)
@@ -99,6 +102,7 @@ export const helperNameMap: Record = {
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
+ [RESOLVE_SKIP_COMPONENT]: `resolveSkipComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
[WITH_DIRECTIVES]: `withDirectives`,
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index aeb96cc2b4a..40755178def 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -466,6 +466,18 @@ export function traverseNode(
traverseNode(node.branches[i], context)
}
break
+ case NodeTypes.SKIP:
+ // in non-SSR mode, `alternate` already includes `consequent` content,
+ // so no need to traverse `consequent` node
+ // during `inSSR` transform, we need to traverse both since we use the cloned nodes,
+ // see `createBranchNode` in `vSkip.ts`
+ if (context.inSSR) {
+ const { consequent } = node
+ if (consequent.type === NodeTypes.IF_BRANCH)
+ traverseNode(consequent, context)
+ }
+ traverseNode(node.alternate, context)
+ break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
diff --git a/packages/compiler-core/src/transforms/cacheStatic.ts b/packages/compiler-core/src/transforms/cacheStatic.ts
index 8d5961643c1..d0edd075378 100644
--- a/packages/compiler-core/src/transforms/cacheStatic.ts
+++ b/packages/compiler-core/src/transforms/cacheStatic.ts
@@ -341,6 +341,7 @@ export function getConstantType(
case NodeTypes.IF:
case NodeTypes.FOR:
case NodeTypes.IF_BRANCH:
+ case NodeTypes.SKIP:
return ConstantTypes.NOT_CONSTANT
case NodeTypes.INTERPOLATION:
case NodeTypes.TEXT_CALL:
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index 76ca1d44353..d3087c08d98 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -173,7 +173,12 @@ export const transformElement: NodeTransform = (node, context) => {
vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
- const { slots, hasDynamicSlots } = buildSlots(node, context)
+ const { slots, hasDynamicSlots } = buildSlots(
+ node as ComponentNode,
+ context,
+ undefined,
+ true,
+ )
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index 54c505407a3..f2e1fd5e848 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -217,7 +217,7 @@ function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
}
}
-function createCodegenNodeForBranch(
+export function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext,
diff --git a/packages/compiler-core/src/transforms/vSkip.ts b/packages/compiler-core/src/transforms/vSkip.ts
new file mode 100644
index 00000000000..c2ef333f945
--- /dev/null
+++ b/packages/compiler-core/src/transforms/vSkip.ts
@@ -0,0 +1,215 @@
+import {
+ type ComponentNode,
+ type DirectiveNode,
+ type ElementNode,
+ ElementTypes,
+ type ExpressionNode,
+ type IfBranchNode,
+ NodeTypes,
+ type SimpleExpressionNode,
+ type SkipNode,
+ type SourceLocation,
+ type TemplateChildNode,
+ type VNodeCall,
+ createCallExpression,
+ createConditionalExpression,
+ createSimpleExpression,
+} from '../ast'
+import {
+ type NodeTransform,
+ type TransformContext,
+ createStructuralDirectiveTransform,
+} from '../transform'
+import {
+ CREATE_COMMENT,
+ ErrorCodes,
+ RESOLVE_SKIP_COMPONENT,
+ WITH_MEMO,
+ buildSlots,
+ createCompilerError,
+ findDir,
+ findProp,
+ processExpression,
+} from '@vue/compiler-core'
+import { createCodegenNodeForBranch } from './vIf'
+import { validateBrowserExpression } from '../validateExpression'
+import { cloneLoc } from '../parser'
+import { clone } from '@vue/shared'
+
+export const transformSkip: NodeTransform = createStructuralDirectiveTransform(
+ 'skip',
+ (node, dir, context) => {
+ return processSkip(node, dir, context, (skipNode?: SkipNode) => {
+ return () => {
+ const codegenNode = node.codegenNode!
+ if (!skipNode) {
+ if (codegenNode.type === NodeTypes.VNODE_CALL) {
+ codegenNode.tag = getVNodeTag(
+ context,
+ dir.exp!,
+ codegenNode.tag as string,
+ )
+ } else if (
+ codegenNode.type === NodeTypes.JS_CALL_EXPRESSION &&
+ codegenNode.callee === WITH_MEMO
+ ) {
+ const vnodeCall = codegenNode.arguments[1].returns as VNodeCall
+ vnodeCall.tag = getVNodeTag(
+ context,
+ dir.exp!,
+ vnodeCall.tag as string,
+ )
+ }
+ } else {
+ const { consequent, alternate, test } = skipNode!
+ skipNode!.codegenNode = createConditionalExpression(
+ test,
+ consequent.type === NodeTypes.IF_BRANCH
+ ? createCodegenNodeForBranch(consequent, 0, context)
+ : consequent,
+ createCodegenNodeForBranch(alternate, 1, context),
+ )
+ }
+ }
+ })
+ },
+)
+
+export function processSkip(
+ node: ElementNode,
+ dir: DirectiveNode,
+ context: TransformContext,
+ processCodegen?: (skipNode?: SkipNode) => () => void,
+): (() => void) | undefined {
+ const loc = dir.exp ? dir.exp.loc : node.loc
+ if (
+ // v-skip is not allowed on or
+ !(
+ node.type === NodeTypes.ELEMENT &&
+ (node.tagType === ElementTypes.ELEMENT ||
+ node.tagType === ElementTypes.COMPONENT) &&
+ node.tag !== 'template' &&
+ node.tag !== 'slot'
+ )
+ ) {
+ context.onError(createCompilerError(ErrorCodes.X_V_SKIP_MISPLACED, loc))
+ return
+ }
+
+ if (findDir(node, 'for')) {
+ context.onError(createCompilerError(ErrorCodes.X_V_SKIP_WITH_V_FOR, loc))
+ return
+ }
+
+ if (!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim()) {
+ context.onError(createCompilerError(ErrorCodes.X_V_SKIP_NO_EXPRESSION, loc))
+ dir.exp = createSimpleExpression(`true`, false, loc)
+ }
+
+ if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
+ dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
+ }
+
+ if (__DEV__ && __BROWSER__ && dir.exp) {
+ validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
+ }
+
+ // element will be processed as a skip node
+ // - native element
+ // - teleport, since it has children
+ // - component without dynamic slots
+ let processAsSkipNode = false
+ const isComponent = node.tagType === ElementTypes.COMPONENT
+ let children: TemplateChildNode[] = []
+ if (
+ node.tagType === ElementTypes.ELEMENT ||
+ (isComponent && node.tag === 'Teleport')
+ ) {
+ processAsSkipNode = true
+ children = node.children
+ } else if (isComponent) {
+ const { hasDynamicSlots, defaultSlot } = resolveDefaultSlot(node, context)
+ if (!hasDynamicSlots) {
+ if (defaultSlot) {
+ processAsSkipNode = true
+ children = defaultSlot
+ } else {
+ context.onError(
+ createCompilerError(ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT, loc),
+ )
+ }
+ }
+ }
+
+ let skipNode: SkipNode | undefined
+ if (processAsSkipNode) {
+ // if children is empty, create comment node
+ const consequent =
+ children.length !== 0
+ ? createBranchNode(context, node, node.loc, children)
+ : createCallExpression(context.helper(CREATE_COMMENT), [
+ __DEV__ ? '"v-skip"' : '""',
+ 'true',
+ ])
+
+ skipNode = {
+ type: NodeTypes.SKIP,
+ loc: cloneLoc(node.loc),
+ test: dir.exp,
+ consequent,
+ alternate: createBranchNode(context, node, node.loc, [node]),
+ newline: true,
+ codegenNode: undefined,
+ }
+
+ context.replaceNode(skipNode)
+ }
+
+ if (processCodegen) return processCodegen(skipNode)
+}
+
+function resolveDefaultSlot(node: ComponentNode, context: TransformContext) {
+ let defaultSlot: TemplateChildNode[] | undefined = undefined
+ const { slots, hasDynamicSlots } = buildSlots(node, context, undefined, true)
+ // find default slot without slot props if not has dynamic slots
+ if (!hasDynamicSlots && slots.type === NodeTypes.JS_OBJECT_EXPRESSION) {
+ const prop = slots.properties.find(
+ p =>
+ p.type === NodeTypes.JS_PROPERTY &&
+ p.key.type === NodeTypes.SIMPLE_EXPRESSION &&
+ p.key.content === 'default' &&
+ p.value.params === undefined,
+ )
+ if (prop) {
+ defaultSlot = prop.value.returns as TemplateChildNode[]
+ }
+ }
+ return { hasDynamicSlots, defaultSlot }
+}
+
+function createBranchNode(
+ context: TransformContext,
+ node: ElementNode,
+ loc: SourceLocation,
+ children: TemplateChildNode[],
+): IfBranchNode {
+ return {
+ type: NodeTypes.IF_BRANCH,
+ loc,
+ condition: undefined,
+ // using cloned node during `inSSR` transform
+ children: context.inSSR ? clone(children) : children,
+ userKey: findProp(node, `key`),
+ }
+}
+
+function getVNodeTag(
+ context: TransformContext,
+ exp: ExpressionNode,
+ tag: string,
+) {
+ return createCallExpression(context.helper(RESOLVE_SKIP_COMPONENT), [
+ exp,
+ tag,
+ ])
+}
diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts
index db367f39c0c..0f795fcce9c 100644
--- a/packages/compiler-core/src/transforms/vSlot.ts
+++ b/packages/compiler-core/src/transforms/vSlot.ts
@@ -1,8 +1,8 @@
import {
type CallExpression,
+ type ComponentNode,
type ConditionalExpression,
type DirectiveNode,
- type ElementNode,
ElementTypes,
type ExpressionNode,
type FunctionExpression,
@@ -114,13 +114,21 @@ const buildClientSlotFn: SlotFnBuilder = (props, _vForExp, children, loc) =>
// Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component.
export function buildSlots(
- node: ElementNode,
+ node: ComponentNode,
context: TransformContext,
buildSlotFn: SlotFnBuilder = buildClientSlotFn,
+ useCache: boolean = false,
): {
slots: SlotsExpression
hasDynamicSlots: boolean
} {
+ // early return if slots are already built to avoid duplication
+ if (useCache && node.slots) {
+ return {
+ slots: node.slots,
+ hasDynamicSlots: node.hasDynamicSlots,
+ }
+ }
context.helper(WITH_CTX)
const { children, loc } = node
@@ -364,6 +372,8 @@ export function buildSlots(
]) as SlotsExpression
}
+ node.slots = slots
+ node.hasDynamicSlots = hasDynamicSlots
return {
slots,
hasDynamicSlots,
@@ -402,6 +412,10 @@ function hasForwardedSlots(children: TemplateChildNode[]): boolean {
case NodeTypes.IF:
if (hasForwardedSlots(child.branches)) return true
break
+ case NodeTypes.SKIP:
+ // only check `alternate` branch since it contains the `consequent` node
+ if (hasForwardedSlots(child.alternate.children)) return true
+ break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
if (hasForwardedSlots(child.children)) return true
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b49d70bb2fb..093b6bba816 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -397,7 +397,11 @@ export function injectProp(
* we need to get the real props before normalization
*/
let props =
- node.type === NodeTypes.VNODE_CALL ? node.props : node.arguments[2]
+ node.type === NodeTypes.VNODE_CALL
+ ? node.props
+ : node.type === NodeTypes.JS_CALL_EXPRESSION
+ ? node.arguments[2]
+ : undefined
let callPath: CallExpression[] = []
let parentCall: CallExpression | undefined
if (
@@ -459,7 +463,7 @@ export function injectProp(
} else {
node.props = propsWithInjection
}
- } else {
+ } else if (node.type === NodeTypes.JS_CALL_EXPRESSION) {
if (parentCall) {
parentCall.arguments[0] = propsWithInjection
} else {
@@ -524,6 +528,12 @@ export function hasScopeRef(
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.IF:
return node.branches.some(b => hasScopeRef(b, ids))
+ case NodeTypes.SKIP:
+ return (
+ hasScopeRef(node.test, ids) ||
+ // only check `alternate` branch since it contains the `consequent` node
+ node.alternate.children.some(c => hasScopeRef(c, ids))
+ )
case NodeTypes.IF_BRANCH:
if (hasScopeRef(node.condition, ids)) {
return true
diff --git a/packages/compiler-dom/src/errors.ts b/packages/compiler-dom/src/errors.ts
index b47624840ab..c765d13eed6 100644
--- a/packages/compiler-dom/src/errors.ts
+++ b/packages/compiler-dom/src/errors.ts
@@ -21,7 +21,7 @@ export function createDOMCompilerError(
}
export enum DOMErrorCodes {
- X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
+ X_V_HTML_NO_EXPRESSION = 57 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION,
X_V_TEXT_WITH_CHILDREN,
diff --git a/packages/compiler-ssr/__tests__/ssrVSkip.spec.ts b/packages/compiler-ssr/__tests__/ssrVSkip.spec.ts
new file mode 100644
index 00000000000..3b228b349df
--- /dev/null
+++ b/packages/compiler-ssr/__tests__/ssrVSkip.spec.ts
@@ -0,0 +1,610 @@
+import { compile } from '@vue/compiler-ssr'
+
+describe('ssr: v-skip', () => {
+ test('basic', () => {
+ expect(compile(``).code).toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+
+ test('with text children', () => {
+ expect(compile(`{{hello}}
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _push(\`\${_ssrInterpolate(_ctx.hello)}\`)
+ } else {
+ _push(\`\${
+ _ssrInterpolate(_ctx.hello)
+ }
\`)
+ }
+ }"
+ `)
+ })
+
+ test('with element children', () => {
+ expect(compile(`
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _push(\`\`)
+ } else {
+ _push(\`
\`)
+ }
+ }"
+ `)
+ })
+
+ test('with component children', () => {
+ expect(compile(`
`).code)
+ .toMatchInlineSnapshot(`
+ "const { resolveComponent: _resolveComponent } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_MyComponent = _resolveComponent("MyComponent")
+
+ if (_ctx.foo) {
+ _push(_ssrRenderComponent(_component_MyComponent, null, null, _parent))
+ } else {
+ _push(\`\`)
+ _push(_ssrRenderComponent(_component_MyComponent, null, null, _parent))
+ _push(\`
\`)
+ }
+ }"
+ `)
+ })
+
+ test('with multiple children', () => {
+ expect(compile(`
`).code)
+ .toMatchInlineSnapshot(`
+ "const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _push(\`\`)
+ } else {
+ _push(\`
\`)
+ }
+ }"
+ `)
+ })
+
+ test('nested v-skip', () => {
+ expect(compile(``).code)
+ .toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.foo) {
+ _push(\`\`)
+ if (_ctx.bar) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ if (_ctx.bar) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`
\`)
+ }
+ _push(\`
\`)
+ }
+ }"
+ `)
+ })
+
+ test('v-if + v-skip', () => {
+ expect(compile(``).code)
+ .toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\`)
+ if (_ctx.foo) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+
+ test('with key', () => {
+ expect(compile(``).code).toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode, mergeProps: _mergeProps } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+
+ test('v-else + v-skip', () => {
+ expect(compile(``).code)
+ .toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ if (_ctx.nested) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+
+ test('v-else-if + v-skip', () => {
+ expect(
+ compile(``).code,
+ ).toMatchInlineSnapshot(`
+ "const { createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\`)
+ } else if (_ctx.yes) {
+ _push(\`\`)
+ if (_ctx.nested) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ }
+ }"
+ `)
+ })
+
+ test('on component without slot', () => {
+ // equivalent to
+ expect(compile(``).code).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ if (_ctx.foo) {
+ _createCommentVNode("v-skip", true)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, _attrs, null, _parent))
+ }
+ }"
+ `)
+ })
+
+ test('on component with default slot', () => {
+ expect(compile(`{{foo}}`).code)
+ .toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ if (_ctx.ok) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, _attrs, {
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }, _parent))
+ }
+ }"
+ `)
+ })
+
+ test('on component with multiple named slot', () => {
+ expect(
+ compile(
+ `
+ {{default}}
+ {{foo}}
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ if (_ctx.ok) {
+ _push(\`\${_ssrInterpolate(_ctx.default)}\`)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, _attrs, {
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.default)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.default), 1 /* TEXT */)
+ ]
+ }
+ }),
+ foo: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }, _parent))
+ }
+ }"
+ `)
+ })
+
+ test('on component with multiple implicit default slot', () => {
+ expect(
+ compile(
+ `
+
+ {{foo}}
+
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, createVNode: _createVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ if (_ctx.ok) {
+ _push(\`\`)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, _attrs, {
+ foo: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\`)
+ } else {
+ return [
+ _createVNode("span"),
+ _createVNode("div")
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }, _parent))
+ }
+ }"
+ `)
+ })
+
+ test('on component with name default slot + v-if', () => {
+ expect(
+ compile(
+ `
+ {{default}}
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, createSlots: _createSlots, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSkipComponent: _ssrRenderSkipComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ _push(_ssrRenderSkipComponent(_push, _ctx.ok, _component_Comp, _attrs, _createSlots({ _: 2 /* DYNAMIC */ }, [
+ (_ctx.yes)
+ ? {
+ name: "default",
+ fn: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.default)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.default), 1 /* TEXT */)
+ ]
+ }
+ }),
+ key: "0"
+ }
+ : undefined
+ ]), _parent))
+ }"
+ `)
+ })
+
+ test('on component with implicit default slot + v-if', () => {
+ expect(
+ compile(
+ `
+ {{default}}
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, openBlock: _openBlock, createBlock: _createBlock, createCommentVNode: _createCommentVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ if (_ctx.ok) {
+ _push(\`\`)
+ if (_ctx.yes) {
+ _push(\`\${_ssrInterpolate(_ctx.default)}\`)
+ } else {
+ _push(\`\`)
+ }
+ _push(\`\`)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, _attrs, {
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ if (_ctx.yes) {
+ _push(\`\${
+ _ssrInterpolate(_ctx.default)
+ }\`)
+ } else {
+ _push(\`\`)
+ }
+ } else {
+ return [
+ (_ctx.yes)
+ ? (_openBlock(), _createBlock("span", { key: 0 }, _toDisplayString(_ctx.default), 1 /* TEXT */))
+ : _createCommentVNode("v-if", true)
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }, _parent))
+ }
+ }"
+ `)
+ })
+
+ test('on component with dynamic slot', () => {
+ expect(
+ compile(
+ `
+ {{foo}}
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSkipComponent: _ssrRenderSkipComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ _push(_ssrRenderSkipComponent(_push, _ctx.ok, _component_Comp, _attrs, {
+ [_ctx.foo]: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 2 /* DYNAMIC */
+ }, _parent))
+ }"
+ `)
+ })
+
+ test('on component with dynamic slot + default slot', () => {
+ expect(
+ compile(
+ `
+ {{foo}}
+ {{default}}
+ `,
+ ).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSkipComponent: _ssrRenderSkipComponent, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ _push(_ssrRenderSkipComponent(_push, _ctx.ok, _component_Comp, _attrs, {
+ [_ctx.foo]: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.default)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.default), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 2 /* DYNAMIC */
+ }, _parent))
+ }"
+ `)
+ })
+
+ test('on dynamic component with default slot', () => {
+ expect(
+ compile(`{{foo}}`).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, createVNode: _createVNode } = require("vue")
+ const { ssrRenderVNode: _ssrRenderVNode, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.Comp), _attrs, {
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }), _parent)
+ }
+ }"
+ `)
+ })
+
+ test('on dynamic component with dynamic slot', () => {
+ expect(
+ compile(`
+
+ {{foo}}
+
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, toDisplayString: _toDisplayString, createTextVNode: _createTextVNode, createVNode: _createVNode } = require("vue")
+ const { ssrRenderVNode: _ssrRenderVNode, ssrRenderSkipVNode: _ssrRenderSkipVNode, ssrInterpolate: _ssrInterpolate } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ _ssrRenderSkipVNode(_ctx.ok, _push, _createVNode(_resolveDynamicComponent(_ctx.Comp), _attrs, {
+ [_ctx.foo]: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}\`)
+ } else {
+ return [
+ _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */)
+ ]
+ }
+ }),
+ _: 2 /* DYNAMIC */
+ }), _parent)
+ }"
+ `)
+ })
+
+ test('on Teleport', () => {
+ expect(
+ compile(`
+
+ {{foo}}
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx } = require("vue")
+ const { ssrInterpolate: _ssrInterpolate, ssrRenderTeleport: _ssrRenderTeleport } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ if (_ctx.ok) {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}
\`)
+ } else {
+ _ssrRenderTeleport(_push, (_push) => {
+ _push(\`\${_ssrInterpolate(_ctx.foo)}
\`)
+ }, "target", false, _parent)
+ }
+ }"
+ `)
+ })
+
+ test('fragment with component v-skip', () => {
+ // here is verified that when root is a fragment, `_attrs` won't be injected
+ // into skip node's alternate branch
+ expect(
+ compile(`
+
+
+ `).code,
+ ).toMatchInlineSnapshot(`
+ "const { withCtx: _withCtx, resolveComponent: _resolveComponent, createVNode: _createVNode } = require("vue")
+ const { ssrRenderComponent: _ssrRenderComponent } = require("vue/server-renderer")
+
+ return function ssrRender(_ctx, _push, _parent, _attrs) {
+ const _component_Comp = _resolveComponent("Comp")
+
+ _push(\`\`)
+ if (_ctx.ok) {
+ _push(\`\`)
+ } else {
+ _push(_ssrRenderComponent(_component_Comp, null, {
+ default: _withCtx((_, _push, _parent, _scopeId) => {
+ if (_push) {
+ _push(\`\`)
+ } else {
+ return [
+ _createVNode("span")
+ ]
+ }
+ }),
+ _: 1 /* STABLE */
+ }, _parent))
+ }
+ _push(\`\`)
+ }"
+ `)
+ })
+})
diff --git a/packages/compiler-ssr/src/errors.ts b/packages/compiler-ssr/src/errors.ts
index e4fd505d282..a1dace764a1 100644
--- a/packages/compiler-ssr/src/errors.ts
+++ b/packages/compiler-ssr/src/errors.ts
@@ -17,7 +17,7 @@ export function createSSRCompilerError(
}
export enum SSRErrorCodes {
- X_SSR_UNSAFE_ATTR_NAME = 65 /* DOMErrorCodes.__EXTEND_POINT__ */,
+ X_SSR_UNSAFE_ATTR_NAME = 69 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_NO_TELEPORT_TARGET,
X_SSR_INVALID_AST_NODE,
}
diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts
index f8a686555e8..03bc0bb231e 100644
--- a/packages/compiler-ssr/src/index.ts
+++ b/packages/compiler-ssr/src/index.ts
@@ -27,6 +27,7 @@ import { ssrTransformModel } from './transforms/ssrVModel'
import { ssrTransformShow } from './transforms/ssrVShow'
import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttrs'
import { ssrInjectCssVars } from './transforms/ssrInjectCssVars'
+import { ssrTransformSkip } from './transforms/ssrVSkip'
export function compile(
source: string | RootNode,
@@ -56,6 +57,7 @@ export function compile(
hoistStatic: false,
nodeTransforms: [
ssrTransformIf,
+ ssrTransformSkip,
ssrTransformFor,
trackVForSlotScopes,
transformExpression,
diff --git a/packages/compiler-ssr/src/runtimeHelpers.ts b/packages/compiler-ssr/src/runtimeHelpers.ts
index 0e2c8c67bed..6130bdd216a 100644
--- a/packages/compiler-ssr/src/runtimeHelpers.ts
+++ b/packages/compiler-ssr/src/runtimeHelpers.ts
@@ -2,7 +2,11 @@ import { registerRuntimeHelpers } from '@vue/compiler-dom'
export const SSR_INTERPOLATE: unique symbol = Symbol(`ssrInterpolate`)
export const SSR_RENDER_VNODE: unique symbol = Symbol(`ssrRenderVNode`)
+export const SSR_RENDER_SKIP_VNODE: unique symbol = Symbol(`ssrRenderSkipVNode`)
export const SSR_RENDER_COMPONENT: unique symbol = Symbol(`ssrRenderComponent`)
+export const SSR_RENDER_SKIP_COMPONENT: unique symbol = Symbol(
+ `ssrRenderSkipComponent`,
+)
export const SSR_RENDER_SLOT: unique symbol = Symbol(`ssrRenderSlot`)
export const SSR_RENDER_SLOT_INNER: unique symbol = Symbol(`ssrRenderSlotInner`)
export const SSR_RENDER_CLASS: unique symbol = Symbol(`ssrRenderClass`)
@@ -31,7 +35,9 @@ export const SSR_GET_DIRECTIVE_PROPS: unique symbol =
export const ssrHelpers: Record = {
[SSR_INTERPOLATE]: `ssrInterpolate`,
[SSR_RENDER_VNODE]: `ssrRenderVNode`,
+ [SSR_RENDER_SKIP_VNODE]: `ssrRenderSkipVNode`,
[SSR_RENDER_COMPONENT]: `ssrRenderComponent`,
+ [SSR_RENDER_SKIP_COMPONENT]: `ssrRenderSkipComponent`,
[SSR_RENDER_SLOT]: `ssrRenderSlot`,
[SSR_RENDER_SLOT_INNER]: `ssrRenderSlotInner`,
[SSR_RENDER_CLASS]: `ssrRenderClass`,
diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts
index 536cbb5c1e9..d8e3b0b00aa 100644
--- a/packages/compiler-ssr/src/ssrCodegenTransform.ts
+++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts
@@ -28,6 +28,7 @@ import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrProcessComponent } from './transforms/ssrTransformComponent'
import { ssrProcessElement } from './transforms/ssrTransformElement'
import { SSRErrorCodes, createSSRCompilerError } from './errors'
+import { ssrProcessSkip } from './transforms/ssrVSkip'
// Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal
@@ -217,6 +218,9 @@ export function processChildren(
case NodeTypes.IF_BRANCH:
// no-op - handled by ssrProcessIf
break
+ case NodeTypes.SKIP:
+ ssrProcessSkip(child, context)
+ break
case NodeTypes.TEXT_CALL:
case NodeTypes.COMPOUND_EXPRESSION:
// no-op - these two types can never appear as template child node since
diff --git a/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts b/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts
index b1aac0d74c2..33c6cc545c5 100644
--- a/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts
+++ b/packages/compiler-ssr/src/transforms/ssrInjectFallthroughAttrs.ts
@@ -68,7 +68,13 @@ export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
}
injectFallthroughAttrs(node.children[0])
} else if (hasSingleChild(parent)) {
- injectFallthroughAttrs(node)
+ if (node.type === NodeTypes.SKIP) {
+ // for skip node, inject fallthrough attrs to the alternate branch
+ const children = filterChild(node.alternate)
+ injectFallthroughAttrs(children[0])
+ } else {
+ injectFallthroughAttrs(node)
+ }
}
}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index cad1ee81028..867d61d9605 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -55,7 +55,7 @@ import {
ssrProcessTransitionGroup,
ssrTransformTransitionGroup,
} from './ssrTransformTransitionGroup'
-import { extend, isArray, isObject, isPlainObject, isSymbol } from '@vue/shared'
+import { clone, extend, isObject, isSymbol } from '@vue/shared'
import { buildSSRProps } from './ssrTransformElement'
import {
ssrProcessTransition,
@@ -370,17 +370,3 @@ function subTransform(
// node/client branches
// - hoists are not enabled for the client branch here
}
-
-function clone(v: any): any {
- if (isArray(v)) {
- return v.map(clone)
- } else if (isPlainObject(v)) {
- const res: any = {}
- for (const key in v) {
- res[key] = clone(v[key as keyof typeof v])
- }
- return res
- } else {
- return v
- }
-}
diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts
index 0e3880247a1..c84ca659784 100644
--- a/packages/compiler-ssr/src/transforms/ssrVIf.ts
+++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts
@@ -63,7 +63,7 @@ export function ssrProcessIf(
}
}
-function processIfBranch(
+export function processIfBranch(
branch: IfBranchNode,
context: SSRTransformContext,
disableNestedFragments = false,
diff --git a/packages/compiler-ssr/src/transforms/ssrVSkip.ts b/packages/compiler-ssr/src/transforms/ssrVSkip.ts
new file mode 100644
index 00000000000..c6e53d66ebd
--- /dev/null
+++ b/packages/compiler-ssr/src/transforms/ssrVSkip.ts
@@ -0,0 +1,64 @@
+import {
+ type ComponentNode,
+ type NodeTransform,
+ NodeTypes,
+ type SkipNode,
+ createCallExpression,
+ createIfStatement,
+ createStructuralDirectiveTransform,
+ processSkip,
+} from '@vue/compiler-core'
+import { processIfBranch } from './ssrVIf'
+import type { SSRTransformContext } from '../ssrCodegenTransform'
+import {
+ SSR_RENDER_COMPONENT,
+ SSR_RENDER_SKIP_COMPONENT,
+ SSR_RENDER_SKIP_VNODE,
+ SSR_RENDER_VNODE,
+} from '../runtimeHelpers'
+
+export const ssrTransformSkip: NodeTransform =
+ createStructuralDirectiveTransform('skip', (node, dir, context) => {
+ return processSkip(node, dir, context, (skipNode?: SkipNode) => {
+ return () => {
+ // for non-skipNode, rewrite the ssrCodegenNode
+ // `ssrRenderComponent` -> `ssrRenderSkipComponent`
+ // `ssrRenderVNode` -> `ssrRenderSkipVNode`
+ if (!skipNode && (node as ComponentNode).ssrCodegenNode) {
+ const {
+ callee,
+ arguments: args,
+ loc,
+ } = (node as ComponentNode).ssrCodegenNode!
+ if (callee === SSR_RENDER_COMPONENT) {
+ ;(node as ComponentNode).ssrCodegenNode = createCallExpression(
+ context.helper(SSR_RENDER_SKIP_COMPONENT),
+ [`_push`, dir.exp!, ...args],
+ loc,
+ )
+ } else if (callee === SSR_RENDER_VNODE) {
+ ;(node as ComponentNode).ssrCodegenNode = createCallExpression(
+ context.helper(SSR_RENDER_SKIP_VNODE),
+ [dir.exp!, ...args],
+ loc,
+ )
+ }
+ }
+ }
+ })
+ })
+
+export function ssrProcessSkip(
+ node: SkipNode,
+ context: SSRTransformContext,
+): void {
+ const { consequent, alternate, test } = node
+ const ifStatement = createIfStatement(
+ test,
+ consequent.type === NodeTypes.IF_BRANCH
+ ? processIfBranch(consequent, context)
+ : consequent,
+ processIfBranch(alternate, context),
+ )
+ context.pushStatement(ifStatement)
+}
diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index 56011d06359..858d77ed503 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -2337,4 +2337,166 @@ describe('SSR hydration', () => {
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
})
})
+
+ describe('v-skip', () => {
+ test('on native element', async () => {
+ const App = {
+ setup() {
+ const toggle = ref(true)
+ return { toggle }
+ },
+ template: `
+
+ {{toggle}}
+ `,
+ }
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ 'true',
+ )
+
+ triggerEvent('click', container.querySelector('#toggleBtn')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ 'false
',
+ )
+ })
+
+ test('on component with default slot', async () => {
+ const Child = {
+ template: `
`,
+ }
+ const App = {
+ components: { Child },
+ setup() {
+ const toggle = ref(true)
+ return { toggle }
+ },
+ template: `
+
+ {{toggle}}
+ `,
+ }
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ 'true',
+ )
+
+ triggerEvent('click', container.querySelector('#toggleBtn')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ 'false
',
+ )
+ })
+
+ test('on component with default slot + v-if', async () => {
+ const Child = {
+ template: `
`,
+ }
+ const App = {
+ components: { Child },
+ setup() {
+ const toggle = ref(true)
+ return { toggle }
+ },
+ template: `
+
+ {{toggle}}
+ `,
+ }
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ 'true',
+ )
+
+ triggerEvent('click', container.querySelector('#toggleBtn')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ 'false
',
+ )
+ })
+
+ test('on component with dynamic slot', async () => {
+ const Child = {
+ template: `
`,
+ }
+ const App = {
+ components: { Child },
+ setup() {
+ const toggle = ref(true)
+ const slotName = ref('default')
+ return { toggle, slotName }
+ },
+ template: `
+
+
+
+ {{toggle}}
+
+
+ `,
+ }
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ 'true',
+ )
+
+ triggerEvent('click', container.querySelector('#toggleBtn')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ 'false
',
+ )
+ })
+
+ test('on dynamic component with dynamic slot', async () => {
+ const Child = {
+ template: `
`,
+ }
+ const App = {
+ setup() {
+ const toggle = ref(true)
+ const slotName = ref('default')
+ return { toggle, slotName, Child }
+ },
+ template: `
+
+
+
+ {{toggle}}
+
+
+ `,
+ }
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ // hydrate
+ createSSRApp(App).mount(container)
+ expect(container.innerHTML).toBe(
+ 'true',
+ )
+
+ triggerEvent('click', container.querySelector('#toggleBtn')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(
+ 'false
',
+ )
+ })
+ })
})
diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts
index aa6532c2811..6e2d6fcebdc 100644
--- a/packages/runtime-core/src/helpers/resolveAssets.ts
+++ b/packages/runtime-core/src/helpers/resolveAssets.ts
@@ -9,6 +9,12 @@ import type { Directive } from '../directives'
import { camelize, capitalize, isString } from '@vue/shared'
import { warn } from '../warning'
import type { VNodeTypes } from '../vnode'
+import {
+ type ComponentPublicInstance,
+ createCommentVNode,
+ defineComponent,
+ renderSlot,
+} from '@vue/runtime-core'
export const COMPONENTS = 'components'
export const DIRECTIVES = 'directives'
@@ -138,3 +144,23 @@ function resolve(registry: Record | undefined, name: string) {
registry[capitalize(camelize(name))])
)
}
+
+let _comp: ConcreteComponent | undefined
+/**
+ * @private
+ */
+export function resolveSkipComponent(
+ isSkip: boolean,
+ Comp: ConcreteComponent,
+): ConcreteComponent {
+ return isSkip
+ ? _comp ||
+ (_comp = defineComponent({
+ render(this: ComponentPublicInstance) {
+ return renderSlot(this.$slots, 'default', undefined, () => [
+ createCommentVNode('v-skip'),
+ ])
+ },
+ }))
+ : Comp
+}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 3871167b3ee..2cefe3eda8c 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -144,6 +144,7 @@ export {
resolveComponent,
resolveDirective,
resolveDynamicComponent,
+ resolveSkipComponent,
} from './helpers/resolveAssets'
// For integration with runtime compiler
export { registerRuntimeCompiler, isRuntimeOnly } from './component'
diff --git a/packages/server-renderer/src/helpers/ssrRenderSkipComponent.ts b/packages/server-renderer/src/helpers/ssrRenderSkipComponent.ts
new file mode 100644
index 00000000000..bc36f385fd6
--- /dev/null
+++ b/packages/server-renderer/src/helpers/ssrRenderSkipComponent.ts
@@ -0,0 +1,66 @@
+import {
+ type Component,
+ type ComponentInternalInstance,
+ type Slots,
+ type VNode,
+ createVNode,
+} from 'vue'
+import {
+ type Props,
+ type PushFn,
+ type SSRBuffer,
+ renderComponentVNode,
+ renderVNode as ssrRenderVNode,
+} from '../render'
+import { type SSRSlots, ssrRenderSlot } from './ssrRenderSlot'
+
+export function ssrRenderSkipComponent(
+ push: PushFn,
+ isSkip: boolean,
+ comp: Component,
+ props: Props | null = null,
+ children: Slots | SSRSlots | null = null,
+ parentComponent: ComponentInternalInstance | null = null,
+ slotScopeId?: string,
+): SSRBuffer | Promise {
+ if (isSkip) {
+ // only render default slot without slot props
+ ssrRenderSlot(
+ children!,
+ 'default',
+ {},
+ null,
+ push,
+ parentComponent!,
+ slotScopeId,
+ )
+ return []
+ }
+ return renderComponentVNode(
+ createVNode(comp, props, children),
+ parentComponent,
+ slotScopeId,
+ )
+}
+
+export function ssrRenderSkipVNode(
+ isSkip: boolean,
+ push: PushFn,
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance,
+ slotScopeId?: string,
+): void {
+ if (isSkip) {
+ ssrRenderSlot(
+ vnode.children as Slots,
+ 'default',
+ {},
+ null,
+ push,
+ parentComponent,
+ slotScopeId,
+ )
+ } else {
+ ssrRenderVNode(push, vnode, parentComponent, slotScopeId)
+ }
+}
diff --git a/packages/server-renderer/src/internal.ts b/packages/server-renderer/src/internal.ts
index 3a2054066c3..7cdf2cdcd03 100644
--- a/packages/server-renderer/src/internal.ts
+++ b/packages/server-renderer/src/internal.ts
@@ -3,6 +3,10 @@ export { renderVNode as ssrRenderVNode } from './render'
export { ssrRenderComponent } from './helpers/ssrRenderComponent'
export { ssrRenderSlot, ssrRenderSlotInner } from './helpers/ssrRenderSlot'
export { ssrRenderTeleport } from './helpers/ssrRenderTeleport'
+export {
+ ssrRenderSkipComponent,
+ ssrRenderSkipVNode,
+} from './helpers/ssrRenderSkipComponent'
export {
ssrRenderClass,
ssrRenderStyle,
diff --git a/packages/shared/src/general.ts b/packages/shared/src/general.ts
index 9c6a2313240..ee8e98564db 100644
--- a/packages/shared/src/general.ts
+++ b/packages/shared/src/general.ts
@@ -90,7 +90,7 @@ export const isReservedProp: (key: string) => boolean = /*@__PURE__*/ makeMap(
export const isBuiltInDirective: (key: string) => boolean =
/*@__PURE__*/ makeMap(
- 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo',
+ 'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo,skip',
)
const cacheStringFunction = string>(fn: T): T => {
@@ -217,3 +217,17 @@ export function genCacheKey(source: string, options: any): string {
)
)
}
+
+export function clone(v: any): any {
+ if (isArray(v)) {
+ return v.map(clone)
+ } else if (isPlainObject(v)) {
+ const res: any = {}
+ for (const key in v) {
+ res[key] = clone(v[key as keyof typeof v])
+ }
+ return res
+ } else {
+ return v
+ }
+}