test(ssr): tests for utils and props rendering
This commit is contained in:
parent
730d329f79
commit
6e06810add
@ -223,7 +223,7 @@ export function createVNode(
|
|||||||
if (klass != null && !isString(klass)) {
|
if (klass != null && !isString(klass)) {
|
||||||
props.class = normalizeClass(klass)
|
props.class = normalizeClass(klass)
|
||||||
}
|
}
|
||||||
if (style != null) {
|
if (isObject(style)) {
|
||||||
// reactive state objects need to be cloned since they are likely to be
|
// reactive state objects need to be cloned since they are likely to be
|
||||||
// mutated
|
// mutated
|
||||||
if (isReactive(style) && !isArray(style)) {
|
if (isReactive(style) && !isArray(style)) {
|
||||||
|
@ -1 +0,0 @@
|
|||||||
test('ssr: escape HTML', () => {})
|
|
@ -1,19 +1,124 @@
|
|||||||
describe('ssr: render props', () => {
|
import { renderProps, renderClass, renderStyle } from '../src'
|
||||||
test('class', () => {})
|
|
||||||
|
|
||||||
test('style', () => {
|
describe('ssr: renderProps', () => {
|
||||||
// only render numbers for properties that allow no unit numbers
|
test('ignore reserved props', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
key: 1,
|
||||||
|
ref: () => {},
|
||||||
|
onClick: () => {}
|
||||||
|
})
|
||||||
|
).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('normal attrs', () => {})
|
test('normal attrs', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
id: 'foo',
|
||||||
|
title: 'bar'
|
||||||
|
})
|
||||||
|
).toBe(` id="foo" title="bar"`)
|
||||||
|
})
|
||||||
|
|
||||||
test('boolean attrs', () => {})
|
test('escape attrs', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
id: '"><script'
|
||||||
|
})
|
||||||
|
).toBe(` id=""><script"`)
|
||||||
|
})
|
||||||
|
|
||||||
test('enumerated attrs', () => {})
|
test('boolean attrs', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
checked: true,
|
||||||
|
multiple: false
|
||||||
|
})
|
||||||
|
).toBe(` checked`) // boolean attr w/ false should be ignored
|
||||||
|
})
|
||||||
|
|
||||||
test('ignore falsy values', () => {})
|
test('ignore falsy values', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
foo: false,
|
||||||
|
title: null,
|
||||||
|
baz: undefined
|
||||||
|
})
|
||||||
|
).toBe(` foo="false"`) // non boolean should render `false` as is
|
||||||
|
})
|
||||||
|
|
||||||
test('props to attrs', () => {})
|
test('props to attrs', () => {
|
||||||
|
expect(
|
||||||
test('ignore non-renderable props', () => {})
|
renderProps({
|
||||||
|
readOnly: true, // simple lower case conversion
|
||||||
|
htmlFor: 'foobar' // special cases
|
||||||
|
})
|
||||||
|
).toBe(` readonly for="foobar"`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ssr: renderClass', () => {
|
||||||
|
test('via renderProps', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
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`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('escape class values', () => {
|
||||||
|
expect(renderClass(`"><script`)).toBe(`"><script`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ssr: renderStyle', () => {
|
||||||
|
test('via renderProps', () => {
|
||||||
|
expect(
|
||||||
|
renderProps({
|
||||||
|
style: {
|
||||||
|
color: 'red'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).toBe(` style="color:red;"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('standalone', () => {
|
||||||
|
expect(renderStyle(`color:red`)).toBe(`color:red`)
|
||||||
|
expect(
|
||||||
|
renderStyle({
|
||||||
|
color: `red`
|
||||||
|
})
|
||||||
|
).toBe(`color:red;`)
|
||||||
|
expect(
|
||||||
|
renderStyle([
|
||||||
|
{ color: `red` },
|
||||||
|
{ fontSize: `15px` } // case conversion
|
||||||
|
])
|
||||||
|
).toBe(`color:red;font-size:15px;`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('number handling', () => {
|
||||||
|
expect(
|
||||||
|
renderStyle({
|
||||||
|
fontSize: 15, // should be ignored since font-size requires unit
|
||||||
|
opacity: 0.5
|
||||||
|
})
|
||||||
|
).toBe(`opacity:0.5;`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('escape inline CSS', () => {
|
||||||
|
expect(renderStyle(`"><script`)).toBe(`"><script`)
|
||||||
|
expect(
|
||||||
|
renderStyle({
|
||||||
|
color: `"><script`
|
||||||
|
})
|
||||||
|
).toBe(`color:"><script;`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
38
packages/server-renderer/__tests__/ssrUtils.spec.ts
Normal file
38
packages/server-renderer/__tests__/ssrUtils.spec.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { escapeHtml, interpolate } from '../src'
|
||||||
|
|
||||||
|
test('ssr: escapeHTML', () => {
|
||||||
|
expect(escapeHtml(`foo`)).toBe(`foo`)
|
||||||
|
expect(escapeHtml(true)).toBe(`true`)
|
||||||
|
expect(escapeHtml(false)).toBe(`false`)
|
||||||
|
expect(escapeHtml(`a && b`)).toBe(`a && b`)
|
||||||
|
expect(escapeHtml(`"foo"`)).toBe(`"foo"`)
|
||||||
|
expect(escapeHtml(`'bar'`)).toBe(`'bar'`)
|
||||||
|
expect(escapeHtml(`<div>`)).toBe(`<div>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ssr: interpolate', () => {
|
||||||
|
expect(interpolate(0)).toBe(`0`)
|
||||||
|
expect(interpolate(`foo`)).toBe(`foo`)
|
||||||
|
expect(interpolate(`<div>`)).toBe(`<div>`)
|
||||||
|
// should escape interpolated values
|
||||||
|
expect(interpolate([1, 2, 3])).toBe(
|
||||||
|
escapeHtml(JSON.stringify([1, 2, 3], null, 2))
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
interpolate({
|
||||||
|
foo: 1,
|
||||||
|
bar: `<div>`
|
||||||
|
})
|
||||||
|
).toBe(
|
||||||
|
escapeHtml(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
foo: 1,
|
||||||
|
bar: `<div>`
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
@ -1,11 +1,3 @@
|
|||||||
import { toDisplayString } from 'vue'
|
|
||||||
import { escape } from './escape'
|
|
||||||
|
|
||||||
export { escape }
|
|
||||||
|
|
||||||
export function interpolate(value: unknown) {
|
|
||||||
return escape(toDisplayString(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
export { renderToString, renderComponent, renderSlot } from './renderToString'
|
export { renderToString, renderComponent, renderSlot } from './renderToString'
|
||||||
export { renderClass, renderStyle, renderProps } from './renderProps'
|
export { renderClass, renderStyle, renderProps } from './renderProps'
|
||||||
|
export { escapeHtml, interpolate } from './ssrUtils'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { escape } from './escape'
|
import { escapeHtml } from './ssrUtils'
|
||||||
import {
|
import {
|
||||||
normalizeClass,
|
normalizeClass,
|
||||||
normalizeStyle,
|
normalizeStyle,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
isOn,
|
isOn,
|
||||||
isSSRSafeAttrName,
|
isSSRSafeAttrName,
|
||||||
isBooleanAttr
|
isBooleanAttr
|
||||||
} from '@vue/shared/src'
|
} from '@vue/shared'
|
||||||
|
|
||||||
export function renderProps(
|
export function renderProps(
|
||||||
props: Record<string, unknown>,
|
props: Record<string, unknown>,
|
||||||
@ -34,7 +34,7 @@ export function renderProps(
|
|||||||
ret += ` ${attrKey}`
|
ret += ` ${attrKey}`
|
||||||
}
|
}
|
||||||
} else if (isSSRSafeAttrName(attrKey)) {
|
} else if (isSSRSafeAttrName(attrKey)) {
|
||||||
ret += ` ${attrKey}="${escape(value)}"`
|
ret += ` ${attrKey}="${escapeHtml(value)}"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,13 +42,16 @@ export function renderProps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderClass(raw: unknown): string {
|
export function renderClass(raw: unknown): string {
|
||||||
return escape(normalizeClass(raw))
|
return escapeHtml(normalizeClass(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderStyle(raw: unknown): string {
|
export function renderStyle(raw: unknown): string {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
if (isString(raw)) {
|
||||||
|
return escapeHtml(raw)
|
||||||
|
}
|
||||||
const styles = normalizeStyle(raw)
|
const styles = normalizeStyle(raw)
|
||||||
let ret = ''
|
let ret = ''
|
||||||
for (const key in styles) {
|
for (const key in styles) {
|
||||||
@ -62,5 +65,5 @@ export function renderStyle(raw: unknown): string {
|
|||||||
ret += `${normalizedKey}:${value};`
|
ret += `${normalizedKey}:${value};`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return escape(ret)
|
return escapeHtml(ret)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
isVoidTag
|
isVoidTag
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { renderProps } from './renderProps'
|
import { renderProps } from './renderProps'
|
||||||
import { escape } from './escape'
|
import { escapeHtml } from './ssrUtils'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createComponentInstance,
|
createComponentInstance,
|
||||||
@ -105,7 +105,7 @@ function renderComponentVNode(
|
|||||||
const instance = createComponentInstance(vnode, parentComponent)
|
const instance = createComponentInstance(vnode, parentComponent)
|
||||||
const res = setupComponent(
|
const res = setupComponent(
|
||||||
instance,
|
instance,
|
||||||
null /* parentSuspense */,
|
null /* parentSuspense (no need to track for SSR) */,
|
||||||
true /* isSSR */
|
true /* isSSR */
|
||||||
)
|
)
|
||||||
if (isPromise(res)) {
|
if (isPromise(res)) {
|
||||||
@ -225,15 +225,15 @@ function renderElement(
|
|||||||
push(props.innerHTML)
|
push(props.innerHTML)
|
||||||
} else if (props.textContent) {
|
} else if (props.textContent) {
|
||||||
hasChildrenOverride = true
|
hasChildrenOverride = true
|
||||||
push(escape(props.textContent))
|
push(escapeHtml(props.textContent))
|
||||||
} else if (tag === 'textarea' && props.value) {
|
} else if (tag === 'textarea' && props.value) {
|
||||||
hasChildrenOverride = true
|
hasChildrenOverride = true
|
||||||
push(escape(props.value))
|
push(escapeHtml(props.value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasChildrenOverride) {
|
if (!hasChildrenOverride) {
|
||||||
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
|
||||||
push(escape(children as string))
|
push(escapeHtml(children as string))
|
||||||
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
|
||||||
renderVNodeChildren(
|
renderVNodeChildren(
|
||||||
push,
|
push,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { toDisplayString } from '@vue/shared'
|
||||||
|
|
||||||
const escapeRE = /["'&<>]/
|
const escapeRE = /["'&<>]/
|
||||||
|
|
||||||
export function escape(string: unknown) {
|
export function escapeHtml(string: unknown) {
|
||||||
const str = '' + string
|
const str = '' + string
|
||||||
const match = escapeRE.exec(str)
|
const match = escapeRE.exec(str)
|
||||||
|
|
||||||
@ -43,3 +45,7 @@ export function escape(string: unknown) {
|
|||||||
|
|
||||||
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
|
return lastIndex !== index ? html + str.substring(lastIndex, index) : html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function interpolate(value: unknown) {
|
||||||
|
return escapeHtml(toDisplayString(value))
|
||||||
|
}
|
@ -15,8 +15,8 @@ export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs)
|
|||||||
// The full list is needed during SSR to produce the correct initial markup.
|
// The full list is needed during SSR to produce the correct initial markup.
|
||||||
export const isBooleanAttr = /*#__PURE__*/ makeMap(
|
export const isBooleanAttr = /*#__PURE__*/ makeMap(
|
||||||
specialBooleanAttrs +
|
specialBooleanAttrs +
|
||||||
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,ismap,` +
|
`,async,autofocus,autoplay,controls,default,defer,disabled,hidden,` +
|
||||||
`loop,nomodule,open,required,reversed,scoped,seamless,` +
|
`loop,open,required,reversed,scoped,seamless,` +
|
||||||
`checked,muted,multiple,selected`
|
`checked,muted,multiple,selected`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user