diff options
Diffstat (limited to 'node_modules/vue/src/server')
22 files changed, 2481 insertions, 0 deletions
diff --git a/node_modules/vue/src/server/bundle-renderer/create-bundle-renderer.js b/node_modules/vue/src/server/bundle-renderer/create-bundle-renderer.js new file mode 100644 index 00000000..05c60c35 --- /dev/null +++ b/node_modules/vue/src/server/bundle-renderer/create-bundle-renderer.js @@ -0,0 +1,151 @@ +/* @flow */ + +import { createPromiseCallback } from '../util' +import { createBundleRunner } from './create-bundle-runner' +import type { Renderer, RenderOptions } from '../create-renderer' +import { createSourceMapConsumers, rewriteErrorTrace } from './source-map-support' + +const fs = require('fs') +const path = require('path') +const PassThrough = require('stream').PassThrough + +const INVALID_MSG = + 'Invalid server-rendering bundle format. Should be a string ' + + 'or a bundle Object of type:\n\n' + +`{ + entry: string; + files: { [filename: string]: string; }; + maps: { [filename: string]: string; }; +}\n` + +// The render bundle can either be a string (single bundled file) +// or a bundle manifest object generated by vue-ssr-webpack-plugin. +type RenderBundle = { + basedir?: string; + entry: string; + files: { [filename: string]: string; }; + maps: { [filename: string]: string; }; + modules?: { [filename: string]: Array<string> }; +}; + +export function createBundleRendererCreator ( + createRenderer: (options?: RenderOptions) => Renderer +) { + return function createBundleRenderer ( + bundle: string | RenderBundle, + rendererOptions?: RenderOptions = {} + ) { + let files, entry, maps + let basedir = rendererOptions.basedir + + // load bundle if given filepath + if ( + typeof bundle === 'string' && + /\.js(on)?$/.test(bundle) && + path.isAbsolute(bundle) + ) { + if (fs.existsSync(bundle)) { + const isJSON = /\.json$/.test(bundle) + basedir = basedir || path.dirname(bundle) + bundle = fs.readFileSync(bundle, 'utf-8') + if (isJSON) { + try { + bundle = JSON.parse(bundle) + } catch (e) { + throw new Error(`Invalid JSON bundle file: ${bundle}`) + } + } + } else { + throw new Error(`Cannot locate bundle file: ${bundle}`) + } + } + + if (typeof bundle === 'object') { + entry = bundle.entry + files = bundle.files + basedir = basedir || bundle.basedir + maps = createSourceMapConsumers(bundle.maps) + if (typeof entry !== 'string' || typeof files !== 'object') { + throw new Error(INVALID_MSG) + } + } else if (typeof bundle === 'string') { + entry = '__vue_ssr_bundle__' + files = { '__vue_ssr_bundle__': bundle } + maps = {} + } else { + throw new Error(INVALID_MSG) + } + + const renderer = createRenderer(rendererOptions) + + const run = createBundleRunner( + entry, + files, + basedir, + rendererOptions.runInNewContext + ) + + return { + renderToString: (context?: Object, cb: any) => { + if (typeof context === 'function') { + cb = context + context = {} + } + + let promise + if (!cb) { + ({ promise, cb } = createPromiseCallback()) + } + + run(context).catch(err => { + rewriteErrorTrace(err, maps) + cb(err) + }).then(app => { + if (app) { + renderer.renderToString(app, context, (err, res) => { + rewriteErrorTrace(err, maps) + cb(err, res) + }) + } + }) + + return promise + }, + + renderToStream: (context?: Object) => { + const res = new PassThrough() + run(context).catch(err => { + rewriteErrorTrace(err, maps) + // avoid emitting synchronously before user can + // attach error listener + process.nextTick(() => { + res.emit('error', err) + }) + }).then(app => { + if (app) { + const renderStream = renderer.renderToStream(app, context) + + renderStream.on('error', err => { + rewriteErrorTrace(err, maps) + res.emit('error', err) + }) + + // relay HTMLStream special events + if (rendererOptions && rendererOptions.template) { + renderStream.on('beforeStart', () => { + res.emit('beforeStart') + }) + renderStream.on('beforeEnd', () => { + res.emit('beforeEnd') + }) + } + + renderStream.pipe(res) + } + }) + + return res + } + } + } +} diff --git a/node_modules/vue/src/server/bundle-renderer/create-bundle-runner.js b/node_modules/vue/src/server/bundle-renderer/create-bundle-runner.js new file mode 100644 index 00000000..16c33c41 --- /dev/null +++ b/node_modules/vue/src/server/bundle-renderer/create-bundle-runner.js @@ -0,0 +1,150 @@ +import { isPlainObject } from 'shared/util' + +const vm = require('vm') +const path = require('path') +const resolve = require('resolve') +const NativeModule = require('module') + +function createSandbox (context) { + const sandbox = { + Buffer, + console, + process, + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate, + __VUE_SSR_CONTEXT__: context + } + sandbox.global = sandbox + return sandbox +} + +function compileModule (files, basedir, runInNewContext) { + const compiledScripts = {} + const resolvedModules = {} + + function getCompiledScript (filename) { + if (compiledScripts[filename]) { + return compiledScripts[filename] + } + const code = files[filename] + const wrapper = NativeModule.wrap(code) + const script = new vm.Script(wrapper, { + filename, + displayErrors: true + }) + compiledScripts[filename] = script + return script + } + + function evaluateModule (filename, sandbox, evaluatedFiles = {}) { + if (evaluatedFiles[filename]) { + return evaluatedFiles[filename] + } + + const script = getCompiledScript(filename) + const compiledWrapper = runInNewContext === false + ? script.runInThisContext() + : script.runInNewContext(sandbox) + const m = { exports: {}} + const r = file => { + file = path.posix.join('.', file) + if (files[file]) { + return evaluateModule(file, sandbox, evaluatedFiles) + } else if (basedir) { + return require( + resolvedModules[file] || + (resolvedModules[file] = resolve.sync(file, { basedir })) + ) + } else { + return require(file) + } + } + compiledWrapper.call(m.exports, m.exports, r, m) + + const res = Object.prototype.hasOwnProperty.call(m.exports, 'default') + ? m.exports.default + : m.exports + evaluatedFiles[filename] = res + return res + } + return evaluateModule +} + +function deepClone (val) { + if (isPlainObject(val)) { + const res = {} + for (const key in val) { + res[key] = deepClone(val[key]) + } + return res + } else if (Array.isArray(val)) { + return val.slice() + } else { + return val + } +} + +export function createBundleRunner (entry, files, basedir, runInNewContext) { + const evaluate = compileModule(files, basedir, runInNewContext) + if (runInNewContext !== false && runInNewContext !== 'once') { + // new context mode: creates a fresh context and re-evaluate the bundle + // on each render. Ensures entire application state is fresh for each + // render, but incurs extra evaluation cost. + return (userContext = {}) => new Promise(resolve => { + userContext._registeredComponents = new Set() + const res = evaluate(entry, createSandbox(userContext)) + resolve(typeof res === 'function' ? res(userContext) : res) + }) + } else { + // direct mode: instead of re-evaluating the whole bundle on + // each render, it simply calls the exported function. This avoids the + // module evaluation costs but requires the source code to be structured + // slightly differently. + let runner // lazy creation so that errors can be caught by user + let initialContext + return (userContext = {}) => new Promise(resolve => { + if (!runner) { + const sandbox = runInNewContext === 'once' + ? createSandbox() + : global + // the initial context is only used for collecting possible non-component + // styles injected by vue-style-loader. + initialContext = sandbox.__VUE_SSR_CONTEXT__ = {} + runner = evaluate(entry, sandbox) + // On subsequent renders, __VUE_SSR_CONTEXT__ will not be available + // to prevent cross-request pollution. + delete sandbox.__VUE_SSR_CONTEXT__ + if (typeof runner !== 'function') { + throw new Error( + 'bundle export should be a function when using ' + + '{ runInNewContext: false }.' + ) + } + } + userContext._registeredComponents = new Set() + + // vue-style-loader styles imported outside of component lifecycle hooks + if (initialContext._styles) { + userContext._styles = deepClone(initialContext._styles) + // #6353 ensure "styles" is exposed even if no styles are injected + // in component lifecycles. + // the renderStyles fn is exposed by vue-style-loader >= 3.0.3 + const renderStyles = initialContext._renderStyles + if (renderStyles) { + Object.defineProperty(userContext, 'styles', { + enumerable: true, + get () { + return renderStyles(userContext._styles) + } + }) + } + } + + resolve(runner(userContext)) + }) + } +} diff --git a/node_modules/vue/src/server/bundle-renderer/source-map-support.js b/node_modules/vue/src/server/bundle-renderer/source-map-support.js new file mode 100644 index 00000000..1010532e --- /dev/null +++ b/node_modules/vue/src/server/bundle-renderer/source-map-support.js @@ -0,0 +1,45 @@ +/* @flow */ + +const SourceMapConsumer = require('source-map').SourceMapConsumer + +const filenameRE = /\(([^)]+\.js):(\d+):(\d+)\)$/ + +export function createSourceMapConsumers (rawMaps: Object) { + const maps = {} + Object.keys(rawMaps).forEach(file => { + maps[file] = new SourceMapConsumer(rawMaps[file]) + }) + return maps +} + +export function rewriteErrorTrace (e: any, mapConsumers: { + [key: string]: SourceMapConsumer +}) { + if (e && typeof e.stack === 'string') { + e.stack = e.stack.split('\n').map(line => { + return rewriteTraceLine(line, mapConsumers) + }).join('\n') + } +} + +function rewriteTraceLine (trace: string, mapConsumers: { + [key: string]: SourceMapConsumer +}) { + const m = trace.match(filenameRE) + const map = m && mapConsumers[m[1]] + if (m != null && map) { + const originalPosition = map.originalPositionFor({ + line: Number(m[2]), + column: Number(m[3]) + }) + if (originalPosition.source != null) { + const { source, line, column } = originalPosition + const mappedPosition = `(${source.replace(/^webpack:\/\/\//, '')}:${String(line)}:${String(column)})` + return trace.replace(filenameRE, mappedPosition) + } else { + return trace + } + } else { + return trace + } +} diff --git a/node_modules/vue/src/server/create-basic-renderer.js b/node_modules/vue/src/server/create-basic-renderer.js new file mode 100644 index 00000000..2d53976d --- /dev/null +++ b/node_modules/vue/src/server/create-basic-renderer.js @@ -0,0 +1,37 @@ +/* @flow */ + +import { createWriteFunction } from './write' +import { createRenderFunction } from './render' +import type { RenderOptions } from './create-renderer' + +export function createBasicRenderer ({ + modules = [], + directives = {}, + isUnaryTag = (() => false), + cache +}: RenderOptions = {}) { + const render = createRenderFunction(modules, directives, isUnaryTag, cache) + + return function renderToString ( + component: Component, + context: any, + done: any + ): void { + if (typeof context === 'function') { + done = context + context = {} + } + let result = '' + const write = createWriteFunction(text => { + result += text + return false + }, done) + try { + render(component, write, context, () => { + done(null, result) + }) + } catch (e) { + done(e) + } + } +} diff --git a/node_modules/vue/src/server/create-renderer.js b/node_modules/vue/src/server/create-renderer.js new file mode 100644 index 00000000..c045a426 --- /dev/null +++ b/node_modules/vue/src/server/create-renderer.js @@ -0,0 +1,120 @@ +/* @flow */ + +import RenderStream from './render-stream' +import { createWriteFunction } from './write' +import { createRenderFunction } from './render' +import { createPromiseCallback } from './util' +import TemplateRenderer from './template-renderer/index' +import type { ClientManifest } from './template-renderer/index' + +export type Renderer = { + renderToString: (component: Component, context: any, cb: any) => ?Promise<string>; + renderToStream: (component: Component, context?: Object) => stream$Readable; +}; + +type RenderCache = { + get: (key: string, cb?: Function) => string | void; + set: (key: string, val: string) => void; + has?: (key: string, cb?: Function) => boolean | void; +}; + +export type RenderOptions = { + modules?: Array<(vnode: VNode) => ?string>; + directives?: Object; + isUnaryTag?: Function; + cache?: RenderCache; + template?: string; + inject?: boolean; + basedir?: string; + shouldPreload?: Function; + shouldPrefetch?: Function; + clientManifest?: ClientManifest; + runInNewContext?: boolean | 'once'; +}; + +export function createRenderer ({ + modules = [], + directives = {}, + isUnaryTag = (() => false), + template, + inject, + cache, + shouldPreload, + shouldPrefetch, + clientManifest +}: RenderOptions = {}): Renderer { + const render = createRenderFunction(modules, directives, isUnaryTag, cache) + const templateRenderer = new TemplateRenderer({ + template, + inject, + shouldPreload, + shouldPrefetch, + clientManifest + }) + + return { + renderToString ( + component: Component, + context: any, + cb: any + ): ?Promise<string> { + if (typeof context === 'function') { + cb = context + context = {} + } + if (context) { + templateRenderer.bindRenderFns(context) + } + + // no callback, return Promise + let promise + if (!cb) { + ({ promise, cb } = createPromiseCallback()) + } + + let result = '' + const write = createWriteFunction(text => { + result += text + return false + }, cb) + try { + render(component, write, context, err => { + if (template) { + result = templateRenderer.renderSync(result, context) + } + if (err) { + cb(err) + } else { + cb(null, result) + } + }) + } catch (e) { + cb(e) + } + + return promise + }, + + renderToStream ( + component: Component, + context?: Object + ): stream$Readable { + if (context) { + templateRenderer.bindRenderFns(context) + } + const renderStream = new RenderStream((write, done) => { + render(component, write, context, done) + }) + if (!template) { + return renderStream + } else { + const templateStream = templateRenderer.createStream(context) + renderStream.on('error', err => { + templateStream.emit('error', err) + }) + renderStream.pipe(templateStream) + return templateStream + } + } + } +} diff --git a/node_modules/vue/src/server/optimizing-compiler/codegen.js b/node_modules/vue/src/server/optimizing-compiler/codegen.js new file mode 100644 index 00000000..26097c07 --- /dev/null +++ b/node_modules/vue/src/server/optimizing-compiler/codegen.js @@ -0,0 +1,260 @@ +/* @flow */ + +// The SSR codegen is essentially extending the default codegen to handle +// SSR-optimizable nodes and turn them into string render fns. In cases where +// a node is not optimizable it simply falls back to the default codegen. + +import { + genIf, + genFor, + genData, + genText, + genElement, + genChildren, + CodegenState +} from 'compiler/codegen/index' + +import { + genAttrSegments, + genDOMPropSegments, + genClassSegments, + genStyleSegments, + applyModelTransform +} from './modules' + +import { escape } from 'web/server/util' +import { optimizability } from './optimizer' +import type { CodegenResult } from 'compiler/codegen/index' + +export type StringSegment = { + type: number; + value: string; +}; + +// segment types +export const RAW = 0 +export const INTERPOLATION = 1 +export const EXPRESSION = 2 + +export function generate ( + ast: ASTElement | void, + options: CompilerOptions +): CodegenResult { + const state = new CodegenState(options) + const code = ast ? genSSRElement(ast, state) : '_c("div")' + return { + render: `with(this){return ${code}}`, + staticRenderFns: state.staticRenderFns + } +} + +function genSSRElement (el: ASTElement, state: CodegenState): string { + if (el.for && !el.forProcessed) { + return genFor(el, state, genSSRElement) + } else if (el.if && !el.ifProcessed) { + return genIf(el, state, genSSRElement) + } else if (el.tag === 'template' && !el.slotTarget) { + return el.ssrOptimizability === optimizability.FULL + ? genChildrenAsStringNode(el, state) + : genSSRChildren(el, state) || 'void 0' + } + + switch (el.ssrOptimizability) { + case optimizability.FULL: + // stringify whole tree + return genStringElement(el, state) + case optimizability.SELF: + // stringify self and check children + return genStringElementWithChildren(el, state) + case optimizability.CHILDREN: + // generate self as VNode and stringify children + return genNormalElement(el, state, true) + case optimizability.PARTIAL: + // generate self as VNode and check children + return genNormalElement(el, state, false) + default: + // bail whole tree + return genElement(el, state) + } +} + +function genNormalElement (el, state, stringifyChildren) { + const data = el.plain ? undefined : genData(el, state) + const children = stringifyChildren + ? `[${genChildrenAsStringNode(el, state)}]` + : genSSRChildren(el, state, true) + return `_c('${el.tag}'${ + data ? `,${data}` : '' + }${ + children ? `,${children}` : '' + })` +} + +function genSSRChildren (el, state, checkSkip) { + return genChildren(el, state, checkSkip, genSSRElement, genSSRNode) +} + +function genSSRNode (el, state) { + return el.type === 1 + ? genSSRElement(el, state) + : genText(el) +} + +function genChildrenAsStringNode (el, state) { + return el.children.length + ? `_ssrNode(${flattenSegments(childrenToSegments(el, state))})` + : '' +} + +function genStringElement (el, state) { + return `_ssrNode(${elementToString(el, state)})` +} + +function genStringElementWithChildren (el, state) { + const children = genSSRChildren(el, state, true) + return `_ssrNode(${ + flattenSegments(elementToOpenTagSegments(el, state)) + },"</${el.tag}>"${ + children ? `,${children}` : '' + })` +} + +function elementToString (el, state) { + return `(${flattenSegments(elementToSegments(el, state))})` +} + +function elementToSegments (el, state): Array<StringSegment> { + // v-for / v-if + if (el.for && !el.forProcessed) { + el.forProcessed = true + return [{ + type: EXPRESSION, + value: genFor(el, state, elementToString, '_ssrList') + }] + } else if (el.if && !el.ifProcessed) { + el.ifProcessed = true + return [{ + type: EXPRESSION, + value: genIf(el, state, elementToString, '"<!---->"') + }] + } else if (el.tag === 'template') { + return childrenToSegments(el, state) + } + + const openSegments = elementToOpenTagSegments(el, state) + const childrenSegments = childrenToSegments(el, state) + const { isUnaryTag } = state.options + const close = (isUnaryTag && isUnaryTag(el.tag)) + ? [] + : [{ type: RAW, value: `</${el.tag}>` }] + return openSegments.concat(childrenSegments, close) +} + +function elementToOpenTagSegments (el, state): Array<StringSegment> { + applyModelTransform(el, state) + let binding + const segments = [{ type: RAW, value: `<${el.tag}` }] + // attrs + if (el.attrs) { + segments.push.apply(segments, genAttrSegments(el.attrs)) + } + // domProps + if (el.props) { + segments.push.apply(segments, genDOMPropSegments(el.props, el.attrs)) + } + // v-bind="object" + if ((binding = el.attrsMap['v-bind'])) { + segments.push({ type: EXPRESSION, value: `_ssrAttrs(${binding})` }) + } + // v-bind.prop="object" + if ((binding = el.attrsMap['v-bind.prop'])) { + segments.push({ type: EXPRESSION, value: `_ssrDOMProps(${binding})` }) + } + // class + if (el.staticClass || el.classBinding) { + segments.push.apply( + segments, + genClassSegments(el.staticClass, el.classBinding) + ) + } + // style & v-show + if (el.staticStyle || el.styleBinding || el.attrsMap['v-show']) { + segments.push.apply( + segments, + genStyleSegments( + el.attrsMap.style, + el.staticStyle, + el.styleBinding, + el.attrsMap['v-show'] + ) + ) + } + // _scopedId + if (state.options.scopeId) { + segments.push({ type: RAW, value: ` ${state.options.scopeId}` }) + } + segments.push({ type: RAW, value: `>` }) + return segments +} + +function childrenToSegments (el, state): Array<StringSegment> { + let binding + if ((binding = el.attrsMap['v-html'])) { + return [{ type: EXPRESSION, value: `_s(${binding})` }] + } + if ((binding = el.attrsMap['v-text'])) { + return [{ type: INTERPOLATION, value: `_s(${binding})` }] + } + if (el.tag === 'textarea' && (binding = el.attrsMap['v-model'])) { + return [{ type: INTERPOLATION, value: `_s(${binding})` }] + } + return el.children + ? nodesToSegments(el.children, state) + : [] +} + +function nodesToSegments ( + children: Array<ASTNode>, + state: CodegenState +): Array<StringSegment> { + const segments = [] + for (let i = 0; i < children.length; i++) { + const c = children[i] + if (c.type === 1) { + segments.push.apply(segments, elementToSegments(c, state)) + } else if (c.type === 2) { + segments.push({ type: INTERPOLATION, value: c.expression }) + } else if (c.type === 3) { + segments.push({ type: RAW, value: escape(c.text) }) + } + } + return segments +} + +function flattenSegments (segments: Array<StringSegment>): string { + const mergedSegments = [] + let textBuffer = '' + + const pushBuffer = () => { + if (textBuffer) { + mergedSegments.push(JSON.stringify(textBuffer)) + textBuffer = '' + } + } + + for (let i = 0; i < segments.length; i++) { + const s = segments[i] + if (s.type === RAW) { + textBuffer += s.value + } else if (s.type === INTERPOLATION) { + pushBuffer() + mergedSegments.push(`_ssrEscape(${s.value})`) + } else if (s.type === EXPRESSION) { + pushBuffer() + mergedSegments.push(`(${s.value})`) + } + } + pushBuffer() + + return mergedSegments.join('+') +} diff --git a/node_modules/vue/src/server/optimizing-compiler/index.js b/node_modules/vue/src/server/optimizing-compiler/index.js new file mode 100644 index 00000000..893630d6 --- /dev/null +++ b/node_modules/vue/src/server/optimizing-compiler/index.js @@ -0,0 +1,20 @@ +/* @flow */ + +import { parse } from 'compiler/parser/index' +import { generate } from './codegen' +import { optimize } from './optimizer' +import { createCompilerCreator } from 'compiler/create-compiler' + +export const createCompiler = createCompilerCreator(function baseCompile ( + template: string, + options: CompilerOptions +): CompiledResult { + const ast = parse(template.trim(), options) + optimize(ast, options) + const code = generate(ast, options) + return { + ast, + render: code.render, + staticRenderFns: code.staticRenderFns + } +}) diff --git a/node_modules/vue/src/server/optimizing-compiler/modules.js b/node_modules/vue/src/server/optimizing-compiler/modules.js new file mode 100644 index 00000000..69a7be87 --- /dev/null +++ b/node_modules/vue/src/server/optimizing-compiler/modules.js @@ -0,0 +1,126 @@ +/* @flow */ + +import { + RAW, + // INTERPOLATION, + EXPRESSION +} from './codegen' + +import { + propsToAttrMap, + isRenderableAttr +} from 'web/server/util' + +import { + isBooleanAttr, + isEnumeratedAttr +} from 'web/util/attrs' + +import type { StringSegment } from './codegen' +import type { CodegenState } from 'compiler/codegen/index' + +type Attr = { name: string; value: string }; + +const plainStringRE = /^"(?:[^"\\]|\\.)*"$|^'(?:[^'\\]|\\.)*'$/ + +// let the model AST transform translate v-model into appropriate +// props bindings +export function applyModelTransform (el: ASTElement, state: CodegenState) { + if (el.directives) { + for (let i = 0; i < el.directives.length; i++) { + const dir = el.directives[i] + if (dir.name === 'model') { + state.directives.model(el, dir, state.warn) + // remove value for textarea as its converted to text + if (el.tag === 'textarea' && el.props) { + el.props = el.props.filter(p => p.name !== 'value') + } + break + } + } + } +} + +export function genAttrSegments ( + attrs: Array<Attr> +): Array<StringSegment> { + return attrs.map(({ name, value }) => genAttrSegment(name, value)) +} + +export function genDOMPropSegments ( + props: Array<Attr>, + attrs: ?Array<Attr> +): Array<StringSegment> { + const segments = [] + props.forEach(({ name, value }) => { + name = propsToAttrMap[name] || name.toLowerCase() + if (isRenderableAttr(name) && + !(attrs && attrs.some(a => a.name === name)) + ) { + segments.push(genAttrSegment(name, value)) + } + }) + return segments +} + +function genAttrSegment (name: string, value: string): StringSegment { + if (plainStringRE.test(value)) { + // force double quote + value = value.replace(/^'|'$/g, '"') + // force enumerated attr to "true" + if (isEnumeratedAttr(name) && value !== `"false"`) { + value = `"true"` + } + return { + type: RAW, + value: isBooleanAttr(name) + ? ` ${name}="${name}"` + : value === '""' + ? ` ${name}` + : ` ${name}="${JSON.parse(value)}"` + } + } else { + return { + type: EXPRESSION, + value: `_ssrAttr(${JSON.stringify(name)},${value})` + } + } +} + +export function genClassSegments ( + staticClass: ?string, + classBinding: ?string +): Array<StringSegment> { + if (staticClass && !classBinding) { + return [{ type: RAW, value: ` class=${staticClass}` }] + } else { + return [{ + type: EXPRESSION, + value: `_ssrClass(${staticClass || 'null'},${classBinding || 'null'})` + }] + } +} + +export function genStyleSegments ( + staticStyle: ?string, + parsedStaticStyle: ?string, + styleBinding: ?string, + vShowExpression: ?string +): Array<StringSegment> { + if (staticStyle && !styleBinding && !vShowExpression) { + return [{ type: RAW, value: ` style=${JSON.stringify(staticStyle)}` }] + } else { + return [{ + type: EXPRESSION, + value: `_ssrStyle(${ + parsedStaticStyle || 'null' + },${ + styleBinding || 'null' + }, ${ + vShowExpression + ? `{ display: (${vShowExpression}) ? '' : 'none' }` + : 'null' + })` + }] + } +} diff --git a/node_modules/vue/src/server/optimizing-compiler/optimizer.js b/node_modules/vue/src/server/optimizing-compiler/optimizer.js new file mode 100644 index 00000000..d24acc55 --- /dev/null +++ b/node_modules/vue/src/server/optimizing-compiler/optimizer.js @@ -0,0 +1,140 @@ +/* @flow */ + +/** + * In SSR, the vdom tree is generated only once and never patched, so + * we can optimize most element / trees into plain string render functions. + * The SSR optimizer walks the AST tree to detect optimizable elements and trees. + * + * The criteria for SSR optimizability is quite a bit looser than static tree + * detection (which is designed for client re-render). In SSR we bail only for + * components/slots/custom directives. + */ + +import { no, makeMap, isBuiltInTag } from 'shared/util' + +// optimizability constants +export const optimizability = { + FALSE: 0, // whole sub tree un-optimizable + FULL: 1, // whole sub tree optimizable + SELF: 2, // self optimizable but has some un-optimizable children + CHILDREN: 3, // self un-optimizable but have fully optimizable children + PARTIAL: 4 // self un-optimizable with some un-optimizable children +} + +let isPlatformReservedTag + +export function optimize (root: ?ASTElement, options: CompilerOptions) { + if (!root) return + isPlatformReservedTag = options.isReservedTag || no + walk(root, true) +} + +function walk (node: ASTNode, isRoot?: boolean) { + if (isUnOptimizableTree(node)) { + node.ssrOptimizability = optimizability.FALSE + return + } + // root node or nodes with custom directives should always be a VNode + const selfUnoptimizable = isRoot || hasCustomDirective(node) + const check = child => { + if (child.ssrOptimizability !== optimizability.FULL) { + node.ssrOptimizability = selfUnoptimizable + ? optimizability.PARTIAL + : optimizability.SELF + } + } + if (selfUnoptimizable) { + node.ssrOptimizability = optimizability.CHILDREN + } + if (node.type === 1) { + for (let i = 0, l = node.children.length; i < l; i++) { + const child = node.children[i] + walk(child) + check(child) + } + if (node.ifConditions) { + for (let i = 1, l = node.ifConditions.length; i < l; i++) { + const block = node.ifConditions[i].block + walk(block, isRoot) + check(block) + } + } + if (node.ssrOptimizability == null || + (!isRoot && (node.attrsMap['v-html'] || node.attrsMap['v-text'])) + ) { + node.ssrOptimizability = optimizability.FULL + } else { + node.children = optimizeSiblings(node) + } + } else { + node.ssrOptimizability = optimizability.FULL + } +} + +function optimizeSiblings (el) { + const children = el.children + const optimizedChildren = [] + + let currentOptimizableGroup = [] + const pushGroup = () => { + if (currentOptimizableGroup.length) { + optimizedChildren.push({ + type: 1, + parent: el, + tag: 'template', + attrsList: [], + attrsMap: {}, + children: currentOptimizableGroup, + ssrOptimizability: optimizability.FULL + }) + } + currentOptimizableGroup = [] + } + + for (let i = 0; i < children.length; i++) { + const c = children[i] + if (c.ssrOptimizability === optimizability.FULL) { + currentOptimizableGroup.push(c) + } else { + // wrap fully-optimizable adjacent siblings inside a template tag + // so that they can be optimized into a single ssrNode by codegen + pushGroup() + optimizedChildren.push(c) + } + } + pushGroup() + return optimizedChildren +} + +function isUnOptimizableTree (node: ASTNode): boolean { + if (node.type === 2 || node.type === 3) { // text or expression + return false + } + return ( + isBuiltInTag(node.tag) || // built-in (slot, component) + !isPlatformReservedTag(node.tag) || // custom component + !!node.component || // "is" component + isSelectWithModel(node) // <select v-model> requires runtime inspection + ) +} + +const isBuiltInDir = makeMap('text,html,show,on,bind,model,pre,cloak,once') + +function hasCustomDirective (node: ASTNode): ?boolean { + return ( + node.type === 1 && + node.directives && + node.directives.some(d => !isBuiltInDir(d.name)) + ) +} + +// <select v-model> cannot be optimized because it requires a runtime check +// to determine proper selected option +function isSelectWithModel (node: ASTNode): boolean { + return ( + node.type === 1 && + node.tag === 'select' && + node.directives != null && + node.directives.some(d => d.name === 'model') + ) +} diff --git a/node_modules/vue/src/server/optimizing-compiler/runtime-helpers.js b/node_modules/vue/src/server/optimizing-compiler/runtime-helpers.js new file mode 100644 index 00000000..6f65de04 --- /dev/null +++ b/node_modules/vue/src/server/optimizing-compiler/runtime-helpers.js @@ -0,0 +1,150 @@ +/* @flow */ + +import { escape, isSSRUnsafeAttr } from 'web/server/util' +import { isObject, extend } from 'shared/util' +import { renderAttr } from 'web/server/modules/attrs' +import { renderClass } from 'web/util/class' +import { genStyle } from 'web/server/modules/style' +import { normalizeStyleBinding } from 'web/util/style' + +import { + normalizeChildren, + simpleNormalizeChildren +} from 'core/vdom/helpers/normalize-children' + +import { + propsToAttrMap, + isRenderableAttr +} from 'web/server/util' + +const ssrHelpers = { + _ssrEscape: escape, + _ssrNode: renderStringNode, + _ssrList: renderStringList, + _ssrAttr: renderAttr, + _ssrAttrs: renderAttrs, + _ssrDOMProps: renderDOMProps, + _ssrClass: renderSSRClass, + _ssrStyle: renderSSRStyle +} + +export function installSSRHelpers (vm: Component) { + if (vm._ssrNode) { + return + } + let Vue = vm.constructor + while (Vue.super) { + Vue = Vue.super + } + extend(Vue.prototype, ssrHelpers) + if (Vue.FunctionalRenderContext) { + extend(Vue.FunctionalRenderContext.prototype, ssrHelpers) + } +} + +class StringNode { + isString: boolean; + open: string; + close: ?string; + children: ?Array<any>; + + constructor ( + open: string, + close?: string, + children?: Array<any>, + normalizationType?: number + ) { + this.isString = true + this.open = open + this.close = close + if (children) { + this.children = normalizationType === 1 + ? simpleNormalizeChildren(children) + : normalizationType === 2 + ? normalizeChildren(children) + : children + } else { + this.children = void 0 + } + } +} + +function renderStringNode ( + open: string, + close?: string, + children?: Array<any>, + normalizationType?: number +): StringNode { + return new StringNode(open, close, children, normalizationType) +} + +function renderStringList ( + val: any, + render: ( + val: any, + keyOrIndex: string | number, + index?: number + ) => string +): string { + let ret = '' + let i, l, keys, key + if (Array.isArray(val) || typeof val === 'string') { + for (i = 0, l = val.length; i < l; i++) { + ret += render(val[i], i) + } + } else if (typeof val === 'number') { + for (i = 0; i < val; i++) { + ret += render(i + 1, i) + } + } else if (isObject(val)) { + keys = Object.keys(val) + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i] + ret += render(val[key], key, i) + } + } + return ret +} + +function renderAttrs (obj: Object): string { + let res = '' + for (const key in obj) { + if (isSSRUnsafeAttr(key)) { + continue + } + res += renderAttr(key, obj[key]) + } + return res +} + +function renderDOMProps (obj: Object): string { + let res = '' + for (const key in obj) { + const attr = propsToAttrMap[key] || key.toLowerCase() + if (isRenderableAttr(attr)) { + res += renderAttr(attr, obj[key]) + } + } + return res +} + +function renderSSRClass ( + staticClass: ?string, + dynamic: any +): string { + const res = renderClass(staticClass, dynamic) + return res === '' ? res : ` class="${escape(res)}"` +} + +function renderSSRStyle ( + staticStyle: ?Object, + dynamic: any, + extra: ?Object +): string { + const style = {} + if (staticStyle) extend(style, staticStyle) + if (dynamic) extend(style, normalizeStyleBinding(dynamic)) + if (extra) extend(style, extra) + const res = genStyle(style) + return res === '' ? res : ` style=${JSON.stringify(escape(res))}` +} diff --git a/node_modules/vue/src/server/render-context.js b/node_modules/vue/src/server/render-context.js new file mode 100644 index 00000000..178140cd --- /dev/null +++ b/node_modules/vue/src/server/render-context.js @@ -0,0 +1,130 @@ +/* @flow */ + +import { isUndef } from 'shared/util' + +type RenderState = { + type: 'Element'; + rendered: number; + total: number; + children: Array<VNode>; + endTag: string; +} | { + type: 'Fragment'; + rendered: number; + total: number; + children: Array<VNode>; +} | { + type: 'Component'; + prevActive: Component; +} | { + type: 'ComponentWithCache'; + buffer: Array<string>; + bufferIndex: number; + componentBuffer: Array<Set<Class<Component>>>; + key: string; +}; + +export class RenderContext { + userContext: ?Object; + activeInstance: Component; + renderStates: Array<RenderState>; + write: (text: string, next: Function) => void; + renderNode: (node: VNode, isRoot: boolean, context: RenderContext) => void; + next: () => void; + done: (err: ?Error) => void; + + modules: Array<(node: VNode) => ?string>; + directives: Object; + isUnaryTag: (tag: string) => boolean; + + cache: any; + get: ?(key: string, cb: Function) => void; + has: ?(key: string, cb: Function) => void; + + constructor (options: Object) { + this.userContext = options.userContext + this.activeInstance = options.activeInstance + this.renderStates = [] + + this.write = options.write + this.done = options.done + this.renderNode = options.renderNode + + this.isUnaryTag = options.isUnaryTag + this.modules = options.modules + this.directives = options.directives + + const cache = options.cache + if (cache && (!cache.get || !cache.set)) { + throw new Error('renderer cache must implement at least get & set.') + } + this.cache = cache + this.get = cache && normalizeAsync(cache, 'get') + this.has = cache && normalizeAsync(cache, 'has') + + this.next = this.next.bind(this) + } + + next () { + const lastState = this.renderStates[this.renderStates.length - 1] + if (isUndef(lastState)) { + return this.done() + } + switch (lastState.type) { + case 'Element': + case 'Fragment': + const { children, total } = lastState + const rendered = lastState.rendered++ + if (rendered < total) { + this.renderNode(children[rendered], false, this) + } else { + this.renderStates.pop() + if (lastState.type === 'Element') { + this.write(lastState.endTag, this.next) + } else { + this.next() + } + } + break + case 'Component': + this.renderStates.pop() + this.activeInstance = lastState.prevActive + this.next() + break + case 'ComponentWithCache': + this.renderStates.pop() + const { buffer, bufferIndex, componentBuffer, key } = lastState + const result = { + html: buffer[bufferIndex], + components: componentBuffer[bufferIndex] + } + this.cache.set(key, result) + if (bufferIndex === 0) { + // this is a top-level cached component, + // exit caching mode. + this.write.caching = false + } else { + // parent component is also being cached, + // merge self into parent's result + buffer[bufferIndex - 1] += result.html + const prev = componentBuffer[bufferIndex - 1] + result.components.forEach(c => prev.add(c)) + } + buffer.length = bufferIndex + componentBuffer.length = bufferIndex + this.next() + break + } + } +} + +function normalizeAsync (cache, method) { + const fn = cache[method] + if (isUndef(fn)) { + return + } else if (fn.length > 1) { + return (key, cb) => fn.call(cache, key, cb) + } else { + return (key, cb) => cb(fn.call(cache, key)) + } +} diff --git a/node_modules/vue/src/server/render-stream.js b/node_modules/vue/src/server/render-stream.js new file mode 100644 index 00000000..d76012af --- /dev/null +++ b/node_modules/vue/src/server/render-stream.js @@ -0,0 +1,94 @@ +/* @flow */ + +/** + * Original RenderStream implementation by Sasha Aickin (@aickin) + * Licensed under the Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Modified by Evan You (@yyx990803) + */ + +const stream = require('stream') + +import { isTrue, isUndef } from 'shared/util' +import { createWriteFunction } from './write' + +export default class RenderStream extends stream.Readable { + buffer: string; + render: (write: Function, done: Function) => void; + expectedSize: number; + write: Function; + next: Function; + end: Function; + done: boolean; + + constructor (render: Function) { + super() + this.buffer = '' + this.render = render + this.expectedSize = 0 + + this.write = createWriteFunction((text, next) => { + const n = this.expectedSize + this.buffer += text + if (this.buffer.length >= n) { + this.next = next + this.pushBySize(n) + return true // we will decide when to call next + } + return false + }, err => { + this.emit('error', err) + }) + + this.end = () => { + // the rendering is finished; we should push out the last of the buffer. + this.done = true + this.push(this.buffer) + } + } + + pushBySize (n: number) { + const bufferToPush = this.buffer.substring(0, n) + this.buffer = this.buffer.substring(n) + this.push(bufferToPush) + } + + tryRender () { + try { + this.render(this.write, this.end) + } catch (e) { + this.emit('error', e) + } + } + + tryNext () { + try { + this.next() + } catch (e) { + this.emit('error', e) + } + } + + _read (n: number) { + this.expectedSize = n + // it's possible that the last chunk added bumped the buffer up to > 2 * n, + // which means we will need to go through multiple read calls to drain it + // down to < n. + if (isTrue(this.done)) { + this.push(null) + return + } + if (this.buffer.length >= n) { + this.pushBySize(n) + return + } + if (isUndef(this.next)) { + // start the rendering chain. + this.tryRender() + } else { + // continue with the rendering. + this.tryNext() + } + } +} diff --git a/node_modules/vue/src/server/render.js b/node_modules/vue/src/server/render.js new file mode 100644 index 00000000..592a6175 --- /dev/null +++ b/node_modules/vue/src/server/render.js @@ -0,0 +1,396 @@ +/* @flow */ + +import { escape } from 'web/server/util' +import { SSR_ATTR } from 'shared/constants' +import { RenderContext } from './render-context' +import { generateComponentTrace } from 'core/util/debug' +import { ssrCompileToFunctions } from 'web/server/compiler' +import { installSSRHelpers } from './optimizing-compiler/runtime-helpers' + +import { isDef, isUndef, isTrue } from 'shared/util' + +import { + createComponent, + createComponentInstanceForVnode +} from 'core/vdom/create-component' + +let warned = Object.create(null) +const warnOnce = msg => { + if (!warned[msg]) { + warned[msg] = true + console.warn(`\n\u001b[31m${msg}\u001b[39m\n`) + } +} + +const onCompilationError = (err, vm) => { + const trace = vm ? generateComponentTrace(vm) : '' + throw new Error(`\n\u001b[31m${err}${trace}\u001b[39m\n`) +} + +const normalizeRender = vm => { + const { render, template, _scopeId } = vm.$options + if (isUndef(render)) { + if (template) { + const compiled = ssrCompileToFunctions(template, { + scopeId: _scopeId, + warn: onCompilationError + }, vm) + + vm.$options.render = compiled.render + vm.$options.staticRenderFns = compiled.staticRenderFns + } else { + throw new Error( + `render function or template not defined in component: ${ + vm.$options.name || vm.$options._componentTag || 'anonymous' + }` + ) + } + } +} + +function renderNode (node, isRoot, context) { + if (node.isString) { + renderStringNode(node, context) + } else if (isDef(node.componentOptions)) { + renderComponent(node, isRoot, context) + } else if (isDef(node.tag)) { + renderElement(node, isRoot, context) + } else if (isTrue(node.isComment)) { + if (isDef(node.asyncFactory)) { + // async component + renderAsyncComponent(node, isRoot, context) + } else { + context.write(`<!--${node.text}-->`, context.next) + } + } else { + context.write( + node.raw ? node.text : escape(String(node.text)), + context.next + ) + } +} + +function registerComponentForCache (options, write) { + // exposed by vue-loader, need to call this if cache hit because + // component lifecycle hooks will not be called. + const register = options._ssrRegister + if (write.caching && isDef(register)) { + write.componentBuffer[write.componentBuffer.length - 1].add(register) + } + return register +} + +function renderComponent (node, isRoot, context) { + const { write, next, userContext } = context + + // check cache hit + const Ctor = node.componentOptions.Ctor + const getKey = Ctor.options.serverCacheKey + const name = Ctor.options.name + const cache = context.cache + const registerComponent = registerComponentForCache(Ctor.options, write) + + if (isDef(getKey) && isDef(cache) && isDef(name)) { + const key = name + '::' + getKey(node.componentOptions.propsData) + const { has, get } = context + if (isDef(has)) { + has(key, hit => { + if (hit === true && isDef(get)) { + get(key, res => { + if (isDef(registerComponent)) { + registerComponent(userContext) + } + res.components.forEach(register => register(userContext)) + write(res.html, next) + }) + } else { + renderComponentWithCache(node, isRoot, key, context) + } + }) + } else if (isDef(get)) { + get(key, res => { + if (isDef(res)) { + if (isDef(registerComponent)) { + registerComponent(userContext) + } + res.components.forEach(register => register(userContext)) + write(res.html, next) + } else { + renderComponentWithCache(node, isRoot, key, context) + } + }) + } + } else { + if (isDef(getKey) && isUndef(cache)) { + warnOnce( + `[vue-server-renderer] Component ${ + Ctor.options.name || '(anonymous)' + } implemented serverCacheKey, ` + + 'but no cache was provided to the renderer.' + ) + } + if (isDef(getKey) && isUndef(name)) { + warnOnce( + `[vue-server-renderer] Components that implement "serverCacheKey" ` + + `must also define a unique "name" option.` + ) + } + renderComponentInner(node, isRoot, context) + } +} + +function renderComponentWithCache (node, isRoot, key, context) { + const write = context.write + write.caching = true + const buffer = write.cacheBuffer + const bufferIndex = buffer.push('') - 1 + const componentBuffer = write.componentBuffer + componentBuffer.push(new Set()) + context.renderStates.push({ + type: 'ComponentWithCache', + key, + buffer, + bufferIndex, + componentBuffer + }) + renderComponentInner(node, isRoot, context) +} + +function renderComponentInner (node, isRoot, context) { + const prevActive = context.activeInstance + // expose userContext on vnode + node.ssrContext = context.userContext + const child = context.activeInstance = createComponentInstanceForVnode( + node, + context.activeInstance + ) + normalizeRender(child) + const childNode = child._render() + childNode.parent = node + context.renderStates.push({ + type: 'Component', + prevActive + }) + renderNode(childNode, isRoot, context) +} + +function renderAsyncComponent (node, isRoot, context) { + const factory = node.asyncFactory + + const resolve = comp => { + if (comp.__esModule && comp.default) { + comp = comp.default + } + const { data, children, tag } = node.asyncMeta + const nodeContext = node.asyncMeta.context + const resolvedNode: any = createComponent( + comp, + data, + nodeContext, + children, + tag + ) + if (resolvedNode) { + if (resolvedNode.componentOptions) { + // normal component + renderComponent(resolvedNode, isRoot, context) + } else if (!Array.isArray(resolvedNode)) { + // single return node from functional component + renderNode(resolvedNode, isRoot, context) + } else { + // multiple return nodes from functional component + context.renderStates.push({ + type: 'Fragment', + children: resolvedNode, + rendered: 0, + total: resolvedNode.length + }) + context.next() + } + } else { + // invalid component, but this does not throw on the client + // so render empty comment node + context.write(`<!---->`, context.next) + } + } + + if (factory.resolved) { + resolve(factory.resolved) + return + } + + const reject = context.done + let res + try { + res = factory(resolve, reject) + } catch (e) { + reject(e) + } + if (res) { + if (typeof res.then === 'function') { + res.then(resolve, reject).catch(reject) + } else { + // new syntax in 2.3 + const comp = res.component + if (comp && typeof comp.then === 'function') { + comp.then(resolve, reject).catch(reject) + } + } + } +} + +function renderStringNode (el, context) { + const { write, next } = context + if (isUndef(el.children) || el.children.length === 0) { + write(el.open + (el.close || ''), next) + } else { + const children: Array<VNode> = el.children + context.renderStates.push({ + type: 'Element', + children, + rendered: 0, + total: children.length, + endTag: el.close + }) + write(el.open, next) + } +} + +function renderElement (el, isRoot, context) { + const { write, next } = context + + if (isTrue(isRoot)) { + if (!el.data) el.data = {} + if (!el.data.attrs) el.data.attrs = {} + el.data.attrs[SSR_ATTR] = 'true' + } + + if (el.fnOptions) { + registerComponentForCache(el.fnOptions, write) + } + + const startTag = renderStartingTag(el, context) + const endTag = `</${el.tag}>` + if (context.isUnaryTag(el.tag)) { + write(startTag, next) + } else if (isUndef(el.children) || el.children.length === 0) { + write(startTag + endTag, next) + } else { + const children: Array<VNode> = el.children + context.renderStates.push({ + type: 'Element', + children, + rendered: 0, + total: children.length, + endTag + }) + write(startTag, next) + } +} + +function hasAncestorData (node: VNode) { + const parentNode = node.parent + return isDef(parentNode) && (isDef(parentNode.data) || hasAncestorData(parentNode)) +} + +function getVShowDirectiveInfo (node: VNode): ?VNodeDirective { + let dir: VNodeDirective + let tmp + + while (isDef(node)) { + if (node.data && node.data.directives) { + tmp = node.data.directives.find(dir => dir.name === 'show') + if (tmp) { + dir = tmp + } + } + node = node.parent + } + return dir +} + +function renderStartingTag (node: VNode, context) { + let markup = `<${node.tag}` + const { directives, modules } = context + + // construct synthetic data for module processing + // because modules like style also produce code by parent VNode data + if (isUndef(node.data) && hasAncestorData(node)) { + node.data = {} + } + if (isDef(node.data)) { + // check directives + const dirs = node.data.directives + if (dirs) { + for (let i = 0; i < dirs.length; i++) { + const name = dirs[i].name + const dirRenderer = directives[name] + if (dirRenderer && name !== 'show') { + // directives mutate the node's data + // which then gets rendered by modules + dirRenderer(node, dirs[i]) + } + } + } + + // v-show directive needs to be merged from parent to child + const vshowDirectiveInfo = getVShowDirectiveInfo(node) + if (vshowDirectiveInfo) { + directives.show(node, vshowDirectiveInfo) + } + + // apply other modules + for (let i = 0; i < modules.length; i++) { + const res = modules[i](node) + if (res) { + markup += res + } + } + } + // attach scoped CSS ID + let scopeId + const activeInstance = context.activeInstance + if (isDef(activeInstance) && + activeInstance !== node.context && + isDef(scopeId = activeInstance.$options._scopeId) + ) { + markup += ` ${(scopeId: any)}` + } + if (isDef(node.fnScopeId)) { + markup += ` ${node.fnScopeId}` + } else { + while (isDef(node)) { + if (isDef(scopeId = node.context.$options._scopeId)) { + markup += ` ${scopeId}` + } + node = node.parent + } + } + return markup + '>' +} + +export function createRenderFunction ( + modules: Array<(node: VNode) => ?string>, + directives: Object, + isUnaryTag: Function, + cache: any +) { + return function render ( + component: Component, + write: (text: string, next: Function) => void, + userContext: ?Object, + done: Function + ) { + warned = Object.create(null) + const context = new RenderContext({ + activeInstance: component, + userContext, + write, done, renderNode, + isUnaryTag, modules, directives, + cache + }) + installSSRHelpers(component) + normalizeRender(component) + renderNode(component._render(), true, context) + } +} 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() + } +} diff --git a/node_modules/vue/src/server/util.js b/node_modules/vue/src/server/util.js new file mode 100644 index 00000000..908f8c9d --- /dev/null +++ b/node_modules/vue/src/server/util.js @@ -0,0 +1,18 @@ +/* @flow */ + +export const isJS = (file: string): boolean => /\.js(\?[^.]+)?$/.test(file) + +export const isCSS = (file: string): boolean => /\.css(\?[^.]+)?$/.test(file) + +export function createPromiseCallback () { + let resolve, reject + const promise: Promise<string> = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + const cb = (err: Error, res?: string) => { + if (err) return reject(err) + resolve(res || '') + } + return { promise, cb } +} diff --git a/node_modules/vue/src/server/webpack-plugin/client.js b/node_modules/vue/src/server/webpack-plugin/client.js new file mode 100644 index 00000000..5f2cd4bd --- /dev/null +++ b/node_modules/vue/src/server/webpack-plugin/client.js @@ -0,0 +1,70 @@ +const hash = require('hash-sum') +const uniq = require('lodash.uniq') +import { isJS } from './util' + +export default class VueSSRClientPlugin { + constructor (options = {}) { + this.options = Object.assign({ + filename: 'vue-ssr-client-manifest.json' + }, options) + } + + apply (compiler) { + compiler.plugin('emit', (compilation, cb) => { + const stats = compilation.getStats().toJson() + + const allFiles = uniq(stats.assets + .map(a => a.name)) + + const initialFiles = uniq(Object.keys(stats.entrypoints) + .map(name => stats.entrypoints[name].assets) + .reduce((assets, all) => all.concat(assets), []) + .filter(isJS)) + + const asyncFiles = allFiles + .filter(isJS) + .filter(file => initialFiles.indexOf(file) < 0) + + const manifest = { + publicPath: stats.publicPath, + all: allFiles, + initial: initialFiles, + async: asyncFiles, + modules: { /* [identifier: string]: Array<index: number> */ } + } + + const assetModules = stats.modules.filter(m => m.assets.length) + const fileToIndex = file => manifest.all.indexOf(file) + stats.modules.forEach(m => { + // ignore modules duplicated in multiple chunks + if (m.chunks.length === 1) { + const cid = m.chunks[0] + const chunk = stats.chunks.find(c => c.id === cid) + if (!chunk || !chunk.files) { + return + } + const files = manifest.modules[hash(m.identifier)] = chunk.files.map(fileToIndex) + // find all asset modules associated with the same chunk + assetModules.forEach(m => { + if (m.chunks.some(id => id === cid)) { + files.push.apply(files, m.assets.map(fileToIndex)) + } + }) + } + }) + + // const debug = (file, obj) => { + // require('fs').writeFileSync(__dirname + '/' + file, JSON.stringify(obj, null, 2)) + // } + // debug('stats.json', stats) + // debug('client-manifest.json', manifest) + + const json = JSON.stringify(manifest, null, 2) + compilation.assets[this.options.filename] = { + source: () => json, + size: () => json.length + } + cb() + }) + } +} diff --git a/node_modules/vue/src/server/webpack-plugin/server.js b/node_modules/vue/src/server/webpack-plugin/server.js new file mode 100644 index 00000000..8ffa58b0 --- /dev/null +++ b/node_modules/vue/src/server/webpack-plugin/server.js @@ -0,0 +1,66 @@ +import { validate, isJS } from './util' + +export default class VueSSRServerPlugin { + constructor (options = {}) { + this.options = Object.assign({ + filename: 'vue-ssr-server-bundle.json' + }, options) + } + + apply (compiler) { + validate(compiler) + + compiler.plugin('emit', (compilation, cb) => { + const stats = compilation.getStats().toJson() + const entryName = Object.keys(stats.entrypoints)[0] + const entryInfo = stats.entrypoints[entryName] + + if (!entryInfo) { + // #5553 + return cb() + } + + const entryAssets = entryInfo.assets.filter(isJS) + + if (entryAssets.length > 1) { + throw new Error( + `Server-side bundle should have one single entry file. ` + + `Avoid using CommonsChunkPlugin in the server config.` + ) + } + + const entry = entryAssets[0] + if (!entry || typeof entry !== 'string') { + throw new Error( + `Entry "${entryName}" not found. Did you specify the correct entry option?` + ) + } + + const bundle = { + entry, + files: {}, + maps: {} + } + + stats.assets.forEach(asset => { + if (asset.name.match(/\.js$/)) { + bundle.files[asset.name] = compilation.assets[asset.name].source() + } else if (asset.name.match(/\.js\.map$/)) { + bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source()) + } + // do not emit anything else for server + delete compilation.assets[asset.name] + }) + + const json = JSON.stringify(bundle, null, 2) + const filename = this.options.filename + + compilation.assets[filename] = { + source: () => json, + size: () => json.length + } + + cb() + }) + } +} diff --git a/node_modules/vue/src/server/webpack-plugin/util.js b/node_modules/vue/src/server/webpack-plugin/util.js new file mode 100644 index 00000000..d22e4b2c --- /dev/null +++ b/node_modules/vue/src/server/webpack-plugin/util.js @@ -0,0 +1,24 @@ +const { red, yellow } = require('chalk') + +const prefix = `[vue-server-renderer-webpack-plugin]` +const warn = exports.warn = msg => console.error(red(`${prefix} ${msg}\n`)) +const tip = exports.tip = msg => console.log(yellow(`${prefix} ${msg}\n`)) + +export const validate = compiler => { + if (compiler.options.target !== 'node') { + warn('webpack config `target` should be "node".') + } + + if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') { + warn('webpack config `output.libraryTarget` should be "commonjs2".') + } + + if (!compiler.options.externals) { + tip( + 'It is recommended to externalize dependencies in the server build for ' + + 'better build performance.' + ) + } +} + +export { isJS, isCSS } from '../util' diff --git a/node_modules/vue/src/server/write.js b/node_modules/vue/src/server/write.js new file mode 100644 index 00000000..e6420818 --- /dev/null +++ b/node_modules/vue/src/server/write.js @@ -0,0 +1,50 @@ +/* @flow */ + +const MAX_STACK_DEPTH = 1000 +const noop = _ => _ + +const defer = typeof process !== 'undefined' && process.nextTick + ? process.nextTick + : typeof Promise !== 'undefined' + ? fn => Promise.resolve().then(fn) + : typeof setTimeout !== 'undefined' + ? setTimeout + : noop + +if (defer === noop) { + throw new Error( + 'Your JavaScript runtime does not support any asynchronous primitives ' + + 'that are required by vue-server-renderer. Please use a polyfill for ' + + 'either Promise or setTimeout.' + ) +} + +export function createWriteFunction ( + write: (text: string, next: Function) => boolean, + onError: Function +): Function { + let stackDepth = 0 + const cachedWrite = (text, next) => { + if (text && cachedWrite.caching) { + cachedWrite.cacheBuffer[cachedWrite.cacheBuffer.length - 1] += text + } + const waitForNext = write(text, next) + if (waitForNext !== true) { + if (stackDepth >= MAX_STACK_DEPTH) { + defer(() => { + try { next() } catch (e) { + onError(e) + } + }) + } else { + stackDepth++ + next() + stackDepth-- + } + } + } + cachedWrite.caching = false + cachedWrite.cacheBuffer = [] + cachedWrite.componentBuffer = [] + return cachedWrite +} |
