1 | #!/usr/bin/env node
|
2 |
|
3 | const parseArgs = require('minimist')
|
4 |
|
5 | const argv = parseArgs(process.argv.slice(2), {
|
6 | alias: {
|
7 | p: 'port',
|
8 | H: 'hostname',
|
9 | g: 'gzip',
|
10 | s: 'silent',
|
11 | colors: 'colors',
|
12 | o: 'open',
|
13 | c: 'cache',
|
14 | m: 'micro',
|
15 | history: 'history',
|
16 | https: 'https',
|
17 | C: 'cert',
|
18 | K: 'key',
|
19 | P: 'proxy',
|
20 | h: 'help'
|
21 | },
|
22 | boolean: ['g', 'https', 'colors', 'S', 'history', 'h'],
|
23 | string: ['H', 'C', 'K'],
|
24 | default: {
|
25 | p: process.env.PORT || 4000,
|
26 | H: process.env.HOSTNAME || '0.0.0.0',
|
27 | g: true,
|
28 | c: 24 * 60 * 60,
|
29 | m: 1,
|
30 | colors: true
|
31 | }
|
32 | })
|
33 |
|
34 | if (argv.help) {
|
35 | console.log(`
|
36 | Description
|
37 | Start a HTTP(S) server on a folder.
|
38 |
|
39 | Usage
|
40 | $ quasar serve [path]
|
41 | $ quasar serve . # serve current folder
|
42 |
|
43 | If you serve a SSR folder built with the CLI then
|
44 | control is yielded to /index.js and params have no effect.
|
45 |
|
46 | Options
|
47 | --port, -p Port to use (default: 8080)
|
48 | --hostname, -H Address to use (default: 0.0.0.0)
|
49 | --gzip, -g Compress content (default: true)
|
50 | --silent, -s Supress log message
|
51 | --colors Log messages with colors (default: true)
|
52 | --open, -o Open browser window after starting
|
53 | --cache, -c <number> Cache time (max-age) in seconds;
|
54 | Does not apply to /service-worker.js
|
55 | (default: 86400 - 24 hours)
|
56 | --micro, -m <seconds> Use micro-cache (default: 1 second)
|
57 | --history Use history api fallback;
|
58 | All requests fallback to index.html
|
59 | --https Enable HTTPS
|
60 | --cert, -C [path] Path to SSL cert file (Optional)
|
61 | --key, -K [path] Path to SSL key file (Optional)
|
62 | --proxy <file.js> Proxy specific requests defined in file;
|
63 | File must export Array ({ path, rule })
|
64 | See example below. "rule" is defined at:
|
65 | https://github.com/chimurai/http-proxy-middleware
|
66 | --help, -h Displays this message
|
67 |
|
68 | Proxy file example
|
69 | module.exports = [
|
70 | {
|
71 | path: '/api',
|
72 | rule: { target: 'http://www.example.org' }
|
73 | }
|
74 | ]
|
75 | --> will be transformed into app.use(path, httpProxyMiddleware(rule))
|
76 | `)
|
77 | process.exit(0)
|
78 | }
|
79 |
|
80 | const
|
81 | fs = require('fs'),
|
82 | path = require('path')
|
83 |
|
84 | const root = getAbsolutePath(argv._[0] || '.')
|
85 | const resolve = p => path.resolve(root, p)
|
86 |
|
87 | function getAbsolutePath (pathParam) {
|
88 | return path.isAbsolute(pathParam)
|
89 | ? pathParam
|
90 | : path.join(process.cwd(), pathParam)
|
91 | }
|
92 |
|
93 | const
|
94 | pkgFile = resolve('package.json'),
|
95 | indexFile = resolve('index.js')
|
96 |
|
97 | let ssrDetected = false
|
98 |
|
99 | if (fs.existsSync(pkgFile) && fs.existsSync(indexFile)) {
|
100 | const pkg = require(pkgFile)
|
101 | if (pkg.quasar && pkg.quasar.ssr) {
|
102 | console.log('Quasar SSR folder detected.')
|
103 | console.log('Yielding control to its own webserver.')
|
104 | console.log()
|
105 | ssrDetected = true
|
106 | require(indexFile)
|
107 | }
|
108 | }
|
109 |
|
110 | if (ssrDetected === false) {
|
111 | let green, grey, red
|
112 |
|
113 | if (argv.colors) {
|
114 | const chalk = require('chalk')
|
115 | green = chalk.green
|
116 | grey = chalk.grey
|
117 | red = chalk.red
|
118 | }
|
119 | else {
|
120 | green = grey = red = text => text
|
121 | }
|
122 |
|
123 | const
|
124 | express = require('express'),
|
125 | microCacheSeconds = argv.micro
|
126 | ? parseInt(argv.micro, 10)
|
127 | : false
|
128 |
|
129 | function serve (path, cache) {
|
130 | return express.static(resolve(path), {
|
131 | maxAge: cache ? parseInt(argv.cache, 10) * 1000 : 0
|
132 | })
|
133 | }
|
134 |
|
135 | const app = express()
|
136 |
|
137 | if (!argv.silent) {
|
138 | app.get('*', (req, res, next) => {
|
139 | console.log(
|
140 | `GET ${green(req.url)} ${grey('[' + req.ip + ']')} ${new Date()}`
|
141 | )
|
142 | next()
|
143 | })
|
144 | }
|
145 |
|
146 | if (argv.gzip) {
|
147 | const compression = require('compression')
|
148 | app.use(compression({ threshold: 0 }))
|
149 | }
|
150 |
|
151 | const serviceWorkerFile = resolve('service-worker.js')
|
152 | if (fs.existsSync(serviceWorkerFile)) {
|
153 | app.use('/service-worker.js', serve('service-worker.js'))
|
154 | }
|
155 |
|
156 | if (argv.history) {
|
157 | const history = require('connect-history-api-fallback')
|
158 | app.use(history())
|
159 | }
|
160 |
|
161 | app.use('/', serve('.', true))
|
162 |
|
163 | if (microCacheSeconds) {
|
164 | const microcache = require('route-cache')
|
165 | app.use(
|
166 | microcache.cacheSeconds(
|
167 | microCacheSeconds,
|
168 | req => req.originalUrl
|
169 | )
|
170 | )
|
171 | }
|
172 |
|
173 | if (argv.proxy) {
|
174 | let file = argv.proxy = getAbsolutePath(argv.proxy)
|
175 | if (!fs.existsSync(file)) {
|
176 | console.error('Proxy definition file not found! ' + file)
|
177 | process.exit(1)
|
178 | }
|
179 | file = require(file)
|
180 |
|
181 | const proxy = require('http-proxy-middleware')
|
182 | file.forEach(entry => {
|
183 | app.use(entry.path, proxy(entry.rule))
|
184 | })
|
185 | }
|
186 |
|
187 | app.get('*', (req, res) => {
|
188 | res.setHeader('Content-Type', 'text/html')
|
189 | res.status(404).send('404 | Page Not Found')
|
190 | if (!argv.silent) {
|
191 | console.log(red(` 404 on ${req.url}`))
|
192 | }
|
193 | })
|
194 |
|
195 | function getHostname (host) {
|
196 | return host === '0.0.0.0'
|
197 | ? 'localhost'
|
198 | : host
|
199 | }
|
200 |
|
201 | getServer(app).listen(argv.port, argv.hostname, () => {
|
202 | const
|
203 | url = `http${argv.https ? 's' : ''}://${getHostname(argv.hostname)}:${argv.port}`,
|
204 | { version } = require('../package.json')
|
205 |
|
206 | const info = [
|
207 | ['Quasar CLI', `v${version}`],
|
208 | ['Listening at', url],
|
209 | ['Web server root', root],
|
210 | argv.https ? ['HTTPS', 'enabled'] : '',
|
211 | argv.gzip ? ['Gzip', 'enabled'] : '',
|
212 | ['Cache (max-age)', argv.cache || 'disabled'],
|
213 | microCacheSeconds ? ['Micro-cache', microCacheSeconds + 's'] : '',
|
214 | argv.history ? ['History mode', 'enabled'] : '',
|
215 | argv.proxy ? ['Proxy definitions', argv.proxy] : ''
|
216 | ]
|
217 | .filter(msg => msg)
|
218 | .map(msg => ' ' + msg[0].padEnd(20, '.') + ' ' + green(msg[1]))
|
219 |
|
220 | console.log('\n' + info.join('\n') + '\n')
|
221 |
|
222 | if (argv.open) {
|
223 | const isMinimalTerminal = require('../lib/helpers/is-minimal-terminal')
|
224 | if (!isMinimalTerminal) {
|
225 | const opn = require('opn')
|
226 | opn(url)
|
227 | }
|
228 | }
|
229 | })
|
230 |
|
231 | function getServer (app) {
|
232 | if (!argv.https) {
|
233 | return app
|
234 | }
|
235 |
|
236 | let fakeCert, key, cert
|
237 |
|
238 | if (argv.key && argv.cert) {
|
239 | key = getAbsolutePath(argv.key)
|
240 | cert = getAbsolutePath(argv.cert)
|
241 |
|
242 | if (fs.existsSync(key)) {
|
243 | key = fs.readFileSync(key)
|
244 | }
|
245 | else {
|
246 | console.error('SSL key file not found!' + key)
|
247 | process.exit(1)
|
248 | }
|
249 |
|
250 | if (fs.existsSync(cert)) {
|
251 | cert = fs.readFileSync(cert)
|
252 | }
|
253 | else {
|
254 | console.error('SSL cert file not found!' + cert)
|
255 | process.exit(1)
|
256 | }
|
257 | }
|
258 | else {
|
259 | // Use a self-signed certificate if no certificate was configured.
|
260 | // Cycle certs every 24 hours
|
261 | const certPath = path.join(__dirname, '../ssl-server.pem')
|
262 | let certExists = fs.existsSync(certPath)
|
263 |
|
264 | if (certExists) {
|
265 | const certStat = fs.statSync(certPath)
|
266 | const certTtl = 1000 * 60 * 60 * 24
|
267 | const now = new Date()
|
268 |
|
269 | // cert is more than 30 days old
|
270 | if ((now - certStat.ctime) / certTtl > 30) {
|
271 | console.log(' SSL Certificate is more than 30 days old. Removing.')
|
272 | const { removeSync } = require('fs-extra')
|
273 | removeSync(certPath)
|
274 | certExists = false
|
275 | }
|
276 | }
|
277 |
|
278 | if (!certExists) {
|
279 | console.log(' Generating self signed SSL Certificate...')
|
280 | console.log(' DO NOT use this self-signed certificate in production!')
|
281 |
|
282 | const selfsigned = require('selfsigned')
|
283 | const pems = selfsigned.generate(
|
284 | [{ name: 'commonName', value: 'localhost' }],
|
285 | {
|
286 | algorithm: 'sha256',
|
287 | days: 30,
|
288 | keySize: 2048,
|
289 | extensions: [{
|
290 | name: 'basicConstraints',
|
291 | cA: true
|
292 | }, {
|
293 | name: 'keyUsage',
|
294 | keyCertSign: true,
|
295 | digitalSignature: true,
|
296 | nonRepudiation: true,
|
297 | keyEncipherment: true,
|
298 | dataEncipherment: true
|
299 | }, {
|
300 | name: 'subjectAltName',
|
301 | altNames: [
|
302 | {
|
303 | // type 2 is DNS
|
304 | type: 2,
|
305 | value: 'localhost'
|
306 | },
|
307 | {
|
308 | type: 2,
|
309 | value: 'localhost.localdomain'
|
310 | },
|
311 | {
|
312 | type: 2,
|
313 | value: 'lvh.me'
|
314 | },
|
315 | {
|
316 | type: 2,
|
317 | value: '*.lvh.me'
|
318 | },
|
319 | {
|
320 | type: 2,
|
321 | value: '[::1]'
|
322 | },
|
323 | {
|
324 | // type 7 is IP
|
325 | type: 7,
|
326 | ip: '127.0.0.1'
|
327 | },
|
328 | {
|
329 | type: 7,
|
330 | ip: 'fe80::1'
|
331 | }
|
332 | ]
|
333 | }]
|
334 | }
|
335 | )
|
336 |
|
337 | try {
|
338 | fs.writeFileSync(certPath, pems.private + pems.cert, { encoding: 'utf-8' })
|
339 | }
|
340 | catch (err) {
|
341 | console.error(' Cannot write certificate file ' + certPath)
|
342 | console.error(' Aborting...')
|
343 | process.exit(1)
|
344 | }
|
345 | }
|
346 |
|
347 | fakeCert = fs.readFileSync(certPath)
|
348 | }
|
349 |
|
350 | return require('https').createServer({
|
351 | key: key || fakeCert,
|
352 | cert: cert || fakeCert
|
353 | }, app)
|
354 | }
|
355 | }
|