UNPKG

@quasar/app

Version:

Quasar Framework local CLI

298 lines (244 loc) 7.91 kB
const webpack = require('webpack') const WebpackDevServer = require('webpack-dev-server') const appPaths = require('./app-paths') const logger = require('./helpers/logger') const openBrowser = require('./helpers/open-browser') const log = logger('app:dev-server') let alreadyNotified = false module.exports = class DevServer { constructor (quasarConfig) { this.quasarConfig = quasarConfig } async listen () { const webpackConfig = this.quasarConfig.getWebpackConfig() const cfg = this.quasarConfig.getBuildConfig() log(`Booting up...`) log() return new Promise(resolve => ( cfg.ctx.mode.ssr ? this.listenSSR(webpackConfig, cfg, resolve) : this.listenCSR(webpackConfig, cfg, resolve) )) } listenCSR (webpackConfig, cfg, resolve) { const compiler = webpack(webpackConfig.renderer || webpackConfig) compiler.hooks.done.tap('done-compiling', compiler => { if (this.__started) { return } // start dev server if there are no errors if (compiler.compilation.errors && compiler.compilation.errors.length > 0) { return } this.__started = true server.listen(cfg.devServer.port, cfg.devServer.host, () => { resolve() if (alreadyNotified) { return } alreadyNotified = true if (cfg.__devServer.open && ['spa', 'pwa'].includes(cfg.ctx.modeName)) { openBrowser({ url: cfg.build.APP_URL, opts: cfg.__devServer.openOptions }) } }) }) // start building & launch server const server = new WebpackDevServer(compiler, cfg.devServer) this.__cleanup = () => { this.__cleanup = null return new Promise(resolve => { server.close(resolve) }) } } listenSSR (webpackConfig, cfg, resolve) { const fs = require('fs') const LRU = require('lru-cache') const express = require('express') const chokidar = require('chokidar') const { createBundleRenderer } = require('vue-server-renderer') const ouchInstance = require('./helpers/cli-error-handling').getOuchInstance() const SsrExtension = require('./ssr/ssr-extension') let renderer function createRenderer (bundle, options) { // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer return createBundleRenderer(bundle, { ...options, ...(cfg.build.preloadChunks !== true ? { shouldPreload: () => false, shouldPrefetch: () => false } : {} ), // for component caching cache: new LRU({ max: 1000, maxAge: 1000 * 60 * 15 }), // recommended for performance runInNewContext: false }) } function render (req, res) { const startTime = Date.now() res.setHeader('Content-Type', 'text/html') const handleError = err => { if (err.url) { res.redirect(err.url) } else if (err.code === 404) { res.status(404).send('404 | Page Not Found') } else { ouchInstance.handleException(err, req, res, output => { console.error(`${req.url} -> error during render`) console.error(err.stack) }) } } const context = { url: req.url, req, res } renderer.renderToString(context, (err, html) => { if (err) { handleError(err) return } if (cfg.__meta) { html = context.$getMetaHTML(html) } console.log(`${req.url} -> request took: ${Date.now() - startTime}ms`) res.send(html) }) } let bundle let template let clientManifest let pwa let ready const readyPromise = new Promise(r => { ready = r }) function update () { if (bundle && clientManifest) { renderer = createRenderer(bundle, { template, clientManifest, basedir: appPaths.resolve.app('.') }) ready() } } // read template from disk and watch const { getIndexHtml } = require('./ssr/html-template') const templatePath = appPaths.resolve.app(cfg.sourceFiles.indexHtmlTemplate) function getTemplate () { return getIndexHtml(fs.readFileSync(templatePath, 'utf-8'), cfg) } template = getTemplate() const htmlWatcher = chokidar.watch(templatePath).on('change', () => { template = getTemplate() console.log('index.template.html template updated.') update() }) const serverCompiler = webpack(webpackConfig.server) const clientCompiler = webpack(webpackConfig.client) serverCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => { errors.forEach(err => console.error(err)) warnings.forEach(err => console.warn(err)) if (errors.length > 0) { cb() return } bundle = JSON.parse(assets['../vue-ssr-server-bundle.json'].source()) update() cb() }) clientCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => { errors.forEach(err => console.error(err)) warnings.forEach(err => console.warn(err)) if (errors.length > 0) { cb() return } if (cfg.ctx.mode.pwa) { pwa = { manifest: assets['manifest.json'].source(), serviceWorker: assets['service-worker.js'].source() } } clientManifest = JSON.parse(assets['../vue-ssr-client-manifest.json'].source()) update() cb() }) const serverCompilerWatcher = serverCompiler.watch({}, () => {}) const originalAfter = cfg.devServer.after // start building & launch server const server = new WebpackDevServer(clientCompiler, { ...cfg.devServer, after: app => { if (cfg.ctx.mode.pwa) { app.use('/manifest.json', (req, res) => { res.setHeader('Content-Type', 'application/json') res.send(pwa.manifest) }) app.use('/service-worker.js', (req, res) => { res.setHeader('Content-Type', 'text/javascript') res.send(pwa.serviceWorker) }) } app.use('/statics', express.static(appPaths.resolve.src('statics'), { maxAge: 0 })) originalAfter && originalAfter(app) SsrExtension.getModule().extendApp({ app, ssr: { renderToString ({ req, res }, fn) { const context = { url: req.url, req, res } renderer.renderToString(context, (err, html) => { if (err) { handleError(err) return } if (cfg.__meta) { html = context.$getMetaHTML(html) } fn(err, html) }) }, settings: Object.assign( {}, JSON.parse(cfg.ssr.__templateOpts), { debug: true } ) } }) app.get('*', render) } }) readyPromise.then(() => { server.listen(cfg.devServer.port, cfg.devServer.host, () => { resolve() if (cfg.__devServer.open) { openBrowser({ url: cfg.build.APP_URL, opts: cfg.__devServer.openOptions }) } }) }) this.__cleanup = () => { this.__cleanup = null htmlWatcher.close() return Promise.all([ new Promise(resolve => { server.close(resolve) }), new Promise(resolve => { serverCompilerWatcher.close(resolve) }) ]) } } stop () { if (this.__cleanup) { log(`Shutting down`) return this.__cleanup() } } }