]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: analyze slots to find default slot when possible
authordaiwei <daiwei521@126.com>
Sun, 26 Jan 2025 01:56:03 +0000 (09:56 +0800)
committerdaiwei <daiwei521@126.com>
Sun, 26 Jan 2025 01:56:03 +0000 (09:56 +0800)
packages/compiler-core/__tests__/transforms/__snapshots__/vSkip.spec.ts.snap
packages/compiler-core/__tests__/transforms/vSkip.spec.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/transforms/vSkip.ts
packages/compiler-ssr/__tests__/ssrVSkip.spec.ts

index a975095208baefa845e04b2ce34f096fdeb9942b..5b8456bda9747d1ca0aafbed0ac0f4c81aaf0b94 100644 (file)
@@ -36,33 +36,216 @@ return function render(_ctx, _cache) {
 }"
 `;
 
-exports[`compiler: v-skip > transform > on component 1`] = `
+exports[`compiler: v-skip > transform > on Teleport 1`] = `
 "const _Vue = Vue
 
 return function render(_ctx, _cache) {
   with (_ctx) {
-    const { resolveComponent: _resolveComponent, resolveSkipComponent: _resolveSkipComponent, openBlock: _openBlock, createBlock: _createBlock } = _Vue
+    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, 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"]),
+      _: 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, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, Fragment: _Fragment, createBlock: _createBlock } = _Vue
 
     const _component_Comp = _resolveComponent("Comp")
 
-    return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _component_Comp)))
+    return (_ctx.ok)
+      ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [
+          (_ctx.yes)
+            ? (_openBlock(), _createElementBlock("span", { key: 0 }, "default"))
+            : _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 }, "default"))
+              : _createCommentVNode("v-if", true)
+          ]),
+          _: 1 /* STABLE */
+        }))
+  }
+}"
+`;
+
+exports[`compiler: v-skip > transform > on component with multiple implicit slot 1`] = `
+"const _Vue = Vue
+
+return function render(_ctx, _cache) {
+  with (_ctx) {
+    const { withCtx: _withCtx, createElementVNode: _createElementVNode, 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(() => ["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, 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(() => ["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 1`] = `
+exports[`compiler: v-skip > transform > on dynamic component with dynamic slot 1`] = `
 "const _Vue = Vue
 
 return function render(_ctx, _cache) {
   with (_ctx) {
-    const { renderSlot: _renderSlot, resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx, openBlock: _openBlock, createBlock: _createBlock, resolveSkipComponent: _resolveSkipComponent } = _Vue
+    const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, openBlock: _openBlock, createBlock: _createBlock, resolveSkipComponent: _resolveSkipComponent } = _Vue
 
     return (_openBlock(), _createBlock(_resolveSkipComponent(_ctx.ok, _resolveDynamicComponent(_ctx.Comp)), null, {
-      default: _withCtx(() => [
-        _renderSlot(_ctx.$slots, "default")
-      ]),
-      _: 3 /* FORWARDED */
-    }))
+      [_ctx.foo]: _withCtx(() => ["foo"]),
+      _: 2 /* DYNAMIC */
+    }, 1024 /* DYNAMIC_SLOTS */))
   }
 }"
 `;
index c9002fb87ff1e067440fd433772e2c91de0d0724..a9760b9855f78d274165288661c28d552c9adc93 100644 (file)
@@ -255,28 +255,25 @@ describe('compiler: v-skip', () => {
       expect(generate(root).code).toMatchSnapshot()
     })
 
-    test('on component', () => {
+    test('on component without slot', () => {
+      // equivalent to <Comp v-if="ok"/>
       const { root, node } = parseWithSkipTransform(`<Comp v-skip="ok"/>`) as {
         root: RootNode
-        node: ComponentNode
+        node: SkipNode
       }
-      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(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.todo('on component with default slot', () => {
+    test('on component with default slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">foo</Comp>`,
-      )
+      ) 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)
@@ -294,13 +291,13 @@ describe('compiler: v-skip', () => {
       expect(generate(root).code).toMatchSnapshot()
     })
 
