Created
December 16, 2018 05:42
-
-
Save AlbertMarashi/dcd441d2842682485af1ec15815abdb3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require("fs") | |
const path = require("path") | |
const {createBundleRenderer} = require("vue-server-renderer") | |
const LRU = require("lru-cache") | |
const find = require("find") | |
const {VueLoaderPlugin} = require("vue-loader") | |
const webpack = require("webpack") | |
const MFS = require("memory-fs") | |
const {promisify} = require('util') | |
const {Union} = require('unionfs') | |
/** | |
* @typedef CompiledObjectType | |
* @prop {String} server | |
* @prop {String} client | |
* @prop {String} filePath | |
*/ | |
/** | |
* @typedef WebpackConfigType | |
* @prop {string} server | |
* @prop {string} client | |
* @prop {Object} config | |
*/ | |
/** | |
* @typedef VueOptionsType | |
* @prop {String} title | |
* @prop {Object} head | |
* @prop {Object[]} head.scripts | |
* @prop {Object[]} head.metas | |
* @prop {Object[]} head.styles | |
* @prop {Layout} template | |
*/ | |
/** | |
* @typedef ConfigObjectType | |
* @prop {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache | |
* @prop {String} viewsPath | |
* @prop {String} vueVersion | |
* @prop {(VueOptionsType|Object)} head | |
* @prop {Object} data | |
*/ | |
class Renderer { | |
/** | |
* @prop {{max: number, maxAge: number}} cacheOptions - cacheoptions for LRU cache | |
* @prop {LRU} lruCache - LRU Cache | |
* @prop {vueServerRenderer} renderer - instance of vue server renderer | |
* @prop {String} viewsPath - Path that contains .vue files | |
* @prop {String} nodeModulesPath - Path of node_modules folder | |
* Webpack config here | |
*/ | |
constructor() { | |
this.lruCache = LRU({ | |
max: 500, | |
maxAge: 1000 * 60 * 60, | |
}) | |
/** | |
* File System Setup | |
*/ | |
let mfs = new MFS() | |
let ufs = new Union() | |
this.mfs = mfs | |
this.ufs = ufs | |
.use(mfs) | |
.use(fs) | |
//common config with resolve here | |
this.webpackServerConfig = { | |
mode: "development", | |
target: "async-node", | |
output: { | |
libraryTarget: "commonjs2", | |
}, | |
module: { | |
rules: [{ | |
test: /\.vue$/, | |
loader: "vue-loader", | |
}, | |
{ | |
test: /\.js$/, | |
loader: "babel-loader", | |
}, | |
{ | |
test: /\.css$/, | |
use: [ | |
"vue-style-loader", | |
"css-loader", | |
], | |
}, | |
{ | |
test: /\.styl(us)?$/, | |
use: [ | |
'vue-style-loader', | |
'css-loader', | |
'stylus-loader' | |
] | |
} | |
], | |
}, | |
plugins: [ | |
new VueLoaderPlugin(), | |
] | |
}; | |
this.webpackClientConfig = { | |
output: { | |
}, | |
mode: "development", | |
module: { | |
rules: [{ | |
test: /\.vue$/, | |
loader: "vue-loader", | |
}, | |
{ | |
test: /\.js$/, | |
loader: "babel-loader", | |
}, | |
{ | |
test: /\.css$/, | |
use: [ | |
"vue-style-loader", | |
"css-loader", | |
], | |
}, | |
{ | |
test: /\.styl(us)?$/, | |
use: [ | |
'vue-style-loader', | |
'css-loader', | |
'stylus-loader' | |
] | |
} | |
], | |
}, | |
plugins: [ | |
new VueLoaderPlugin(), | |
], | |
} | |
//precompile all vue files | |
} | |
set viewsPath(value){ | |
this.$mainPath = value | |
this.mfs.mkdirpSync(value) | |
} | |
get viewsPath(){ | |
return this.$mainPath | |
} | |
/** | |
* Pre-compile all Vue files that | |
* | |
* @returns {Promise<void>} | |
*/ | |
preCompile(){ | |
return new Promise((resolve, reject)=>{ | |
/** | |
* @param {array} files | |
*/ | |
async function files(files){ | |
for (let filePath of files) { | |
try { | |
await this.MakeVueClass(filePath, {}, filePath.replace(this.viewsPath, "")) | |
if (!process.env.TEST) { | |
const debug = `Precached -> ${filePath}`; | |
console.info(debug); | |
} | |
} catch (error) { | |
console.error(`Error Precaching \nfilePath -> ${filePath}\n-------\n${error}`) | |
} | |
} | |
resolve() | |
} | |
find.file(/\.vue$/, this.viewsPath, files) | |
}) | |
} | |
/** | |
* @param {string} filePath | |
* @param {object} data | |
* @returns {Promise<WebpackConfigType>} | |
*/ | |
async BuildConfig(filePath, data) { | |
const fullPath = path.resolve(this.viewsPath, filePath) | |
const serverPath = path.join(fullPath, "server-entry.js") | |
const clientPath = path.join(fullPath, "client-entry.js") | |
const clientBundlePath = `${filePath}.client-bundle.js` | |
const serverBundlePath = `${filePath}.server-bundle.js` | |
let store = data || {} | |
const appImport = `import Vue from "vue" | |
import App from '${fullPath.replace(/\\/g, '/')}' | |
function createApp (data) { | |
const app = new Vue({ | |
data, | |
render: h => h(App) | |
}) | |
return app | |
}` | |
const server = `${appImport} | |
export default async (context) => { | |
return await createApp(context) | |
}` | |
const client = `${appImport} | |
const store = ${JSON.stringify(store)} | |
const app = createApp(store) | |
app.$mount("#app")` | |
this.mfs.mkdirpSync(fullPath) | |
this.mfs.writeFileSync(serverPath, server, "utf-8") | |
this.mfs.writeFileSync(clientPath, client, "utf-8") | |
let webpackServerConfig = Object.assign({}, this.webpackServerConfig) | |
let webpackClientConfig = Object.assign({}, this.webpackClientConfig) | |
var resolve = { | |
modules: [ | |
path.resolve(this.viewsPath), | |
path.resolve(this.nodeModulesPath), | |
path.resolve(__dirname, 'imports') | |
] | |
} | |
webpackServerConfig.entry = serverPath | |
webpackServerConfig.output.path = this.viewsPath | |
webpackServerConfig.output.filename = serverBundlePath | |
webpackServerConfig.resolve = resolve | |
webpackClientConfig.entry = clientPath | |
webpackClientConfig.output.path = this.viewsPath | |
webpackClientConfig.output.filename = clientBundlePath | |
webpackClientConfig.resolve = resolve | |
return { | |
serverPath: path.join(this.viewsPath, serverBundlePath), | |
clientPath: path.join(this.viewsPath, clientBundlePath), | |
config: [webpackServerConfig, webpackClientConfig], | |
} | |
} | |
/** | |
* | |
* @param {WebpackConfigType} config | |
* @param {string} filePath | |
* @returns {Promise<{client: string, server: string, clientBundlePath: string}>} | |
*/ | |
async MakeBundle(config) { | |
const serverBundleFile = config.serverPath | |
const clientBundleFile = config.clientPath | |
try { | |
const serverBundle = this.mfs.readFileSync(serverBundleFile, "utf-8") | |
const clientBundle = this.mfs.readFileSync(clientBundleFile, "utf-8") | |
return { | |
server: serverBundle, | |
client: clientBundle | |
} | |
} catch (error) { | |
const compiler = webpack(config.config); | |
compiler.inputFileSystem = this.ufs | |
compiler.outputFileSystem = this.mfs | |
compiler.run = promisify(compiler.run) | |
let run = await compiler.run() | |
let stats = run.stats | |
for(let index in stats){ | |
let stat = stats[index] | |
if (stat.hasErrors()) { | |
throw stat.compilation.errors | |
} | |
const serverBundle = this.mfs.readFileSync(serverBundleFile, "utf-8") | |
const clientBundle = this.mfs.readFileSync(clientBundleFile, "utf-8") | |
return { | |
server: serverBundle, | |
client: clientBundle, | |
} | |
} | |
} | |
} | |
/** | |
* | |
* @param {string} filePath - .vue file path to render | |
* @param {Object} data | |
* @param {string} vueFile | |
* @returns {Promise<{renderer: {renderToStream: Function, renderToString: Function}, client: string, clientBundlePath: string}>} | |
*/ | |
async MakeVueClass(filePath, data) { | |
var config = await this.BuildConfig(filePath) | |
var bundle = await this.MakeBundle(config, filePath) | |
const renderer = createBundleRenderer(bundle.server, { | |
runInNewContext: false, | |
inject: false, | |
template: `<!DOCTYPE html> | |
<html> | |
<head> | |
{{{ meta }}} | |
{{{ renderResourceHints() }}} | |
{{{ renderStyles() }}} | |
</head> | |
<body> | |
<!--vue-ssr-outlet--> | |
<script>${ bundle.client }</script> | |
</body> | |
</html>` | |
}) | |
return renderer | |
} | |
/** | |
* render returns promise string which contians the rendered .vue file | |
* @param {string} vueFile - full path to vue component | |
* @param {Object} data - data to be inserted when generating vue class | |
* @param {Object} vueOptions - vue options to be used when generating head | |
* @returns {Promise<string>} | |
*/ | |
async render(vueFile, data, vueOptions) { | |
var renderer = await this.MakeVueClass(vueFile, data) | |
return await renderer.renderToString({}) | |
} | |
/** | |
* renderStream returns a stream from res.renderVue to the client | |
* @param {string} vueFile - full path to .vue component | |
* @param {Object} data - data to be inserted when generating vue class | |
* @param {VueOptionsType} vueOptions - vue options to be used when generating head | |
* @return {Promise<NodeJS.ReadableStream>} | |
*/ | |
async renderStream(vueFile, data, vueOptions) { | |
var renderer = await this.MakeVueClass(vueFile, data) | |
return await renderer.renderToStream({}) | |
} | |
/** | |
* @param {string} bundleFileName | |
* @returns {Promise<string>} bundle | |
*/ | |
async getBundleFile(bundleFileName) { | |
const clientBundle = this.mfs.readFileSync(bundleFileName, "utf-8") | |
return clientBundle | |
} | |
} | |
module.exports = Renderer |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment