gatsby
Version: 
Blazing fast modern site generator for React
258 lines (226 loc) • 7.65 kB
JavaScript
import React from "react"
import PropTypes from "prop-types"
import loader, { PageResourceStatus } from "./loader"
import { maybeGetBrowserRedirect } from "./redirect-utils.js"
import { apiRunner } from "./api-runner-browser"
import emitter from "./emitter"
import { RouteAnnouncerProps } from "./route-announcer-props"
import {
  navigate as reachNavigate,
  globalHistory,
} from "@gatsbyjs/reach-router"
import { parsePath } from "gatsby-link"
function maybeRedirect(pathname) {
  const redirect = maybeGetBrowserRedirect(pathname)
  const { hash, search } = window.location
  if (redirect != null) {
    window.___replace(redirect.toPath + search + hash)
    return true
  } else {
    return false
  }
}
// Catch unhandled chunk loading errors and force a restart of the app.
let nextRoute = ``
window.addEventListener(`unhandledrejection`, event => {
  if (/loading chunk \d* failed./i.test(event.reason)) {
    if (nextRoute) {
      window.location.pathname = nextRoute
    }
  }
})
const onPreRouteUpdate = (location, prevLocation) => {
  if (!maybeRedirect(location.pathname)) {
    nextRoute = location.pathname
    apiRunner(`onPreRouteUpdate`, { location, prevLocation })
  }
}
const onRouteUpdate = (location, prevLocation) => {
  if (!maybeRedirect(location.pathname)) {
    apiRunner(`onRouteUpdate`, { location, prevLocation })
    if (
      process.env.GATSBY_QUERY_ON_DEMAND &&
      process.env.GATSBY_QUERY_ON_DEMAND_LOADING_INDICATOR === `true`
    ) {
      emitter.emit(`onRouteUpdate`, { location, prevLocation })
    }
  }
}
const navigate = (to, options = {}) => {
  // Support forward/backward navigation with numbers
  // navigate(-2) (jumps back 2 history steps)
  // navigate(2)  (jumps forward 2 history steps)
  if (typeof to === `number`) {
    globalHistory.navigate(to)
    return
  }
  const { pathname, search, hash } = parsePath(to)
  const redirect = maybeGetBrowserRedirect(pathname)
  // If we're redirecting, just replace the passed in pathname
  // to the one we want to redirect to.
  if (redirect) {
    to = redirect.toPath + search + hash
  }
  // If we had a service worker update, no matter the path, reload window and
  // reset the pathname whitelist
  if (window.___swUpdated) {
    window.location = pathname + search + hash
    return
  }
  // Start a timer to wait for a second before transitioning and showing a
  // loader in case resources aren't around yet.
  const timeoutId = setTimeout(() => {
    emitter.emit(`onDelayedLoadPageResources`, { pathname })
    apiRunner(`onRouteUpdateDelayed`, {
      location: window.location,
    })
  }, 1000)
  loader.loadPage(pathname + search).then(pageResources => {
    // If no page resources, then refresh the page
    // Do this, rather than simply `window.location.reload()`, so that
    // pressing the back/forward buttons work - otherwise when pressing
    // back, the browser will just change the URL and expect JS to handle
    // the change, which won't always work since it might not be a Gatsby
    // page.
    if (!pageResources || pageResources.status === PageResourceStatus.Error) {
      window.history.replaceState({}, ``, location.href)
      window.location = pathname
      clearTimeout(timeoutId)
      return
    }
    // If the loaded page has a different compilation hash to the
    // window, then a rebuild has occurred on the server. Reload.
    if (process.env.NODE_ENV === `production` && pageResources) {
      if (
        pageResources.page.webpackCompilationHash !==
        window.___webpackCompilationHash
      ) {
        // Purge plugin-offline cache
        if (
          `serviceWorker` in navigator &&
          navigator.serviceWorker.controller !== null &&
          navigator.serviceWorker.controller.state === `activated`
        ) {
          navigator.serviceWorker.controller.postMessage({
            gatsbyApi: `clearPathResources`,
          })
        }
        window.location = pathname + search + hash
      }
    }
    reachNavigate(to, options)
    clearTimeout(timeoutId)
  })
}
function shouldUpdateScroll(prevRouterProps, { location }) {
  const { pathname, hash } = location
  const results = apiRunner(`shouldUpdateScroll`, {
    prevRouterProps,
    // `pathname` for backwards compatibility
    pathname,
    routerProps: { location },
    getSavedScrollPosition: args => [
      0,
      // FIXME this is actually a big code smell, we should fix this
      // eslint-disable-next-line @babel/no-invalid-this
      this._stateStorage.read(args, args.key),
    ],
  })
  if (results.length > 0) {
    // Use the latest registered shouldUpdateScroll result, this allows users to override plugin's configuration
    // @see https://github.com/gatsbyjs/gatsby/issues/12038
    return results[results.length - 1]
  }
  if (prevRouterProps) {
    const {
      location: { pathname: oldPathname },
    } = prevRouterProps
    if (oldPathname === pathname) {
      // Scroll to element if it exists, if it doesn't, or no hash is provided,
      // scroll to top.
      return hash ? decodeURI(hash.slice(1)) : [0, 0]
    }
  }
  return true
}
function init() {
  // The "scroll-behavior" package expects the "action" to be on the location
  // object so let's copy it over.
  globalHistory.listen(args => {
    args.location.action = args.action
  })
  window.___push = to => navigate(to, { replace: false })
  window.___replace = to => navigate(to, { replace: true })
  window.___navigate = (to, options) => navigate(to, options)
}
class RouteAnnouncer extends React.Component {
  constructor(props) {
    super(props)
    this.announcementRef = React.createRef()
  }
  componentDidUpdate(prevProps, nextProps) {
    requestAnimationFrame(() => {
      let pageName = `new page at ${this.props.location.pathname}`
      if (document.title) {
        pageName = document.title
      }
      const pageHeadings = document.querySelectorAll(`#gatsby-focus-wrapper h1`)
      if (pageHeadings && pageHeadings.length) {
        pageName = pageHeadings[0].textContent
      }
      const newAnnouncement = `Navigated to ${pageName}`
      if (this.announcementRef.current) {
        const oldAnnouncement = this.announcementRef.current.innerText
        if (oldAnnouncement !== newAnnouncement) {
          this.announcementRef.current.innerText = newAnnouncement
        }
      }
    })
  }
  render() {
    return <div {...RouteAnnouncerProps} ref={this.announcementRef}></div>
  }
}
const compareLocationProps = (prevLocation, nextLocation) => {
  if (prevLocation.href !== nextLocation.href) {
    return true
  }
  if (prevLocation?.state?.key !== nextLocation?.state?.key) {
    return true
  }
  return false
}
// Fire on(Pre)RouteUpdate APIs
class RouteUpdates extends React.Component {
  constructor(props) {
    super(props)
    onPreRouteUpdate(props.location, null)
  }
  componentDidMount() {
    onRouteUpdate(this.props.location, null)
  }
  shouldComponentUpdate(nextProps) {
    if (compareLocationProps(this.props.location, nextProps.location)) {
      onPreRouteUpdate(nextProps.location, this.props.location)
      return true
    }
    return false
  }
  componentDidUpdate(prevProps) {
    if (compareLocationProps(prevProps.location, this.props.location)) {
      onRouteUpdate(this.props.location, prevProps.location)
    }
  }
  render() {
    return (
      <React.Fragment>
        {this.props.children}
        <RouteAnnouncer location={location} />
      </React.Fragment>
    )
  }
}
RouteUpdates.propTypes = {
  location: PropTypes.object.isRequired,
}
export { init, shouldUpdateScroll, RouteUpdates, maybeGetBrowserRedirect }