diff --git a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap index 7af32808b..4076e61db 100644 --- a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap @@ -1,51 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = ` -"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createBlock as _createBlock, setScopeId as _setScopeId } from \\"vue\\" -_pushScopeId(\\"test\\") +_setScopeId(\\"test\\") const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"hello\\", -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"world\\", -1 /* HOISTED */) -_popScopeId() +_setScopeId(null) -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { return (_openBlock(), _createBlock(\\"div\\", null, [ _hoisted_1, _createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */), _hoisted_2 ])) -})" +}" `; exports[`scopeId compiler support should wrap default slot 1`] = ` -"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, { - default: _withId(() => [ + default: _withCtx(() => [ _createVNode(\\"div\\") ]), _: 1 /* STABLE */ })) -})" +}" `; exports[`scopeId compiler support should wrap dynamic slots 1`] = ` -"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, renderList as _renderList, createSlots as _createSlots, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, _createSlots({ _: 2 /* DYNAMIC */ }, [ (_ctx.ok) ? { name: \\"foo\\", - fn: _withId(() => [ + fn: _withCtx(() => [ _createVNode(\\"div\\") ]) } @@ -53,39 +50,29 @@ export const render = /*#__PURE__*/_withId((_ctx, _cache) => { _renderList(_ctx.list, (i) => { return { name: i, - fn: _withId(() => [ + fn: _withCtx(() => [ _createVNode(\\"div\\") ]) } }) ]), 1024 /* DYNAMIC_SLOTS */)) -})" +}" `; exports[`scopeId compiler support should wrap named slots 1`] = ` -"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") +"import { toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, createVNode as _createVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\" -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { +export function render(_ctx, _cache) { const _component_Child = _resolveComponent(\\"Child\\") return (_openBlock(), _createBlock(_component_Child, null, { - foo: _withId(({ msg }) => [ + foo: _withCtx(({ msg }) => [ _createTextVNode(_toDisplayString(msg), 1 /* TEXT */) ]), - bar: _withId(() => [ + bar: _withCtx(() => [ _createVNode(\\"div\\") ]), _: 1 /* STABLE */ })) -})" -`; - -exports[`scopeId compiler support should wrap render function 1`] = ` -"import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, withScopeId as _withScopeId } from \\"vue\\" -const _withId = /*#__PURE__*/_withScopeId(\\"test\\") - -export const render = /*#__PURE__*/_withId((_ctx, _cache) => { - return (_openBlock(), _createBlock(\\"div\\")) -})" +}" `; diff --git a/packages/compiler-core/__tests__/scopeId.spec.ts b/packages/compiler-core/__tests__/scopeId.spec.ts index 710c65bff..3c1d6d554 100644 --- a/packages/compiler-core/__tests__/scopeId.spec.ts +++ b/packages/compiler-core/__tests__/scopeId.spec.ts @@ -1,12 +1,13 @@ import { baseCompile } from '../src/compile' -import { - WITH_SCOPE_ID, - PUSH_SCOPE_ID, - POP_SCOPE_ID -} from '../src/runtimeHelpers' +import { SET_SCOPE_ID } from '../src/runtimeHelpers' import { PatchFlags } from '@vue/shared' import { genFlagText } from './testUtils' +/** + * Ensure all slot functions are wrapped with _withCtx + * which sets the currentRenderingInstance and currentScopeId when rendering + * the slot. + */ describe('scopeId compiler support', () => { test('should only work in module mode', () => { expect(() => { @@ -14,25 +15,12 @@ describe('scopeId compiler support', () => { }).toThrow(`"scopeId" option is only supported in module mode`) }) - test('should wrap render function', () => { - const { ast, code } = baseCompile(`
`, { - mode: 'module', - scopeId: 'test' - }) - expect(ast.helpers).toContain(WITH_SCOPE_ID) - expect(code).toMatch(`const _withId = /*#__PURE__*/_withScopeId("test")`) - expect(code).toMatch( - `export const render = /*#__PURE__*/_withId((_ctx, _cache) => {` - ) - expect(code).toMatchSnapshot() - }) - test('should wrap default slot', () => { const { code } = baseCompile(`
`, { mode: 'module', scopeId: 'test' }) - expect(code).toMatch(`default: _withId(() => [`) + expect(code).toMatch(`default: _withCtx(() => [`) expect(code).toMatchSnapshot() }) @@ -48,8 +36,8 @@ describe('scopeId compiler support', () => { scopeId: 'test' } ) - expect(code).toMatch(`foo: _withId(({ msg }) => [`) - expect(code).toMatch(`bar: _withId(() => [`) + expect(code).toMatch(`foo: _withCtx(({ msg }) => [`) + expect(code).toMatch(`bar: _withCtx(() => [`) expect(code).toMatchSnapshot() }) @@ -65,8 +53,8 @@ describe('scopeId compiler support', () => { scopeId: 'test' } ) - expect(code).toMatch(/name: "foo",\s+fn: _withId\(/) - expect(code).toMatch(/name: i,\s+fn: _withId\(/) + expect(code).toMatch(/name: "foo",\s+fn: _withCtx\(/) + expect(code).toMatch(/name: i,\s+fn: _withCtx\(/) expect(code).toMatchSnapshot() }) @@ -79,19 +67,18 @@ describe('scopeId compiler support', () => { hoistStatic: true } ) - expect(ast.helpers).toContain(PUSH_SCOPE_ID) - expect(ast.helpers).toContain(POP_SCOPE_ID) + expect(ast.helpers).toContain(SET_SCOPE_ID) expect(ast.hoists.length).toBe(2) expect(code).toMatch( [ - `_pushScopeId("test")`, + `_setScopeId("test")`, `const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "hello", ${genFlagText( PatchFlags.HOISTED )})`, `const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "world", ${genFlagText( PatchFlags.HOISTED )})`, - `_popScopeId()` + `_setScopeId(null)` ].join('\n') ) expect(code).toMatchSnapshot() diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index 20f7e33ec..cd5723132 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -43,9 +43,7 @@ import { SET_BLOCK_TRACKING, CREATE_COMMENT, CREATE_TEXT, - PUSH_SCOPE_ID, - POP_SCOPE_ID, - WITH_SCOPE_ID, + SET_SCOPE_ID, WITH_DIRECTIVES, CREATE_BLOCK, OPEN_BLOCK, @@ -197,12 +195,11 @@ export function generate( indent, deindent, newline, - scopeId, ssr } = context + const hasHelpers = ast.helpers.length > 0 const useWithBlock = !prefixIdentifiers && mode !== 'module' - const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' const isSetupInlined = !__BROWSER__ && !!options.inline // preambles @@ -212,7 +209,7 @@ export function generate( ? createCodegenContext(ast, options) : context if (!__BROWSER__ && mode === 'module') { - genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined) + genModulePreamble(ast, preambleContext, isSetupInlined) } else { genFunctionPreamble(ast, preambleContext) } @@ -229,14 +226,7 @@ export function generate( ? args.map(arg => `${arg}: any`).join(',') : args.join(', ') - if (genScopeId) { - if (isSetupInlined) { - push(`${PURE_ANNOTATION}_withId(`) - } else { - push(`const ${functionName} = ${PURE_ANNOTATION}_withId(`) - } - } - if (isSetupInlined || genScopeId) { + if (isSetupInlined) { push(`(${signature}) => {`) } else { push(`function ${functionName}(${signature}) {`) @@ -301,10 +291,6 @@ export function generate( deindent() push(`}`) - if (genScopeId) { - push(`)`) - } - return { ast, code: context.code, @@ -375,23 +361,20 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) { function genModulePreamble( ast: RootNode, context: CodegenContext, - genScopeId: boolean, inline?: boolean ) { const { push, - helper, newline, - scopeId, optimizeImports, - runtimeModuleName + runtimeModuleName, + scopeId, + mode } = context - if (genScopeId) { - ast.helpers.push(WITH_SCOPE_ID) - if (ast.hoists.length) { - ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID) - } + const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' + if (genScopeId && ast.hoists.length) { + ast.helpers.push(SET_SCOPE_ID) } // generate import statements for helpers @@ -434,13 +417,6 @@ function genModulePreamble( newline() } - if (genScopeId) { - push( - `const _withId = ${PURE_ANNOTATION}${helper(WITH_SCOPE_ID)}("${scopeId}")` - ) - newline() - } - genHoists(ast.hoists, context) newline() @@ -480,7 +456,7 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { // push scope Id before initializing hoisted vnodes so that these vnodes // get the proper scopeId as well. if (genScopeId) { - push(`${helper(PUSH_SCOPE_ID)}("${scopeId}")`) + push(`${helper(SET_SCOPE_ID)}("${scopeId}")`) newline() } @@ -493,7 +469,7 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) { }) if (genScopeId) { - push(`${helper(POP_SCOPE_ID)}()`) + push(`${helper(SET_SCOPE_ID)}(null)`) newline() } context.pure = false @@ -817,15 +793,11 @@ function genFunctionExpression( node: FunctionExpression, context: CodegenContext ) { - const { push, indent, deindent, scopeId, mode } = context + const { push, indent, deindent } = context const { params, returns, body, newline, isSlot } = node - // slot functions also need to push scopeId before rendering its content - const genScopeId = - !__BROWSER__ && isSlot && scopeId != null && mode !== 'function' - if (genScopeId) { - push(`_withId(`) - } else if (isSlot) { + if (isSlot) { + // wrap slot functions with owner context push(`_${helperNameMap[WITH_CTX]}(`) } push(`(`, node) @@ -855,7 +827,7 @@ function genFunctionExpression( deindent() push(`}`) } - if (genScopeId || isSlot) { + if (isSlot) { push(`)`) } } diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index f40c94c3d..5172b8c7f 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -25,9 +25,7 @@ export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``) export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) -export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``) -export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) -export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``) +export const SET_SCOPE_ID = Symbol(__DEV__ ? `setScopeId` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const UNREF = Symbol(__DEV__ ? `unref` : ``) export const IS_REF = Symbol(__DEV__ ? `isRef` : ``) @@ -61,9 +59,7 @@ export const helperNameMap: any = { [CAPITALIZE]: `capitalize`, [TO_HANDLER_KEY]: `toHandlerKey`, [SET_BLOCK_TRACKING]: `setBlockTracking`, - [PUSH_SCOPE_ID]: `pushScopeId`, - [POP_SCOPE_ID]: `popScopeId`, - [WITH_SCOPE_ID]: `withScopeId`, + [SET_SCOPE_ID]: `setScopeId`, [WITH_CTX]: `withCtx`, [UNREF]: `unref`, [IS_REF]: `isRef` diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts index 5d7951c1f..954a7d44f 100644 --- a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts @@ -10,13 +10,11 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { withScopeId as _withScopeId } from \\"vue\\" - import { ssrRenderAttrs as _ssrRenderAttrs } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") + "import { ssrRenderAttrs as _ssrRenderAttrs } from \\"@vue/server-renderer\\" - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { _push(\`hello
\`) - })" + }" `) }) @@ -28,15 +26,14 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createTextVNode as _createTextVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createTextVNode as _createTextVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`foo\`) } else { @@ -47,7 +44,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) @@ -58,15 +55,14 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) } else { @@ -77,7 +73,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) @@ -88,20 +84,19 @@ describe('ssr: scopeId', () => { mode: 'module' }).code ).toMatchInlineSnapshot(` - "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode, withScopeId as _withScopeId } from \\"vue\\" + "import { resolveComponent as _resolveComponent, withCtx as _withCtx, createVNode as _createVNode } from \\"vue\\" import { ssrRenderComponent as _ssrRenderComponent } from \\"@vue/server-renderer\\" - const _withId = /*#__PURE__*/_withScopeId(\\"data-v-xxxxxxx\\") - export const ssrRender = /*#__PURE__*/_withId((_ctx, _push, _parent, _attrs) => { + export function ssrRender(_ctx, _push, _parent, _attrs) { const _component_foo = _resolveComponent(\\"foo\\") const _component_bar = _resolveComponent(\\"bar\\") _push(_ssrRenderComponent(_component_foo, _attrs, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`hello\`) _push(_ssrRenderComponent(_component_bar, null, { - default: _withId((_, _push, _parent, _scopeId) => { + default: _withCtx((_, _push, _parent, _scopeId) => { if (_push) { _push(\`\`) } else { @@ -111,12 +106,12 @@ describe('ssr: scopeId', () => { } }), _: 1 /* STABLE */ - }, _parent)) + }, _parent, _scopeId)) } else { return [ _createVNode(\\"span\\", null, \\"hello\\"), _createVNode(_component_bar, null, { - default: _withId(() => [ + default: _withCtx(() => [ _createVNode(\\"span\\") ]), _: 1 /* STABLE */ @@ -126,7 +121,7 @@ describe('ssr: scopeId', () => { }), _: 1 /* STABLE */ }, _parent)) - })" + }" `) }) }) diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts index 668a80e87..2219ff077 100644 --- a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts @@ -6,7 +6,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null) }" `) }) @@ -16,7 +16,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent, null) }" `) }) @@ -26,7 +26,7 @@ describe('ssr: ', () => { "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") return function ssrRender(_ctx, _push, _parent, _attrs) { - _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent) + _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent, null) }" `) }) @@ -40,7 +40,7 @@ describe('ssr: ', () => { _ssrRenderSlot(_ctx.$slots, \\"foo\\", { p: 1, bar: \\"2\\" - }, null, _push, _parent) + }, null, _push, _parent, null) }" `) }) @@ -53,7 +53,49 @@ describe('ssr: ', () => { return function ssrRender(_ctx, _push, _parent, _attrs) { _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => { _push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`) - }, _push, _parent) + }, _push, _parent, null) + }" + `) + }) + + test('with scopeId', async () => { + expect( + compile(``, { + scopeId: 'hello' + }).code + ).toMatchInlineSnapshot(` + "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\") + }" + `) + }) + + test('with forwarded scopeId', async () => { + expect( + compile(``, { + scopeId: 'hello' + }).code + ).toMatchInlineSnapshot(` + "const { resolveComponent: _resolveComponent, withCtx: _withCtx, renderSlot: _renderSlot } = require(\\"vue\\") + const { ssrRenderSlot: _ssrRenderSlot, ssrRenderComponent: _ssrRenderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + const _component_Comp = _resolveComponent(\\"Comp\\") + + _push(_ssrRenderComponent(_component_Comp, _attrs, { + default: _withCtx((_, _push, _parent, _scopeId) => { + if (_push) { + _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId) + } else { + return [ + _renderSlot(_ctx.$slots, \\"default\\") + ] + } + }), + _: 3 /* FORWARDED */ + }, _parent)) }" `) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 5acf8000b..de96b7efc 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -203,6 +203,12 @@ export function ssrProcessComponent( vnodeBranch ) } + + // component is inside a slot, inherit slot scope Id + if (context.withSlotScopeId) { + node.ssrCodegenNode!.arguments.push(`_scopeId`) + } + if (typeof component === 'string') { // static component context.pushStatement( diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts index b40d17ab4..b2b2de4f5 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts @@ -21,9 +21,11 @@ export const ssrTransformSlotOutlet: NodeTransform = (node, context) => { `_ctx.$slots`, slotName, slotProps || `{}`, - `null`, // fallback content placeholder. + // fallback content placeholder. will be replaced in the process phase + `null`, `_push`, - `_parent` + `_parent`, + context.scopeId ? `"${context.scopeId}-s"` : `null` ] ) } @@ -34,6 +36,7 @@ export function ssrProcessSlotOutlet( context: SSRTransformContext ) { const renderCall = node.ssrCodegenNode! + // has fallback content if (node.children.length) { const fallbackRenderFn = createFunctionExpression([]) @@ -41,5 +44,13 @@ export function ssrProcessSlotOutlet( // _renderSlot(slots, name, props, fallback, ...) renderCall.arguments[3] = fallbackRenderFn } + + // Forwarded . Add slot scope id + if (context.withSlotScopeId) { + const scopeId = renderCall.arguments[6] as string + renderCall.arguments[6] = + scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId` + } + context.pushStatement(node.ssrCodegenNode!) } diff --git a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts index 0b3e7f593..1cc7fe01e 100644 --- a/packages/runtime-core/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-core/__tests__/components/KeepAlive.spec.ts @@ -14,8 +14,7 @@ import { ComponentPublicInstance, Ref, cloneVNode, - provide, - withScopeId + provide } from '@vue/runtime-test' import { KeepAliveProps } from '../../src/components/KeepAlive' @@ -804,14 +803,13 @@ describe('KeepAlive', () => { test('should work with cloned root due to scopeId / fallthrough attrs', async () => { const viewRef = ref('one') const instanceRef = ref(null) - const withId = withScopeId('foo') const App = { __scopeId: 'foo', - render: withId(() => { + render: () => { return h(KeepAlive, null, { default: () => h(views[viewRef.value], { ref: instanceRef }) }) - }) + } } render(h(App), root) expect(serializeInner(root)).toBe(`
one
`) diff --git a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts index 99f2292b7..0cc8f3bab 100644 --- a/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts +++ b/packages/runtime-core/__tests__/helpers/renderSlot.spec.ts @@ -8,7 +8,7 @@ import { Fragment, createCommentVNode } from '../../src' -import { PatchFlags } from '@vue/shared/src' +import { PatchFlags } from '@vue/shared' describe('renderSlot', () => { it('should render slot', () => { @@ -37,7 +37,7 @@ describe('renderSlot', () => { return [createVNode('div', null, 'foo', PatchFlags.TEXT)] }, // mock instance - {} as any + { type: {} } as any ) // manual invocation should not track diff --git a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts deleted file mode 100644 index f570c7f0c..000000000 --- a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { withScopeId } from '../../src/helpers/scopeId' -import { h, render, nodeOps, serializeInner } from '@vue/runtime-test' - -describe('scopeId runtime support', () => { - const withParentId = withScopeId('parent') - const withChildId = withScopeId('child') - - test('should attach scopeId', () => { - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h('div', [h('div')]) - }) - } - const root = nodeOps.createElement('div') - render(h(App), root) - expect(serializeInner(root)).toBe(`
`) - }) - - test('should attach scopeId to components in parent component', () => { - const Child = { - __scopeId: 'child', - render: withChildId(() => { - return h('div') - }) - } - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h('div', [h(Child)]) - }) - } - - const root = nodeOps.createElement('div') - render(h(App), root) - expect(serializeInner(root)).toBe( - `
` - ) - }) - - test('should work on slots', () => { - const Child = { - __scopeId: 'child', - render: withChildId(function(this: any) { - return h('div', this.$slots.default()) - }) - } - const withChild2Id = withScopeId('child2') - const Child2 = { - __scopeId: 'child2', - render: withChild2Id(() => h('span')) - } - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h( - Child, - withParentId(() => { - return [h('div'), h(Child2)] - }) - ) - }) - } - const root = nodeOps.createElement('div') - render(h(App), root) - // slot content should have: - // - scopeId from parent - // - slotted scopeId (with `-s` postfix) from child (the tree owner) - expect(serializeInner(root)).toBe( - `
` + - `
` + - // component inside slot should have: - // - scopeId from template context - // - slotted scopeId from slot owner - // - its own scopeId - `` + - `
` - ) - }) - - // #1988 - test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => { - const withParentId = withScopeId('parent') - const App = { - __scopeId: 'parent', - render: withParentId(() => { - return h(Child) - }) - } - - function Child() { - return h(Child2, { class: 'foo' }) - } - - function Child2() { - return h('div') - } - Child2.inheritAttrs = false - - const root = nodeOps.createElement('div') - render(h(App), root) - - expect(serializeInner(root)).toBe(`
`) - }) -}) diff --git a/packages/runtime-core/__tests__/scopeId.spec.ts b/packages/runtime-core/__tests__/scopeId.spec.ts new file mode 100644 index 000000000..e81af6c56 --- /dev/null +++ b/packages/runtime-core/__tests__/scopeId.spec.ts @@ -0,0 +1,178 @@ +import { + h, + render, + nodeOps, + serializeInner, + renderSlot +} from '@vue/runtime-test' +import { setScopeId, withCtx } from '../src/componentRenderContext' + +describe('scopeId runtime support', () => { + test('should attach scopeId', () => { + const App = { + __scopeId: 'parent', + render: () => h('div', [h('div')]) + } + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe(`
`) + }) + + test('should attach scopeId to components in parent component', () => { + const Child = { + __scopeId: 'child', + render: () => h('div') + } + const App = { + __scopeId: 'parent', + render: () => h('div', [h(Child)]) + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe( + `
` + ) + }) + + // :slotted basic + test('should work on slots', () => { + const Child = { + __scopeId: 'child', + render(this: any) { + return h('div', renderSlot(this.$slots, 'default')) + } + } + const Child2 = { + __scopeId: 'child2', + render: () => h('span') + } + const App = { + __scopeId: 'parent', + render: () => { + return h( + Child, + withCtx(() => { + return [h('div'), h(Child2)] + }) + ) + } + } + const root = nodeOps.createElement('div') + render(h(App), root) + // slot content should have: + // - scopeId from parent + // - slotted scopeId (with `-s` postfix) from child (the tree owner) + expect(serializeInner(root)).toBe( + `
` + + `
` + + // component inside slot should have: + // - scopeId from template context + // - slotted scopeId from slot owner + // - its own scopeId + `` + + `
` + ) + }) + + // #2892 + test(':slotted on forwarded slots', async () => { + const Wrapper = { + __scopeId: 'wrapper', + render(this: any) { + //
+ return h('div', { class: 'wrapper' }, [ + renderSlot(this.$slots, 'default') + ]) + } + } + + const Slotted = { + __scopeId: 'slotted', + render(this: any) { + // + return h(Wrapper, null, { + default: withCtx(() => [renderSlot(this.$slots, 'default')]) + }) + } + } + + // simulate hoisted node + setScopeId('root') + const hoisted = h('div', 'hoisted') + setScopeId(null) + + const Root = { + __scopeId: 'root', + render(this: any) { + //
hoisted
{{ dynamic }}
+ return h(Slotted, null, { + default: withCtx(() => { + return [hoisted, h('div', 'dynamic')] + }) + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Root), root) + expect(serializeInner(root)).toBe( + `
` + + `
hoisted
` + + `
dynamic
` + + `
` + ) + + const Root2 = { + __scopeId: 'root', + render(this: any) { + // + // + //
hoisted
{{ dynamic }}
+ //
+ //
+ return h(Slotted, null, { + default: withCtx(() => [ + h(Wrapper, null, { + default: withCtx(() => [hoisted, h('div', 'dynamic')]) + }) + ]) + }) + } + } + const root2 = nodeOps.createElement('div') + render(h(Root2), root2) + expect(serializeInner(root2)).toBe( + `
` + + `
` + + `
hoisted
` + + `
dynamic
` + + `
` + + `
` + ) + }) + + // #1988 + test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => { + const App = { + __scopeId: 'parent', + render: () => { + return h(Child) + } + } + + function Child() { + return h(Child2, { class: 'foo' }) + } + + function Child2() { + return h('div') + } + Child2.inheritAttrs = false + + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serializeInner(root)).toBe(`
`) + }) +}) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index 56f1e7b43..b5a501053 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -14,7 +14,7 @@ import { Data } from '../src/component' import { ShapeFlags, PatchFlags } from '@vue/shared' import { h, reactive, isReactive, setBlockTracking } from '../src' import { createApp, nodeOps, serializeInner } from '@vue/runtime-test' -import { setCurrentRenderingInstance } from '../src/componentRenderUtils' +import { setCurrentRenderingInstance } from '../src/componentRenderContext' describe('vnode', () => { test('create with just tag', () => { @@ -231,8 +231,8 @@ describe('vnode', () => { // ref normalizes to [currentRenderingInstance, ref] test('cloneVNode ref normalization', () => { - const mockInstance1 = {} as any - const mockInstance2 = {} as any + const mockInstance1 = { type: {} } as any + const mockInstance2 = { type: {} } as any setCurrentRenderingInstance(mockInstance1) const original = createVNode('div', { ref: 'foo' }) @@ -272,8 +272,8 @@ describe('vnode', () => { }) test('cloneVNode ref merging', () => { - const mockInstance1 = {} as any - const mockInstance2 = {} as any + const mockInstance1 = { type: {} } as any + const mockInstance2 = { type: {} } as any setCurrentRenderingInstance(mockInstance1) const original = createVNode('div', { ref: 'foo' }) diff --git a/packages/runtime-core/src/apiInject.ts b/packages/runtime-core/src/apiInject.ts index 402113cd1..a1ec6126b 100644 --- a/packages/runtime-core/src/apiInject.ts +++ b/packages/runtime-core/src/apiInject.ts @@ -1,6 +1,6 @@ import { isFunction } from '@vue/shared' import { currentInstance } from './component' -import { currentRenderingInstance } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' export interface InjectionKey extends Symbol {} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9b1420362..bfb7736b4 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -51,10 +51,8 @@ import { } from '@vue/shared' import { SuspenseBoundary } from './components/Suspense' import { CompilerOptions } from '@vue/compiler-core' -import { - currentRenderingInstance, - markAttrsAccessed -} from './componentRenderUtils' +import { markAttrsAccessed } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { startMeasure, endMeasure } from './profiling' export type Data = Record diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index c493fea0b..bbdb03360 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -35,10 +35,8 @@ import { } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { Slots } from './componentSlots' -import { - currentRenderingInstance, - markAttrsAccessed -} from './componentRenderUtils' +import { markAttrsAccessed } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' import { UnionToIntersection } from './helpers/typeUtils' diff --git a/packages/runtime-core/src/componentRenderContext.ts b/packages/runtime-core/src/componentRenderContext.ts new file mode 100644 index 000000000..78297dc5a --- /dev/null +++ b/packages/runtime-core/src/componentRenderContext.ts @@ -0,0 +1,57 @@ +import { ComponentInternalInstance } from './component' +import { isRenderingCompiledSlot } from './helpers/renderSlot' +import { closeBlock, openBlock } from './vnode' + +/** + * mark the current rendering instance for asset resolution (e.g. + * resolveComponent, resolveDirective) during render + */ +export let currentRenderingInstance: ComponentInternalInstance | null = null +export let currentScopeId: string | null = null + +export function setCurrentRenderingInstance( + instance: ComponentInternalInstance | null +) { + currentRenderingInstance = instance + currentScopeId = (instance && instance.type.__scopeId) || null +} + +/** + * Set scope id when creating hoisted vnodes. + * @private compiler helper + */ +export function setScopeId(id: string | null) { + currentScopeId = id +} + +/** + * Wrap a slot function to memoize current rendering instance + * @private compiler helper + */ +export function withCtx( + fn: Function, + ctx: ComponentInternalInstance | null = currentRenderingInstance +) { + if (!ctx) return fn + const renderFnWithContext = (...args: any[]) => { + // If a user calls a compiled slot inside a template expression (#1745), it + // can mess up block tracking, so by default we need to push a null block to + // avoid that. This isn't necessary if rendering a compiled ``. + if (!isRenderingCompiledSlot) { + openBlock(true /* null block that disables tracking */) + } + const prevInstance = currentRenderingInstance + setCurrentRenderingInstance(ctx) + const res = fn(...args) + setCurrentRenderingInstance(prevInstance) + if (!isRenderingCompiledSlot) { + closeBlock() + } + return res + } + // mark this as a compiled slot function. + // this is used in vnode.ts -> normalizeChildren() to set the slot + // rendering flag. + renderFnWithContext._c = true + return renderFnWithContext +} diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index c1454e96c..89cdcf19c 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -18,18 +18,7 @@ import { warn } from './warning' import { isHmrUpdating } from './hmr' import { NormalizedProps } from './componentProps' import { isEmitListener } from './componentEmits' - -/** - * mark the current rendering instance for asset resolution (e.g. - * resolveComponent, resolveDirective) during render - */ -export let currentRenderingInstance: ComponentInternalInstance | null = null - -export function setCurrentRenderingInstance( - instance: ComponentInternalInstance | null -) { - currentRenderingInstance = instance -} +import { setCurrentRenderingInstance } from './componentRenderContext' /** * dev only flag to track whether $attrs was used during render. @@ -63,7 +52,7 @@ export function renderComponentRoot( } = instance let result - currentRenderingInstance = instance + setCurrentRenderingInstance(instance) if (__DEV__) { accessedAttrs = false } @@ -215,8 +204,8 @@ export function renderComponentRoot( handleError(err, instance, ErrorCodes.RENDER_FUNCTION) result = createVNode(Comment) } - currentRenderingInstance = null + setCurrentRenderingInstance(null) return result } diff --git a/packages/runtime-core/src/componentSlots.ts b/packages/runtime-core/src/componentSlots.ts index 4a6a1b753..fbb009697 100644 --- a/packages/runtime-core/src/componentSlots.ts +++ b/packages/runtime-core/src/componentSlots.ts @@ -17,7 +17,7 @@ import { } from '@vue/shared' import { warn } from './warning' import { isKeepAlive } from './components/KeepAlive' -import { withCtx } from './helpers/withRenderContext' +import { withCtx } from './componentRenderContext' import { isHmrUpdating } from './hmr' export type Slot = (...args: any[]) => VNode[] diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index 6ad53fda7..b6d04cbba 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -112,6 +112,7 @@ const KeepAliveImpl = { instance, parentSuspense, isSVG, + vnode.slotScopeIds, optimized ) queuePostRenderEffect(() => { diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 1b5adfed3..0ec78125b 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -46,6 +46,7 @@ export const SuspenseImpl = { parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, // platform-specific impl passed from renderer rendererInternals: RendererInternals @@ -58,6 +59,7 @@ export const SuspenseImpl = { parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, rendererInternals ) @@ -69,6 +71,8 @@ export const SuspenseImpl = { anchor, parentComponent, isSVG, + slotScopeIds, + optimized, rendererInternals ) } @@ -92,6 +96,7 @@ function mountSuspense( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals ) { @@ -108,6 +113,7 @@ function mountSuspense( hiddenContainer, anchor, isSVG, + slotScopeIds, optimized, rendererInternals )) @@ -120,7 +126,8 @@ function mountSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds ) // now check if we have encountered any async deps if (suspense.deps > 0) { @@ -133,7 +140,8 @@ function mountSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds ) setActiveBranch(suspense, vnode.ssFallback!) } else { @@ -149,6 +157,8 @@ function patchSuspense( anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, isSVG: boolean, + slotScopeIds: string[] | null, + optimized: boolean, { p: patch, um: unmount, o: { createElement } }: RendererInternals ) { const suspense = (n2.suspense = n1.suspense)! @@ -169,7 +179,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -181,7 +193,9 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newFallback) } @@ -214,7 +228,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -226,7 +242,9 @@ function patchSuspense( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newFallback) } @@ -239,7 +257,9 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) // force resolve suspense.resolve(true) @@ -252,7 +272,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { suspense.resolve() @@ -269,7 +291,9 @@ function patchSuspense( anchor, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, newBranch) } else { @@ -289,7 +313,9 @@ function patchSuspense( null, parentComponent, suspense, - isSVG + isSVG, + slotScopeIds, + optimized ) if (suspense.deps <= 0) { // incoming branch has no async deps, resolve now. @@ -352,6 +378,7 @@ function createSuspenseBoundary( hiddenContainer: RendererElement, anchor: RendererNode | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, isHydrating = false @@ -507,7 +534,9 @@ function createSuspenseBoundary( anchor, parentComponent, null, // fallback tree will not have suspense context - isSVG + isSVG, + slotScopeIds, + optimized ) setActiveBranch(suspense, fallbackVNode) } @@ -632,6 +661,7 @@ function hydrateSuspense( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, rendererInternals: RendererInternals, hydrateNode: ( @@ -639,6 +669,7 @@ function hydrateSuspense( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => Node | null ): Node | null { @@ -651,6 +682,7 @@ function hydrateSuspense( document.createElement('div'), null, isSVG, + slotScopeIds, optimized, rendererInternals, true /* hydrating */ @@ -666,6 +698,7 @@ function hydrateSuspense( (suspense.pendingBranch = vnode.ssContent!), parentComponent, suspense, + slotScopeIds, optimized ) if (suspense.deps === 0) { diff --git a/packages/runtime-core/src/components/Teleport.ts b/packages/runtime-core/src/components/Teleport.ts index fa61c636a..e75455f96 100644 --- a/packages/runtime-core/src/components/Teleport.ts +++ b/packages/runtime-core/src/components/Teleport.ts @@ -71,6 +71,7 @@ export const TeleportImpl = { parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, internals: RendererInternals ) { @@ -115,6 +116,7 @@ export const TeleportImpl = { parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -144,7 +146,8 @@ export const TeleportImpl = { currentContainer, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) // even in block tree mode we need to make sure all root-level nodes // in the teleport inherit previous DOM references so that they can @@ -158,7 +161,9 @@ export const TeleportImpl = { currentAnchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + false ) } @@ -283,6 +288,7 @@ function hydrateTeleport( vnode: TeleportVNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean, { o: { nextSibling, parentNode, querySelector } @@ -293,6 +299,7 @@ function hydrateTeleport( container: Element, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => Node | null ): Node | null { @@ -313,6 +320,7 @@ function hydrateTeleport( parentNode(node)!, parentComponent, parentSuspense, + slotScopeIds, optimized ) vnode.targetAnchor = targetNode @@ -324,6 +332,7 @@ function hydrateTeleport( target, parentComponent, parentSuspense, + slotScopeIds, optimized ) } diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index c909e9a2e..20f25d03e 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -15,7 +15,7 @@ import { VNode } from './vnode' import { isFunction, EMPTY_OBJ, makeMap } from '@vue/shared' import { warn } from './warning' import { ComponentInternalInstance, Data } from './component' -import { currentRenderingInstance } from './componentRenderUtils' +import { currentRenderingInstance } from './componentRenderContext' import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling' import { ComponentPublicInstance } from './componentPublicInstance' diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 420c4ffc1..56cdd3dcd 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -53,6 +53,10 @@ export function renderSlot( ? PatchFlags.STABLE_FRAGMENT : PatchFlags.BAIL ) + // TODO (optimization) only add slot scope id if :slotted is used + if (rendered.scopeId) { + rendered.slotScopeIds = [rendered.scopeId + '-s'] + } isRenderingCompiledSlot-- return rendered } diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts index 986079a95..1d6a96bf9 100644 --- a/packages/runtime-core/src/helpers/resolveAssets.ts +++ b/packages/runtime-core/src/helpers/resolveAssets.ts @@ -1,10 +1,10 @@ -import { currentRenderingInstance } from '../componentRenderUtils' import { currentInstance, ConcreteComponent, ComponentOptions, getComponentName } from '../component' +import { currentRenderingInstance } from '../componentRenderContext' import { Directive } from '../directives' import { camelize, capitalize, isString } from '@vue/shared' import { warn } from '../warning' diff --git a/packages/runtime-core/src/helpers/scopeId.ts b/packages/runtime-core/src/helpers/scopeId.ts deleted file mode 100644 index fbefe04a4..000000000 --- a/packages/runtime-core/src/helpers/scopeId.ts +++ /dev/null @@ -1,36 +0,0 @@ -// SFC scoped style ID management. -// These are only used in esm-bundler builds, but since exports cannot be -// conditional, we can only drop inner implementations in non-bundler builds. - -import { withCtx } from './withRenderContext' - -export let currentScopeId: string | null = null -const scopeIdStack: string[] = [] - -/** - * @private - */ -export function pushScopeId(id: string) { - scopeIdStack.push((currentScopeId = id)) -} - -/** - * @private - */ -export function popScopeId() { - scopeIdStack.pop() - currentScopeId = scopeIdStack[scopeIdStack.length - 1] || null -} - -/** - * @private - */ -export function withScopeId(id: string): (fn: T) => T { - return ((fn: Function) => - withCtx(function(this: any) { - pushScopeId(id) - const res = fn.apply(this, arguments) - popScopeId() - return res - })) as any -} diff --git a/packages/runtime-core/src/helpers/withRenderContext.ts b/packages/runtime-core/src/helpers/withRenderContext.ts deleted file mode 100644 index 88a29ae32..000000000 --- a/packages/runtime-core/src/helpers/withRenderContext.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Slot } from '../componentSlots' -import { - setCurrentRenderingInstance, - currentRenderingInstance -} from '../componentRenderUtils' -import { ComponentInternalInstance } from '../component' -import { isRenderingCompiledSlot } from './renderSlot' -import { closeBlock, openBlock } from '../vnode' - -/** - * Wrap a slot function to memoize current rendering instance - * @private - */ -export function withCtx( - fn: Slot, - ctx: ComponentInternalInstance | null = currentRenderingInstance -) { - if (!ctx) return fn - const renderFnWithContext = (...args: any[]) => { - // If a user calls a compiled slot inside a template expression (#1745), it - // can mess up block tracking, so by default we need to push a null block to - // avoid that. This isn't necessary if rendering a compiled ``. - if (!isRenderingCompiledSlot) { - openBlock(true /* null block that disables tracking */) - } - const owner = currentRenderingInstance - setCurrentRenderingInstance(ctx) - const res = fn(...args) - setCurrentRenderingInstance(owner) - if (!isRenderingCompiledSlot) { - closeBlock() - } - return res - } - renderFnWithContext._c = true - return renderFnWithContext -} diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index c7c88eb65..cad23524e 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -63,7 +63,7 @@ export function createHydrationFunctions( return } hasMismatch = false - hydrateNode(container.firstChild!, vnode, null, null) + hydrateNode(container.firstChild!, vnode, null, null, null) flushPostFlushCbs() if (hasMismatch && !__TEST__) { // this error should show up in production @@ -76,6 +76,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized = false ): Node | null => { const isFragmentStart = isComment(node) && node.data === '[' @@ -85,6 +86,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, isFragmentStart ) @@ -147,6 +149,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } @@ -164,6 +167,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } @@ -171,6 +175,7 @@ export function createHydrationFunctions( // when setting up the render effect, if the initial vnode already // has .el set, the component will perform hydration instead of mount // on its sub-tree. + vnode.slotScopeIds = slotScopeIds const container = parentNode(node)! const hydrateComponent = () => { mountComponent( @@ -205,6 +210,7 @@ export function createHydrationFunctions( vnode as TeleportVNode, parentComponent, parentSuspense, + slotScopeIds, optimized, rendererInternals, hydrateChildren @@ -217,6 +223,7 @@ export function createHydrationFunctions( parentComponent, parentSuspense, isSVGContainer(parentNode(node)!), + slotScopeIds, optimized, rendererInternals, hydrateNode @@ -238,6 +245,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => { optimized = optimized || !!vnode.dynamicChildren @@ -291,6 +299,7 @@ export function createHydrationFunctions( el, parentComponent, parentSuspense, + slotScopeIds, optimized ) let hasWarned = false @@ -330,6 +339,7 @@ export function createHydrationFunctions( container: Element, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ): Node | null => { optimized = optimized || !!parentVNode.dynamicChildren @@ -346,6 +356,7 @@ export function createHydrationFunctions( vnode, parentComponent, parentSuspense, + slotScopeIds, optimized ) } else { @@ -365,7 +376,8 @@ export function createHydrationFunctions( null, parentComponent, parentSuspense, - isSVGContainer(container) + isSVGContainer(container), + slotScopeIds ) } } @@ -377,8 +389,16 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, optimized: boolean ) => { + const { slotScopeIds: fragmentSlotScopeIds } = vnode + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds + ? slotScopeIds.concat(fragmentSlotScopeIds) + : fragmentSlotScopeIds + } + const container = parentNode(node)! const next = hydrateChildren( nextSibling(node)!, @@ -386,6 +406,7 @@ export function createHydrationFunctions( container, parentComponent, parentSuspense, + slotScopeIds, optimized ) if (next && isComment(next) && next.data === ']') { @@ -405,6 +426,7 @@ export function createHydrationFunctions( vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, + slotScopeIds: string[] | null, isFragment: boolean ): Node | null => { hasMismatch = true @@ -446,7 +468,8 @@ export function createHydrationFunctions( next, parentComponent, parentSuspense, - isSVGContainer(container) + isSVGContainer(container), + slotScopeIds ) return next } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index a24672226..98ba289f5 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -227,12 +227,11 @@ export { HMRRuntime } from './hmr' // For compiler generated code // should sync with '@vue/compiler-core/src/runtimeConstants.ts' -export { withCtx } from './helpers/withRenderContext' +export { withCtx, setScopeId } from './componentRenderContext' export { renderList } from './helpers/renderList' export { toHandlers } from './helpers/toHandlers' export { renderSlot } from './helpers/renderSlot' export { createSlots } from './helpers/createSlots' -export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId' export { openBlock, createBlock, @@ -257,10 +256,8 @@ export { transformVNodeArgs } from './vnode' // change without notice between versions. User code should never rely on them. import { createComponentInstance, setupComponent } from './component' -import { - renderComponentRoot, - setCurrentRenderingInstance -} from './componentRenderUtils' +import { renderComponentRoot } from './componentRenderUtils' +import { setCurrentRenderingInstance } from './componentRenderContext' import { isVNode, normalizeVNode } from './vnode' const _ssrUtils = { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 3089b1027..f2a35794c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -177,6 +177,7 @@ type PatchFn = ( parentComponent?: ComponentInternalInstance | null, parentSuspense?: SuspenseBoundary | null, isSVG?: boolean, + slotScopeIds?: string[] | null, optimized?: boolean ) => void @@ -187,6 +188,7 @@ type MountChildrenFn = ( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean, start?: number ) => void @@ -199,7 +201,8 @@ type PatchChildrenFn = ( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, - optimized?: boolean + slotScopeIds: string[] | null, + optimized: boolean ) => void type PatchBlockChildrenFn = ( @@ -208,7 +211,8 @@ type PatchBlockChildrenFn = ( fallbackContainer: RendererElement, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, - isSVG: boolean + isSVG: boolean, + slotScopeIds: string[] | null ) => void type MoveFn = ( @@ -469,6 +473,7 @@ function baseCreateRenderer( parentComponent = null, parentSuspense = null, isSVG = false, + slotScopeIds = null, optimized = false ) => { // patching & not same type, unmount old tree @@ -507,6 +512,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) break @@ -520,6 +526,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { @@ -531,6 +538,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.TELEPORT) { @@ -542,6 +550,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, internals ) @@ -554,6 +563,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, internals ) @@ -676,6 +686,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' @@ -687,10 +698,19 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { - patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) + patchElement( + n1, + n2, + parentComponent, + parentSuspense, + isSVG, + slotScopeIds, + optimized + ) } } @@ -701,19 +721,12 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { let el: RendererElement let vnodeHook: VNodeHook | undefined | null - const { - type, - props, - shapeFlag, - transition, - scopeId, - patchFlag, - dirs - } = vnode + const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode if ( !__DEV__ && vnode.el && @@ -744,6 +757,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG && type !== 'foreignObject', + slotScopeIds, optimized || !!vnode.dynamicChildren ) } @@ -773,7 +787,7 @@ function baseCreateRenderer( } } // scopeId - setScopeId(el, scopeId, vnode, parentComponent) + setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent) } if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { Object.defineProperty(el, '__vnode', { @@ -813,30 +827,32 @@ function baseCreateRenderer( const setScopeId = ( el: RendererElement, - scopeId: string | false | null, vnode: VNode, + scopeId: string | null, + slotScopeIds: string[] | null, parentComponent: ComponentInternalInstance | null ) => { if (scopeId) { hostSetScopeId(el, scopeId) } - if (parentComponent) { - const treeOwnerId = parentComponent.type.__scopeId - // vnode's own scopeId and the current patched component's scopeId is - // different - this is a slot content node. - if (treeOwnerId && treeOwnerId !== scopeId) { - hostSetScopeId(el, treeOwnerId + '-s') + if (slotScopeIds) { + for (let i = 0; i < slotScopeIds.length; i++) { + hostSetScopeId(el, slotScopeIds[i]) } + } + if (parentComponent) { let subTree = parentComponent.subTree - if (__DEV__ && subTree.type === Fragment) { + if (__DEV__ && subTree.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT) { subTree = filterSingleRoot(subTree.children as VNodeArrayChildren) || subTree } if (vnode === subTree) { + const parentVNode = parentComponent.vnode setScopeId( el, - parentComponent.vnode.scopeId, - parentComponent.vnode, + parentVNode, + parentVNode.scopeId, + parentVNode.slotScopeIds, parentComponent.parent ) } @@ -851,6 +867,7 @@ function baseCreateRenderer( parentSuspense, isSVG, optimized, + slotScopeIds, start = 0 ) => { for (let i = start; i < children.length; i++) { @@ -865,7 +882,8 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, - optimized + optimized, + slotScopeIds ) } } @@ -876,6 +894,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { const el = (n2.el = n1.el!) @@ -993,7 +1012,8 @@ function baseCreateRenderer( el, parentComponent, parentSuspense, - areChildrenSVG + areChildrenSVG, + slotScopeIds ) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) @@ -1007,7 +1027,9 @@ function baseCreateRenderer( null, parentComponent, parentSuspense, - areChildrenSVG + areChildrenSVG, + slotScopeIds, + false ) } @@ -1026,7 +1048,8 @@ function baseCreateRenderer( fallbackContainer, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] @@ -1054,6 +1077,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, true ) } @@ -1119,16 +1143,24 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))! const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))! - let { patchFlag, dynamicChildren } = n2 + let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2 if (patchFlag > 0) { optimized = true } + // check if this is a slot fragment with :slotted scope ids + if (fragmentSlotScopeIds) { + slotScopeIds = slotScopeIds + ? slotScopeIds.concat(fragmentSlotScopeIds) + : fragmentSlotScopeIds + } + if (__DEV__ && isHmrUpdating) { // HMR updated, force full diff patchFlag = 0 @@ -1149,6 +1181,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1168,7 +1201,8 @@ function baseCreateRenderer( container, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds ) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) @@ -1195,6 +1229,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1209,8 +1244,10 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { + n2.slotScopeIds = slotScopeIds if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( @@ -1382,7 +1419,8 @@ function baseCreateRenderer( initialVNode.el as Node, subTree, instance, - parentSuspense + parentSuspense, + null ) if (__DEV__) { endMeasure(instance, `hydrate`) @@ -1543,6 +1581,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized = false ) => { const c1 = n1 && n1.children @@ -1563,6 +1602,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) return @@ -1576,6 +1616,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) return @@ -1604,6 +1645,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1625,6 +1667,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1640,6 +1683,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { c1 = c1 || EMPTY_ARR @@ -1660,6 +1704,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } @@ -1682,6 +1727,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized, commonLength ) @@ -1697,6 +1743,7 @@ function baseCreateRenderer( parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, + slotScopeIds: string[] | null, optimized: boolean ) => { let i = 0 @@ -1721,6 +1768,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1746,6 +1794,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) } else { @@ -1776,7 +1825,9 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + optimized ) i++ } @@ -1878,6 +1929,7 @@ function baseCreateRenderer( parentComponent, parentSuspense, isSVG, + slotScopeIds, optimized ) patched++ @@ -1905,7 +1957,9 @@ function baseCreateRenderer( anchor, parentComponent, parentSuspense, - isSVG + isSVG, + slotScopeIds, + optimized ) } else if (moved) { // move if: diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 92d7c4f42..e46c98426 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -32,9 +32,11 @@ import { import { DirectiveBinding } from './directives' import { TransitionHooks } from './components/BaseTransition' import { warn } from './warning' -import { currentScopeId } from './helpers/scopeId' import { TeleportImpl, isTeleport } from './components/Teleport' -import { currentRenderingInstance } from './componentRenderUtils' +import { + currentRenderingInstance, + currentScopeId +} from './componentRenderContext' import { RendererNode, RendererElement } from './renderer' import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets' import { hmrDirtyComponents } from './hmr' @@ -133,7 +135,18 @@ export interface VNode< props: (VNodeProps & ExtraProps) | null key: string | number | null ref: VNodeNormalizedRef | null - scopeId: string | null // SFC only + /** + * SFC only. This is assigned on vnode creation using currentScopeId + * which is set alongside currentRenderingInstance. + */ + scopeId: string | null + /** + * SFC only. This is assigned to: + * - Slot fragment vnodes with :slotted SFC styles. + * - Component vnodes (during patch/hydration) so that its root node can + * inherit the component's slotScopeIds + */ + slotScopeIds: string[] | null children: VNodeNormalizedChildren component: ComponentInternalInstance | null dirs: DirectiveBinding[] | null @@ -398,6 +411,7 @@ function _createVNode( key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, + slotScopeIds: null, children: null, component: null, suspense: null, @@ -479,6 +493,7 @@ export function cloneVNode( : normalizeRef(extraProps) : ref, scopeId: vnode.scopeId, + slotScopeIds: vnode.slotScopeIds, children: __DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children) ? (children as VNode[]).map(deepCloneVNode) diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index a02bf365c..806ca210b 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -2,13 +2,13 @@ import { createApp, h, createCommentVNode, - withScopeId, resolveComponent, ComponentOptions, ref, defineComponent, createTextVNode, - createStaticVNode + createStaticVNode, + withCtx } from 'vue' import { escapeHtml } from '@vue/shared' import { renderToString } from '../src/renderToString' @@ -634,34 +634,32 @@ function testRender(type: string, render: typeof renderToString) { describe('scopeId', () => { // note: here we are only testing scopeId handling for vdom serialization. // compiled srr render functions will include scopeId directly in strings. - const withId = withScopeId('data-v-test') - const withChildId = withScopeId('data-v-child') test('basic', async () => { - expect( - await render( - withId(() => { - return h('div') - })() - ) - ).toBe(`
`) + const Foo = { + __scopeId: 'data-v-test', + render() { + return h('div') + } + } + expect(await render(h(Foo))).toBe(`
`) }) test('with slots', async () => { const Child = { __scopeId: 'data-v-child', - render: withChildId(function(this: any) { + render: function(this: any) { return h('div', this.$slots.default()) - }) + } } const Parent = { __scopeId: 'data-v-test', - render: withId(() => { + render: () => { return h(Child, null, { - default: withId(() => h('span', 'slot')) + default: withCtx(() => h('span', 'slot')) }) - }) + } } expect(await render(h(Parent))).toBe( diff --git a/packages/server-renderer/__tests__/ssrScopeId.spec.ts b/packages/server-renderer/__tests__/ssrScopeId.spec.ts index 8b58fc66b..7726739e4 100644 --- a/packages/server-renderer/__tests__/ssrScopeId.spec.ts +++ b/packages/server-renderer/__tests__/ssrScopeId.spec.ts @@ -1,11 +1,9 @@ -import { createApp, withScopeId } from 'vue' +import { createApp, mergeProps, withCtx } 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') - +describe('ssr: scopedId runtime behavior', () => { + test('id on component root', async () => { const Child = { ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { push(`
`) @@ -13,19 +11,19 @@ describe('ssr: scoped id on component root', () => { } const Comp = { - ssrRender: withParentId((ctx: any, push: any, parent: any) => { + __scopeId: 'parent', + ssrRender: (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') - + test('id and :slotted on component root', async () => { const Child = { + //
ssrRender: (_: any, push: any, _parent: any, attrs: any) => { push(``) } @@ -34,29 +32,126 @@ describe('ssr: scoped id on component root', () => { const Wrapper = { __scopeId: 'wrapper', ssrRender: (ctx: any, push: any, parent: any) => { - ssrRenderSlot(ctx.$slots, 'default', {}, null, push, parent) + // + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'wrapper-s' + ) } } const Comp = { - ssrRender: withParentId((_: any, push: any, parent: any) => { + __scopeId: 'parent', + ssrRender: (_: any, push: any, parent: any) => { + // push( ssrRenderComponent( Wrapper, null, { - default: withParentId((_: any, push: any, parent: any) => { - push(ssrRenderComponent(Child, null, null, parent)) - }), + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + push(ssrRenderComponent(Child, null, null, parent, scopeId)) + } + ), _: 1 } as any, parent ) ) - }) + } } const result = await renderToString(createApp(Comp)) expect(result).toBe(`
`) }) + + // #2892 + test(':slotted on forwarded slots', async () => { + const Wrapper = { + __scopeId: 'wrapper', + ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { + //
+ push( + `` + ) + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'wrapper-s' + ) + push(``) + } + } + + const Slotted = { + __scopeId: 'slotted', + ssrRender: (ctx: any, push: any, parent: any, attrs: any) => { + // + push( + ssrRenderComponent( + Wrapper, + attrs, + { + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + ssrRenderSlot( + ctx.$slots, + 'default', + {}, + null, + push, + parent, + 'slotted-s' + scopeId + ) + } + ), + _: 1 + } as any, + parent + ) + ) + } + } + + const Root = { + __scopeId: 'root', + //
+ ssrRender: (_: any, push: any, parent: any, attrs: any) => { + push( + ssrRenderComponent( + Slotted, + attrs, + { + default: withCtx( + (_: any, push: any, parent: any, scopeId: string) => { + push(`
`) + } + ), + _: 1 + } as any, + parent + ) + ) + } + } + + const result = await renderToString(createApp(Root)) + expect(result).toBe( + `
` + + `
` + + `
` + ) + }) }) diff --git a/packages/server-renderer/src/helpers/ssrRenderComponent.ts b/packages/server-renderer/src/helpers/ssrRenderComponent.ts index 000f2b482..4709f23a6 100644 --- a/packages/server-renderer/src/helpers/ssrRenderComponent.ts +++ b/packages/server-renderer/src/helpers/ssrRenderComponent.ts @@ -6,10 +6,12 @@ export function ssrRenderComponent( comp: Component, props: Props | null = null, children: Slots | SSRSlots | null = null, - parentComponent: ComponentInternalInstance | null = null + parentComponent: ComponentInternalInstance | null = null, + slotScopeId?: string ): SSRBuffer | Promise { return renderComponentVNode( createVNode(comp, props, children), - parentComponent + parentComponent, + slotScopeId ) } diff --git a/packages/server-renderer/src/helpers/ssrRenderSlot.ts b/packages/server-renderer/src/helpers/ssrRenderSlot.ts index 5ee6113a1..3f3589a3b 100644 --- a/packages/server-renderer/src/helpers/ssrRenderSlot.ts +++ b/packages/server-renderer/src/helpers/ssrRenderSlot.ts @@ -15,13 +15,13 @@ export function ssrRenderSlot( slotProps: Props, fallbackRenderFn: (() => void) | null, push: PushFn, - parentComponent: ComponentInternalInstance + parentComponent: ComponentInternalInstance, + slotScopeId?: string | null ) { // template-compiled slots are always rendered as fragments push(``) const slotFn = slots[slotName] if (slotFn) { - const scopeId = parentComponent && parentComponent.type.__scopeId const slotBuffer: SSRBufferItem[] = [] const bufferedPush = (item: SSRBufferItem) => { slotBuffer.push(item) @@ -30,7 +30,7 @@ export function ssrRenderSlot( slotProps, bufferedPush, parentComponent, - scopeId ? ` ${scopeId}-s` : `` + slotScopeId ? ' ' + slotScopeId : '' ) if (Array.isArray(ret)) { // normal slot diff --git a/packages/server-renderer/src/render.ts b/packages/server-renderer/src/render.ts index 938a4f83a..fd40f3be4 100644 --- a/packages/server-renderer/src/render.ts +++ b/packages/server-renderer/src/render.ts @@ -80,7 +80,8 @@ export function createBuffer() { export function renderComponentVNode( vnode: VNode, - parentComponent: ComponentInternalInstance | null = null + parentComponent: ComponentInternalInstance | null = null, + slotScopeId?: string ): SSRBuffer | Promise { const instance = createComponentInstance(vnode, parentComponent, null) const res = setupComponent(instance, true /* isSSR */) @@ -97,14 +98,15 @@ export function renderComponentVNode( warn(`[@vue/server-renderer]: Uncaught error in serverPrefetch:\n`, err) }) } - return p.then(() => renderComponentSubTree(instance)) + return p.then(() => renderComponentSubTree(instance, slotScopeId)) } else { - return renderComponentSubTree(instance) + return renderComponentSubTree(instance, slotScopeId) } } function renderComponentSubTree( - instance: ComponentInternalInstance + instance: ComponentInternalInstance, + slotScopeId?: string ): SSRBuffer | Promise { const comp = instance.type as Component const { getBuffer, push } = createBuffer() @@ -133,13 +135,10 @@ function renderComponentSubTree( // 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] = '' + if (slotScopeId) attrs[slotScopeId.trim()] = '' } // set current rendering instance for asset resolution