refactor(ssr): prefix ssr helpers

This commit is contained in:
Evan You 2020-02-06 12:07:25 -05:00
parent f3e70b3733
commit bc8f91d181
16 changed files with 256 additions and 136 deletions

View File

@ -292,7 +292,7 @@ export interface ArrayExpression extends Node {
export interface FunctionExpression extends Node {
type: NodeTypes.JS_FUNCTION_EXPRESSION
params: ExpressionNode | ExpressionNode[] | undefined
params: ExpressionNode | string | (ExpressionNode | string)[] | undefined
returns?: TemplateChildNode | TemplateChildNode[] | JSChildNode
body?: BlockStatement
newline: boolean

View File

@ -41,7 +41,12 @@ export {
transformExpression,
processExpression
} from './transforms/transformExpression'
export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot'
export {
buildSlots,
SlotFnBuilder,
trackVForSlotScopes,
trackSlotScopes
} from './transforms/vSlot'
export { resolveComponentType, buildProps } from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet'

View File

@ -93,11 +93,27 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
}
}
export type SlotFnBuilder = (
slotProps: ExpressionNode | undefined,
slotChildren: TemplateChildNode[],
loc: SourceLocation
) => FunctionExpression
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
createFunctionExpression(
props,
children,
false /* newline */,
true /* isSlot */,
children.length ? children[0].loc : loc
)
// Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component.
export function buildSlots(
node: ElementNode,
context: TransformContext
context: TransformContext,
buildSlotFn: SlotFnBuilder = buildClientSlotFn
): {
slots: ObjectExpression | CallExpression
hasDynamicSlots: boolean
@ -106,6 +122,11 @@ export function buildSlots(
const slotsProperties: Property[] = []
const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
const buildDefaultSlotProperty = (
props: ExpressionNode | undefined,
children: TemplateChildNode[]
) => createObjectProperty(`default`, buildSlotFn(props, children, loc))
// If the slot is inside a v-for or another v-slot, force it to be dynamic
// since it likely uses a scope variable.
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
@ -125,7 +146,7 @@ export function buildSlots(
createCompilerError(ErrorCodes.X_V_SLOT_NAMED_SLOT_ON_COMPONENT, loc)
)
}
slotsProperties.push(buildDefaultSlot(exp, children, loc))
slotsProperties.push(buildDefaultSlotProperty(exp, children))
}
// 2. Iterate through children and check for template slots
@ -174,14 +195,7 @@ export function buildSlots(
hasDynamicSlots = true
}
const slotFunction = createFunctionExpression(
slotProps,
slotChildren,
false /* newline */,
true /* isSlot */,
slotChildren.length ? slotChildren[0].loc : slotLoc
)
const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
// check if this slot is conditional (v-if/v-for)
let vIf: DirectiveNode | undefined
let vElse: DirectiveNode | undefined
@ -280,7 +294,7 @@ export function buildSlots(
if (!onComponentDefaultSlot) {
if (!hasTemplateSlots) {
// implicit default slot (on component)
slotsProperties.push(buildDefaultSlot(undefined, children, loc))
slotsProperties.push(buildDefaultSlotProperty(undefined, children))
} else if (implicitDefaultChildren.length) {
// implicit default slot (mixed with named slots)
if (hasNamedDefaultSlot) {
@ -292,7 +306,7 @@ export function buildSlots(
)
} else {
slotsProperties.push(
buildDefaultSlot(undefined, implicitDefaultChildren, loc)
buildDefaultSlotProperty(undefined, implicitDefaultChildren)
)
}
}
@ -317,23 +331,6 @@ export function buildSlots(
}
}
function buildDefaultSlot(
slotProps: ExpressionNode | undefined,
children: TemplateChildNode[],
loc: SourceLocation
): Property {
return createObjectProperty(
`default`,
createFunctionExpression(
slotProps,
children,
false /* newline */,
true /* isSlot */,
children.length ? children[0].loc : loc
)
)
}
function buildDynamicSlot(
name: ExpressionNode,
fn: FunctionExpression

View File

@ -1,33 +1,33 @@
import { registerRuntimeHelpers } from '@vue/compiler-dom'
export const SSR_INTERPOLATE = Symbol(`interpolate`)
export const SSR_RENDER_COMPONENT = Symbol(`renderComponent`)
export const SSR_RENDER_SLOT = Symbol(`renderSlot`)
export const SSR_RENDER_CLASS = Symbol(`renderClass`)
export const SSR_RENDER_STYLE = Symbol(`renderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`renderAttrs`)
export const SSR_RENDER_ATTR = Symbol(`renderAttr`)
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`renderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`renderList`)
export const SSR_LOOSE_EQUAL = Symbol(`looseEqual`)
export const SSR_LOOSE_CONTAIN = Symbol(`looseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`renderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`getDynamicModelProps`)
export const SSR_INTERPOLATE = Symbol(`ssrInterpolate`)
export const SSR_RENDER_COMPONENT = Symbol(`ssrRenderComponent`)
export const SSR_RENDER_SLOT = Symbol(`ssrRenderSlot`)
export const SSR_RENDER_CLASS = Symbol(`ssrRenderClass`)
export const SSR_RENDER_STYLE = Symbol(`ssrRenderStyle`)
export const SSR_RENDER_ATTRS = Symbol(`ssrRenderAttrs`)
export const SSR_RENDER_ATTR = Symbol(`ssrRenderAttr`)
export const SSR_RENDER_DYNAMIC_ATTR = Symbol(`ssrRenderDynamicAttr`)
export const SSR_RENDER_LIST = Symbol(`ssrRenderList`)
export const SSR_LOOSE_EQUAL = Symbol(`ssrLooseEqual`)
export const SSR_LOOSE_CONTAIN = Symbol(`ssrLooseContain`)
export const SSR_RENDER_DYNAMIC_MODEL = Symbol(`ssrRenderDynamicModel`)
export const SSR_GET_DYNAMIC_MODEL_PROPS = Symbol(`ssrGetDynamicModelProps`)
export const ssrHelpers = {
[SSR_INTERPOLATE]: `_interpolate`,
[SSR_RENDER_COMPONENT]: `_renderComponent`,
[SSR_RENDER_SLOT]: `_renderSlot`,
[SSR_RENDER_CLASS]: `_renderClass`,
[SSR_RENDER_STYLE]: `_renderStyle`,
[SSR_RENDER_ATTRS]: `_renderAttrs`,
[SSR_RENDER_ATTR]: `_renderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_renderDynamicAttr`,
[SSR_RENDER_LIST]: `_renderList`,
[SSR_LOOSE_EQUAL]: `_looseEqual`,
[SSR_LOOSE_CONTAIN]: `_looseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `_renderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `_getDynamicModelProps`
[SSR_INTERPOLATE]: `_ssrInterpolate`,
[SSR_RENDER_COMPONENT]: `_ssrRenderComponent`,
[SSR_RENDER_SLOT]: `_ssrRenderSlot`,
[SSR_RENDER_CLASS]: `_ssrRenderClass`,
[SSR_RENDER_STYLE]: `_ssrRenderStyle`,
[SSR_RENDER_ATTRS]: `_ssrRenderAttrs`,
[SSR_RENDER_ATTR]: `_ssrRenderAttr`,
[SSR_RENDER_DYNAMIC_ATTR]: `_ssrRenderDynamicAttr`,
[SSR_RENDER_LIST]: `_ssrRenderList`,
[SSR_LOOSE_EQUAL]: `_ssrLooseEqual`,
[SSR_LOOSE_CONTAIN]: `_ssrLooseContain`,
[SSR_RENDER_DYNAMIC_MODEL]: `_ssrRenderDynamicModel`,
[SSR_GET_DYNAMIC_MODEL_PROPS]: `_ssrGetDynamicModelProps`
}
// Note: these are helpers imported from @vue/server-renderer

View File

@ -1,16 +1,16 @@
import { interpolate } from '../src/helpers/interpolate'
import { ssrInterpolate } from '../src/helpers/ssrInterpolate'
import { escapeHtml } from '@vue/shared'
test('ssr: interpolate', () => {
expect(interpolate(0)).toBe(`0`)
expect(interpolate(`foo`)).toBe(`foo`)
expect(interpolate(`<div>`)).toBe(`&lt;div&gt;`)
expect(ssrInterpolate(0)).toBe(`0`)
expect(ssrInterpolate(`foo`)).toBe(`foo`)
expect(ssrInterpolate(`<div>`)).toBe(`&lt;div&gt;`)
// should escape interpolated values
expect(interpolate([1, 2, 3])).toBe(
expect(ssrInterpolate([1, 2, 3])).toBe(
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
)
expect(
interpolate({
ssrInterpolate({
foo: 1,
bar: `<div>`
})

View File

@ -1,15 +1,15 @@
import {
renderAttrs,
renderClass,
renderStyle,
renderAttr
} from '../src/helpers/renderAttrs'
ssrRenderAttrs,
ssrRenderClass,
ssrRenderStyle,
ssrRenderAttr
} from '../src/helpers/ssrRenderAttrs'
import { escapeHtml } from '@vue/shared'
describe('ssr: renderAttrs', () => {
test('ignore reserved props', () => {
expect(
renderAttrs({
ssrRenderAttrs({
key: 1,
ref: () => {},
onClick: () => {}
@ -19,7 +19,7 @@ describe('ssr: renderAttrs', () => {
test('normal attrs', () => {
expect(
renderAttrs({
ssrRenderAttrs({
id: 'foo',
title: 'bar'
})
@ -28,7 +28,7 @@ describe('ssr: renderAttrs', () => {
test('escape attrs', () => {
expect(
renderAttrs({
ssrRenderAttrs({
id: '"><script'
})
).toBe(` id="&quot;&gt;&lt;script"`)
@ -36,7 +36,7 @@ describe('ssr: renderAttrs', () => {
test('boolean attrs', () => {
expect(
renderAttrs({
ssrRenderAttrs({
checked: true,
multiple: false
})
@ -45,7 +45,7 @@ describe('ssr: renderAttrs', () => {
test('ignore falsy values', () => {
expect(
renderAttrs({
ssrRenderAttrs({
foo: false,
title: null,
baz: undefined
@ -55,7 +55,7 @@ describe('ssr: renderAttrs', () => {
test('ingore non-renderable values', () => {
expect(
renderAttrs({
ssrRenderAttrs({
foo: {},
bar: [],
baz: () => {}
@ -65,7 +65,7 @@ describe('ssr: renderAttrs', () => {
test('props to attrs', () => {
expect(
renderAttrs({
ssrRenderAttrs({
readOnly: true, // simple lower case conversion
htmlFor: 'foobar' // special cases
})
@ -74,7 +74,7 @@ describe('ssr: renderAttrs', () => {
test('preserve name on custom element', () => {
expect(
renderAttrs(
ssrRenderAttrs(
{
fooBar: 'ok'
},
@ -86,16 +86,16 @@ describe('ssr: renderAttrs', () => {
describe('ssr: renderAttr', () => {
test('basic', () => {
expect(renderAttr('foo', 'bar')).toBe(` foo="bar"`)
expect(ssrRenderAttr('foo', 'bar')).toBe(` foo="bar"`)
})
test('null and undefined', () => {
expect(renderAttr('foo', null)).toBe(``)
expect(renderAttr('foo', undefined)).toBe(``)
expect(ssrRenderAttr('foo', null)).toBe(``)
expect(ssrRenderAttr('foo', undefined)).toBe(``)
})
test('escape', () => {
expect(renderAttr('foo', '<script>')).toBe(
expect(ssrRenderAttr('foo', '<script>')).toBe(
` foo="${escapeHtml(`<script>`)}"`
)
})
@ -104,28 +104,28 @@ describe('ssr: renderAttr', () => {
describe('ssr: renderClass', () => {
test('via renderProps', () => {
expect(
renderAttrs({
ssrRenderAttrs({
class: ['foo', 'bar']
})
).toBe(` class="foo bar"`)
})
test('standalone', () => {
expect(renderClass(`foo`)).toBe(`foo`)
expect(renderClass([`foo`, `bar`])).toBe(`foo bar`)
expect(renderClass({ foo: true, bar: false })).toBe(`foo`)
expect(renderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
expect(ssrRenderClass(`foo`)).toBe(`foo`)
expect(ssrRenderClass([`foo`, `bar`])).toBe(`foo bar`)
expect(ssrRenderClass({ foo: true, bar: false })).toBe(`foo`)
expect(ssrRenderClass([{ foo: true, bar: false }, `baz`])).toBe(`foo baz`)
})
test('escape class values', () => {
expect(renderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
expect(ssrRenderClass(`"><script`)).toBe(`&quot;&gt;&lt;script`)
})
})
describe('ssr: renderStyle', () => {
test('via renderProps', () => {
expect(
renderAttrs({
ssrRenderAttrs({
style: {
color: 'red'
}
@ -134,14 +134,14 @@ describe('ssr: renderStyle', () => {
})
test('standalone', () => {
expect(renderStyle(`color:red`)).toBe(`color:red`)
expect(ssrRenderStyle(`color:red`)).toBe(`color:red`)
expect(
renderStyle({
ssrRenderStyle({
color: `red`
})
).toBe(`color:red;`)
expect(
renderStyle([
ssrRenderStyle([
{ color: `red` },
{ fontSize: `15px` } // case conversion
])
@ -150,7 +150,7 @@ describe('ssr: renderStyle', () => {
test('number handling', () => {
expect(
renderStyle({
ssrRenderStyle({
fontSize: 15, // should be ignored since font-size requires unit
opacity: 0.5
})
@ -158,9 +158,9 @@ describe('ssr: renderStyle', () => {
})
test('escape inline CSS', () => {
expect(renderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
expect(ssrRenderStyle(`"><script`)).toBe(`&quot;&gt;&lt;script`)
expect(
renderStyle({
ssrRenderStyle({
color: `"><script`
})
).toBe(`color:&quot;&gt;&lt;script;`)

View File

@ -8,7 +8,7 @@ import {
} from 'vue'
import { escapeHtml } from '@vue/shared'
import { renderToString, renderComponent } from '../src/renderToString'
import { renderSlot } from '../src/helpers/renderSlot'
import { ssrRenderSlot } from '../src/helpers/ssrRenderSlot'
describe('ssr: renderToString', () => {
test('should apply app context', async () => {
@ -132,7 +132,7 @@ describe('ssr: renderToString', () => {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
renderSlot(
ssrRenderSlot(
ctx.$slots,
'default',
{ msg: 'from slot' },
@ -195,7 +195,7 @@ describe('ssr: renderToString', () => {
props: ['msg'],
ssrRender(ctx: any, push: any, parent: any) {
push(`<div class="child">`)
renderSlot(
ssrRenderSlot(
ctx.$slots,
'default',
{ msg: 'from slot' },

View File

@ -0,0 +1,115 @@
import {
ssrRenderDynamicModel,
ssrGetDynamicModelProps
// ssrGetDynamicModelProps
} from '../src/helpers/ssrVModelHelpers'
describe('ssr: v-model helpers', () => {
test('ssrRenderDynamicModel', () => {
expect(ssrRenderDynamicModel(null, 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('text', 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('email', 'foo', null)).toBe(` value="foo"`)
expect(ssrRenderDynamicModel('checkbox', true, null)).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', false, null)).toBe(``)
expect(ssrRenderDynamicModel('checkbox', [1], '1')).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', [1], 1)).toBe(` checked`)
expect(ssrRenderDynamicModel('checkbox', [1], 0)).toBe(``)
expect(ssrRenderDynamicModel('radio', 'foo', 'foo')).toBe(` checked`)
expect(ssrRenderDynamicModel('radio', 1, '1')).toBe(` checked`)
expect(ssrRenderDynamicModel('radio', 1, 0)).toBe(``)
})
test('ssrGetDynamicModelProps', () => {
expect(ssrGetDynamicModelProps({}, 'foo')).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'text'
},
'foo'
)
).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'email'
},
'foo'
)
).toMatchObject({ value: 'foo' })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox'
},
true
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox'
},
false
)
).toBe(null)
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: '1'
},
[1]
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: 1
},
[1]
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'checkbox',
value: 0
},
[1]
)
).toBe(null)
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: 'foo'
},
'foo'
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: '1'
},
1
)
).toMatchObject({ checked: true })
expect(
ssrGetDynamicModelProps(
{
type: 'radio',
value: 0
},
1
)
).toBe(null)
})
})

View File

@ -1,5 +1,5 @@
import { escapeHtml, toDisplayString } from '@vue/shared'
export function interpolate(value: unknown): string {
export function ssrInterpolate(value: unknown): string {
return escapeHtml(toDisplayString(value))
}

View File

@ -14,7 +14,7 @@ import {
const shouldIgnoreProp = makeMap(`key,ref,innerHTML,textContent`)
export function renderAttrs(
export function ssrRenderAttrs(
props: Record<string, unknown>,
tag?: string
): string {
@ -29,18 +29,18 @@ export function renderAttrs(
}
const value = props[key]
if (key === 'class') {
ret += ` class="${renderClass(value)}"`
ret += ` class="${ssrRenderClass(value)}"`
} else if (key === 'style') {
ret += ` style="${renderStyle(value)}"`
ret += ` style="${ssrRenderStyle(value)}"`
} else {
ret += renderDynamicAttr(key, value, tag)
ret += ssrRenderDynamicAttr(key, value, tag)
}
}
return ret
}
// render an attr with dynamic (unknown) key.
export function renderDynamicAttr(
export function ssrRenderDynamicAttr(
key: string,
value: unknown,
tag?: string
@ -63,7 +63,7 @@ export function renderDynamicAttr(
// Render a v-bind attr with static key. The key is pre-processed at compile
// time and we only need to check and escape value.
export function renderAttr(key: string, value: unknown): string {
export function ssrRenderAttr(key: string, value: unknown): string {
if (!isRenderableValue(value)) {
return ``
}
@ -78,11 +78,11 @@ function isRenderableValue(value: unknown): boolean {
return type === 'string' || type === 'number' || type === 'boolean'
}
export function renderClass(raw: unknown): string {
export function ssrRenderClass(raw: unknown): string {
return escapeHtml(normalizeClass(raw))
}
export function renderStyle(raw: unknown): string {
export function ssrRenderStyle(raw: unknown): string {
if (!raw) {
return ''
}

View File

@ -1,6 +1,6 @@
import { isArray, isString, isObject } from '@vue/shared'
export function renderList(
export function ssrRenderList(
source: unknown,
renderItem: (value: unknown, key: string | number, index?: number) => void
) {

View File

@ -9,7 +9,7 @@ export type SSRSlot = (
parentComponent: ComponentInternalInstance | null
) => void
export function renderSlot(
export function ssrRenderSlot(
slots: Slots | SSRSlots,
slotName: string,
slotProps: Props,

View File

@ -1,42 +1,45 @@
import { looseEqual as _looseEqual, looseIndexOf } from '@vue/shared'
import { renderAttr } from './renderAttrs'
import { looseEqual, looseIndexOf } from '@vue/shared'
import { ssrRenderAttr } from './ssrRenderAttrs'
export const looseEqual = _looseEqual as (a: unknown, b: unknown) => boolean
export const ssrLooseEqual = looseEqual as (a: unknown, b: unknown) => boolean
export function looseContain(arr: unknown[], value: unknown): boolean {
export function ssrLooseContain(arr: unknown[], value: unknown): boolean {
return looseIndexOf(arr, value) > -1
}
// for <input :type="type" v-model="model" value="value">
export function renderDynamicModel(
export function ssrRenderDynamicModel(
type: unknown,
model: unknown,
value: unknown
) {
switch (type) {
case 'radio':
return _looseEqual(model, value) ? ' checked' : ''
return looseEqual(model, value) ? ' checked' : ''
case 'checkbox':
return (Array.isArray(model)
? looseContain(model, value)
? ssrLooseContain(model, value)
: model)
? ' checked'
: ''
default:
// text types
return renderAttr('value', model)
return ssrRenderAttr('value', model)
}
}
// for <input v-bind="obj" v-model="model">
export function getDynamicModelProps(existingProps: any = {}, model: unknown) {
export function ssrGetDynamicModelProps(
existingProps: any = {},
model: unknown
) {
const { type, value } = existingProps
switch (type) {
case 'radio':
return _looseEqual(model, value) ? { checked: true } : null
return looseEqual(model, value) ? { checked: true } : null
case 'checkbox':
return (Array.isArray(model)
? looseContain(model, value)
? ssrLooseContain(model, value)
: model)
? { checked: true }
: null

View File

@ -2,22 +2,22 @@
export { renderToString } from './renderToString'
// internal runtime helpers
export { renderComponent as _renderComponent } from './renderToString'
export { renderSlot as _renderSlot } from './helpers/renderSlot'
export { renderComponent as _ssrRenderComponent } from './renderToString'
export { ssrRenderSlot as _ssrRenderSlot } from './helpers/ssrRenderSlot'
export {
renderClass as _renderClass,
renderStyle as _renderStyle,
renderAttrs as _renderAttrs,
renderAttr as _renderAttr,
renderDynamicAttr as _renderDynamicAttr
} from './helpers/renderAttrs'
export { interpolate as _interpolate } from './helpers/interpolate'
export { renderList as _renderList } from './helpers/renderList'
ssrRenderClass as _ssrRenderClass,
ssrRenderStyle as _ssrRenderStyle,
ssrRenderAttrs as _ssrRenderAttrs,
ssrRenderAttr as _ssrRenderAttr,
ssrRenderDynamicAttr as _ssrRenderDynamicAttr
} from './helpers/ssrRenderAttrs'
export { ssrInterpolate as _ssrInterpolate } from './helpers/ssrInterpolate'
export { ssrRenderList as _ssrRenderList } from './helpers/ssrRenderList'
// v-model helpers
export {
looseEqual as _looseEqual,
looseContain as _looseContain,
renderDynamicModel as _renderDynamicModel,
getDynamicModelProps as _getDynamicModelProps
} from './helpers/vModelHelpers'
ssrLooseEqual as _ssrLooseEqual,
ssrLooseContain as _ssrLooseContain,
ssrRenderDynamicModel as _ssrRenderDynamicModel,
ssrGetDynamicModelProps as _ssrGetDynamicModelProps
} from './helpers/ssrVModelHelpers'

View File

@ -21,8 +21,8 @@ import {
isVoidTag,
escapeHtml
} from '@vue/shared'
import { renderAttrs } from './helpers/renderAttrs'
import { SSRSlots } from './helpers/renderSlot'
import { ssrRenderAttrs } from './helpers/ssrRenderAttrs'
import { SSRSlots } from './helpers/ssrRenderSlot'
const {
isVNode,
@ -217,7 +217,7 @@ function renderElement(
// TODO directives
if (props !== null) {
openTag += renderAttrs(props, tag)
openTag += ssrRenderAttrs(props, tag)
}
if (scopeId !== null) {