Skip to content

Commit

Permalink
Improve NextURL
Browse files Browse the repository at this point in the history
  • Loading branch information
javivelasco committed Dec 3, 2021
1 parent 992f09c commit 073122a
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 136 deletions.
206 changes: 109 additions & 97 deletions packages/next/server/web/next-url.ts
@@ -1,66 +1,77 @@
import type { PathLocale } from '../../shared/lib/i18n/normalize-locale-path'
import type { DomainLocale, I18NConfig } from '../config-shared'
import { getLocaleMetadata } from '../../shared/lib/i18n/get-locale-metadata'
import cookie from 'next/dist/compiled/cookie'
import { replaceBasePath } from '../router'

/**
* TODO
*
* - Add comments to the URLNext API.
* - Move internals to be using symbols for its shape.
* - Make sure logging does not show any implementation details.
* - Include in the event payload the nextJS configuration
*/
import cookie from 'next/dist/compiled/cookie'

interface Options {
base?: string | URL
basePath?: string
headers?: { [key: string]: string | string[] | undefined }
i18n?: I18NConfig | null
trailingSlash?: boolean
}

const REGEX_LOCALHOST_HOSTNAME =
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1)/
const Internal = Symbol('NextURLInternal')

export class NextURL extends URL {
private _basePath: string
private _locale?: {
defaultLocale: string
domain?: DomainLocale
locale: string
path: PathLocale
redirect?: string
trailingSlash?: boolean
}
private _options: Options
private _url: URL

constructor(input: string, options: Options = {}) {
const url = createWHATWGURL(input)
super(url)
this._options = options
this._basePath = ''
this._url = url
this.analyzeUrl()
[Internal]: {
url: URL
options: Options
basePath: string
locale?: {
defaultLocale: string
domain?: DomainLocale
locale: string
path: PathLocale
redirect?: string
trailingSlash?: boolean
}
}

get absolute() {
return this._url.hostname !== 'localhost'
constructor(input: string | URL, base?: string | URL, opts?: Options)
constructor(input: string | URL, opts?: Options)
constructor(
input: string | URL,
baseOrOpts?: string | URL | Options,
opts?: Options
) {
super('http://127.0.0.1') // This works as a placeholder

let base: undefined | string | URL
let options: Options

if (baseOrOpts instanceof URL || typeof baseOrOpts === 'string') {
base = baseOrOpts
options = opts || {}
} else {
options = opts || baseOrOpts || {}
}

this[Internal] = {
url: parseURL(input, base ?? options.base),
options: options,
basePath: '',
}

this.analyzeUrl()
}

analyzeUrl() {
const { headers = {}, basePath, i18n } = this._options
private analyzeUrl() {
const { headers = {}, basePath, i18n } = this[Internal].options

if (basePath && this._url.pathname.startsWith(basePath)) {
this._url.pathname = replaceBasePath(this._url.pathname, basePath)
this._basePath = basePath
if (basePath && this[Internal].url.pathname.startsWith(basePath)) {
this[Internal].url.pathname = replaceBasePath(
this[Internal].url.pathname,
basePath
)
this[Internal].basePath = basePath
} else {
this._basePath = ''
this[Internal].basePath = ''
}

if (i18n) {
this._locale = getLocaleMetadata({
this[Internal].locale = getLocaleMetadata({
cookies: () => {
const value = headers['cookie']
return value
Expand All @@ -73,154 +84,156 @@ export class NextURL extends URL {
i18n: i18n,
},
url: {
hostname: this._url.hostname || null,
pathname: this._url.pathname,
hostname: this[Internal].url.hostname || null,
pathname: this[Internal].url.pathname,
},
})

if (this._locale?.path.detectedLocale) {
this._url.pathname = this._locale.path.pathname
if (this[Internal].locale?.path.detectedLocale) {
this[Internal].url.pathname = this[Internal].locale!.path.pathname
}
}
}

formatPathname() {
const { i18n } = this._options
let pathname = this._url.pathname
private formatPathname() {
const { i18n } = this[Internal].options
let pathname = this[Internal].url.pathname

if (this._locale?.locale && i18n?.defaultLocale !== this._locale?.locale) {
pathname = `/${this._locale?.locale}${pathname}`
if (
this[Internal].locale?.locale &&
i18n?.defaultLocale !== this[Internal].locale?.locale
) {
pathname = `/${this[Internal].locale?.locale}${pathname}`
}

if (this._basePath) {
pathname = `${this._basePath}${pathname}`
if (this[Internal].basePath) {
pathname = `${this[Internal].basePath}${pathname}`
}

return pathname
}

get locale() {
if (!this._locale) {
throw new TypeError(`The URL is not configured with i18n`)
}

return this._locale.locale
public get locale() {
return this[Internal].locale?.locale ?? ''
}

set locale(locale: string) {
if (!this._locale) {
throw new TypeError(`The URL is not configured with i18n`)
public set locale(locale: string) {
if (
!this[Internal].locale ||
!this[Internal].options.i18n?.locales.includes(locale)
) {
throw new TypeError(
`The NextURL configuration includes no locale "${locale}"`
)
}

this._locale.locale = locale
this[Internal].locale!.locale = locale
}

get defaultLocale() {
return this._locale?.defaultLocale
return this[Internal].locale?.defaultLocale
}

get domainLocale() {
return this._locale?.domain
return this[Internal].locale?.domain
}

get searchParams() {
return this._url.searchParams
return this[Internal].url.searchParams
}

get host() {
return this.absolute ? this._url.host : ''
return this[Internal].url.host
}

set host(value: string) {
this._url.host = value
this[Internal].url.host = value
}

get hostname() {
return this.absolute ? this._url.hostname : ''
return this[Internal].url.hostname
}

set hostname(value: string) {
this._url.hostname = value || 'localhost'
this[Internal].url.hostname = value
}

get port() {
return this.absolute ? this._url.port : ''
return this[Internal].url.port
}

set port(value: string) {
this._url.port = value
this[Internal].url.port = value
}

get protocol() {
return this.absolute ? this._url.protocol : ''
return this[Internal].url.protocol
}

set protocol(value: string) {
this._url.protocol = value
this[Internal].url.protocol = value
}

get href() {
const pathname = this.formatPathname()
return this.absolute
? `${this.protocol}//${this.host}${pathname}${this._url.search}`
: `${pathname}${this._url.search}`
return `${this.protocol}//${this.host}${pathname}${this[Internal].url.search}`
}

set href(url: string) {
this._url = createWHATWGURL(url)
this[Internal].url = parseURL(url)
this.analyzeUrl()
}

get origin() {
return this.absolute ? this._url.origin : ''
return this[Internal].url.origin
}

get pathname() {
return this._url.pathname
return this[Internal].url.pathname
}

set pathname(value: string) {
this._url.pathname = value
this[Internal].url.pathname = value
}

get hash() {
return this._url.hash
return this[Internal].url.hash
}

set hash(value: string) {
this._url.hash = value
this[Internal].url.hash = value
}

get search() {
return this._url.search
return this[Internal].url.search
}

set search(value: string) {
this._url.search = value
this[Internal].url.search = value
}

get password() {
return this._url.password
return this[Internal].url.password
}

set password(value: string) {
this._url.password = value
this[Internal].url.password = value
}

get username() {
return this._url.username
return this[Internal].url.username
}

set username(value: string) {
this._url.username = value
this[Internal].url.username = value
}

get basePath() {
return this._basePath
return this[Internal].basePath
}

set basePath(value: string) {
this._basePath = value.startsWith('/') ? value : `/${value}`
this[Internal].basePath = value.startsWith('/') ? value : `/${value}`
}

toString() {
Expand All @@ -232,13 +245,12 @@ export class NextURL extends URL {
}
}

function createWHATWGURL(url: string) {
url = url.replace(REGEX_LOCALHOST_HOSTNAME, 'localhost')
return isRelativeURL(url)
? new URL(url.replace(/^\/+/, '/'), new URL('https://localhost'))
: new URL(url)
}
const REGEX_LOCALHOST_HOSTNAME =
/(?!^https?:\/\/)(127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}|::1|localhost)/

function isRelativeURL(url: string) {
return url.startsWith('/')
function parseURL(url: string | URL, base?: string | URL) {
return new URL(
String(url).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost'),
base && String(base).replace(REGEX_LOCALHOST_HOSTNAME, 'localhost')
)
}

0 comments on commit 073122a

Please sign in to comment.