Repository: vuejs/vue
Pull request: [feature] allow template to be function in vue-server-renderer

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [ ] Bugfix
- [x] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [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
- [ ] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [x] 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:**

@yyx990803 I've found a solution to #9205 

It's a very simple addition, to the `vue-server-renderer` package that has tonnes of use-cases and actually makes the `vue-server-renderer` much easier to use, as it provides a template function to the bundle renderer template compiler.

This isn't a breaking change and doesn't really need any tests, as it is a user-provided function

It would be amazing if this could be added in **2.6**, so I could use it in my package

### Use cases
1. Allows head management within `single file components` (see #9205)
2. Allows async functions within the `template` option
3. Makes the `vue-server-renderer` templater much easier to use, and allows users to use javascript interpolations. For example: `<!--vue-ssr-outlet-->` becomes `${result}`

#### Bonus
I'm creating a serverside rendering templating engine for **Vue** that will use this feature. It includes:
- A super simple head management system that exists entirely within the `<template>`, like native html
- full server-side rendering with Webpack
- CommonJS based, so users can easily implement it into koa/express projects
- templating using slots
- runtime compilation of Vue files (which are cached)

The package is aimed for users who want to start using Vue but want to keep express/koa routing which will make building serverside applications much easier for users, and will increase Vue adoption in the process

Changed files:
filename: src/server/create-renderer.js
patch: @@ -23,7 +23,7 @@ export type RenderOptions = {
   directives?: Object;
   isUnaryTag?: Function;
   cache?: RenderCache;
-  template?: string;
+  template?: string | (content: string, context: any) => string;
   inject?: boolean;
   basedir?: string;
   shouldPreload?: Function;
@@ -82,14 +82,26 @@ export function createRenderer ({
       }, cb)
       try {
         render(component, write, context, err => {
+          if (err) {
+            return cb(err)
+          }
           if (context && context.rendered) {
             context.rendered(context)
           }
           if (template) {
-            result = templateRenderer.renderSync(result, context)
-          }
-          if (err) {
-            cb(err)
+            try {
+              const res = templateRenderer.render(result, context)
+              if (typeof res !== 'string') {
+                // function template returning promise
+                res
+                  .then(html => cb(null, html))
+                  .catch(cb)
+              } else {
+                cb(null, res)
+              }
+            } catch (e) {
+              cb(e)
+            }
           } else {
             cb(null, result)
           }
@@ -119,6 +131,8 @@ export function createRenderer ({
           })
         }
         return renderStream
+      } else if (typeof template === 'function') {
+        throw new Error(`function template is only supported in renderToString.`)
       } else {
         const templateStream = templateRenderer.createStream(context)
         renderStream.on('error', err => {

filename: src/server/template-renderer/index.js
patch: @@ -11,7 +11,7 @@ import type { ParsedTemplate } from './parse-template'
 import type { AsyncFileMapper } from './create-async-file-mapper'
 
 type TemplateRendererOptions = {
-  template: ?string;
+  template?: string | (content: string, context: any) => string;
   inject?: boolean;
   clientManifest?: ClientManifest;
   shouldPreload?: (file: string, type: string) => boolean;
@@ -42,7 +42,7 @@ type Resource = {
 export default class TemplateRenderer {
   options: TemplateRendererOptions;
   inject: boolean;
-  parsedTemplate: ParsedTemplate | null;
+  parsedTemplate: ParsedTemplate | Function | null;
   publicPath: string;
   clientManifest: ClientManifest;
   preloadFiles: Array<Resource>;
@@ -55,8 +55,12 @@ export default class TemplateRenderer {
     this.inject = options.inject !== false
     // if no template option is provided, the renderer is created
     // as a utility object for rendering assets like preload links and scripts.
-    this.parsedTemplate = options.template
-      ? parseTemplate(options.template)
+    
+    const { template } = options
+    this.parsedTemplate = template
+      ? typeof template === 'string'
+        ? parseTemplate(template)
+        : template
       : null
 
     // function used to serialize initial state JSON
@@ -89,12 +93,17 @@ export default class TemplateRenderer {
   }
 
   // render synchronously given rendered app content and render context
-  renderSync (content: string, context: ?Object) {
+  render (content: string, context: ?Object): string | Promise<string> {
     const template = this.parsedTemplate
     if (!template) {
-      throw new Error('renderSync cannot be called without a template.')
+      throw new Error('render cannot be called without a template.')
     }
     context = context || {}
+
+    if (typeof template === 'function') {
+      return template(content, context)
+    }
+
     if (this.inject) {
       return (
         template.head(context) +