aboutsummaryrefslogtreecommitdiff
path: root/node_modules/vue/src/server/template-renderer
diff options
context:
space:
mode:
authorruki <waruqi@gmail.com>2018-11-08 00:38:48 +0800
committerruki <waruqi@gmail.com>2018-11-07 21:53:09 +0800
commit26105034da4fcce7ac883c899d781f016559310d (patch)
treec459a5dc4e3aa0972d9919033ece511ce76dd129 /node_modules/vue/src/server/template-renderer
parent2c77f00f1a7ecb6c8192f9c16d3b2001b254a107 (diff)
downloadxmake-docs-26105034da4fcce7ac883c899d781f016559310d.tar.gz
xmake-docs-26105034da4fcce7ac883c899d781f016559310d.zip
switch to vuepress
Diffstat (limited to 'node_modules/vue/src/server/template-renderer')
-rw-r--r--node_modules/vue/src/server/template-renderer/create-async-file-mapper.js53
-rw-r--r--node_modules/vue/src/server/template-renderer/index.js257
-rw-r--r--node_modules/vue/src/server/template-renderer/parse-template.js42
-rw-r--r--node_modules/vue/src/server/template-renderer/template-stream.js82
4 files changed, 434 insertions, 0 deletions
diff --git a/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js b/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js
new file mode 100644
index 00000000..64c0647a
--- /dev/null
+++ b/node_modules/vue/src/server/template-renderer/create-async-file-mapper.js
@@ -0,0 +1,53 @@
+/* @flow */
+
+/**
+ * Creates a mapper that maps components used during a server-side render
+ * to async chunk files in the client-side build, so that we can inline them
+ * directly in the rendered HTML to avoid waterfall requests.
+ */
+
+import type { ClientManifest } from './index'
+
+export type AsyncFileMapper = (files: Array<string>) => Array<string>;
+
+export function createMapper (
+ clientManifest: ClientManifest
+): AsyncFileMapper {
+ const map = createMap(clientManifest)
+ // map server-side moduleIds to client-side files
+ return function mapper (moduleIds: Array<string>): Array<string> {
+ const res = new Set()
+ for (let i = 0; i < moduleIds.length; i++) {
+ const mapped = map.get(moduleIds[i])
+ if (mapped) {
+ for (let j = 0; j < mapped.length; j++) {
+ res.add(mapped[j])
+ }
+ }
+ }
+ return Array.from(res)
+ }
+}
+
+function createMap (clientManifest) {
+ const map = new Map()
+ Object.keys(clientManifest.modules).forEach(id => {
+ map.set(id, mapIdToFile(id, clientManifest))
+ })
+ return map
+}
+
+function mapIdToFile (id, clientManifest) {
+ const files = []
+ const fileIndices = clientManifest.modules[id]
+ if (fileIndices) {
+ fileIndices.forEach(index => {
+ const file = clientManifest.all[index]
+ // only include async files or non-js assets
+ if (clientManifest.async.indexOf(file) > -1 || !(/\.js($|\?)/.test(file))) {
+ files.push(file)
+ }
+ })
+ }
+ return files
+}
diff --git a/node_modules/vue/src/server/template-renderer/index.js b/node_modules/vue/src/server/template-renderer/index.js
new file mode 100644
index 00000000..dabc08a1
--- /dev/null
+++ b/node_modules/vue/src/server/template-renderer/index.js
@@ -0,0 +1,257 @@
+/* @flow */
+
+const path = require('path')
+const serialize = require('serialize-javascript')
+
+import { isJS, isCSS } from '../util'
+import TemplateStream from './template-stream'
+import { parseTemplate } from './parse-template'
+import { createMapper } from './create-async-file-mapper'
+import type { ParsedTemplate } from './parse-template'
+import type { AsyncFileMapper } from './create-async-file-mapper'
+
+type TemplateRendererOptions = {
+ template: ?string;
+ inject?: boolean;
+ clientManifest?: ClientManifest;
+ shouldPreload?: (file: string, type: string) => boolean;
+ shouldPrefetch?: (file: string, type: string) => boolean;
+};
+
+export type ClientManifest = {
+ publicPath: string;
+ all: Array<string>;
+ initial: Array<string>;
+ async: Array<string>;
+ modules: {
+ [id: string]: Array<number>;
+ },
+ hasNoCssVersion?: {
+ [file: string]: boolean;
+ }
+};
+
+type Resource = {
+ file: string;
+ extension: string;
+ fileWithoutQuery: string;
+ asType: string;
+};
+
+export default class TemplateRenderer {
+ options: TemplateRendererOptions;
+ inject: boolean;
+ parsedTemplate: ParsedTemplate | null;
+ publicPath: string;
+ clientManifest: ClientManifest;
+ preloadFiles: Array<Resource>;
+ prefetchFiles: Array<Resource>;
+ mapFiles: AsyncFileMapper;
+
+ constructor (options: TemplateRendererOptions) {
+ this.options = options
+ 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)
+ : null
+
+ // extra functionality with client manifest
+ if (options.clientManifest) {
+ const clientManifest = this.clientManifest = options.clientManifest
+ this.publicPath = clientManifest.publicPath.replace(/\/$/, '')
+ // preload/prefetch directives
+ this.preloadFiles = (clientManifest.initial || []).map(normalizeFile)
+ this.prefetchFiles = (clientManifest.async || []).map(normalizeFile)
+ // initial async chunk mapping
+ this.mapFiles = createMapper(clientManifest)
+ }
+ }
+
+ bindRenderFns (context: Object) {
+ const renderer: any = this
+ ;['ResourceHints', 'State', 'Scripts', 'Styles'].forEach(type => {
+ context[`render${type}`] = renderer[`render${type}`].bind(renderer, context)
+ })
+ // also expose getPreloadFiles, useful for HTTP/2 push
+ context.getPreloadFiles = renderer.getPreloadFiles.bind(renderer, context)
+ }
+
+ // render synchronously given rendered app content and render context
+ renderSync (content: string, context: ?Object) {
+ const template = this.parsedTemplate
+ if (!template) {
+ throw new Error('renderSync cannot be called without a template.')
+ }
+ context = context || {}
+ if (this.inject) {
+ return (
+ template.head(context) +
+ (context.head || '') +
+ this.renderResourceHints(context) +
+ this.renderStyles(context) +
+ template.neck(context) +
+ content +
+ this.renderState(context) +
+ this.renderScripts(context) +
+ template.tail(context)
+ )
+ } else {
+ return (
+ template.head(context) +
+ template.neck(context) +
+ content +
+ template.tail(context)
+ )
+ }
+ }
+
+ renderStyles (context: Object): string {
+ const cssFiles = this.clientManifest
+ ? this.clientManifest.all.filter(isCSS)
+ : []
+ return (
+ // render links for css files
+ (cssFiles.length
+ ? cssFiles.map(file => `<link rel="stylesheet" href="${this.publicPath}/${file}">`).join('')
+ : '') +
+ // context.styles is a getter exposed by vue-style-loader which contains
+ // the inline component styles collected during SSR
+ (context.styles || '')
+ )
+ }
+
+ renderResourceHints (context: Object): string {
+ return this.renderPreloadLinks(context) + this.renderPrefetchLinks(context)
+ }
+
+ getPreloadFiles (context: Object): Array<Resource> {
+ const usedAsyncFiles = this.getUsedAsyncFiles(context)
+ if (this.preloadFiles || usedAsyncFiles) {
+ return (this.preloadFiles || []).concat(usedAsyncFiles || [])
+ } else {
+ return []
+ }
+ }
+
+ renderPreloadLinks (context: Object): string {
+ const files = this.getPreloadFiles(context)
+ const shouldPreload = this.options.shouldPreload
+ if (files.length) {
+ return files.map(({ file, extension, fileWithoutQuery, asType }) => {
+ let extra = ''
+ // by default, we only preload scripts or css
+ if (!shouldPreload && asType !== 'script' && asType !== 'style') {
+ return ''
+ }
+ // user wants to explicitly control what to preload
+ if (shouldPreload && !shouldPreload(fileWithoutQuery, asType)) {
+ return ''
+ }
+ if (asType === 'font') {
+ extra = ` type="font/${extension}" crossorigin`
+ }
+ return `<link rel="preload" href="${
+ this.publicPath}/${file
+ }"${
+ asType !== '' ? ` as="${asType}"` : ''
+ }${
+ extra
+ }>`
+ }).join('')
+ } else {
+ return ''
+ }
+ }
+
+ renderPrefetchLinks (context: Object): string {
+ const shouldPrefetch = this.options.shouldPrefetch
+ if (this.prefetchFiles) {
+ const usedAsyncFiles = this.getUsedAsyncFiles(context)
+ const alreadyRendered = file => {
+ return usedAsyncFiles && usedAsyncFiles.some(f => f.file === file)
+ }
+ return this.prefetchFiles.map(({ file, fileWithoutQuery, asType }) => {
+ if (shouldPrefetch && !shouldPrefetch(fileWithoutQuery, asType)) {
+ return ''
+ }
+ if (alreadyRendered(file)) {
+ return ''
+ }
+ return `<link rel="prefetch" href="${this.publicPath}/${file}">`
+ }).join('')
+ } else {
+ return ''
+ }
+ }
+
+ renderState (context: Object, options?: Object): string {
+ const {
+ contextKey = 'state',
+ windowKey = '__INITIAL_STATE__'
+ } = options || {}
+ const state = serialize(context[contextKey], { isJSON: true })
+ const autoRemove = process.env.NODE_ENV === 'production'
+ ? ';(function(){var s;(s=document.currentScript||document.scripts[document.scripts.length-1]).parentNode.removeChild(s);}());'
+ : ''
+ return context[contextKey]
+ ? `<script>window.${windowKey}=${state}${autoRemove}</script>`
+ : ''
+ }
+
+ renderScripts (context: Object): string {
+ if (this.clientManifest) {
+ const initial = this.preloadFiles
+ const async = this.getUsedAsyncFiles(context)
+ const needed = [initial[0]].concat(async || [], initial.slice(1))
+ return needed.filter(({ file }) => isJS(file)).map(({ file }) => {
+ return `<script src="${this.publicPath}/${file}" defer></script>`
+ }).join('')
+ } else {
+ return ''
+ }
+ }
+
+ getUsedAsyncFiles (context: Object): ?Array<Resource> {
+ if (!context._mappedFiles && context._registeredComponents && this.mapFiles) {
+ const registered = Array.from(context._registeredComponents)
+ context._mappedFiles = this.mapFiles(registered).map(normalizeFile)
+ }
+ return context._mappedFiles
+ }
+
+ // create a transform stream
+ createStream (context: ?Object): TemplateStream {
+ if (!this.parsedTemplate) {
+ throw new Error('createStream cannot be called without a template.')
+ }
+ return new TemplateStream(this, this.parsedTemplate, context || {})
+ }
+}
+
+function normalizeFile (file: string): Resource {
+ const withoutQuery = file.replace(/\?.*/, '')
+ const extension = path.extname(withoutQuery).slice(1)
+ return {
+ file,
+ extension,
+ fileWithoutQuery: withoutQuery,
+ asType: getPreloadType(extension)
+ }
+}
+
+function getPreloadType (ext: string): string {
+ if (ext === 'js') {
+ return 'script'
+ } else if (ext === 'css') {
+ return 'style'
+ } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) {
+ return 'image'
+ } else if (/woff2?|ttf|otf|eot/.test(ext)) {
+ return 'font'
+ } else {
+ // not exhausting all possibilities here, but above covers common cases
+ return ''
+ }
+}
diff --git a/node_modules/vue/src/server/template-renderer/parse-template.js b/node_modules/vue/src/server/template-renderer/parse-template.js
new file mode 100644
index 00000000..1ccfe899
--- /dev/null
+++ b/node_modules/vue/src/server/template-renderer/parse-template.js
@@ -0,0 +1,42 @@
+/* @flow */
+
+const compile = require('lodash.template')
+const compileOptions = {
+ escape: /{{([^{][\s\S]+?[^}])}}/g,
+ interpolate: /{{{([\s\S]+?)}}}/g
+}
+
+export type ParsedTemplate = {
+ head: (data: any) => string;
+ neck: (data: any) => string;
+ tail: (data: any) => string;
+};
+
+export function parseTemplate (
+ template: string,
+ contentPlaceholder?: string = '<!--vue-ssr-outlet-->'
+): ParsedTemplate {
+ if (typeof template === 'object') {
+ return template
+ }
+
+ let i = template.indexOf('</head>')
+ const j = template.indexOf(contentPlaceholder)
+
+ if (j < 0) {
+ throw new Error(`Content placeholder not found in template.`)
+ }
+
+ if (i < 0) {
+ i = template.indexOf('<body>')
+ if (i < 0) {
+ i = j
+ }
+ }
+
+ return {
+ head: compile(template.slice(0, i), compileOptions),
+ neck: compile(template.slice(i, j), compileOptions),
+ tail: compile(template.slice(j + contentPlaceholder.length), compileOptions)
+ }
+}
diff --git a/node_modules/vue/src/server/template-renderer/template-stream.js b/node_modules/vue/src/server/template-renderer/template-stream.js
new file mode 100644
index 00000000..ed4db78f
--- /dev/null
+++ b/node_modules/vue/src/server/template-renderer/template-stream.js
@@ -0,0 +1,82 @@
+/* @flow */
+
+const Transform = require('stream').Transform
+import type TemplateRenderer from './index'
+import type { ParsedTemplate } from './parse-template'
+
+export default class TemplateStream extends Transform {
+ started: boolean;
+ renderer: TemplateRenderer;
+ template: ParsedTemplate;
+ context: Object;
+ inject: boolean;
+
+ constructor (
+ renderer: TemplateRenderer,
+ template: ParsedTemplate,
+ context: Object
+ ) {
+ super()
+ this.started = false
+ this.renderer = renderer
+ this.template = template
+ this.context = context || {}
+ this.inject = renderer.inject
+ }
+
+ _transform (data: Buffer | string, encoding: string, done: Function) {
+ if (!this.started) {
+ this.emit('beforeStart')
+ this.start()
+ }
+ this.push(data)
+ done()
+ }
+
+ start () {
+ this.started = true
+ this.push(this.template.head(this.context))
+
+ if (this.inject) {
+ // inline server-rendered head meta information
+ if (this.context.head) {
+ this.push(this.context.head)
+ }
+
+ // inline preload/prefetch directives for initial/async chunks
+ const links = this.renderer.renderResourceHints(this.context)
+ if (links) {
+ this.push(links)
+ }
+
+ // CSS files and inline server-rendered CSS collected by vue-style-loader
+ const styles = this.renderer.renderStyles(this.context)
+ if (styles) {
+ this.push(styles)
+ }
+ }
+
+ this.push(this.template.neck(this.context))
+ }
+
+ _flush (done: Function) {
+ this.emit('beforeEnd')
+
+ if (this.inject) {
+ // inline initial store state
+ const state = this.renderer.renderState(this.context)
+ if (state) {
+ this.push(state)
+ }
+
+ // embed scripts needed
+ const scripts = this.renderer.renderScripts(this.context)
+ if (scripts) {
+ this.push(scripts)
+ }
+ }
+
+ this.push(this.template.tail(this.context))
+ done()
+ }
+}