Skip to content

Commit

Permalink
[WIP] Add domain routing support
Browse files Browse the repository at this point in the history
  • Loading branch information
cvolant committed Mar 1, 2023
1 parent 40baf73 commit fba2fa0
Show file tree
Hide file tree
Showing 20 changed files with 214 additions and 131 deletions.
13 changes: 13 additions & 0 deletions example/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ const nextConfig = withTranslateRoutes({
i18n: {
locales: ['en', 'fr'],
defaultLocale: 'en',
localeDetection: false,
domains: [
{
domain: 'localhost:3000',
defaultLocale: 'fr',
http: true,
},
{
domain: 'localhost:3001',
defaultLocale: 'en',
http: true,
},
],
},

translateRoutes: {
Expand Down
5 changes: 2 additions & 3 deletions src/plugin/createNtrData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { parsePages } from './parsePages'
export const createNtrData = (nextConfig: NextConfig, customPagesPath?: string): TNtrData => {
const {
pageExtensions = ['js', 'ts', 'jsx', 'tsx'],
i18n: { defaultLocale, locales = [] },
i18n,
translateRoutes: { debug, routesDataFileName, routesTree: customRoutesTree, pagesDirectory } = {},
} = nextConfig as NextConfigWithNTR
const pagesPath = customPagesPath || getPagesPath(pagesDirectory)
Expand All @@ -17,8 +17,7 @@ export const createNtrData = (nextConfig: NextConfig, customPagesPath?: string):

return {
debug,
defaultLocale,
locales,
i18n,
routesTree,
}
}
Expand Down
45 changes: 16 additions & 29 deletions src/plugin/getRouteBranchReRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Redirect, Rewrite } from 'next/dist/lib/load-custom-routes'
import { pathToRegexp } from 'path-to-regexp'

import { getNtrData } from '../react/ntrData'
import { isDefaultLocale } from '../shared/isDefaultLocale'
import { ignoreSegmentPathRegex } from '../shared/regex'
import type { TReRoutes, TRouteBranch, TRouteSegment } from '../types'
import { fileNameToPath } from './fileNameToPaths'
Expand Down Expand Up @@ -49,22 +51,14 @@ const mergeOrRegex = (existingRegex: string, newPossiblity: string) => {
/**
* Get redirects and rewrites for a page
*/
export const getPageReRoutes = <L extends string>({
locales,
routeSegments,
defaultLocale,
}: {
locales: L[]
routeSegments: TRouteSegment<L>[]
defaultLocale?: L
}): TReRoutes => {
export const getPageReRoutes = ({ routeSegments }: { routeSegments: TRouteSegment[] }): TReRoutes => {
/** If there is only one path possible: it is common to all locales and to files. No redirection nor rewrite is needed. */
if (!routeSegments.some(({ paths }) => Object.keys(paths).length > 1)) {
return { rewrites: [], redirects: [] }
}

/** Get a translated path or base path */
const getPath = (locale: L | 'default') =>
const getPath = (locale: string) =>
`/${routeSegments
.map(({ paths }) => paths[locale] || paths.default)
.filter((pathPart) => pathPart && !ignoreSegmentPathRegex.test(pathPart))
Expand All @@ -80,6 +74,8 @@ export const getPageReRoutes = <L extends string>({
.filter(Boolean) // Filter out falsy values
.join('/')}`

const { i18n } = getNtrData()

/**
* ```
* [
Expand All @@ -90,7 +86,7 @@ export const getPageReRoutes = <L extends string>({
* ```
* Each locale cannot appear more than once. Item is ignored if its path would be the same as basePath.
*/
const sourceList = locales.reduce((acc, locale) => {
const sourceList = i18n.locales.reduce((acc, locale) => {
const source = getPath(locale)
if (source === basePath) {
return acc
Expand All @@ -100,11 +96,11 @@ export const getPageReRoutes = <L extends string>({
...acc.filter((sourceItem) => sourceItem.source !== source),
{ source, sourceLocales: [...sourceLocales, locale] },
]
}, [] as { sourceLocales: L[]; source: string }[])
}, [] as { sourceLocales: string[]; source: string }[])

const redirects = locales.reduce((acc, locale) => {
const redirects = i18n.locales.reduce((acc, locale) => {
const localePath = getPath(locale)
const destination = `${locale === defaultLocale ? '' : `/${locale}`}${sourceToDestination(localePath)}`
const destination = `${isDefaultLocale(locale, i18n) ? '' : `/${locale}`}${sourceToDestination(localePath)}`

return [
...acc,
Expand Down Expand Up @@ -203,16 +199,12 @@ export const getPageReRoutes = <L extends string>({
/**
* Generate reroutes in route branch to feed the rewrite section of next.config
*/
export const getRouteBranchReRoutes = <L extends string>({
locales,
export const getRouteBranchReRoutes = ({
routeBranch: { children, ...routeSegment },
previousRouteSegments = [],
defaultLocale,
}: {
locales: L[]
routeBranch: TRouteBranch<L>
previousRouteSegments?: TRouteSegment<L>[]
defaultLocale?: L
routeBranch: TRouteBranch
previousRouteSegments?: TRouteSegment[]
}): TReRoutes => {
const routeSegments = [...previousRouteSegments, routeSegment]

Expand All @@ -221,19 +213,14 @@ export const getRouteBranchReRoutes = <L extends string>({
(acc, child) => {
const childReRoutes =
child.name === 'index'
? getPageReRoutes({ locales, routeSegments, defaultLocale })
: getRouteBranchReRoutes({
locales,
routeBranch: child,
previousRouteSegments: routeSegments,
defaultLocale,
})
? getPageReRoutes({ routeSegments })
: getRouteBranchReRoutes({ routeBranch: child, previousRouteSegments: routeSegments })
return {
redirects: [...acc.redirects, ...childReRoutes.redirects],
rewrites: [...acc.rewrites, ...childReRoutes.rewrites],
}
},
{ redirects: [], rewrites: [] } as TReRoutes,
)
: getPageReRoutes({ locales, routeSegments, defaultLocale })
: getPageReRoutes({ routeSegments })
}
16 changes: 6 additions & 10 deletions src/plugin/parsePages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@ import { fileNameToPath } from './fileNameToPaths'
import { getPagesDir, isRoutesFileName } from './routesFiles'

/** Get path and path translations from name and all translations #childrenOrder */
const getRouteSegment = <L extends string>(
name: string,
routeSegmentsData: TRouteSegmentsData<L>,
isDirectory?: boolean,
): TRouteSegment<L> => {
const getRouteSegment = (name: string, routeSegmentsData: TRouteSegmentsData, isDirectory?: boolean): TRouteSegment => {
const routeSegmentData = routeSegmentsData?.[isDirectory ? '/' : name]
const { default: defaultPath = fileNameToPath(name), ...localized } =
typeof routeSegmentData === 'object' ? routeSegmentData : { default: routeSegmentData }
const paths = {
default: defaultPath,
...localized,
} as TRouteSegmentPaths<L>
} as TRouteSegmentPaths
return {
name,
paths,
Expand Down Expand Up @@ -49,12 +45,12 @@ export type TParsePageTreeProps = {
/**
* Recursively parse pages directory and build a page tree object
*/
export const parsePages = <L extends string>({
export const parsePages = ({
directoryPath: propDirectoryPath,
pageExtensions,
isSubBranch,
routesDataFileName,
}: TParsePageTreeProps): TRouteBranch<L> => {
}: TParsePageTreeProps): TRouteBranch => {
const directoryPath = propDirectoryPath || getPagesDir()
const directoryItems = fs.readdirSync(directoryPath)
const routesFileName = directoryItems.find((directoryItem) => isRoutesFileName(directoryItem, routesDataFileName))
Expand All @@ -65,7 +61,7 @@ export const parsePages = <L extends string>({
routeSegmentsFileContent
? (/\.yaml$/.test(routesFileName as string) ? YAML : JSON).parse(routeSegmentsFileContent)
: {}
) as TRouteSegmentsData<L>
) as TRouteSegmentsData
const directoryPathParts = directoryPath.replace(/[\\/]/, '').split(/[\\/]/)
const name = isSubBranch ? directoryPathParts[directoryPathParts.length - 1] : ''

Expand Down Expand Up @@ -93,7 +89,7 @@ export const parsePages = <L extends string>({
]
}
return acc
}, [] as TRouteBranch<L>[])
}, [] as TRouteBranch[])
.sort((childA, childB) => getOrderWeight(childA) - getOrderWeight(childB))

return {
Expand Down
9 changes: 5 additions & 4 deletions src/plugin/withTranslateRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { Redirect, Rewrite } from 'next/dist/lib/load-custom-routes'
import type { NextConfig } from 'next/dist/server/config-shared'
import type { Configuration as WebpackConfiguration, FileCacheOptions } from 'webpack'

import { setNtrData } from '../react/ntrData'
import { ntrMessagePrefix } from '../shared/withNtrPrefix'
import { NextConfigWithNTR } from '../types'
import type { NextConfigWithNTR } from '../types'
import { createNtrData } from './createNtrData'
import { getPagesPath } from './getPagesPath'
import { getRouteBranchReRoutes } from './getRouteBranchReRoutes'
Expand Down Expand Up @@ -42,10 +43,10 @@ export const withTranslateRoutes = (userNextConfig: NextConfigWithNTR): NextConf
const pagesPath = getPagesPath(pagesDirectory)

const ntrData = createNtrData(userNextConfig, pagesPath)
setNtrData(ntrData)

const { routesTree, locales, defaultLocale } = ntrData

const { redirects, rewrites } = getRouteBranchReRoutes({ locales, routeBranch: routesTree, defaultLocale })
const { routesTree } = ntrData
const { redirects, rewrites } = getRouteBranchReRoutes({ routeBranch: routesTree })
const sortedRedirects = sortBySpecificity(redirects)
const sortedRewrites = sortBySpecificity(rewrites)

Expand Down
2 changes: 1 addition & 1 deletion src/react/enhanceNextRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const enhancePushReplace =
const enhancePrefetch =
<R extends NextRouter | SingletonRouter>(router: R) =>
(inputUrl: string, asPath?: string, options?: PrefetchOptions) => {
const locale = getLocale(router, options?.locale)
const locale = getLocale({ router, locale: options?.locale, url: inputUrl })
const parsedInputUrl = urlToFileUrl(inputUrl, locale)

if (getNtrData().debug === 'withPrefetch') {
Expand Down
5 changes: 3 additions & 2 deletions src/react/fileUrlToUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { normalizePathTrailingSlash } from 'next/dist/client/normalize-trailing-
import { parse as parsePathPattern, compile as compilePath } from 'path-to-regexp'
import { format as formatUrl, UrlObject } from 'url'

import { isDefaultLocale } from '../shared/isDefaultLocale'
import { ignoreSegmentPathRegex, optionalMatchAllFilepathPartRegex } from '../shared/regex'
import { ntrMessagePrefix } from '../shared/withNtrPrefix'
import type { TRouteBranch } from '../types'
Expand Down Expand Up @@ -117,7 +118,7 @@ export const fileUrlToUrl = (url: UrlObject | URL | string, locale: string, { th
try {
const { pathname, query, hash } = fileUrlToFileUrlObject(url)

const { routesTree, defaultLocale } = getNtrData()
const { routesTree, i18n } = getNtrData()

const pathParts = (pathname || '/')
.replace(/^\/|\/$/g, '')
Expand All @@ -134,7 +135,7 @@ export const fileUrlToUrl = (url: UrlObject | URL | string, locale: string, { th
}
}

return `${locale !== defaultLocale ? `/${locale}` : ''}${formatUrl({
return `${!isDefaultLocale(locale, i18n) ? `/${locale}` : ''}${formatUrl({
pathname: newPathname,
query,
hash,
Expand Down
30 changes: 26 additions & 4 deletions src/react/getLocale.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { NextRouter, SingletonRouter } from 'next/router'
import { UrlObject, parse as parseUrl } from 'url'

import { getNtrData } from './ntrData'

export const getLocale = (
{ locale, defaultLocale, locales }: NextRouter | SingletonRouter,
explicitLocale?: string | false,
) => explicitLocale || locale || defaultLocale || locales?.[0] || getNtrData().defaultLocale || getNtrData().locales[0]
export const getLocale = ({
router,
locale: explicitLocale,
url,
}: {
router?: NextRouter | SingletonRouter
locale?: string | false
url?: string | UrlObject | URL
} = {}) => {
if (explicitLocale) {
return explicitLocale
}
const { i18n } = getNtrData()

// explicitLocale === false if opted-out of automatically handling the locale prefixing
// Cf. https://nextjs.org/docs/advanced-features/i18n-routing#transition-between-locales
if (explicitLocale === false && url) {
const { pathname } = typeof url === 'string' ? parseUrl(url) : url
const localeSegment = pathname?.split('/')[1]
if (localeSegment && i18n.locales.includes(localeSegment)) {
return localeSegment
}
}
return router?.locale || router?.defaultLocale || i18n.defaultLocale || router?.locales?.[0] || i18n.locales[0]
}
74 changes: 57 additions & 17 deletions src/react/removeLangPrefix.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,67 @@
import { isDefaultLocale } from '../shared/isDefaultLocale'
import { getNtrData } from './ntrData'

export function removeLangPrefix(pathname: string, toArray?: false, locale?: string): string
export function removeLangPrefix(pathname: string, toArray: true, locale?: string): string[]
export function removeLangPrefix(pathname: string, toArray?: boolean, givenLocale?: string): string | string[] {
const pathParts = pathname.split('/').filter(Boolean)
const { routesTree, defaultLocale, locales } = getNtrData()
/**
* Remove both the lang prefix and the root prefix from a pathname
*
* (The root prefix is a prefix that can be added for a locale between the locale prefix
* and the rest of the pathname. It is defined in the root _routes.json file for the "/" key.)
*/
export function removeLangPrefix(
pathname: string,
/**
* If locale is explicitely given, removeLangPrefix will use it,
* it it is not, removeLangPrefix will try to deduce the locale from the pathname
*/
locale?: string,
): string {
const {
routesTree,
i18n,
i18n: { locales },
} = getNtrData()
let lang = locale
let root = ''

const getLangRoot = (lang: string) => routesTree.paths[lang] || routesTree.paths.default

const defaultLocaleRoot = defaultLocale && getLangRoot(defaultLocale)
const hasLangPrefix = givenLocale ? pathParts[0] === givenLocale : locales.includes(pathParts[0])
const hasDefaultLocalePrefix = !hasLangPrefix && !!defaultLocaleRoot && pathParts[0] === defaultLocaleRoot
const hasGivenLocalePrefix = givenLocale ? pathParts[hasLangPrefix ? 1 : 0] === getLangRoot(givenLocale) : false
if (locale) {
root = getLangRoot(locale)
} else {
const prefixLocale = locales.find((locale) => new RegExp(`\\/${locale}(\\/|$)`).test(pathname))
if (prefixLocale) {
lang = prefixLocale
root = getLangRoot(prefixLocale)
} else {
for (const l of locales) {
if (isDefaultLocale(l, i18n)) {
lang = l
root = getLangRoot(l)
break
}
}
}
}

let remainingPathname: string | undefined = undefined

if (!lang) {
return pathname
}

const fullPrefix = `/${lang}/${root}`

if (!hasLangPrefix && !hasDefaultLocalePrefix && !hasGivenLocalePrefix) {
return toArray ? pathParts : pathname
if (root && pathname.startsWith(fullPrefix)) {
remainingPathname = pathname.slice(fullPrefix.length)
} else if (root && pathname.startsWith(`/${root}`)) {
remainingPathname = pathname.slice(root.length + 1)
} else if (pathname.startsWith(`/${lang}`)) {
remainingPathname = pathname.slice(lang.length + 1)
}

const locale = givenLocale || hasLangPrefix ? pathParts[0] : defaultLocale
const localeRootParts = (locale || hasGivenLocalePrefix) && getLangRoot(locale)?.split('/')
const nbPathPartsToRemove =
(hasLangPrefix ? 1 : 0) +
(localeRootParts && (!hasLangPrefix || pathParts[1] === localeRootParts[0]) ? localeRootParts.length : 0)
if (typeof remainingPathname === 'string' && /^($|\/)/.test(remainingPathname)) {
return remainingPathname
}

return toArray ? pathParts.slice(nbPathPartsToRemove) : `/${pathParts.slice(nbPathPartsToRemove).join('/')}`
return pathname
}
Loading

0 comments on commit fba2fa0

Please sign in to comment.