UNPKG

@automattic/calypso-url

Version:

This library provides both generic and Calypso-specific utilities for handling URLs.

156 lines (131 loc) 4.04 kB
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; }