@automattic/calypso-url
Version:
This library provides both generic and Calypso-specific utilities for handling URLs.
156 lines (131 loc) • 4.04 kB
text/typescript
import { determineUrlType, URL_TYPE } from './url-type';
const BASE_URL = `http://__domain__.invalid`;
interface UrlParts {
protocol: string;
host: string;
hostname: string;
port: string;
origin: string;
pathname: string;
hash: string;
search: string;
searchParams: URLSearchParams;
username: string;
password: string;
}
interface OptionalUrlParts {
protocol?: string;
host?: string;
hostname?: string;
port?: string;
origin?: string;
pathname?: string;
hash?: string;
search?: string;
searchParams?: URLSearchParams;
username?: string;
password?: string;
}
type UrlPartKey = keyof UrlParts;
const EMPTY_URL: Readonly< UrlParts > = Object.freeze( {
protocol: '',
host: '',
hostname: '',
port: '',
origin: '',
pathname: '',
hash: '',
search: '',
searchParams: new URLSearchParams(),
username: '',
password: '',
} );
const URL_PART_KEYS = Object.keys( EMPTY_URL ) as UrlPartKey[];
function pickUrlParts(
parsedUrl: URL | undefined,
include: UrlPartKey[] = URL_PART_KEYS
): UrlParts {
const pickedUrl = { ...EMPTY_URL };
include.forEach( < T extends UrlPartKey >( name: T ) => {
pickedUrl[ name ] = parsedUrl?.[ name ] ?? EMPTY_URL[ name ];
} );
return pickedUrl;
}
/**
* Returns the various available URL parts.
*
* @param url the URL to analyze
* @returns the URL parts
*/
export function getUrlParts( url: string | URL ): UrlParts {
const urlType = determineUrlType( url );
// Invalid URL; return empty URL parts.
if ( urlType === URL_TYPE.INVALID ) {
return { ...EMPTY_URL };
}
const parsed = url instanceof URL ? url : new URL( url, BASE_URL );
// Absolute URL; pick all parts.
if ( urlType === URL_TYPE.ABSOLUTE ) {
return pickUrlParts( parsed );
}
// Scheme-relative URL; pick everything except the protocol and origin.
if ( urlType === URL_TYPE.SCHEME_RELATIVE ) {
return pickUrlParts(
parsed,
URL_PART_KEYS.filter( ( item ) => item !== 'protocol' && item !== 'origin' )
);
}
// Path-absolute or path-relative URL; pick only a few parts.
const pathPartKeys: UrlPartKey[] = [ 'pathname', 'hash', 'search', 'searchParams' ];
const pathParts = pickUrlParts( parsed, pathPartKeys );
// Path-relative URLs require special handling, because they cannot be transformed
// into an absolute URL without potentially losing path information.
// E.g. `../foo?bar=baz` becomes `<base>/foo?bar=baz` when fed to `new URL()`
// with a base, losing the traversal into the parent directory.
// We need to handle these with string functions instead.
if ( urlType === URL_TYPE.PATH_RELATIVE ) {
pathParts.pathname = ( url as string ).split( /[?#]/, 1 )[ 0 ];
}
return pathParts;
}
/**
* Returns a URL object built from the provided URL parts.
*
* @param parts the provided URL parts.
* @returns the generated URL object.
*/
export function getUrlFromParts( parts: OptionalUrlParts ): URL {
if ( ! parts?.protocol && ! parts?.origin ) {
throw new Error( 'getUrlFromParts: protocol missing.' );
}
if ( ! parts.host && ! parts.hostname && ! parts.origin ) {
throw new Error( 'getUrlFromParts: host missing.' );
}
const result = new URL( BASE_URL );
// Apply 'origin' first, since it includes protocol, hostname, and port.
if ( parts.origin ) {
try {
const origin = new URL( parts.origin );
result.host = origin.host;
result.protocol = origin.protocol;
} catch {
throw new Error( 'getUrlFromParts: invalid origin.' );
}
}
// Apply 'host' next, since it includes both hostname and port.
result.host = parts.host || result.host;
// Apply 'searchParams' object if it's specified.
if ( parts.searchParams?.toString ) {
result.search = parts.searchParams.toString();
}
for ( const part of URL_PART_KEYS ) {
if ( part !== 'host' && part !== 'origin' && part !== 'searchParams' ) {
const value = parts[ part ];
// Avoid some implementation bugs, e.g. https://bugs.webkit.org/show_bug.cgi?id=127958.
if ( value && value !== result[ part ] ) {
result[ part ] = value;
}
}
}
return result;
}