UNPKG

vue-easy-renderer

Version:

Vue.js 2.0 server-side renderer for *.vue file with Node.js.

241 lines (224 loc) 6.61 kB
// const path = require('path'); const vm = require('vm'); const webpack = require('webpack'); const nodeVersion = require('node-version'); const nodeExternals = require('webpack-node-externals'); const webpackMerge = require('webpack-merge'); const ErrorTypes = require('../error'); const cacheMap = new Map(); const compilingWaitingQueueMap = new Map(); const defaultOptions = { basePath: __dirname, watch: false, global: Object.create(null), config: Object.create(null), outputPath: '/tmp/vue_ssr', }; /** * Compiler Class * * @class Compiler * @implements {ICompiler} */ class Compiler { constructor(fs, options) { this.options = Object.assign({}, defaultOptions, options); this.fs = fs; delete this.options.config.output; } /** * dynamic import * e.g. * const component = await compiler.import('component.vue'); * * @param {string} request * @returns {Promise<any>} * @memberof Compiler */ import(request) { if (Compiler.cacheMap.has(request)) { return Promise.resolve(Compiler.cacheMap.get(request)); } const compilingWaitingQueue = compilingWaitingQueueMap.get(request); if (compilingWaitingQueue) { return new Promise((resolve, reject) => compilingWaitingQueue.push({ resolve, reject })); } const resultPromise = new Promise((resolve, reject) => compilingWaitingQueueMap.set(request, [{ resolve, reject }])); return this.load([request]).then(() => resultPromise); } /** * compile file * * @param {Array<string>} filePaths * @returns {Promise<void>} * @memberof Compiler */ compile(filePaths) { const fileMap = new Map(); filePaths.forEach((filePath) => { fileMap.set(Compiler.getFileNameByPath(filePath), filePath); }); const webpackConfig = this.getConfig(fileMap); const serverCompiler = webpack(webpackConfig); serverCompiler.outputFileSystem = this.fs; const runner = this.options.watch ? cb => serverCompiler.watch({}, cb) : cb => serverCompiler.run(cb); return new Promise((resolve, reject) => { runner((error, stats) => { if (error) { reject(new ErrorTypes.CompilerError(error)); return; } const info = stats.toJson(); if (stats.hasErrors()) { const e = new ErrorTypes.CompilerError(); e.errors = info.errors; reject(e); } else { resolve(); } }); }); } /** * load file into cache * * @param {Array<string>} filePaths * @returns {Promise<void>} * @memberof Compiler */ load(filePaths) { if (filePaths.length === 0) return Promise.resolve(); filePaths.forEach((filePath) => { if (!compilingWaitingQueueMap.has(filePath)) { compilingWaitingQueueMap.set(filePath, []); } }); return this.compile(filePaths).then(() => Promise.all(filePaths.map(filePath => new Promise((resolve, reject) => { const fileName = Compiler.getFileNameByPath(filePath); this.fs.readFile(path.normalize(`${this.options.outputPath}/${fileName}.js`), (error, data) => { const compilingWaitingQueue = compilingWaitingQueueMap.get(filePath); if (error) { if (compilingWaitingQueue) { compilingWaitingQueue.forEach(callback => callback.reject(error)); } reject(error); return; } const object = this.getObject(data.toString()); Compiler.cacheMap.set(filePath, object); if (compilingWaitingQueue) { compilingWaitingQueue.forEach(callback => callback.resolve(object)); } compilingWaitingQueueMap.delete(filePath); resolve(); }); }))).then()); } /** * * @param {string} sourceFile * @returns {*} * @memberof Compiler */ getObject(sourceFile) { const sandboxGlobal = Object.assign({}, global, { module, require }, this.options.global); const sandbox = vm.createContext(sandboxGlobal); return vm.runInContext(sourceFile, sandbox); } /** * get webpack config * * @param {Map<string, string>} fileMap * @returns {Object} * @memberof Compiler */ getConfig(fileMap) { const entry = Object.create(null); [...fileMap.entries()].forEach(([fileName, filePath]) => { entry[fileName] = [filePath]; }); const defaultConfig = { entry, target: 'node', output: { path: this.options.outputPath, filename: '[name].js', libraryTarget: 'commonjs2', }, module: { rules: [{ test: /\.vue$/, use: { loader: 'vue-loader', options: { loaders: { js: { loader: 'babel-loader', options: { presets: [ ['env', { targets: { node: Number(nodeVersion.major) } }], ], plugins: ['transform-object-rest-spread'], babelrc: false, }, }, css: 'null-loader', sass: 'null-loader', scss: 'null-loader', less: 'null-loader', }, }, }, }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['env', { targets: { node: Number(nodeVersion.major) } }], ], plugins: ['transform-object-rest-spread'], babelrc: false, }, }, }, { test: /\.css$|\.scss|\.sass|\.less$/, use: { loader: 'null-loader', }, }], }, externals: [nodeExternals()], context: this.options.basePath, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 'process.env.VUE_ENV': '"server"', }), ], }; return webpackMerge.smart(defaultConfig, this.options.config); } /** * get file name by path * * @static * @param {string} filePath * @returns {string} * @memberof Compiler */ static getFileNameByPath(filePath) { const pathHexStr = (new Buffer(filePath)).toString('hex'); return `${path.basename(filePath)}.${pathHexStr}`; } } Compiler.cacheMap = cacheMap; module.exports = Compiler;