Repository: vuejs/vue
Pull request: fix(src/server/render.js): fixes component directives rendering (fix #10733)

This is a follow-up of https://github.com/vuejs/vue/pull/11287 which resolves #10733.

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

**The PR fulfills these requirements:**

- [ ] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch - **No** because `dev` seems to be outdated.
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**


Changed files:
filename: packages/server-renderer/src/render.ts
patch: @@ -206,6 +206,11 @@ function renderComponentInner(node, isRoot, context) {
       type: 'Component',
       prevActive
     })
+    if (isDef(node.data) && isDef(node.data.directives)) {
+      childNode.data = childNode.data || {}
+      childNode.data.directives = node.data.directives
+      childNode.isComponentRootElement = true
+    }
     renderNode(childNode, isRoot, context)
   }
 
@@ -372,7 +377,10 @@ function renderStartingTag(node: VNode, context) {
           if (dirRenderer) {
             // directives mutate the node's data
             // which then gets rendered by modules
-            dirRenderer(node, dirs[i])
+            dirRenderer(
+              node.isComponentRootElement ? node.parent : node,
+              dirs[i]
+            )
           }
         }
       }

filename: packages/server-renderer/test/ssr-string.spec.ts
patch: @@ -1086,7 +1086,7 @@ describe('SSR: renderToString', () => {
     )
   })
 
-  it('custom directives', done => {
+  it('custom directives on raw element', done => {
     const renderer = createRenderer({
       directives: {
         'class-prefixer': (node, dir) => {
@@ -1129,6 +1129,98 @@ describe('SSR: renderToString', () => {
     )
   })
 
+  it('custom directives on component', done => {
+    const Test = {
+      template: '<span>hello world</span>'
+    }
+    const renderer = createRenderer({
+      directives: {
+        'class-prefixer': (node, dir) => {
+          if (node.data.class) {
+            node.data.class = `${dir.value}-${node.data.class}`
+          }
+          if (node.data.staticClass) {
+            node.data.staticClass = `${dir.value}-${node.data.staticClass}`
+          }
+        }
+      }
+    })
+    renderer.renderToString(
+      new Vue({
+        template:
+          '<p><Test v-class-prefixer="\'my\'" class="class1" :class="\'class2\'" /></p>',
+        components: { Test }
+      }),
+      (err, result) => {
+        expect(err).toBeNull()
+        expect(result).toContain(
+          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
+        )
+        done()
+      }
+    )
+  })
+
+  it('custom directives on element root of a component', done => {
+    const Test = {
+      template:
+        '<span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span>'
+    }
+    const renderer = createRenderer({
+      directives: {
+        'class-prefixer': (node, dir) => {
+          if (node.data.class) {
+            node.data.class = `${dir.value}-${node.data.class}`
+          }
+          if (node.data.staticClass) {
+            node.data.staticClass = `${dir.value}-${node.data.staticClass}`
+          }
+        }
+      }
+    })
+    renderer.renderToString(
+      new Vue({
+        template: '<p><Test /></p>',
+        components: { Test }
+      }),
+      (err, result) => {
+        expect(err).toBeNull()
+        expect(result).toContain(
+          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
+        )
+        done()
+      }
+    )
+  })
+
+  it('custom directives on element with parent element', done => {
+    const renderer = createRenderer({
+      directives: {
+        'class-prefixer': (node, dir) => {
+          if (node.data.class) {
+            node.data.class = `${dir.value}-${node.data.class}`
+          }
+          if (node.data.staticClass) {
+            node.data.staticClass = `${dir.value}-${node.data.staticClass}`
+          }
+        }
+      }
+    })
+    renderer.renderToString(
+      new Vue({
+        template:
+          '<p><span v-class-prefixer="\'my\'" class="class1" :class="\'class2\'">hello world</span></p>'
+      }),
+      (err, result) => {
+        expect(err).toBeNull()
+        expect(result).toContain(
+          '<p data-server-rendered="true"><span class="my-class1 my-class2">hello world</span></p>'
+        )
+        done()
+      }
+    )
+  })
+
   it('should not warn for custom directives that do not have server-side implementation', done => {
     renderToString(
       new Vue({

filename: src/core/vdom/vnode.ts
patch: @@ -33,6 +33,7 @@ export default class VNode {
   fnOptions?: ComponentOptions | null // for SSR caching
   devtoolsMeta?: Object | null // used to store functional render context for devtools
   fnScopeId?: string | null // functional scope id support
+  isComponentRootElement?: boolean | null // for SSR directives
 
   constructor(
     tag?: string,