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'],