markserv 1.17.3 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/server.js CHANGED
@@ -5,13 +5,13 @@ const path = require('path')
5
5
  const fs = require('fs')
6
6
 
7
7
  const chalk = require('chalk')
8
- const opn = require('opn')
8
+ const opn = require('open')
9
9
  const Promise = require('bluebird')
10
10
  const connect = require('connect')
11
11
  const less = require('less')
12
12
  const send = require('send')
13
- const liveReload = require('livereload')
14
- const connectLiveReload = require('connect-livereload')
13
+ const WebSocket = require('ws')
14
+ const getPort = require('get-port')
15
15
  const implant = require('implant')
16
16
  const deepmerge = require('deepmerge')
17
17
  const handlebars = require('handlebars')
@@ -111,13 +111,12 @@ const log = (str, flags, err) => {
111
111
  if (flags.silent) {
112
112
  return
113
113
  }
114
+
114
115
  if (str) {
115
- // eslint-disable-next-line no-console
116
116
  console.log(str)
117
117
  }
118
118
 
119
119
  if (err) {
120
- // eslint-disable-next-line no-console
121
120
  console.error(err)
122
121
  }
123
122
  }
@@ -147,8 +146,8 @@ const markdownToHTML = markdownText => new Promise((resolve, reject) => {
147
146
 
148
147
  try {
149
148
  result = md.render(markdownText)
150
- } catch (err) {
151
- return reject(err)
149
+ } catch (error) {
150
+ return reject(error)
152
151
  }
153
152
 
154
153
  resolve(result)
@@ -160,18 +159,19 @@ const getFile = path => new Promise((resolve, reject) => {
160
159
  if (err) {
161
160
  return reject(err)
162
161
  }
162
+
163
163
  resolve(data)
164
164
  })
165
165
  })
166
166
 
167
167
  // Get Custom Less CSS to use in all Markdown files
168
168
  const buildLessStyleSheet = cssPath =>
169
- new Promise(resolve =>
169
+ new Promise((resolve, reject) =>
170
170
  getFile(cssPath).then(data =>
171
171
  less.render(data).then(data =>
172
172
  resolve(data.css)
173
173
  )
174
- )
174
+ ).catch(reject)
175
175
  )
176
176
 
