Skip to content

Instantly share code, notes, and snippets.

@AlbertMarashi
Created December 16, 2018 05:42
Show Gist options
  • Save AlbertMarashi/dcd441d2842682485af1ec15815abdb3 to your computer and use it in GitHub Desktop.
Save AlbertMarashi/dcd441d2842682485af1ec15815abdb3 to your computer and use it in GitHub Desktop.
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