Pārlūkot izejas kodu

ssr: resolve require() calls relative to bundle (fix #4936)

Evan You 9 gadi atpakaļ
vecāks
revīzija
8d88512837

+ 1 - 0
package.json

@@ -101,6 +101,7 @@
     "nightwatch": "^0.9.9",
     "nightwatch-helpers": "^1.2.0",
     "phantomjs-prebuilt": "^2.1.1",
+    "resolve": "^1.2.0",
     "rollup": "^0.41.4",
     "rollup-plugin-alias": "^1.2.0",
     "rollup-plugin-babel": "^2.4.0",

+ 35 - 6
packages/vue-server-renderer/README.md

@@ -76,12 +76,31 @@ app.get('/', (req, res) => {
 
 ---
 
-### createBundleRenderer([bundle](#creating-the-server-bundle), [[rendererOptions](#renderer-options)])
+### createBundleRenderer(bundle, [[rendererOptions](#renderer-options)])
 
-Creates a `bundleRenderer` instance using pre-compiled application bundle (see [Creating the Server Bundle](#creating-the-server-bundle)). For each render call, the code will be re-run in a new context using Node.js' `vm` module. This ensures your application state is discrete between requests, and you don't need to worry about structuring your application in a limiting pattern just for the sake of SSR.
+Creates a `bundleRenderer` instance using pre-compiled application bundle. The `bundle` argument can be one of the following:
+
+- An absolute path to generated bundle file (`.js` or `.json`). Must start with `/` to be treated as a file path.
+
+- A bundle object generated by `vue-ssr-webpack-plugin`.
+
+- A string of JavaScript code.
+
+See [Creating the Server Bundle](#creating-the-server-bundle) for more details.
+
+For each render call, the code will be re-run in a new context using Node.js' `vm` module. This ensures your application state is discrete between requests, and you don't need to worry about structuring your application in a limiting pattern just for the sake of SSR.
 
 ``` js
-const bundleRenderer = require('vue-server-renderer').createBundleRenderer(bundle)
+const createBundleRenderer = require('vue-server-renderer').createBundleRenderer
+
+// absolute filepath
+let renderer = createBundleRenderer('/path/to/bundle.json')
+
+// bundle object
+let renderer = createBundleRenderer({ ... })
+
+// code (not recommended for lack of source map support)
+let renderer = createBundleRenderer(bundledCode)
 ```
 
 ---
@@ -190,6 +209,16 @@ const renderer = createRenderer({
 
 ---
 
+### basedir
+
+> New in 2.2.0
+
+Explicitly declare the base directory for the server bundle to resolve node_modules from. This is only needed if your generated bundle file is placed in a different location from where the externalized NPM dependencies are installed.
+
+Note that the `basedir` is automatically inferred if you use `vue-ssr-webpack-plugin` or provide an absolute path to `createBundleRenderer` as the first argument, so in most cases you don't need to provide this option. However, this option does allow you to explicitly overwrite the inferred value.
+
+---
+
 ### directives
 
 Allows you to provide server-side implementations for your custom directives:
@@ -220,7 +249,7 @@ Instead, it's more straightforward to run our app "fresh", in a sandboxed contex
 
 <img width="973" alt="screen shot 2016-08-11 at 6 06 57 pm" src="https://cloud.githubusercontent.com/assets/499550/17607895/786a415a-5fee-11e6-9c11-45a2cfdf085c.png">
 
-The application bundle can be either a string of bundled code, or a special object of the following type:
+The application bundle can be either a string of bundled code (not recommended due to lack of source map support), or a special object of the following type:
 
 ``` js
 type RenderBundle = {
@@ -230,9 +259,9 @@ type RenderBundle = {
 }
 ```
 
-Although theoretically you can use any build tool to generate the bundle, it is recommended to use webpack + `vue-loader` + [vue-ssr-webpack-plugin](https://github.com/vuejs/vue-ssr-webpack-plugin) for this purpose. This setup works seamlessly even if you use webpack's on-demand code splitting features such as dynamic `import()`.
+Although theoretically you can use any build tool to generate the bundle, it is recommended to use webpack + `vue-loader` + [vue-ssr-webpack-plugin](https://github.com/vuejs/vue-ssr-webpack-plugin) for this purpose. The plugin will automatically turn the build output into a single JSON file that you can then pass to `createBundleRenderer`. This setup works seamlessly even if you use webpack's on-demand code splitting features such as dynamic `import()`.
 
-The usual workflow is setting up a base webpack configuration file for the client-side, then modify it to generate the server-side bundle with the following changes:
+The typical workflow is setting up a base webpack configuration file for the client-side, then modify it to generate the server-side bundle with the following changes:
 
 1. Set `target: 'node'` and `output: { libraryTarget: 'commonjs2' }` in your webpack config.
 

+ 1 - 0
packages/vue-server-renderer/package.json

@@ -20,6 +20,7 @@
   "dependencies": {
     "he": "^1.1.0",
     "de-indent": "^1.0.2",
+    "resolve": "^1.2.0",
     "source-map": "0.5.6",
     "vue-ssr-html-stream": "^2.1.0"
   },

+ 26 - 1
src/server/create-bundle-renderer.js

@@ -4,6 +4,8 @@ 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 =
@@ -18,6 +20,7 @@ const INVALID_MSG =
 // 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; };
@@ -31,9 +34,29 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
     const renderer = createRenderer(rendererOptions)
 
     let files, entry, maps
+    let basedir = rendererOptions && rendererOptions.basedir
+
+    // load bundle if given filepath
+    if (typeof bundle === 'string' && bundle.charAt(0) === '/') {
+      if (fs.existsSync(bundle)) {
+        basedir = basedir || path.dirname(bundle)
+        bundle = fs.readFileSync(bundle, 'utf-8')
+        if (/\.json$/.test(bundle)) {
+          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)
@@ -45,7 +68,9 @@ export function createBundleRendererCreator (createRenderer: () => Renderer) {
     } else {
       throw new Error(INVALID_MSG)
     }
-    const run = createBundleRunner(entry, files)
+
+    const run = createBundleRunner(entry, files, basedir)
+
     return {
       renderToString: (context?: Object, cb: (err: ?Error, res: ?string) => void) => {
         if (typeof context === 'function') {

+ 17 - 10
src/server/create-bundle-runner.js

@@ -1,26 +1,28 @@
-const NativeModule = require('module')
 const vm = require('vm')
 const path = require('path')
+const resolve = require('resolve')
+const NativeModule = require('module')
 
 function createContext (context) {
   const sandbox = {
     Buffer,
-    clearImmediate,
-    clearInterval,
-    clearTimeout,
-    setImmediate,
-    setInterval,
-    setTimeout,
     console,
     process,
+    setTimeout,
+    setInterval,
+    setImmediate,
+    clearTimeout,
+    clearInterval,
+    clearImmediate,
     __VUE_SSR_CONTEXT__: context
   }
   sandbox.global = sandbox
   return sandbox
 }
 
-function compileModule (files) {
+function compileModule (files, basedir) {
   const compiledScripts = {}
+  const reoslvedModules = {}
 
   function getCompiledScript (filename) {
     if (compiledScripts[filename]) {
@@ -48,6 +50,11 @@ function compileModule (files) {
       file = path.join('.', file)
       if (files[file]) {
         return evaluateModule(file, context, evaluatedModules)
+      } else if (basedir) {
+        return require(
+          reoslvedModules[file] ||
+          (reoslvedModules[file] = resolve.sync(file, { basedir }))
+        )
       } else {
         return require(file)
       }
@@ -63,8 +70,8 @@ function compileModule (files) {
   return evaluateModule
 }
 
-export function createBundleRunner (entry, files) {
-  const evaluate = compileModule(files)
+export function createBundleRunner (entry, files, basedir) {
+  const evaluate = compileModule(files, basedir)
   return (_context = {}) => new Promise((resolve, reject) => {
     const context = createContext(_context)
     const res = evaluate(entry, context, {})

+ 1 - 0
src/server/create-renderer.js

@@ -23,6 +23,7 @@ export type RenderOptions = {
   isUnaryTag?: Function;
   cache?: RenderCache;
   template?: string;
+  basedir?: string;
 };
 
 export function createRenderer ({

+ 4 - 0
yarn.lock

@@ -4163,6 +4163,10 @@ resolve@1.1.x, resolve@^1.1.6:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
+resolve@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+
 restore-cursor@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"