From f529dbde236e9eaedbded78e926951a189234f9c Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 23 Mar 2020 14:47:04 -0400 Subject: [PATCH] fix: dynamic component fallback to native element fix #870 --- .../transforms/transformElement.spec.ts | 16 ++++++++++--- .../src/transforms/transformElement.ts | 18 +++++--------- .../__tests__/ssrComponent.spec.ts | 6 ++--- .../__tests__/helpers/resolveAssets.spec.ts | 24 ++++++++++++++++--- packages/runtime-core/__tests__/vnode.spec.ts | 20 ++++++++++++---- .../runtime-core/src/helpers/resolveAssets.ts | 17 +++++++++---- packages/runtime-core/src/vnode.ts | 13 +++++++--- 7 files changed, 80 insertions(+), 34 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index 85800fef..43ba48b9 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -798,9 +798,18 @@ describe('compiler: element transform', () => { describe('dynamic component', () => { test('static binding', () => { const { node, root } = parseWithBind(``) - expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT) + expect(root.helpers).toContain(RESOLVE_DYNAMIC_COMPONENT) expect(node).toMatchObject({ - tag: '_component_foo' + tag: { + callee: RESOLVE_DYNAMIC_COMPONENT, + arguments: [ + { + type: NodeTypes.SIMPLE_EXPRESSION, + content: 'foo', + isStatic: true + } + ] + } }) }) @@ -813,7 +822,8 @@ describe('compiler: element transform', () => { arguments: [ { type: NodeTypes.SIMPLE_EXPRESSION, - content: 'foo' + content: 'foo', + isStatic: false } ] } diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 3123d8ec..155c1dcf 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -204,19 +204,13 @@ export function resolveComponentType( // 1. dynamic component const isProp = node.tag === 'component' && findProp(node, 'is') if (isProp) { - // static - if (isProp.type === NodeTypes.ATTRIBUTE) { - const isType = isProp.value && isProp.value.content - if (isType) { - context.helper(RESOLVE_COMPONENT) - context.components.add(isType) - return toValidAssetId(isType, `component`) - } - } - // dynamic - else if (isProp.exp) { + const exp = + isProp.type === NodeTypes.ATTRIBUTE + ? isProp.value && createSimpleExpression(isProp.value.content, true) + : isProp.exp + if (exp) { return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [ - isProp.exp + exp ]) } } diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 8abae35d..c0275dc9 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -20,13 +20,11 @@ describe('ssr: components', () => { test('dynamic component', () => { expect(compile(``).code) .toMatchInlineSnapshot(` - "const { resolveComponent: _resolveComponent, withCtx: _withCtx } = require(\\"vue\\") + "const { resolveDynamicComponent: _resolveDynamicComponent, withCtx: _withCtx } = require(\\"vue\\") const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent) { - const _component_foo = _resolveComponent(\\"foo\\") - - _push(_ssrRenderComponent(_component_foo, { prop: \\"b\\" }, null, _parent)) + _push(_ssrRenderComponent(_resolveDynamicComponent(\\"foo\\"), { prop: \\"b\\" }, null, _parent)) }" `) diff --git a/packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts b/packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts index 6cac9b24..0e56ba9f 100644 --- a/packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts +++ b/packages/runtime-core/__tests__/helpers/resolveAssets.spec.ts @@ -6,11 +6,14 @@ import { Component, Directive, resolveDynamicComponent, - h + h, + serializeInner } from '@vue/runtime-test' import { mockWarn } from '@vue/shared' describe('resolveAssets', () => { + mockWarn() + test('should work', () => { const FooBar = () => null const BarBaz = { mounted: () => null } @@ -63,8 +66,6 @@ describe('resolveAssets', () => { }) describe('warning', () => { - mockWarn() - test('used outside render() or setup()', () => { resolveComponent('foo') expect( @@ -128,5 +129,22 @@ describe('resolveAssets', () => { expect(bar).toBe(dynamicComponents.bar) expect(baz).toBe(dynamicComponents.baz) }) + + test('resolve dynamic component should fallback to plain element without warning', () => { + const Root = { + setup() { + return () => { + return h(resolveDynamicComponent('div') as string, null, { + default: () => 'hello' + }) + } + } + } + + const app = createApp(Root) + const root = nodeOps.createElement('div') + app.mount(root) + expect(serializeInner(root)).toBe('
hello
') + }) }) }) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index bb84aa49..7c11c2ab 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -106,7 +106,7 @@ describe('vnode', () => { const vnode = createVNode('p', null, ['foo']) expect(vnode.children).toMatchObject(['foo']) expect(vnode.shapeFlag).toBe( - ShapeFlags.ELEMENT + ShapeFlags.ARRAY_CHILDREN + ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN ) }) @@ -114,7 +114,7 @@ describe('vnode', () => { const vnode = createVNode('p', null, { foo: 'foo' }) expect(vnode.children).toMatchObject({ foo: 'foo' }) expect(vnode.shapeFlag).toBe( - ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN + ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN ) }) @@ -122,7 +122,7 @@ describe('vnode', () => { const vnode = createVNode('p', null, nop) expect(vnode.children).toMatchObject({ default: nop }) expect(vnode.shapeFlag).toBe( - ShapeFlags.ELEMENT + ShapeFlags.SLOTS_CHILDREN + ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN ) }) @@ -130,7 +130,19 @@ describe('vnode', () => { const vnode = createVNode('p', null, 'foo') expect(vnode.children).toBe('foo') expect(vnode.shapeFlag).toBe( - ShapeFlags.ELEMENT + ShapeFlags.TEXT_CHILDREN + ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN + ) + }) + + test('element with slots', () => { + const children = [createVNode('span', null, 'hello')] + const vnode = createVNode('div', null, { + default: () => children + }) + + expect(vnode.children).toBe(children) + expect(vnode.shapeFlag).toBe( + ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN ) }) }) diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index 9c3dee57..f0c57559 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -24,10 +24,14 @@ export function resolveComponent(name: string): Component | undefined { export function resolveDynamicComponent( component: unknown -): Component | undefined { +): Component | string | undefined { if (!component) return if (isString(component)) { - return resolveAsset(COMPONENTS, component, currentRenderingInstance) + return ( + resolveAsset(COMPONENTS, component, currentRenderingInstance, false) || + // fallback to plain element + component + ) } else if (isFunction(component) || isObject(component)) { return component } @@ -41,7 +45,8 @@ export function resolveDirective(name: string): Directive | undefined { function resolveAsset( type: typeof COMPONENTS, name: string, - instance?: ComponentInternalInstance | null + instance?: ComponentInternalInstance | null, + warnMissing?: boolean ): Component | undefined // overload 2: directives function resolveAsset( @@ -54,7 +59,8 @@ function resolveAsset( type: typeof COMPONENTS | typeof DIRECTIVES, name: string, instance: ComponentInternalInstance | null = currentRenderingInstance || - currentInstance + currentInstance, + warnMissing = true ) { if (instance) { let camelized, capitalized @@ -75,7 +81,8 @@ function resolveAsset( res = self } } - if (__DEV__ && !res) { + if (__DEV__ && warnMissing && !res) { + debugger warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`) } return res diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 29b1fb0d..78599e63 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -397,9 +397,16 @@ export function normalizeChildren(vnode: VNode, children: unknown) { } else if (isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN } else if (typeof children === 'object') { - type = ShapeFlags.SLOTS_CHILDREN - if (!(children as RawSlots)._) { - ;(children as RawSlots)._ctx = currentRenderingInstance + // in case resolves to native element, the vnode call + // will receive slots object. + if (vnode.shapeFlag & ShapeFlags.ELEMENT && (children as any).default) { + normalizeChildren(vnode, (children as any).default()) + return + } else { + type = ShapeFlags.SLOTS_CHILDREN + if (!(children as RawSlots)._) { + ;(children as RawSlots)._ctx = currentRenderingInstance + } } } else if (isFunction(children)) { children = { default: children, _ctx: currentRenderingInstance }