diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index e71810fc958..cb1636fd17e 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -204,12 +204,15 @@ export function generate( } // enter render function - if (genScopeId && !ssr) { - push(`const render = ${PURE_ANNOTATION}_withId(`) - } if (!ssr) { + if (genScopeId) { + push(`const render = ${PURE_ANNOTATION}_withId(`) + } push(`function render(_ctx, _cache) {`) } else { + if (genScopeId) { + push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) + } push(`function ssrRender(_ctx, _push, _parent, _attrs) {`) } indent() @@ -272,7 +275,7 @@ export function generate( deindent() push(`}`) - if (genScopeId && !ssr) { + if (genScopeId) { push(`)`) } diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts index a2a6452a741..03755ab3fb0 100644 --- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts @@ -6,14 +6,17 @@ describe('ssr: scopeId', () => { test('basic', () => { expect( compile(`
hello
`, { - scopeId + scopeId, + mode: 'module' }).code ).toMatchInlineSnapshot(` - "const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\") + "import { withScopeId as _withScopeId } from \\"vue\\" + import { ssrRenderAttrs as _ssrRenderAttrs } from \\"@vue/server-renderer\\" + const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - return function ssrRender(_ctx, _push, _parent, _attrs) { + export const ssrRender = /*#__PURE__*/_withId(function ssrRender(_ctx, _push, _parent, _attrs) { _push(\`hello\`) - }" + })" `) }) @@ -21,17 +24,19 @@ describe('ssr: scopeId', () => { // should have no branching inside slot expect( compile(`foo`, { - scopeId + scopeId, + mode: 'module' }).code ).toMatchInlineSnapshot(` - "const { resolveComponent: _resolveComponent, withCtx: _withCtx, createTextVNode: _createTextVNode } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createTextVNode as _createTextVNode, withScopeId as _withScopeId } from \\"vue\\" + import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" + const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - return function ssrRender(_ctx, _push, _parent, _attrs) { + export const ssrRender = /*#__PURE__*/_withId(function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withCtx((_, _push, _parent, _scopeId) => { + default: _withId((_, _push, _parent, _scopeId) => { if (_push) { _push(\`foo\`) } else { @@ -42,24 +47,26 @@ describe('ssr: scopeId', () => { }), _: 1 }, _parent)) - }" + })" `) }) test('inside slots (with elements)', () => { expect( compile(`hello`, { - scopeId + scopeId, + mode: 'module' }).code ).toMatchInlineSnapshot(` - "const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" + const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - return function ssrRender(_ctx, _push, _parent, _attrs) { + export const ssrRender = /*#__PURE__*/_withId(function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withCtx((_, _push, _parent, _scopeId) => { + default: _withId((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) } else { @@ -70,29 +77,31 @@ describe('ssr: scopeId', () => { }), _: 1 }, _parent)) - }" + })" `) }) test('nested slots', () => { expect( compile(`hello`, { - scopeId + scopeId, + mode: 'module' }).code ).toMatchInlineSnapshot(` - "const { resolveComponent: _resolveComponent, withCtx: _withCtx, createVNode: _createVNode } = require(\\"vue\\") - const { ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" + const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - return function ssrRender(_ctx, _push, _parent, _attrs) { + export const ssrRender = /*#__PURE__*/_withId(function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") const _component_bar = _resolveComponent(\\"bar\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withCtx((_, _push, _parent, _scopeId) => { + default: _withId((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) _push(_ssrRenderComponent(_component_bar, null, { - default: _withCtx((_, _push, _parent, _scopeId) => { + default: _withId((_, _push, _parent, _scopeId) => { if (_push) { _push(\`\`) } else { @@ -107,7 +116,7 @@ describe('ssr: scopeId', () => { return [ _createVNode(\\"span\\", null, \\"hello\\"), _createVNode(_component_bar, null, { - default: _withCtx(() => [ + default: _withId(() => [ _createVNode(\\"span\\") ]), _: 1 @@ -117,7 +126,7 @@ describe('ssr: scopeId', () => { }), _: 1 }, _parent)) - }" + })" `) }) }) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 05813b9c93e..c158f4ad863 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -150,12 +150,14 @@ export function renderComponentRoot( // inherit scopeId const scopeId = vnode.scopeId - if (scopeId) { - root = cloneVNode(root, { [scopeId]: '' }) - } const treeOwnerId = parent && parent.type.__scopeId - if (treeOwnerId && treeOwnerId !== scopeId) { - root = cloneVNode(root, { [treeOwnerId + '-s']: '' }) + const slotScopeId = + treeOwnerId && treeOwnerId !== scopeId ? treeOwnerId + '-s' : null + if (scopeId || slotScopeId) { + const extras: Data = {} + if (scopeId) extras[scopeId] = '' + if (slotScopeId) extras[slotScopeId] = '' + root = cloneVNode(root, extras) } // inherit directives diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index ab210c621ac..f7985632fb3 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -12,7 +12,7 @@ import { } from 'vue' import { escapeHtml, mockWarn } from '@vue/shared' import { renderToString } from '../src/renderToString' -import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot' +import { ssrRenderSlot, SSRSlot } from '../src/helpers/ssrRenderSlot' import { ssrRenderComponent } from '../src/helpers/ssrRenderComponent' mockWarn() @@ -222,9 +222,9 @@ describe('ssr: renderToString', () => { { msg: 'hello' }, { // optimized slot using string push - default: ({ msg }: any, push: any, _p: any) => { + default: (({ msg }, push, _p) => { push(`${msg}`) - }, + }) as SSRSlot, // important to avoid slots being normalized _: 1 as any }, diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts new file mode 100644 index 00000000000..8b58fc66b93 --- /dev/null +++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts @@ -0,0 +1,62 @@ +import { createApp, withScopeId } from 'vue' +import { renderToString } from '../src/renderToString' +import { ssrRenderComponent, ssrRenderAttrs, ssrRenderSlot } from '../src' + +describe('ssr: scoped id on component root', () => { + test('basic', async () => { + const withParentId = withScopeId('parent') + + const Child = { + ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { + push(``) + } + } + + const Comp = { + ssrRender: withParentId((ctx: any, push: any, parent: any) => { + push(ssrRenderComponent(Child), null, null, parent) + }) + } + + const result = await renderToString(createApp(Comp)) + expect(result).toBe(`
`) + }) + + test('inside slot', async () => { + const withParentId = withScopeId('parent') + + const Child = { + ssrRender: (_: any, push: any, _parent: any, attrs: any) => { + push(``) + } + } + + const Wrapper = { + __scopeId: 'wrapper', + ssrRender: (ctx: any, push: any, parent: any) => { + ssrRenderSlot(ctx.$slots, 'default', {}, null, push, parent) + } + } + + const Comp = { + ssrRender: withParentId((_: any, push: any, parent: any) => { + push( + ssrRenderComponent( + Wrapper, + null, + { + default: withParentId((_: any, push: any, parent: any) => { + push(ssrRenderComponent(Child, null, null, parent)) + }), + _: 1 + } as any, + parent + ) + ) + }) + } + + const result = await renderToString(createApp(Comp)) + expect(result).toBe(`
`) + }) +}) diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 8c96322ab0f..6231b189525 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -1,4 +1,4 @@ -import { ComponentInternalInstance, Slot, Slots } from 'vue' +import { ComponentInternalInstance, Slots } from 'vue' import { Props, PushFn, renderVNodeChildren } from '../render' export type SSRSlots = Record @@ -21,13 +21,16 @@ export function ssrRenderSlot( push(``) const slotFn = slots[slotName] if (slotFn) { - if (slotFn.length > 1) { - // only ssr-optimized slot fns accept more than 1 arguments - const scopeId = parentComponent && parentComponent.type.__scopeId - slotFn(slotProps, push, parentComponent, scopeId ? ` ${scopeId}-s` : ``) - } else { + const scopeId = parentComponent && parentComponent.type.__scopeId + const ret = slotFn( + slotProps, + push, + parentComponent, + scopeId ? ` ${scopeId}-s` : `` + ) + if (Array.isArray(ret)) { // normal slot - renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent) + renderVNodeChildren(push, ret, parentComponent) } } else if (fallbackRenderFn) { fallbackRenderFn() diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index 1846bffcecc..55af4ff09bd 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -109,11 +109,23 @@ function renderComponentSubTree( if (comp.ssrRender) { // optimized + // resolve fallthrough attrs + let attrs = + instance.type.inheritAttrs !== false ? instance.attrs : undefined + + // inherited scopeId + const scopeId = instance.vnode.scopeId + const treeOwnerId = instance.parent && instance.parent.type.__scopeId + const slotScopeId = + treeOwnerId && treeOwnerId !== scopeId ? treeOwnerId + '-s' : null + if (scopeId || slotScopeId) { + attrs = { ...attrs } + if (scopeId) attrs[scopeId] = '' + if (slotScopeId) attrs[slotScopeId] = '' + } + // set current rendering instance for asset resolution setCurrentRenderingInstance(instance) - // fallthrough attrs - const attrs = - instance.type.inheritAttrs !== false ? instance.attrs : undefined comp.ssrRender(instance.proxy, push, instance, attrs) setCurrentRenderingInstance(null) } else if (instance.render) {