<project_root>/src/routes/types.ts
:
export interface IRoute {
name: string
page: string
pattern: string
}
<project_root>/src/routes/next-routes.tsx
:
/* eslint-disable node/no-deprecated-api */
import { Request, Response } from 'express'
import React from 'react'
import { Server } from 'next'
import NextLink from 'next/link'
import NextRouter from 'next/router'
import pathToRegexp, { Key, PathFunction } from 'path-to-regexp'
import { ParsedUrlQuery } from 'querystring'
import { parse, UrlWithParsedQuery } from 'url'
import { IRoute } from './types'
interface IRouteParams {
[key: string]: string
[key: number]: string
}
interface IMatch {
query: ParsedUrlQuery
parsedUrl: UrlWithParsedQuery
pathname?: string
params?: IRouteParams
route?: Route
}
const toQueryString = (query: ParsedUrlQuery) => {
return Object.keys(query)
.filter(key => query[key] !== null && query[key] !== undefined)
.map(key => {
let value = query[key]
if (Array.isArray(value)) {
value = value.join('/')
}
return [
encodeURIComponent(key),
encodeURIComponent(value)
].join('=')
}).join('&')
}
class Route {
name: string;
pattern: string;
page: string;
regex: RegExp;
keys: Key[];
keyNames: any;
toPath: PathFunction<object>;
constructor ({ name, pattern, page = name }: IRoute) {
if (!name && !page) {
throw new Error(`Missing page to render for route '${pattern}'`)
}
this.name = name
this.pattern = pattern || `/${name}`
this.page = page.replace(/(^|\/)index$/, '').replace(/^\/?/, '/')
this.regex = pathToRegexp(this.pattern, this.keys = [])
this.keyNames = this.keys.map(key => key.name)
this.toPath = pathToRegexp.compile(this.pattern)
}
match (pathname?: string) {
if (!pathname) return undefined
const values = this.regex.exec(pathname)
return values
? this.valuesToParams(values.slice(1))
: undefined
}
valuesToParams (values: string[]) {
return values.reduce((params, val, i) => {
if (val === undefined) return params
return Object.assign(params, {
[this.keys[i].name]: decodeURIComponent(val)
})
}, {} as IRouteParams)
}
getHref (query: ParsedUrlQuery = {}) {
return `${this.page}?${toQueryString(query)}`
}
getAs (query: ParsedUrlQuery = {}) {
const as = this.toPath(query) || '/'
const keys = Object.keys(query)
const qsKeys = keys.filter(key => this.keyNames.indexOf(key) === -1)
if (!qsKeys.length) return as
const qsParams = qsKeys.reduce((qs, key) => Object.assign(qs, {
[key]: query[key]
}), {})
return `${as}?${toQueryString(qsParams)}`
}
getUrls (query: ParsedUrlQuery) {
return {
as: this.getAs(query),
href: this.getHref(query)
}
}
}
export class Routes {
routes: Route[];
Link: (props: any) => JSX.Element;
Router: any;
constructor ({
Link = NextLink,
Router = NextRouter
} = {}) {
this.routes = []
this.Link = this.getLink(Link)
this.Router = this.getRouter(Router)
}
add (name: string | IRoute, pattern?: string, page?: string) {
let route: IRoute | undefined
if (name instanceof Object) {
route = Object.assign({}, name)
} else if (typeof name === 'string') {
if (!pattern) throw new Error(`'pattern' is required`)
if (!page) throw new Error(`'page' is required`)
route = {
name,
page,
pattern
}
}
if (!route) throw new Error(`'route' is required`)
if (this.findByName(route.name)) {
throw new Error(`Route '${route.name}' already exists`)
}
this.routes.push(new Route(route))
return this
}
findByName (name: string) {
return this.routes.find(route => route.name === name)
}
match (url: string) {
const parsedUrl = parse(url, true)
const { pathname, query } = parsedUrl
return this.routes.reduce((result, route) => {
if (result.route) return result
const params = route.match(pathname)
if (!params) return result
return { ...result, route, params, query: { ...query, ...params } }
}, { query, parsedUrl } as IMatch)
}
findAndGetUrls (nameOrUrl: string, query: ParsedUrlQuery) {
const route = this.findByName(nameOrUrl)
if (route) {
return {
route,
urls: route.getUrls(query),
byName: true
}
} else {
const { route, query } = this.match(nameOrUrl)
const href = route ? route.getHref(query) : nameOrUrl
const urls = { href, as: nameOrUrl }
return {
route,
urls
}
}
}
getRequestHandler (app: Server, customHandler?: any) {
const nextHandler = app.getRequestHandler()
return (req: Request, res: Response) => {
const { route, query, parsedUrl } = this.match(req.url)
if (route) {
if (customHandler) {
customHandler({ req, res, route, query })
} else {
app.render(req, res, route.page, query)
}
} else {
nextHandler(req, res, parsedUrl)
}
}
}
getLink (Link: any) {
const LinkRoutes = (props: any) => {
const { route, params, to, ...newProps } = props
const nameOrUrl = route || to
if (nameOrUrl) {
Object.assign(newProps, this.findAndGetUrls(nameOrUrl, params).urls)
}
return <Link {...newProps} />
}
return LinkRoutes
}
getRouter (Router: any) {
const wrap = (method: any) => (route: any, params: any, options: any) => {
const { byName, urls: { as, href } } = this.findAndGetUrls(route, params)
return Router[method](href, as, byName ? options : params)
}
Router.pushRoute = wrap('push')
Router.replaceRoute = wrap('replace')
Router.prefetchRoute = wrap('prefetch')
return Router
}
}
<project_root>/src/routes/routes.ts
:
import { Routes } from './next-routes'
import { IRoute } from './types'
const allRoutes: IRoute[] = [
// your routes
]
const routes = new Routes()
allRoutes.forEach(route => routes.add(route))
export default routes
export const { Link, Router } = routes
<project_root>/server/index.ts
:
import express from 'express'
import next from 'next'
import routes from '../src/routes/routes'
const port = parseInt(process.env.PORT || '3000', 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handler = routes.getRequestHandler(app)
app.prepare().then(async () => {
const server = express()
server.use(handler)
server.listen(port, (err) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})