next-dev-server.js 5.9 KB
Newer Older
1
import Server from 'next-server/dist/server/next-server'
T
Tim Neutkens 已提交
2 3
import { join } from 'path'
import HotReloader from './hot-reloader'
4 5
import { route } from 'next-server/dist/server/router'
import { PHASE_DEVELOPMENT_SERVER } from 'next-server/constants'
6
import ErrorDebug from './error-debug'
7 8
import AmpHtmlValidator from 'amphtml-validator'
import { ampValidation } from '../build/output/index'
T
Tim Neutkens 已提交
9 10 11 12 13

export default class DevServer extends Server {
  constructor (options) {
    super(options)
    this.renderOpts.dev = true
14
    this.renderOpts.ErrorDebug = ErrorDebug
15 16 17
    this.devReady = new Promise(resolve => {
      this.setDevReady = resolve
    })
T
Tim Neutkens 已提交
18 19 20 21 22 23 24 25 26 27
  }

  currentPhase () {
    return PHASE_DEVELOPMENT_SERVER
  }

  readBuildId () {
    return 'development'
  }

28 29 30 31 32
  async addExportPathMapRoutes () {
    // Makes `next export` exportPathMap work in development mode.
    // So that the user doesn't have to define a custom server reading the exportPathMap
    if (this.nextConfig.exportPathMap) {
      console.log('Defining routes from exportPathMap')
33
      const exportPathMap = await this.nextConfig.exportPathMap({}, { dev: true, dir: this.dir, outDir: null, distDir: this.distDir, buildId: this.buildId }) // In development we can't give a default path mapping
34
      for (const path in exportPathMap) {
35
        const { page, query = {} } = exportPathMap[path]
36 37 38 39 40 41 42 43 44 45 46

        // We use unshift so that we're sure the routes is defined before Next's default routes
        this.router.add({
          match: route(path),
          fn: async (req, res, params, parsedUrl) => {
            const { query: urlQuery } = parsedUrl

            Object.keys(urlQuery)
              .filter(key => query[key] === undefined)
              .forEach(key => console.warn(`Url defines a query parameter '${key}' that is missing in exportPathMap`))

47
            const mergedQuery = { ...urlQuery, ...query }
48 49 50 51 52 53 54 55

            await this.render(req, res, page, mergedQuery, parsedUrl)
          }
        })
      }
    }
  }

T
Tim Neutkens 已提交
56
  async prepare () {
57
    this.hotReloader = new HotReloader(this.dir, { config: this.nextConfig, buildId: this.buildId })
T
Tim Neutkens 已提交
58
    await super.prepare()
59
    await this.addExportPathMapRoutes()
60
    await this.hotReloader.start()
61
    this.setDevReady()
T
Tim Neutkens 已提交
62 63 64
  }

  async close () {
65 66 67
    if (this.hotReloader) {
      await this.hotReloader.stop()
    }
T
Tim Neutkens 已提交
68 69 70
  }

  async run (req, res, parsedUrl) {
71
    await this.devReady
72
    const { finished } = await this.hotReloader.run(req, res, parsedUrl)
73 74
    if (finished) {
      return
T
Tim Neutkens 已提交
75 76 77 78 79
    }

    return super.run(req, res, parsedUrl)
  }

80 81
  generateRoutes () {
    const routes = super.generateRoutes()
T
Tim Neutkens 已提交
82 83 84 85

    // In development we expose all compiled files for react-error-overlay's line show feature
    // We use unshift so that we're sure the routes is defined before Next's default routes
    routes.unshift({
86
      match: route('/_next/development/:path*'),
T
Tim Neutkens 已提交
87 88 89 90 91 92 93 94 95
      fn: async (req, res, params) => {
        const p = join(this.distDir, ...(params.path || []))
        await this.serveStatic(req, res, p)
      }
    })

    return routes
  }

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
  _filterAmpDevelopmentScript (html, event) {
    if (event.code !== 'DISALLOWED_SCRIPT_TAG') {
      return true
    }

    const snippetChunks = html.split('\n')

    let snippet
    if (
      !(snippet = html.split('\n')[event.line - 1]) ||
      !(snippet = snippet.substring(event.col))
    ) {
      return true
    }

    snippet = snippet + snippetChunks.slice(event.line).join('\n')
    snippet = snippet.substring(0, snippet.indexOf('</script>'))

    return !snippet.includes('data-amp-development-mode-only')
  }

117
  async renderToHTML (req, res, pathname, query, options) {
T
Tim Neutkens 已提交
118 119 120 121 122 123
    const compilationErr = await this.getCompilationError(pathname)
    if (compilationErr) {
      res.statusCode = 500
      return this.renderErrorToHTML(compilationErr, req, res, pathname, query)
    }

124 125 126 127 128 129 130 131 132 133
    // In dev mode we use on demand entries to compile the page before rendering
    try {
      await this.hotReloader.ensurePage(pathname)
    } catch (err) {
      if (err.code === 'ENOENT') {
        res.statusCode = 404
        return this.renderErrorToHTML(null, req, res, pathname, query)
      }
      if (!this.quiet) console.error(err)
    }
134 135 136 137 138 139 140 141 142 143 144 145 146 147
    const html = await super.renderToHTML(req, res, pathname, query, options)
    if (options.amphtml && pathname !== '/_error') {
      await AmpHtmlValidator.getInstance().then(validator => {
        const result = validator.validateString(html)
        ampValidation(
          pathname,
          result.errors
            .filter(e => e.severity === 'ERROR')
            .filter(e => this._filterAmpDevelopmentScript(html, e)),
          result.errors.filter(e => e.severity !== 'ERROR')
        )
      })
    }
    return html
T
Tim Neutkens 已提交
148 149 150
  }

  async renderErrorToHTML (err, req, res, pathname, query) {
151 152
    await this.hotReloader.ensurePage('/_error')

T
Tim Neutkens 已提交
153 154 155 156 157 158
    const compilationErr = await this.getCompilationError(pathname)
    if (compilationErr) {
      res.statusCode = 500
      return super.renderErrorToHTML(compilationErr, req, res, pathname, query)
    }

159
    if (!err && res.statusCode === 500) {
160 161 162
      err = new Error(
        'An undefined error was thrown sometime during render... ' +
          'See https://err.sh/zeit/next.js/threw-undefined'
163
      )
164 165
    }

T
Tim Neutkens 已提交
166 167 168 169 170 171 172 173 174 175
    try {
      const out = await super.renderErrorToHTML(err, req, res, pathname, query)
      return out
    } catch (err2) {
      if (!this.quiet) console.error(err2)
      res.statusCode = 500
      return super.renderErrorToHTML(err2, req, res, pathname, query)
    }
  }

176 177 178 179 180 181
  sendHTML (req, res, html) {
    // In dev, we should not cache pages for any reason.
    res.setHeader('Cache-Control', 'no-store, must-revalidate')
    return super.sendHTML(req, res, html)
  }

T
Tim Neutkens 已提交
182 183 184 185 186 187 188 189 190 191 192 193
  setImmutableAssetCacheControl (res) {
    res.setHeader('Cache-Control', 'no-store, must-revalidate')
  }

  async getCompilationError (page) {
    const errors = await this.hotReloader.getCompilationErrors(page)
    if (errors.length === 0) return

    // Return the very first error we found.
    return errors[0]
  }
}