loading.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. const { resolve } = require('path')
  2. const { readFileSync } = require('fs')
  3. const connect = require('connect')
  4. const serveStatic = require('serve-static')
  5. const getPort = require('get-port-please')
  6. const { json, end, header } = require('node-res')
  7. const { parseStack } = require('./utils')
  8. const SSE = require('./sse')
  9. class LoadingUI {
  10. constructor (options) {
  11. this.options = options
  12. this._lastBroadcast = 0
  13. this.states = []
  14. this.allDone = true
  15. this.hasErrors = false
  16. this.serveIndex = this.serveIndex.bind(this)
  17. this._init()
  18. }
  19. _init () {
  20. // Create a connect middleware stack
  21. this.app = connect()
  22. // Create an SSE handler instance
  23. this.sse = new SSE()
  24. // Fix CORS
  25. this.app.use((req, res, next) => {
  26. res.setHeader('Access-Control-Allow-Origin', '*')
  27. next()
  28. })
  29. // Subscribe to SSR channel
  30. this.app.use('/sse', (req, res) => this.sse.subscribe(req, res))
  31. // Serve state with JSON
  32. this.app.use('/json', (req, res) => json(req, res, this.state))
  33. // Load indexTemplate
  34. const distPath = resolve(__dirname, '../app-dist')
  35. this.indexTemplate = readFileSync(resolve(distPath, 'index.html'), 'utf-8')
  36. // Serve assets
  37. this.app.use('/assets', serveStatic(resolve(distPath, 'assets')))
  38. }
  39. async initAlt ({ url }) {
  40. if (this._server || this.options.baseURLAlt) {
  41. return
  42. }
  43. // Redirect users directly open this port
  44. this.app.use('/', (req, res) => {
  45. res.setHeader('Location', url)
  46. res.statusCode = 307
  47. res.end(url)
  48. })
  49. // Start listening on alternative port
  50. const port = await getPort({ random: true, name: 'nuxt_loading' })
  51. return new Promise((resolve, reject) => {
  52. this._server = this.app.listen(port, (err) => {
  53. if (err) { return reject(err) }
  54. this.options.baseURLAlt = `http://localhost:${port}`
  55. resolve()
  56. })
  57. })
  58. }
  59. close () {
  60. if (this._server) {
  61. return new Promise((resolve, reject) => {
  62. this._server.close((err) => {
  63. if (err) {
  64. return reject(err)
  65. }
  66. resolve()
  67. })
  68. })
  69. }
  70. }
  71. get state () {
  72. return {
  73. error: this.error,
  74. states: this.states,
  75. allDone: this.allDone,
  76. hasErrors: this.hasErrors
  77. }
  78. }
  79. setStates (states) {
  80. this.clearError()
  81. this.states = states
  82. this.allDone = this.states.every(state => state.progress === 0 || state.progress === 100)
  83. this.hasErrors = this.states.some(state => state.hasErrors === true)
  84. this.broadcastState()
  85. }
  86. setError (error) {
  87. this.clearStates(true)
  88. this.error = {
  89. description: error.toString(),
  90. stack: parseStack(error.stack).join('\n')
  91. }
  92. this.broadcastState()
  93. }
  94. clearError () {
  95. this.error = undefined
  96. }
  97. clearStates (hasErrors) {
  98. this.states = []
  99. this.allDone = false
  100. this.hasErrors = !!hasErrors
  101. }
  102. broadcastState () {
  103. const now = new Date()
  104. if ((now - this._lastBroadcast > 500) || this.allDone || this.hasErrors) {
  105. this.sse.broadcast('state', this.state)
  106. this._lastBroadcast = now
  107. }
  108. }
  109. serveIndex (req, res) {
  110. const html = this.indexTemplate
  111. .replace('__STATE__', JSON.stringify(this.state))
  112. .replace('__OPTIONS__', JSON.stringify(this.options))
  113. .replace(/__BASE_URL__/g, this.options.baseURL)
  114. header(res, 'Content-Type', 'text/html')
  115. end(res, html)
  116. }
  117. }
  118. module.exports = LoadingUI