UNPKG

6.84 kBJavaScriptView Raw
1const
2 webpack = require('webpack'),
3 WebpackDevServer = require('webpack-dev-server')
4
5const
6 appPaths = require('./app-paths'),
7 log = require('./helpers/logger')('app:dev-server')
8
9let alreadyNotified = false
10
11function openBrowser (url) {
12 const opn = require('opn')
13 opn(url)
14}
15
16module.exports = class DevServer {
17 constructor (quasarConfig) {
18 this.quasarConfig = quasarConfig
19 }
20
21 async listen () {
22 const
23 webpackConfig = this.quasarConfig.getWebpackConfig(),
24 cfg = this.quasarConfig.getBuildConfig()
25
26 log(`Booting up...`)
27 log()
28
29 return new Promise(resolve => (
30 cfg.ctx.mode.ssr
31 ? this.listenSSR(webpackConfig, cfg, resolve)
32 : this.listenCSR(webpackConfig, cfg, resolve)
33 ))
34 }
35
36 listenCSR (webpackConfig, cfg, resolve) {
37 const compiler = webpack(webpackConfig.renderer || webpackConfig)
38
39 compiler.hooks.done.tap('done-compiling', compiler => {
40 if (this.__started) { return }
41
42 // start dev server if there are no errors
43 if (compiler.compilation.errors && compiler.compilation.errors.length > 0) {
44 return
45 }
46
47 this.__started = true
48
49 server.listen(cfg.devServer.port, cfg.devServer.host, () => {
50 resolve()
51
52 if (alreadyNotified) { return }
53 alreadyNotified = true
54
55 if (cfg.devServer.open && ['spa', 'pwa'].includes(cfg.ctx.modeName)) {
56 openBrowser(cfg.build.APP_URL)
57 }
58 })
59 })
60
61 // start building & launch server
62 const server = new WebpackDevServer(compiler, cfg.devServer)
63
64 this.__cleanup = () => {
65 this.__cleanup = null
66 return new Promise(resolve => {
67 server.close(resolve)
68 })
69 }
70 }
71
72 listenSSR (webpackConfig, cfg, resolve) {
73 const
74 fs = require('fs'),
75 LRU = require('lru-cache'),
76 express = require('express'),
77 chokidar = require('chokidar'),
78 { createBundleRenderer } = require('vue-server-renderer'),
79 ouchInstance = require('./helpers/cli-error-handling').getOuchInstance()
80
81 let renderer
82
83 function createRenderer (bundle, options) {
84 // https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
85 return createBundleRenderer(bundle, Object.assign(options, {
86 // for component caching
87 cache: LRU({
88 max: 1000,
89 maxAge: 1000 * 60 * 15
90 }),
91 // recommended for performance
92 runInNewContext: false
93 }))
94 }
95
96 function render (req, res) {
97 const startTime = Date.now()
98
99 res.setHeader('Content-Type', 'text/html')
100
101 const handleError = err => {
102 if (err.url) {
103 res.redirect(err.url)
104 }
105 else if (err.code === 404) {
106 res.status(404).send('404 | Page Not Found')
107 }
108 else {
109 ouchInstance.handleException(err, req, res, output => {
110 console.error(`${req.url} -> error during render`)
111 console.error(err.stack)
112 })
113 }
114 }
115
116 const context = {
117 url: req.url,
118 req,
119 res
120 }
121
122 renderer.renderToString(context, (err, html) => {
123 if (err) {
124 handleError(err)
125 return
126 }
127 if (cfg.__meta) {
128 html = context.$getMetaHTML(html)
129 }
130 console.log(`${req.url} -> request took: ${Date.now() - startTime}ms`)
131 res.send(html)
132 })
133 }
134
135 let
136 bundle,
137 template,
138 clientManifest,
139 pwa
140
141 let ready
142 const readyPromise = new Promise(r => { ready = r })
143 function update () {
144 if (bundle && clientManifest) {
145 renderer = createRenderer(bundle, {
146 template,
147 clientManifest,
148 basedir: appPaths.resolve.app('.')
149 })
150 ready()
151 }
152 }
153
154 // read template from disk and watch
155 const
156 { getIndexHtml } = require('./ssr/html-template'),
157 templatePath = appPaths.resolve.app(cfg.sourceFiles.indexHtmlTemplate)
158
159 function getTemplate () {
160 return getIndexHtml(fs.readFileSync(templatePath, 'utf-8'), cfg)
161 }
162
163 template = getTemplate()
164 const htmlWatcher = chokidar.watch(templatePath).on('change', () => {
165 template = getTemplate()
166 console.log('index.template.html template updated.')
167 update()
168 })
169
170 const
171 serverCompiler = webpack(webpackConfig.server),
172 clientCompiler = webpack(webpackConfig.client)
173
174 serverCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => {
175 errors.forEach(err => console.error(err))
176 warnings.forEach(err => console.warn(err))
177
178 if (errors.length > 0) {
179 cb()
180 return
181 }
182
183 bundle = JSON.parse(assets['../vue-ssr-server-bundle.json'].source())
184 update()
185
186 cb()
187 })
188
189 clientCompiler.hooks.done.tapAsync('done-compiling', ({ compilation: { errors, warnings, assets }}, cb) => {
190 errors.forEach(err => console.error(err))
191 warnings.forEach(err => console.warn(err))
192
193 if (errors.length > 0) {
194 cb()
195 return
196 }
197
198 if (cfg.ctx.mode.pwa) {
199 pwa = {
200 manifest: assets['manifest.json'].source(),
201 serviceWorker: assets['service-worker.js'].source()
202 }
203 }
204
205 clientManifest = JSON.parse(assets['../vue-ssr-client-manifest.json'].source())
206 update()
207
208 cb()
209 })
210
211 const serverCompilerWatcher = serverCompiler.watch({}, () => {})
212
213 // start building & launch server
214 const server = new WebpackDevServer(clientCompiler, Object.assign(
215 {
216 after: app => {
217 if (cfg.ctx.mode.pwa) {
218 app.use('/manifest.json', (req, res) => {
219 res.setHeader('Content-Type', 'application/json')
220 res.send(pwa.manifest)
221 })
222 app.use('/service-worker.js', (req, res) => {
223 res.setHeader('Content-Type', 'text/javascript')
224 res.send(pwa.serviceWorker)
225 })
226 }
227
228 app.use('/statics', express.static(appPaths.resolve.src('statics'), {
229 maxAge: 0
230 }))
231
232 cfg.__ssrExtension.extendApp({ app })
233
234 app.get('*', render)
235 }
236 },
237 cfg.devServer
238 ))
239
240 readyPromise.then(() => {
241 server.listen(cfg.devServer.port, cfg.devServer.host, () => {
242 resolve()
243 if (cfg.devServer.open) {
244 openBrowser(cfg.build.APP_URL)
245 }
246 })
247 })
248
249 this.__cleanup = () => {
250 this.__cleanup = null
251 htmlWatcher.close()
252 return Promise.all([
253 new Promise(resolve => { server.close(resolve) }),
254 new Promise(resolve => { serverCompilerWatcher.close(resolve) })
255 ])
256 }
257 }
258
259 stop () {
260 if (this.__cleanup) {
261 log(`Shutting down`)
262 return this.__cleanup()
263 }
264 }
265}