1 | const
|
2 | webpack = require('webpack'),
|
3 | WebpackDevServer = require('webpack-dev-server')
|
4 |
|
5 | const
|
6 | appPaths = require('./app-paths'),
|
7 | log = require('./helpers/logger')('app:dev-server')
|
8 |
|
9 | let alreadyNotified = false
|
10 |
|
11 | function openBrowser (url) {
|
12 | const opn = require('opn')
|
13 | opn(url)
|
14 | }
|
15 |
|
16 | module.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 |
|
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 |
|
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 |
|
85 | return createBundleRenderer(bundle, Object.assign(options, {
|
86 |
|
87 | cache: LRU({
|
88 | max: 1000,
|
89 | maxAge: 1000 * 60 * 15
|
90 | }),
|
91 |
|
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 |
|
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 |
|
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 | }
|