-    test.todo('on component with multiple named slot', () => {
+    test('on component with multiple named slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">
           <template #default>default</template>
           <template #foo>foo</template>
         </Comp>`,
-      )
+      ) 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)
@@ -318,14 +315,14 @@ describe('compiler: v-skip', () => {
       expect(generate(root).code).toMatchSnapshot()
     })
 
-    test.todo('on component with multiple implicit slot', () => {
+    test('on component with multiple implicit slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">
           <span/>
           <template #foo>foo</template>
           <div/>
         </Comp>`,
-      )
+      ) 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)
@@ -349,57 +346,98 @@ describe('compiler: v-skip', () => {
       expect(generate(root).code).toMatchSnapshot()
     })
 
-    test.todo('on component with dynamic slot + default slot', () => {
+    test('on component with name default slot + v-if', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">
-          <template #[foo]>foo</template>
-          <template #default>default</template>
+          <template v-if="yes" #default>default</template>
         </Comp>`,
+      ) 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(
+        `<Comp v-skip="ok">
+          <span v-if="yes">default</span>
+        </Comp>`,
+      ) 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.todo('on component with name default slot + v-if', () => {
+    test('on component with dynamic slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">
-          <template v-if="yes" #default>default</template>
+          <template #[foo]>foo</template>
         </Comp>`,
+      ) 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(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(generate(root).code).toMatchSnapshot()
     })
 
-    test.todo('on component with implicit default slot + v-if', () => {
+    test('on component with dynamic slot + default slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<Comp v-skip="ok">
-          <span v-if="yes">default</span>
+          <template #[foo]>foo</template>
+          <template #default>default</template>
         </Comp>`,
+      ) 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(
+        `<component :is="Comp" v-skip="ok">foo</component>`,
+      ) 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', () => {
+    test('on dynamic component with dynamic slot', () => {
       const { root, node } = parseWithSkipTransform(
         `<component :is="Comp" v-skip="ok">
-          <slot/>
+          <template #[foo]>foo</template>
         </component>`,
       ) as { root: RootNode; node: ComponentNode }
       expect(node.type).toBe(NodeTypes.ELEMENT)
@@ -415,9 +453,18 @@ describe('compiler: v-skip', () => {
       expect(generate(root).code).toMatchSnapshot()
     })
 
-    test.todo('on Teleport', () => {})
-
-    test.todo('built-in components', () => {})
+    test('on Teleport', () => {
+      const { root, node } = parseWithSkipTransform(
+        `<teleport to="target" v-skip="ok"/>`,
+      ) 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', () => {
@@ -432,22 +479,35 @@ describe('compiler: v-skip', () => {
       ])
     })
 
+    test('on <template>', () => {
+      const onError = vi.fn()
+      parseWithSkipTransform(`<template v-skip="ok"/>`, { onError })
+      expect(onError.mock.calls[0]).toMatchObject([
+        {
+          code: ErrorCodes.X_V_SKIP_MISPLACED,
+        },
+      ])
+    })
+
     test('on <slot>', () => {
       const onError = vi.fn()
       parseWithSkipTransform(`<slot v-skip="ok"/>`, { onError })
       expect(onError.mock.calls[0]).toMatchObject([
         {
-          code: ErrorCodes.X_V_SKIP_ON_TEMPLATE,
+          code: ErrorCodes.X_V_SKIP_MISPLACED,
         },
       ])
     })
 
-    test('on <template>', () => {
+    test('on component without default slot', () => {
       const onError = vi.fn()
-      parseWithSkipTransform(`<template v-skip="ok"/>`, { onError })
+      parseWithSkipTransform(
+        `<Comp v-skip="ok"><template #foo>foo</template></Comp>`,
+        { onError },
+      )
       expect(onError.mock.calls[0]).toMatchObject([
         {
-          code: ErrorCodes.X_V_SKIP_ON_TEMPLATE,
+          code: ErrorCodes.X_V_SKIP_UNEXPECTED_SLOT,
         },
       ])
     })
index 683981143e2c5cdbf227f2cc03955c1cf20b4c4c..8bd6df968e3951ce2c5f968f0091b94214c9cb0c 100644 (file)
@@ -91,7 +91,7 @@ export enum ErrorCodes {
   X_INVALID_EXPRESSION,
   X_KEEP_ALIVE_INVALID_CHILDREN,
   X_V_SKIP_NO_EXPRESSION,
-  X_V_SKIP_ON_TEMPLATE,
+  X_V_SKIP_MISPLACED,
   X_V_SKIP_UNEXPECTED_SLOT,
   X_V_SKIP_WITH_V_FOR,
 
@@ -184,7 +184,7 @@ export const errorMessages: Record<ErrorCodes, string> = {
   [ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN]: `<KeepAlive> 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_ON_TEMPLATE]: `v-skip cannot be used on <template> or <slot> tags.`,
+  [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.`,
 
index fc877ba84b3aa3ce583e5415aa439eef773ebf7c..3495a00b104eb3c0b2f8ec2a3fb59cdf93acbd82 100644 (file)
@@ -7,6 +7,7 @@ import {
   NodeTypes,
   type SimpleExpressionNode,
   type SkipNode,
+  type TemplateChildNode,
   type VNodeCall,
   createCallExpression,
   createConditionalExpression,
@@ -22,10 +23,10 @@ import {
   ErrorCodes,
   RESOLVE_SKIP_COMPONENT,
   WITH_MEMO,
+  buildSlots,
   createCompilerError,
   findDir,
   findProp,
-  isSlotOutlet,
   processExpression,
 } from '@vue/compiler-core'
 import { createCodegenNodeForBranch } from './vIf'
@@ -38,10 +39,7 @@ export const transformSkip: NodeTransform = createStructuralDirectiveTransform(
     return processSkip(node, dir, context, (skipNode?: SkipNode) => {
       return () => {
         const codegenNode = node.codegenNode!
-        if (
-          node.tagType === ElementTypes.COMPONENT &&
-          node.tag !== 'Teleport'
-        ) {
+        if (!skipNode) {
           if (codegenNode.type === NodeTypes.VNODE_CALL) {
             codegenNode.tag = getVNodeTag(
               context,
@@ -85,15 +83,22 @@ export function processSkip(
 ): (() => void) | undefined {
   const loc = dir.exp ? dir.exp.loc : node.loc
   if (
-    (node.type === NodeTypes.ELEMENT && node.tag === 'template') ||
-    isSlotOutlet(node)
+    // v-skip is not allowed on <template> or <slot>
+    !(
+      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_ON_TEMPLATE, loc))
+    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()) {
@@ -109,12 +114,48 @@ export function processSkip(
     validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
   }
 
-  let skipNode: SkipNode | undefined
+  // 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 ||
-    (node.tagType === ElementTypes.COMPONENT && node.tag === 'Teleport')
+    (isComponent && node.tag === 'Teleport')
   ) {
-    const children = node.children
+    processAsSkipNode = true
+    children = node.children
+  } else if (isComponent) {
+    const { slots, hasDynamicSlots } = buildSlots(
+      node,
+      context,
+      undefined,
+      true,
+    )
+    // find default slot if not has dynamic slots
+    if (!hasDynamicSlots && slots.type === NodeTypes.JS_OBJECT_EXPRESSION) {
+      processAsSkipNode = true
+      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) {
+        children = prop.value.returns as TemplateChildNode[]
+      } 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
index 778a31c48fdcc14d128220334835937d4ed367c1..1a8aed40c22dd8d7983917019048f484821a077f 100644 (file)
@@ -195,13 +195,17 @@ describe('ssr: v-skip', () => {
 
   test('on component', () => {
     expect(compile(`<Comp v-skip="foo"/>`).code).toMatchInlineSnapshot(`
-      "const { resolveComponent: _resolveComponent } = require("vue")
+      "const { withCtx: _withCtx, createCommentVNode: _createCommentVNode, resolveComponent: _resolveComponent } = require("vue")
       const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSkipComponent: _ssrRenderSkipComponent } = require("vue/server-renderer")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         const _component_Comp = _resolveComponent("Comp")
 
-        _push(_ssrRenderSkipComponent(_push, _ctx.foo, _component_Comp, _attrs, null, _parent))
+        if (_ctx.foo) {
+          _createCommentVNode("v-skip", true)
+        } else {
+          _push(_ssrRenderSkipComponent(_push, _ctx.foo, _component_Comp, _attrs, null, _parent))
+        }
       }"
     `)
   })
@@ -324,22 +328,26 @@ describe('ssr: v-skip', () => {
       </component>`,
       ).code,
     ).toMatchInlineSnapshot(`
-      "const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx, renderSlot: _renderSlot, createVNode: _createVNode } = require("vue")
+      "const { withCtx: _withCtx, resolveDynamicComponent: _resolveDynamicComponent, renderSlot: _renderSlot, createVNode: _createVNode } = require("vue")
       const { ssrRenderSlot: _ssrRenderSlot, ssrRenderVNode: _ssrRenderVNode } = require("vue/server-renderer")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
-        _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.Comp), _attrs, {
-          default: _withCtx((_, _push, _parent, _scopeId) => {
-            if (_push) {
-              _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent, _scopeId)
-            } else {
-              return [
-                _renderSlot(_ctx.$slots, "default")
-              ]
-            }
-          }),
-          _: 3 /* FORWARDED */
-        }), _parent)
+        if (_ctx.ok) {
+          _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent, _scopeId)
+        } else {
+          _ssrRenderVNode(_push, _createVNode(_resolveDynamicComponent(_ctx.Comp), _attrs, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _ssrRenderSlot(_ctx.$slots, "default", {}, null, _push, _parent, _scopeId)
+              } else {
+                return [
+                  _renderSlot(_ctx.$slots, "default")
+                ]
+              }
+            }),
+            _: 3 /* FORWARDED */
+          }), _parent)
+        }
       }"
     `)
   })
@@ -351,25 +359,29 @@ describe('ssr: v-skip', () => {
       <Comp v-skip="ok"><span/></Comp>
     `).code,
     ).toMatchInlineSnapshot(`
-      "const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode } = require("vue")
+      "const { withCtx: _withCtx, resolveComponent: _resolveComponent, createVNode: _createVNode } = require("vue")
       const { ssrRenderComponent: _ssrRenderComponent, ssrRenderSkipComponent: _ssrRenderSkipComponent } = require("vue/server-renderer")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         const _component_Comp = _resolveComponent("Comp")
 
         _push(\`<!--[--><div></div>\`)
-        _push(_ssrRenderSkipComponent(_push, _ctx.ok, _component_Comp, null, {
-          default: _withCtx((_, _push, _parent, _scopeId) => {
-            if (_push) {
-              _push(\`<span\${_scopeId}></span>\`)
-            } else {
-              return [
-                _createVNode("span")
-              ]
-            }
-          }),
-          _: 1 /* STABLE */
-        }, _parent))
+        if (_ctx.ok) {
+          _push(\`<span></span>\`)
+        } else {
+          _push(_ssrRenderSkipComponent(_push, _ctx.ok, _component_Comp, null, {
+            default: _withCtx((_, _push, _parent, _scopeId) => {
+              if (_push) {
+                _push(\`<span\${_scopeId}></span>\`)
+              } else {
+                return [
+                  _createVNode("span")
+                ]
+              }
+            }),
+            _: 1 /* STABLE */
+          }, _parent))
+        }
         _push(\`<!--]-->\`)
       }"
     `)