177
177
  const baseTemplate = (templateUrl, handlebarData) => new Promise((resolve, reject) => {
@@ -221,8 +221,8 @@ const dirToHtml = filePath => {
221
221
  prettyPath += '/'
222
222
  }
223
223
 
224
- if (prettyPath.substr(prettyPath.length - 2, 2) === '//') {
225
- prettyPath = prettyPath.substr(0, prettyPath.length - 1)
224
+ if (prettyPath.slice(prettyPath.length - 2, 2) === '//') {
225
+ prettyPath = prettyPath.slice(0, prettyPath.length - 1)
226
226
  }
227
227
 
228
228
  urls.forEach(subPath => {
@@ -296,13 +296,37 @@ const createBreadcrumbs = path => {
296
296
  }
297
297
 
298
298
  // Http_request_handler: handles all the browser requests
299
+ const resolveTheme = flags => {
300
+ if (flags.light) {
301
+ return 'light'
302
+ }
303
+
304
+ if (flags.synthwave) {
305
+ return 'synthwave'
306
+ }
307
+
308
+ if (flags.theme && flags.theme !== 'dark') {
309
+ return flags.theme
310
+ }
311
+
312
+ return 'dark'
313
+ }
314
+
299
315
  const createRequestHandler = flags => {
300
- let dir = flags.dir
316
+ let {dir} = flags
301
317
  const isDir = fs.statSync(dir).isDirectory()
302
318
  if (!isDir) {
303
319
  dir = path.parse(flags.dir).dir
304
320
  }
321
+
305
322
  flags.$openLocation = path.relative(dir, flags.dir)
323
+ const theme = resolveTheme(flags)
324
+ const themeFlags = {
325
+ themeDark: theme === 'dark',
326
+ themeLight: theme === 'light',
327
+ themeSynthwave: theme === 'synthwave',
328
+ themeSolarized: theme === 'solarized'
329
+ }
306
330
 
307
331
  const implantOpts = {
308
332
  maxDepth: 10
@@ -319,53 +343,69 @@ const createRequestHandler = flags => {
319
343
  }),
320
344
 
321
345
  file: (url, opts) => new Promise(resolve => {
346
+ if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
347
+ return resolve(false)
348
+ }
349
+
322
350
  const absUrl = path.join(opts.baseDir, url)
323
351
  getFile(absUrl)
324
352
  .then(data => {
325
353
  msg('implant', style.link(absUrl), flags)
326
354
  resolve(data)
327
355
  })
328
- .catch(err => {
329
- warnmsg('implant 404', style.link(absUrl), flags, err)
356
+ .catch(error => {
357
+ warnmsg('implant 404', style.link(absUrl), flags, error)
330
358
  resolve(false)
331
359
  })
332
360
  }),
333
361
 
334
362
  less: (url, opts) => new Promise(resolve => {
363
+ if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
364
+ return resolve(false)
365
+ }
366
+
335
367
  const absUrl = path.join(opts.baseDir, url)
336
368
  buildLessStyleSheet(absUrl)
337
369
  .then(data => {
338
370
  msg('implant', style.link(absUrl), flags)
339
371
  resolve(data)
340
372
  })
341
- .catch(err => {
342
- warnmsg('implant 404', style.link(absUrl), flags, err)
373
+ .catch(error => {
374
+ warnmsg('implant 404', style.link(absUrl), flags, error)
343
375
  resolve(false)
344
376
  })
345
377
  }),
346
378
 
347
379
  markdown: (url, opts) => new Promise(resolve => {
380
+ if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
381
+ return resolve(false)
382
+ }
383
+
348
384
  const absUrl = path.join(opts.baseDir, url)
349
385
  getFile(absUrl).then(markdownToHTML)
350
386
  .then(data => {
351
387
  msg('implant', style.link(absUrl), flags)
352
388
  resolve(data)
353
389
  })
354
- .catch(err => {
355
- warnmsg('implant 404', style.link(absUrl), flags, err)
390
+ .catch(error => {
391
+ warnmsg('implant 404', style.link(absUrl), flags, error)
356
392
  resolve(false)
357
393
  })
358
394
  }),
359
395
 
360
396
  html: (url, opts) => new Promise(resolve => {
397
+ if (typeof url !== 'string' || typeof (opts && opts.baseDir) !== 'string') {
398
+ return resolve(false)
399
+ }
400
+
361
401
  const absUrl = path.join(opts.baseDir, url)
362
402
  getFile(absUrl)
363
403
  .then(data => {
364
404
  msg('implant', style.link(absUrl), flags)
365
405
  resolve(data)
366
406
  })
367
- .catch(err => {
368
- warnmsg('implant 404', style.link(absUrl), flags, err)
407
+ .catch(error => {
408
+ warnmsg('implant 404', style.link(absUrl), flags, error)
369
409
  resolve(false)
370
410
  })
371
411
  })
@@ -395,7 +435,12 @@ const createRequestHandler = flags => {
395
435
  filePath,
396
436
  errorMsg,
397
437
  errorStack,
398
- referer
438
+ referer,
439
+ theme,
440
+ ...themeFlags,
441
+ hotreload: flags.$hotreload,
442
+ wsPort: flags.$wsPort,
443
+ rootDir: flags.dir
399
444
  }
400
445
 
401
446
  return baseTemplate(templateUrl, handlebarData).then(final => {
@@ -417,6 +462,7 @@ const createRequestHandler = flags => {
417
462
  if (flags.verbose) {
418
463
  msg('{markserv url}', style.link(markservRelFilePath), flags)
419
464
  }
465
+
420
466
  send(req, markservRelFilePath).pipe(res)
421
467
  return
422
468
  }
@@ -435,7 +481,7 @@ const createRequestHandler = flags => {
435
481
  isMarkdown = isType(fileTypes.markdown, filePath)
436
482
  isHtml = isType(fileTypes.html, filePath)
437
483
  }
438
- } catch (err) {
484
+ } catch (error) {
439
485
  const fileName = path.parse(filePath).base
440
486
  if (fileName === 'favicon.ico') {
441
487
  res.writeHead(200, {'Content-Type': 'image/x-icon'})
@@ -444,8 +490,8 @@ const createRequestHandler = flags => {
444
490
  return
445
491
  }
446
492
 
447
- errormsg('404', filePath, flags, err)
448
- errorPage(404, filePath, err)
493
+ errormsg('404', filePath, flags, error)
494
+ errorPage(404, filePath, error)
449
495
  return
450
496
  }
451
497
 
@@ -453,44 +499,62 @@ const createRequestHandler = flags => {
453
499
  if (isMarkdown) {
454
500
  msg('markdown', style.link(prettyPath), flags)
455
501
  getFile(filePath).then(markdownToHTML).then(filePath).then(html => {
456
- return implant(html, implantHandlers, implantOpts).then(output => {
502
+ const contentPromise = flags.templates ?
503
+ implant(html, implantHandlers, implantOpts) :
504
+ Promise.resolve(html)
505
+
506
+ return contentPromise.then(output => {
457
507
  const templateUrl = path.join(__dirname, 'templates/markdown.html')
458
508
 
459
509
  const handlebarData = {
460
510
  title: path.parse(filePath).base,
461
511
  content: output,
462
- pid: process.pid | 'N/A'
512
+ pid: process.pid | 'N/A',
513
+ theme,
514
+ ...themeFlags,
515
+ hotreload: flags.$hotreload,
516
+ wsPort: flags.$wsPort,
517
+ rootDir: flags.dir
463
518
  }
464
519
 
465
520
  return baseTemplate(templateUrl, handlebarData).then(final => {
466
- const lvl2Dir = path.parse(templateUrl).dir
467
- const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
468
-
469
- return implant(final, implantHandlers, lvl2Opts)
470
- .then(output => {
471
- res.writeHead(200, {
472
- 'content-type': 'text/html'
521
+ if (flags.templates) {
522
+ const lvl2Dir = path.parse(templateUrl).dir
523
+ const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
524
+
525
+ return implant(final, implantHandlers, lvl2Opts)
526
+ .then(output => {
527
+ res.writeHead(200, {
528
+ 'content-type': 'text/html'
529
+ })
530
+ res.end(output)
473
531
  })
474
- res.end(output)
475
- })
532
+ }
533
+
534
+ res.writeHead(200, {
535
+ 'content-type': 'text/html'
536
+ })
537
+ res.end(final)
476
538
  })
477
539
  })
478
- }).catch(err => {
479
- // eslint-disable-next-line no-console
480
- console.error(err)
540
+ }).catch(error => {
541
+ console.error(error)
481
542
  })
482
543
  } else if (isHtml) {
483
544
  msg('html', style.link(prettyPath), flags)
484
545
  getFile(filePath).then(html => {
485
- return implant(html, implantHandlers, implantOpts).then(output => {
546
+ const contentPromise = flags.templates ?
547
+ implant(html, implantHandlers, implantOpts) :
548
+ Promise.resolve(html)
549
+
550
+ return contentPromise.then(output => {
486
551
  res.writeHead(200, {
487
552
  'content-type': 'text/html'
488
553
  })
489
554
  res.end(output)
490
555
  })
491
- }).catch(err => {
492
- // eslint-disable-next-line no-console
493
- console.error(err)
556
+ }).catch(error => {
557
+ console.error(error)
494
558
  })
495
559
  } else if (isDir) {
496
560
  try {
@@ -504,24 +568,35 @@ const createRequestHandler = flags => {
504
568
  content: dirToHtml(filePath),
505
569
  title: path.parse(filePath).base,
506
570
  pid: process.pid | 'N/A',
507
- breadcrumbs: createBreadcrumbs(path.relative(dir, filePath))
571
+ breadcrumbs: createBreadcrumbs(path.relative(dir, filePath)),
572
+ theme,
573
+ ...themeFlags,
574
+ hotreload: flags.$hotreload,
575
+ wsPort: flags.$wsPort,
576
+ rootDir: flags.dir
508
577
  }
509
578
 
510
579
  return baseTemplate(templateUrl, handlebarData).then(final => {
511
- const lvl2Dir = path.parse(templateUrl).dir
512
- const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
513
- return implant(final, implantHandlers, lvl2Opts).then(output => {
514
- res.writeHead(200, {
515
- 'content-type': 'text/html'
580
+ if (flags.templates) {
581
+ const lvl2Dir = path.parse(templateUrl).dir
582
+ const lvl2Opts = deepmerge(implantOpts, {baseDir: lvl2Dir})
583
+ return implant(final, implantHandlers, lvl2Opts).then(output => {
584
+ res.writeHead(200, {
585
+ 'content-type': 'text/html'
586
+ })
587
+ res.end(output)
588
+ }).catch(error => {
589
+ console.error(error)
516
590
  })
517
- res.end(output)
518
- }).catch(err => {
519
- // eslint-disable-next-line no-console
520
- console.error(err)
591
+ }
592
+
593
+ res.writeHead(200, {
594
+ 'content-type': 'text/html'
521
595
  })
596
+ res.end(final)
522
597
  })
523
- } catch (err) {
524
- errorPage(500, filePath, err)
598
+ } catch (error) {
599
+ errorPage(500, filePath, error)
525
600
  }
526
601
  } else {
527
602
  // Other: Browser requests other MIME typed file (handled by 'send')
@@ -531,11 +606,8 @@ const createRequestHandler = flags => {
531
606
  }
532
607
  }
533
608
 
534
- const startConnectApp = (liveReloadPort, httpRequestHandler) => {
609
+ const startConnectApp = httpRequestHandler => {
535
610
  return connect()
536
- .use(connectLiveReload({
537
- port: liveReloadPort
538
- }))
539
611
  .use('/', httpRequestHandler)
540
612
  }
541
613
 
@@ -552,33 +624,178 @@ const startHTTPServer = (connectApp, port, flags) => {
552
624
  return httpServer
553
625
  }
554
626
 
555
- const startLiveReloadServer = (liveReloadPort, flags) => {
556
- let dir = flags.dir
627
+ const startHotReload = (wsPort, flags) => {
628
+ let {dir} = flags
557
629
  const isDir = fs.statSync(dir).isDirectory()
558
630
  if (!isDir) {
559
631
  dir = path.parse(flags.dir).dir
560
632
  }
561
633
 
562
- const exts = fileTypes.watch.map(type => type.substr(1))
563
- const exclusions = fileTypes.exclusions.map(exPath => {
564
- return path.join(dir, exPath)
634
+ const wss = new WebSocket.Server({port: wsPort})
635
+ const clients = new Map()
636
+
637
+ wss.on('connection', ws => {
638
+ ws.on('message', data => {
639
+ try {
640
+ const msg_ = JSON.parse(data)
641
+ if (msg_.path) {
642
+ clients.set(ws, msg_.path)
643
+ }
644
+ } catch (_) {}
645
+ })
646
+
647
+ ws.on('close', () => {
648
+ clients.delete(ws)
649
+ })
565
650
  })
566
651
 
567
- return liveReload.createServer({
568
- exts,
569
- exclusions,
570
- port: liveReloadPort
571
- }).watch(path.resolve(dir))
652
+ const sendToClients = (sockets, content) => {
653
+ for (const ws of sockets) {
654
+ if (ws.readyState === WebSocket.OPEN) {
655
+ ws.send(content)
656
+ }
657
+ }
658
+ }
659
+
660
+ // Debounced file watcher
661
+ let debounceTimer
662
+ const watchDir = path.resolve(dir)
663
+
664
+ const handleChange = () => {
665
+ // Group clients by path
666
+ const pathClients = new Map()
667
+ for (const [ws, clientPath] of clients) {
668
+ if (ws.readyState !== WebSocket.OPEN) {
669
+ continue
670
+ }
671
+
672
+ if (!pathClients.has(clientPath)) {
673
+ pathClients.set(clientPath, [])
674
+ }
675
+
676
+ pathClients.get(clientPath).push(ws)
677
+ }
678
+
679
+ const implantOpts = {maxDepth: 10}
680
+
681
+ const implantHandlers = {
682
+ markserv: () => new Promise(resolve => {
683
+ const value = path.relative(dir, __dirname)
684
+ resolve(value)
685
+ }),
686
+
687
+ file: (url, opts) => new Promise(resolve => {
688
+ const absUrl = path.join(opts.baseDir, url)
689
+ getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
690
+ }),
691
+
692
+ less: (url, opts) => new Promise(resolve => {
693
+ const absUrl = path.join(opts.baseDir, url)
694
+ buildLessStyleSheet(absUrl).then(data => resolve(data)).catch(() => resolve(false))
695
+ }),
696
+
697
+ markdown: (url, opts) => new Promise(resolve => {
698
+ const absUrl = path.join(opts.baseDir, url)
699
+ getFile(absUrl).then(markdownToHTML).then(data => resolve(data)).catch(() => resolve(false))
700
+ }),
701
+
702
+ html: (url, opts) => new Promise(resolve => {
703
+ const absUrl = path.join(opts.baseDir, url)
704
+ getFile(absUrl).then(data => resolve(data)).catch(() => resolve(false))
705
+ })
706
+ }
707
+
708
+ for (const [clientPath, sockets] of pathClients) {
709
+ const decodedUrl = getPathFromUrl(decodeURIComponent(clientPath))
710
+ const filePath = path.normalize(unescape(dir) + unescape(decodedUrl))
711
+ const baseDir = path.parse(filePath).dir
712
+ implantOpts.baseDir = baseDir
713
+
714
+ let stat
715
+ let isDir_
716
+ let isMarkdown
717
+
718
+ try {
719
+ stat = fs.statSync(filePath)
720
+ isDir_ = stat.isDirectory()
721
+ if (!isDir_) {
722
+ isMarkdown = isType(fileTypes.markdown, filePath)
723
+ }
724
+ } catch (_) {
725
+ continue
726
+ }
727
+
728
+ if (isMarkdown) {
729
+ getFile(filePath)
730
+ .then(markdownToHTML)
731
+ .then(html => implant(html, implantHandlers, Object.assign({}, implantOpts, {baseDir})))
732
+ .then(output => {
733
+ sendToClients(sockets, output)
734
+ })
735
+ .catch(error => {
736
+ errormsg('hotreload', filePath, flags, error)
737
+ })
738
+ } else if (isDir_) {
739
+ try {
740
+ const content = dirToHtml(filePath)
741
+ const breadcrumbHtml = createBreadcrumbs(path.relative(dir, filePath))
742
+ let headerHtml = '<h1 class="icon folder isfolder">'
743
+ for (const crumb of breadcrumbHtml) {
744
+ headerHtml += `<a href="${crumb.href}">${crumb.text}</a>`
745
+ }
746
+
747
+ headerHtml += '</h1>\n'
748
+ const fullContent = headerHtml + content +
749
+ '<footer><sup><hr> Served by <a href="https://www.npmjs.com/package/markserv">MarkServ</a> | PID: ' + (process.pid || 'N/A') + '</sup></footer>'
750
+
751
+ sendToClients(sockets, fullContent)
752
+ } catch (error) {
753
+ errormsg('hotreload', filePath, flags, error)
754
+ }
755
+ }
756
+ }
757
+ }
758
+
759
+ try {
760
+ fs.watch(watchDir, {recursive: true}, (eventType, filename) => {
761
+ if (!filename) {
762
+ return
763
+ }
764
+
765
+ // Check exclusions
766
+ for (const exclusion of fileTypes.exclusions) {
767
+ if (filename.includes(exclusion.replace(/\/$/, ''))) {
768
+ return
769
+ }
770
+ }
771
+
772
+ // Check if file extension is watched
773
+ const ext = path.extname(filename)
774
+ if (ext && !fileTypes.watch.includes(ext)) {
775
+ return
776
+ }
777
+
778
+ clearTimeout(debounceTimer)
779
+ debounceTimer = setTimeout(handleChange, 150)
780
+ })
781
+ } catch (error) {
782
+ errormsg('watch', watchDir, flags, error)
783
+ }
784
+
785
+ return wss
572
786
  }
573
787
 
574
- const logActiveServerInfo = async (serveURL, httpPort, liveReloadPort, flags) => {
788
+ const logActiveServerInfo = async (serveURL, httpPort, wsPort, flags) => {
575
789
  const dir = path.resolve(flags.dir)
576
790
 
577
791
  const githubLink = 'github.com/markserv'
578
792
 
579
793
  msg('address', style.address(serveURL), flags)
580
794
  msg('path', chalk`{grey ${style.address(dir)}}`, flags)
581
- msg('livereload', chalk`{grey communicating on port: ${style.port(liveReloadPort)}}`, flags)
795
+
796
+ if (wsPort) {
797
+ msg('hotreload', chalk`{grey ws://localhost:${style.port(wsPort)}}`, flags)
798
+ }
582
799
 
583
800
  if (process.pid) {
584
801
  msg('process', chalk`{grey your pid is: ${style.pid(process.pid)}}`, flags)
@@ -596,7 +813,7 @@ const checkForUpgrade = () => new Promise((resolve, reject) => {
596
813
  }
597
814
 
598
815
  analyzeDeps(packageJson).then(analysis => {
599
- const latest = analysis.dependencies.markserv.latest
816
+ const {latest} = analysis.dependencies.markserv
600
817
 
601
818
  switch (analysis.dependencies.markserv.status) {
602
819
  case 'error':
@@ -612,9 +829,9 @@ const checkForUpgrade = () => new Promise((resolve, reject) => {
612
829
  resolve(false)
613
830
  break
614
831
  }
615
- }).catch(err => {
832
+ }).catch(error => {
616
833
  console.log('err')
617
- reject(err)
834
+ reject(error)
618
835
  })
619
836
  })
620
837
 
@@ -628,6 +845,7 @@ const doUpgrade = (newerVersion, flags) => {
628
845
  if (code) {
629
846
  return msg(chalk.bgRed('✨UPGRADE✨'), 'Markserv could not upgrade.', flags)
630
847
  }
848
+
631
849
  msg(chalk.bgRed('✨UPGRADE✨'), 'Upgrade finished!', flags)
632
850
  })
633
851
  }
@@ -660,28 +878,37 @@ const optionalUpgrade = async flags => {
660
878
  }
661
879
 
662
880
  logInstallNotes()
663
- }).catch(err => {
664
- console.error(err)
881
+ }).catch(error => {
882
+ console.error(error)
665
883
  })
666
884
  }
667
885
 
668
886
  const init = async flags => {
669
- const liveReloadPort = flags.livereloadport
670
- const httpPort = flags.port
887
+ const preferredPort = Number(flags.port) || 8642
888
+ const httpPort = flags.port ? preferredPort : await getPort({port: preferredPort})
889
+
890
+ let wsPort = null
891
+ const hotreloadEnabled = flags.hotreload !== false && flags.hotreload !== 'false'
892
+ if (hotreloadEnabled) {
893
+ wsPort = await getPort({port: httpPort + 1})
894
+ }
895
+
896
+ flags.$wsPort = wsPort
897
+ flags.$hotreload = hotreloadEnabled
671
898
 
672
899
  const httpRequestHandler = createRequestHandler(flags)
673
- const connectApp = startConnectApp(liveReloadPort, httpRequestHandler)
900
+ const connectApp = startConnectApp(httpRequestHandler)
674
901
  const httpServer = await startHTTPServer(connectApp, httpPort, flags)
675
902
 
676
- let liveReloadServer
677
- if (liveReloadPort && liveReloadPort !== 'false') {
678
- liveReloadServer = await startLiveReloadServer(liveReloadPort, flags)
903
+ let hotReloadServer
904
+ if (hotreloadEnabled) {
905
+ hotReloadServer = startHotReload(wsPort, flags)
679
906
  }
680
907
 
681
908
  const serveURL = 'http://' + flags.address + ':' + httpPort
682
909
 
683
910
  // Log server info to CLI
684
- logActiveServerInfo(serveURL, httpPort, liveReloadPort, flags)
911
+ logActiveServerInfo(serveURL, httpPort, wsPort, flags)
685
912
 
686
913
  let launchUrl = false
687
914
  if (flags.$openLocation || flags.$pathProvided) {
@@ -691,7 +918,7 @@ const init = async flags => {
691
918
  const service = {
692
919
  pid: process.pid,
693
920
  httpServer,
694
- liveReloadServer,
921
+ hotReloadServer,
695
922
  connectApp,
696
923
  launchUrl
697
924
  }