/* global window */ import _Router from './router' import { execOnce } from '../utils' const SingletonRouter = { router: null, // holds the actual router instance readyCallbacks: [], ready (cb) { if (this.router) return cb() if (typeof window !== 'undefined') { this.readyCallbacks.push(cb) } } } // Create public properties and methods of the router in the SingletonRouter const urlPropertyFields = ['pathname', 'route', 'query', 'asPath'] const propertyFields = ['components'] const routerEvents = ['routeChangeStart', 'beforeHistoryChange', 'routeChangeComplete', 'routeChangeError', 'hashChangeStart', 'hashChangeComplete'] const coreMethodFields = ['push', 'replace', 'reload', 'back', 'prefetch', 'beforePopState'] // Events is a static property on the router, the router doesn't have to be initialized to use it Object.defineProperty(SingletonRouter, 'events', { get () { return _Router.events } }) propertyFields.concat(urlPropertyFields).forEach((field) => { // Here we need to use Object.defineProperty because, we need to return // the property assigned to the actual router // The value might get changed as we change routes and this is the // proper way to access it Object.defineProperty(SingletonRouter, field, { get () { throwIfNoRouter() return SingletonRouter.router[field] } }) }) coreMethodFields.forEach((field) => { SingletonRouter[field] = (...args) => { throwIfNoRouter() return SingletonRouter.router[field](...args) } }) routerEvents.forEach((event) => { SingletonRouter.ready(() => { _Router.events.on(event, (...args) => { const eventField = `on${event.charAt(0).toUpperCase()}${event.substring(1)}` if (SingletonRouter[eventField]) { try { SingletonRouter[eventField](...args) } catch (err) { console.error(`Error when running the Router event: ${eventField}`) console.error(`${err.message}\n${err.stack}`) } } }) }) }) const warnAboutRouterOnAppUpdated = execOnce(() => { console.warn(`Router.onAppUpdated is removed - visit https://err.sh/zeit/next.js/no-on-app-updated-hook for more information.`) }) Object.defineProperty(SingletonRouter, 'onAppUpdated', { get () { return null }, set () { warnAboutRouterOnAppUpdated() return null } }) function throwIfNoRouter () { if (!SingletonRouter.router) { const message = 'No router instance found.\n' + 'You should only use "next/router" inside the client side of your app.\n' throw new Error(message) } } // Export the SingletonRouter and this is the public API. export default SingletonRouter // Reexport the withRoute HOC export { default as withRouter } from './with-router' // INTERNAL APIS // ------------- // (do not use following exports inside the app) // Create a router and assign it as the singleton instance. // This is used in client side when we are initilizing the app. // This should **not** use inside the server. export const createRouter = function (...args) { SingletonRouter.router = new _Router(...args) SingletonRouter.readyCallbacks.forEach(cb => cb()) SingletonRouter.readyCallbacks = [] return SingletonRouter.router } // Export the actual Router class, which is usually used inside the server export const Router = _Router export function _rewriteUrlForNextExport (url) { const [, hash] = url.split('#') url = url.replace(/#.*/, '') let [path, qs] = url.split('?') path = path.replace(/\/$/, '') let newPath = path // Append a trailing slash if this path does not have an extension if (!/\.[^/]+\/?$/.test(path)) { newPath = `${path}/` } if (qs) { newPath = `${newPath}?${qs}` } if (hash) { newPath = `${newPath}#${hash}` } return newPath } // This function is used to create the `withRouter` router instance export function makePublicRouterInstance (router) { const instance = {} for (const property of urlPropertyFields) { if (typeof router[property] === 'object') { instance[property] = {...router[property]} // makes sure query is not stateful continue } instance[property] = router[property] } // Events is a static property on the router, the router doesn't have to be initialized to use it instance.events = _Router.events propertyFields.forEach((field) => { // Here we need to use Object.defineProperty because, we need to return // the property assigned to the actual router // The value might get changed as we change routes and this is the // proper way to access it Object.defineProperty(instance, field, { get () { return router[field] } }) }) coreMethodFields.forEach((field) => { instance[field] = (...args) => { return router[field](...args) } }) return instance }