text: /* global document, window */
  import mitt from '../next-server/lib/mitt'
  
  function supportsPreload (el) {
    try {
      return el.relList.supports('preload')
    } catch {
      return false
    }
  }
  
  const hasPreload = supportsPreload(document.createElement('link'))
  
  function preloadScript (url) {
    const link = document.createElement('link')
    link.rel = 'preload'
    link.crossOrigin = process.crossOrigin
    link.href = encodeURI(url)
    link.as = 'script'
    document.head.appendChild(link)
  }
  
  export default class PageLoader {
    constructor (buildId, assetPrefix) {
      this.buildId = buildId
      this.assetPrefix = assetPrefix
  
      this.pageCache = {}
      this.pageRegisterEvents = mitt()
      this.loadingRoutes = {}
      if (process.env.__NEXT_GRANULAR_CHUNKS) {
        this.promisedBuildManifest = new Promise(resolve => {
          if (window.__BUILD_MANIFEST) {
            resolve(window.__BUILD_MANIFEST)
          } else {
            window.__BUILD_MANIFEST_CB = () => {
              resolve(window.__BUILD_MANIFEST)
            }
          }
        })
      }
    }
  
    // Returns a promise for the dependencies for a particular route
    getDependencies (route) {
      return this.promisedBuildManifest.then(
        man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
      )
    }
  
    normalizeRoute (route) {
      if (route[0] !== '/') {
        throw new Error(`Route name should start with a "/", got "${route}"`)
      }
      route = route.replace(/\/index$/, '/')
  
      if (route === '/') return route
      return route.replace(/\/$/, '')
    }
  
    loadPage (route) {
      route = this.normalizeRoute(route)
  
      return new Promise((resolve, reject) => {
        const fire = ({ error, page }) => {
          this.pageRegisterEvents.off(route, fire)
          delete this.loadingRoutes[route]
  
          if (error) {
            reject(error)
          } else {
            resolve(page)
          }
        }
  
        // If there's a cached version of the page, let's use it.
        const cachedPage = this.pageCache[route]
        if (cachedPage) {
          const { error, page } = cachedPage
          error ? reject(error) : resolve(page)
          return
        }
  
        // Register a listener to get the page
        this.pageRegisterEvents.on(route, fire)
  
        // If the page is loading via SSR, we need to wait for it
        // rather downloading it again.
        if (document.querySelector(`script[data-next-page="${route}"]`)) {
          return
        }
  
        if (!this.loadingRoutes[route]) {
          if (process.env.__NEXT_GRANULAR_CHUNKS) {
            this.getDependencies(route).then(deps => {
              deps.forEach(d => {
                if (!document.querySelector(`script[src^="${d}"]`)) {
                  this.loadScript(d, route, false)
                }
              })
              this.loadRoute(route)
              this.loadingRoutes[route] = true
            })
          } else {
            this.loadRoute(route)
            this.loadingRoutes[route] = true
          }
        }
      })
    }
  
    async loadRoute (route) {
      route = this.normalizeRoute(route)
      let scriptRoute = route === '/' ? '/index.js' : `${route}.js`
  
      const url = `${this.assetPrefix}/_next/static/${encodeURIComponent(
        this.buildId
      )}/pages${scriptRoute}`
      this.loadScript(url, route, true)
    }
  
    loadScript (url, route, isPage) {
      const script = document.createElement('script')
      if (process.env.__NEXT_MODERN_BUILD && 'noModule' in script) {
        script.type = 'module'
        // Only page bundle scripts need to have .module added to url,
        // dependencies already have it added during build manifest creation
        if (isPage) url = url.replace(/\.js$/, '.module.js')
      }
      script.crossOrigin = process.crossOrigin
      script.src = encodeURI(url)
      script.onerror = () => {
        const error = new Error(`Error loading script ${url}`)
        error.code = 'PAGE_LOAD_ERROR'
        this.pageRegisterEvents.emit(route, { error })
      }
      document.body.appendChild(script)
    }
  
    // This method if called by the route code.
    registerPage (route, regFn) {
      const register = () => {
        try {
          const { error, page } = regFn()
          this.pageCache[route] = { error, page }
          this.pageRegisterEvents.emit(route, { error, page })
        } catch (error) {
          this.pageCache[route] = { error }
          this.pageRegisterEvents.emit(route, { error })
        }
      }
  
      if (process.env.NODE_ENV !== 'production') {
        // Wait for webpack to become idle if it's not.
        // More info: https://github.com/zeit/next.js/pull/1511
        if (module.hot && module.hot.status() !== 'idle') {
          console.log(
            `Waiting for webpack to become "idle" to initialize the page: "${route}"`
          )
  
          const check = status => {
            if (status === 'idle') {
              module.hot.removeStatusHandler(check)
              register()
            }
          }
          module.hot.status(check)
          return
        }
      }
  
      register()
    }
  
    async prefetch (route, isDependency) {
      route = this.normalizeRoute(route)
      let scriptRoute = `${route === '/' ? '/index' : route}.js`
  
      if (
        process.env.__NEXT_MODERN_BUILD &&
        'noModule' in document.createElement('script')
      ) {
        scriptRoute = scriptRoute.replace(/\.js$/, '.module.js')
      }
      const url = isDependency
        ? route
        : `${this.assetPrefix}/_next/static/${encodeURIComponent(
          this.buildId
        )}/pages${scriptRoute}`
  
      // n.b. If preload is not supported, we fall back to `loadPage` which has
      // its own deduping mechanism.
      if (
        document.querySelector(
          `link[rel="preload"][href^="${url}"], script[data-next-page="${route}"]`
        )
      ) {
        return
      }
  
      // Inspired by quicklink, license: https://github.com/GoogleChromeLabs/quicklink/blob/master/LICENSE
      let cn
      if ((cn = navigator.connection)) {
        // Don't prefetch if the user is on 2G or if Save-Data is enabled.
        if ((cn.effectiveType || '').indexOf('2g') !== -1 || cn.saveData) {
          return
        }
      }
  
      if (process.env.__NEXT_GRANULAR_CHUNKS && !isDependency) {
        ;(await this.getDependencies(route)).forEach(url => {
          this.prefetch(url, true)
        })
      }
  
      // Feature detection is used to see if preload is supported
      // If not fall back to loading script tags before the page is loaded
      // https://caniuse.com/#feat=link-rel-preload
      if (hasPreload) {
        preloadScript(url)
        return
      }
  
      if (isDependency) {
        // loadPage will automatically handle depencies, so no need to
        // preload them manually
        return
      }
  
      if (document.readyState === 'complete') {
        return this.loadPage(route).catch(() => {})
      } else {
        return new Promise(resolve => {
          window.addEventListener('load', () => {
            this.loadPage(route).then(() => resolve(), () => resolve())
          })
        })
      }
    }
  }
  