bbc1a21c74
Follow-up PR to https://github.com/vercel/next.js/pull/17370 when the path is not prefixed with a locale and the default locale is the detected locale it doesn't redirect to locale prefixed variant. If the default locale path is visited and the default locale is visited this also redirects to the root removing the un-necessary locale in the URL. This also exposes the `defaultLocale` on the router since the RFC mentions `Setting a defaultLocale is required in every i18n library so it'd be useful for Next.js to provide it to the application.` although doesn't explicitly spec where we want to expose it. If we want to expose it differently this can be updated.
170 lines
4.5 KiB
TypeScript
170 lines
4.5 KiB
TypeScript
/* global window */
|
|
import React from 'react'
|
|
import Router, { NextRouter } from '../next-server/lib/router/router'
|
|
import { RouterContext } from '../next-server/lib/router-context'
|
|
|
|
type ClassArguments<T> = T extends new (...args: infer U) => any ? U : any
|
|
|
|
type RouterArgs = ClassArguments<typeof Router>
|
|
|
|
type SingletonRouterBase = {
|
|
router: Router | null
|
|
readyCallbacks: Array<() => any>
|
|
ready(cb: () => any): void
|
|
}
|
|
|
|
export { Router, NextRouter }
|
|
|
|
export type SingletonRouter = SingletonRouterBase & NextRouter
|
|
|
|
const singletonRouter: SingletonRouterBase = {
|
|
router: null, // holds the actual router instance
|
|
readyCallbacks: [],
|
|
ready(cb: () => void) {
|
|
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',
|
|
'components',
|
|
'isFallback',
|
|
'basePath',
|
|
'locale',
|
|
'locales',
|
|
'defaultLocale',
|
|
]
|
|
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
|
|
},
|
|
})
|
|
|
|
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() {
|
|
const router = getRouter() as any
|
|
return router[field] as string
|
|
},
|
|
})
|
|
})
|
|
|
|
coreMethodFields.forEach((field) => {
|
|
// We don't really know the types here, so we add them later instead
|
|
;(singletonRouter as any)[field] = (...args: any[]) => {
|
|
const router = getRouter() as any
|
|
return router[field](...args)
|
|
}
|
|
})
|
|
|
|
routerEvents.forEach((event) => {
|
|
singletonRouter.ready(() => {
|
|
Router.events.on(event, (...args) => {
|
|
const eventField = `on${event.charAt(0).toUpperCase()}${event.substring(
|
|
1
|
|
)}`
|
|
const _singletonRouter = singletonRouter as any
|
|
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}`)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
function getRouter(): Router {
|
|
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)
|
|
}
|
|
return singletonRouter.router
|
|
}
|
|
|
|
// Export the singletonRouter and this is the public API.
|
|
export default singletonRouter as SingletonRouter
|
|
|
|
// Reexport the withRoute HOC
|
|
export { default as withRouter } from './with-router'
|
|
|
|
export function useRouter(): NextRouter {
|
|
return React.useContext(RouterContext)
|
|
}
|
|
|
|
// 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 = (...args: RouterArgs): Router => {
|
|
singletonRouter.router = new Router(...args)
|
|
singletonRouter.readyCallbacks.forEach((cb) => cb())
|
|
singletonRouter.readyCallbacks = []
|
|
|
|
return singletonRouter.router
|
|
}
|
|
|
|
// This function is used to create the `withRouter` router instance
|
|
export function makePublicRouterInstance(router: Router): NextRouter {
|
|
const _router = router as any
|
|
const instance = {} as any
|
|
|
|
for (const property of urlPropertyFields) {
|
|
if (typeof _router[property] === 'object') {
|
|
instance[property] = Object.assign(
|
|
Array.isArray(_router[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
|
|
|
|
coreMethodFields.forEach((field) => {
|
|
instance[field] = (...args: any[]) => {
|
|
return _router[field](...args)
|
|
}
|
|
})
|
|
|
|
return instance
|
|
}
|