diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index 0a71eeefeed..a2f0b5a0697 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -17,6 +17,22 @@ describe('ssr: components', () => { `) }) + test('static style', () => { + expect(compile(`
`).code) + .toMatchInlineSnapshot(` + "const { resolveComponent: _resolveComponent } = require("vue") + const { ssrRenderComponent: _ssrRenderComponent, ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + const _component_foo = _resolveComponent("foo") + + _push(\`\`) + _push(_ssrRenderComponent(_component_foo, { style: {"color":"red"} }, null, _parent)) + _push(\`\`) + }" + `) + }) + // event listeners should still be passed test('event listeners', () => { expect(compile(``).code).toMatchInlineSnapshot(` diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index 504093f3534..4d52e75c43f 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -164,6 +164,35 @@ describe('ssr: element', () => { `) }) + test('static style', () => { + expect( + getCompiledString( + `
`, + ), + ).toMatchInlineSnapshot( + `"\`
\`"`, + ) + }) + + test('static style with escaping', () => { + expect( + getCompiledString( + `
`, + ), + ).toMatchInlineSnapshot( + `"\`
\`"`, + ) + }) + + test('v-bind:style + static style', () => { + expect(getCompiledString(`
`)) + .toMatchInlineSnapshot(` + "\`
\`" + `) + }) + test('static style + v-bind:style', () => { expect(getCompiledString(`
`)) .toMatchInlineSnapshot(` @@ -274,6 +303,13 @@ describe('ssr: element', () => { }>\`" `) + expect(getCompiledString(`
`)) + .toMatchInlineSnapshot(` + "\`\`" + `) + // should merge style and :style expect( getCompiledString( diff --git a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts index 3adfe6480d6..c47f66f7b71 100644 --- a/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts @@ -11,6 +11,17 @@ describe('ssr: attrs fallthrough', () => { `) }) + test('with static style', () => { + expect(compile(`
`).code).toMatchInlineSnapshot(` + "const { mergeProps: _mergeProps } = require("vue") + const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") + + return function ssrRender(_ctx, _push, _parent, _attrs) { + _push(\`
\`) + }" + `) + }) + test('with comments', () => { expect(compile(`
`).code).toMatchInlineSnapshot(` "const { ssrRenderAttrs: _ssrRenderAttrs } = require("vue/server-renderer") diff --git a/packages/compiler-ssr/src/index.ts b/packages/compiler-ssr/src/index.ts index cc1658d783c..07649859769 100644 --- a/packages/compiler-ssr/src/index.ts +++ b/packages/compiler-ssr/src/index.ts @@ -12,11 +12,13 @@ import { transformBind, transformExpression, transformOn, - transformStyle, transformVBindShorthand, } from '@vue/compiler-dom' import { ssrCodegenTransform } from './ssrCodegenTransform' -import { ssrTransformElement } from './transforms/ssrTransformElement' +import { + ssrTransformElement, + ssrTransformStyle, +} from './transforms/ssrTransformElement' import { rawOptionsMap, ssrTransformComponent, @@ -67,7 +69,7 @@ export function compile( ssrTransformElement, ssrTransformComponent, trackSlotScopes, - transformStyle, + ssrTransformStyle, ...(options.nodeTransforms || []), // user transforms ], directiveTransforms: { diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts index 409f8bc716d..de0461c27c0 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts @@ -32,6 +32,7 @@ import { hasDynamicKeyVBind, isStaticArgOf, isStaticExp, + transformStyle, } from '@vue/compiler-dom' import { NO, @@ -39,7 +40,9 @@ import { isBooleanAttr, isBuiltInDirective, isSSRSafeAttrName, + parseStringStyle, propsToAttrMap, + stringifyStyle, } from '@vue/shared' import { SSRErrorCodes, createSSRCompilerError } from '../errors' import { @@ -65,6 +68,16 @@ const rawChildrenMap = new WeakMap< TemplateLiteral['elements'][0] >() +// static styles on plain elements are rendered by ssrTransformElement +export const ssrTransformStyle: NodeTransform = (node, context) => { + if ( + node.type === NodeTypes.ELEMENT && + node.tagType !== ElementTypes.ELEMENT + ) { + transformStyle(node, context) + } +} + export const ssrTransformElement: NodeTransform = (node, context) => { if ( node.type !== NodeTypes.ELEMENT || @@ -101,6 +114,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => { const needMergeProps = hasDynamicVBind || hasCustomDir if (needMergeProps) { + transformStyle(node, context) const { props, directives } = buildProps( node, context, @@ -213,9 +227,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => { // book keeping static/dynamic class merging. let dynamicClassBinding: CallExpression | undefined = undefined let staticClassBinding: string | undefined = undefined - // all style bindings are converted to dynamic by transformStyle. - // but we need to make sure to merge them. let dynamicStyleBinding: CallExpression | undefined = undefined + let staticStyleBinding: string | undefined = undefined + let staticStyleIndex = -1 for (let i = 0; i < node.props.length; i++) { const prop = node.props[i] @@ -274,14 +288,20 @@ export const ssrTransformElement: NodeTransform = (node, context) => { // already has style binding, merge into it. mergeCall(dynamicStyleBinding, value) } else { - openTag.push( - ` style="`, - (dynamicStyleBinding = createCallExpression( - context.helper(SSR_RENDER_STYLE), - [value], - )), - `"`, + dynamicStyleBinding = createCallExpression( + context.helper(SSR_RENDER_STYLE), + [ + staticStyleBinding + ? createArrayExpression([staticStyleBinding, value]) + : value, + ], ) + const parts = [` style="`, dynamicStyleBinding, `"`] + if (staticStyleBinding) { + openTag.splice(staticStyleIndex, 1, ...parts) + } else { + openTag.push(...parts) + } } } else { attrName = @@ -354,6 +374,17 @@ export const ssrTransformElement: NodeTransform = (node, context) => { if (name === 'class' && prop.value) { staticClassBinding = JSON.stringify(prop.value.content) } + if (name === 'style' && prop.value) { + const style = parseStringStyle(prop.value.content) + if (dynamicStyleBinding) { + mergeCall(dynamicStyleBinding, JSON.stringify(style)) + } else { + staticStyleBinding = JSON.stringify(style) + staticStyleIndex = openTag.length + openTag.push(` style="${escapeHtml(stringifyStyle(style))}"`) + } + continue + } openTag.push( ` ${prop.name}` + (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``), diff --git a/packages/server-renderer/__tests__/render.spec.ts b/packages/server-renderer/__tests__/render.spec.ts index c40b2934a5a..d9ef9c32d51 100644 --- a/packages/server-renderer/__tests__/render.spec.ts +++ b/packages/server-renderer/__tests__/render.spec.ts @@ -838,6 +838,44 @@ function testRender(type: string, render: typeof renderToString) { ).toBe(`
hello
`) }) + test('static style', async () => { + expect( + await render( + createApp({ + components: { + Child: { + inheritAttrs: false, + template: `{{ JSON.stringify($attrs.style) }}`, + }, + }, + data: () => ({ dynamic: { color: 'blue' }, obj: { id: 'a' } }), + template: + `
` + + `

` + + `

` + + `

` + + `

` + + `

` + + `

` + + `` + + `` + + `
`, + }), + ), + ).toBe( + `
` + + `

` + + `

` + + `

` + + `

` + + `

` + + `

` + + `` + + `{"color":"red","z-index":"1"}` + + `
`, + ) + }) + test('handle compiler errors', async () => { await render( // render different content since compilation is cached diff --git a/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts b/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts index e8cfa75e77c..4422b3bd689 100644 --- a/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts +++ b/packages/server-renderer/__tests__/ssrAttrFallthrough.spec.ts @@ -32,6 +32,27 @@ describe('ssr: attr fallthrough', () => { ) }) + test('with static style', async () => { + const Child = { + props: ['ok'], + template: `
`, + } + const TransitionChild = { + template: `
`, + } + const Parent = { + props: ['ok'], + components: { Child, TransitionChild }, + template: `
`, + } + expect(await renderToString(createApp(Parent, { ok: true }))).toBe( + `
`, + ) + expect(await renderToString(createApp(Parent, { ok: false }))).toBe( + `
`, + ) + }) + test('with v-model', async () => { const Child = { props: ['text'],