url

js/globals/url.ts

WHATWG URL and URLSearchParams globals.

This is a pure-JS implementation of the common WHATWG URL Standard (https://url.spec.whatwg.org/) surface used by the runtime. It handles absolute URL parsing, relative URL resolution against a base, URL property mutation/serialization, and URLSearchParams construction, mutation, and iteration semantics. No native binding or C library is used; all parsing is done in JS.

The release baseline is intentionally practical rather than WPT-complete. Coverage locks common HTTP(S), file, special and non-special scheme behavior, IDNA/Punycode host serialization, bracketed IPv6 normalization, numeric IPv4 forms, percent-encoding through setters, relative-path/query/hash resolution, and live URLSearchParams mutation during iteration.

Architecture

A parsed URL is stored as a plain object with eight string fields:

{ scheme, username, password, host, port, pathname, search, hash }

This "state object" is the single source of truth for all property getters. Setters mutate the state object directly (e.g. url.hostname = 'foo' sets state.host). Serialization via _serialize() concatenates the fields in the correct order.

URLSearchParams has an #onUpdate callback. When a URLSearchParams instance is attached to a URL (via url.searchParams), mutations to the params object call onUpdate(queryString), which stores the new query string back into url.#state.search. Conversely, when url.search is set directly, url.#params.setQuery() is called to resync the params list. This keeps the URL's search property and its searchParams in sync without a round-trip through the full parser.

URL parsing

_parseURL(input, base) is a hand-rolled parser, not the complete state-machine/tokenizer specified by the WHATWG standard. It handles the release baseline:

Relative resolution follows RFC 3986 §5.2: same-fragment, same-query, protocol-relative, absolute-path, and relative-path references are each handled as a special case before falling through to the generic merge-with- base-directory logic.

_normalizePath() resolves . and .. segments in slash-based output paths. Opaque non-special paths, such as custom:opaque/./value, preserve their path text because relative path merging is not valid for opaque bases.

Default port stripping

The WHATWG spec says that default ports must be excluded from the URL serialization. _parseURL strips default ports on parse (e.g. :80 for HTTP, :443 for HTTPS), so they never appear in url.port or url.href. Setters that modify the scheme or port also strip defaults.

URLSearchParams encoding

URLSearchParams uses application/x-www-form-urlencoded encoding, which differs from URL component percent-encoding in two ways: spaces become + (not %20), and the safe character set is narrower. The _formEncode / _formDecode helpers implement this directly: they walk the string by code point, encode each to UTF-8 bytes, and percent-escape them (decoding reverses the process, running the decoded bytes through a UTF-8 decoder that emits U+FFFD for malformed sequences). The runtime's encodeURIComponent / decodeURIComponent are not used. Mutation methods preserve the observable WHATWG ordering contract for common cases: append() adds to the end, set() keeps the first matching position and removes later duplicates, sort() is stable for duplicate names, and iterators observe live changes.

Host normalization

Domain hostnames are lowercased and serialized through a small Punycode encoder for IDNA-style labels. Bracketed IPv6 addresses are validated, expanded, and compressed to canonical shortest-form text. Special-scheme numeric IPv4 host forms are normalized to dotted decimal.

What is NOT implemented

These omissions are intentional. The implemented subset covers practical runtime URL handling and documented release corpus behavior. Add missing features only when a concrete use case requires them.

// URL and URLSearchParams are available via globalThis

const url = new URL('https://user:pass@example.com:8080/path?q=1#frag');
url.protocol    // 'https:'
url.hostname    // 'example.com'
url.port        // '8080'
url.pathname    // '/path'
url.search      // '?q=1'
url.hash        // '#frag'
url.origin      // 'https://example.com:8080'

// Relative URL resolution
const u = new URL('../other', 'http://example.com/a/b/');
u.href  // 'http://example.com/a/other'

// URLSearchParams
const p = new URLSearchParams('a=1&b=hello+world');
p.get('b')  // 'hello world'
p.toString()  // 'a=1&b=hello+world'

Classes

class URLSearchParams {

WHATWG URLSearchParams global — an ordered multimap of query parameters.

Entries are name/value string pairs; duplicate names are allowed and insertion order is preserved. Serialization uses application/x-www-form-urlencoded encoding (spaces become +), which is what HTML form submission and query strings use — it is not the same as URL percent-encoding.

Instances work standalone, but when obtained through url.searchParams they are live: every mutation writes the re-serialized query string back into the owning URL, and setting url.search resynchronizes the params. Iterators are also live — entries appended during traversal are observed, and deleting the current entry shifts what the iterator sees next.

// URLSearchParams is available via globalThis

const params = new URLSearchParams('a=1&b=hello+world');
params.get('b');        // 'hello world'
params.append('a', '2');
params.getAll('a');     // ['1', '2']
params.set('a', '3');   // collapses duplicates, keeps first position
params.toString();      // 'a=3&b=hello+world'

// Live view attached to a URL
const url = new URL('https://example.com/search');
url.searchParams.set('q', 'fino runtime');
url.href; // 'https://example.com/search?q=fino+runtime'

Constructors

constructor( init?: string | URLSearchParams | [string, string][] | Record<string, string> | null, onUpdate: ( ( qs: string ) => void ) | null = null )

Create URLSearchParams from a query string, another URLSearchParams, entries, or a record.

String input may start with "?". Object input uses own enumerable string keys. The onUpdate callback is internal and lets URL.searchParams update its owning URL.

const params = new URLSearchParams('a=1&b=hello+world');
params.get('b'); // "hello world"

Getters

get size()

Number of stored entries, including duplicates.

const params = new URLSearchParams('a=1&a=2');
params.size; // 2

Methods

append(name: string, value: string): void

Append a new name/value pair and preserve existing entries.

Names and values are string-coerced. Attached URLs are updated after the mutation.

const params = new URLSearchParams();
params.append('a', '1');
params.append('a', '2');
delete(name: string, value?: string): void

Remove entries by name and optional value.

When value is omitted, all entries with the name are removed. When value is provided, only exact name/value pairs are removed.

const params = new URLSearchParams('a=1&a=2');
params.delete('a', '1');
params.toString(); // "a=2"
get(name: string): string | null

Return the first value for name, or null when absent.

new URLSearchParams('a=1&a=2').get('a'); // "1"
getAll(name: string): string[]

Return all values for name in insertion order.

The returned array is new and can be mutated by the caller.

new URLSearchParams('a=1&a=2').getAll('a'); // ["1", "2"]
has(name: string, value?: string): boolean

Return true if a matching entry exists.

With a value argument, both name and value must match.

const params = new URLSearchParams('a=1');
params.has('a', '1'); // true
set(name: string, value: string): void

Set the value for a name (removes existing entries for that name).

If the name exists, the first occurrence is replaced and later duplicates are removed. Otherwise, a new entry is appended.

const params = new URLSearchParams('a=1&a=2');
params.set('a', '3');
params.toString(); // "a=3"
sort()

Sort entries by name using string comparison.

Equal names preserve their relative order. Attached URLs are updated.

const params = new URLSearchParams('b=2&a=1');
params.sort();
params.toString(); // "a=1&b=2"
toString()

Serialize entries as application/x-www-form-urlencoded.

Spaces become "+", duplicate names are preserved, and the output does not include a leading question mark.

new URLSearchParams({ q: 'hello world' }).toString(); // "q=hello+world"
entries()

Iterate over [name, value pairs.

The iterator is live: entries appended during traversal can be observed by the same iterator, matching URLSearchParams iteration semantics.

[...new URLSearchParams('a=1').entries()]; // [["a", "1"]]
keys()

Iterate over names in insertion order.

[...new URLSearchParams('a=1').keys()]; // ["a"]
values()

Iterate over values in insertion order.

[...new URLSearchParams('a=1').values()]; // ["1"]
forEach( callback: ( value: string, name: string, parent: URLSearchParams ) => void, thisArg?: unknown ): void

Call callback for each [name, value pair.

Callback arguments are value, name, and this URLSearchParams object.

const params = new URLSearchParams('a=1');
params.forEach((value, name) => console.log(name, value));

class URL {

WHATWG URL global — parse, inspect, and mutate URLs.

The constructor parses an absolute URL, or a relative reference resolved against a base, and throws TypeError on unparseable input. Component getters read from the parsed state; setters re-normalize their component (lowercasing, default-port stripping, dot-segment resolution, percent-encoding) and, per the WHATWG setter model, silently ignore invalid values rather than throwing. searchParams is a live URLSearchParams view that stays in sync with search in both directions.

The class also hosts the File API object-URL registry: URL.createObjectURL() mints a blob: URL that keeps its Blob alive in a module-level store until URL.revokeObjectURL() drops it.

Installed on globalThis, so application code uses it without importing this internal module.

// URL is available via globalThis

const url = new URL('/search?q=fino', 'https://example.com');
url.href;                  // 'https://example.com/search?q=fino'
url.searchParams.get('q'); // 'fino'

url.port = '8443';
url.hash = 'results';
url.href; // 'https://example.com:8443/search?q=fino#results'

URL.canParse('not a url'); // false
URL.parse('not a url');    // null (non-throwing variant)

Constructors

constructor(input: string | URL, base?: string | URL)

Parse a URL from an absolute input or a relative input with base.

Invalid inputs throw TypeError. The parser lowercases schemes and hosts, strips default ports, normalizes dot segments, and percent-encodes unsafe component characters.

const url = new URL('../b', 'https://example.com/a/c');
url.href; // "https://example.com/b"

Static Methods

static createObjectURL(object: Blob): string

Create a blob: URL for a Blob or File.

The returned URL embeds the current globalThis.location origin when one is available and stores a reference to the Blob until revoked. Each call returns a fresh URL, even for the same Blob.

const url = URL.createObjectURL(new Blob(['hello']));
URL.revokeObjectURL(url);
static revokeObjectURL(url: string): void

Revoke a blob: URL created by URL.createObjectURL().

Revocation is an exact string match. Unknown URLs and non-blob strings are accepted as no-ops, matching browser behavior.

URL.revokeObjectURL('blob:https://example.test/id');
static canParse(input: string | URL, base?: string | URL): boolean

Return true if the input is a parseable URL.

This is equivalent to trying new URL(input, base) and catching failures.

URL.canParse('/a', 'https://example.com'); // true
static parse(input: string | URL, base?: string | URL): URL | null

Parse and return a URL, or null if invalid.

This avoids throwing for validation-style code paths.

const url = URL.parse('not a url');
url; // null

Getters

get href()

Full serialized URL.

Setting href reparses the value and resynchronizes searchParams. Invalid values throw TypeError.

const url = new URL('https://example.com/');
url.href = 'https://example.com/a?x=1';
get origin()

Serialized origin, or the string "null" for opaque origins.

Only http, https, ws, wss, and ftp URLs have a tuple origin; blob: URLs report the origin of their inner URL. Everything else (including file: and custom schemes) serializes as "null".

new URL('https://example.com:443/a').origin; // "https://example.com"
new URL('file:///tmp/x').origin;             // "null"
get protocol()

Scheme with trailing colon.

Setting protocol accepts valid scheme strings and strips a trailing colon. Switching between special and non-special schemes is ignored.

const url = new URL('https://example.com');
url.protocol = 'http:';
get username()

Percent-encoded username component.

const url = new URL('https://example.com');
url.username = 'user name';
url.username; // "user%20name"
get password()

Percent-encoded password component.

const url = new URL('https://example.com');
url.password = 'p@ss';
get host()

Hostname plus optional port.

Setting host lowercases the hostname and strips default ports. Malformed bracketed IPv6 input is ignored.

const url = new URL('https://example.com');
url.host = 'Example.com:443';
url.host; // "example.com"
get hostname()

Hostname without port.

Forbidden host code points cause setter input to be ignored.

const url = new URL('https://example.com');
url.hostname = 'API.EXAMPLE.COM';
get port()

Port string without leading colon.

Empty values clear the port. Non-numeric or out-of-range values are ignored, and default ports serialize as the empty string.

const url = new URL('https://example.com');
url.port = '8443';
get pathname()

Percent-encoded path component.

Setting pathname normalizes dot segments and ensures authority URLs keep a leading slash.

const url = new URL('https://example.com/a');
url.pathname = '/b c';
get search()

Query string with a leading question mark, or empty string.

Setting search accepts values with or without "?" and updates searchParams.

const url = new URL('https://example.com');
url.search = 'a=1';
url.searchParams.get('a'); // "1"
get searchParams()

Live URLSearchParams view of the query string.

Mutating this object updates the URL search component.

const url = new URL('https://example.com');
url.searchParams.set('a', '1');
url.search; // "?a=1"
get hash()

Fragment with a leading hash, or empty string.

Setting hash accepts values with or without "#".

const url = new URL('https://example.com');
url.hash = 'top';

Setters

set href(value)

Replace the full URL by parsing a new absolute URL string.

Invalid input throws TypeError and leaves the previous URL unchanged.

const url = new URL('https://example.com');
url.href = 'https://example.org/path';
set protocol(value)

Replace the scheme when the change is allowed.

Invalid schemes and special-to-non-special transitions are ignored.

const url = new URL('https://example.com');
url.protocol = 'http';
set username(value)

Set the username after userinfo percent-encoding.

const url = new URL('https://example.com');
url.username = 'user name';
set password(value)

Set the password after userinfo percent-encoding.

const url = new URL('https://example.com');
url.password = 'secret';
set host(value)

Set hostname and optional port from a host string.

IPv6 bracket notation is preserved. Default ports are normalized away.

const url = new URL('https://example.com');
url.host = 'localhost:8443';
set hostname(value)

Set hostname without changing the port.

Forbidden host characters cause the assignment to be ignored.

const url = new URL('https://example.com:8443');
url.hostname = 'localhost';
set port(value)

Set or clear the port.

Non-numeric and out-of-range values are ignored. Default ports serialize as empty for the current scheme.

const url = new URL('https://example.com');
url.port = '443';
url.port; // ""
set pathname(value)

Set the path component after normalization and percent-encoding.

const url = new URL('https://example.com/a');
url.pathname = '/b/../c';
set search(value)

Set the query string and refresh searchParams.

A leading question mark is optional. Unsafe query characters are percent-encoded and existing valid percent triplets are preserved.

const url = new URL('https://example.com');
url.search = '?q=hello world';
set hash(value)

Set the fragment component.

A leading hash is optional. Unsafe fragment characters are percent-encoded.

const url = new URL('https://example.com');
url.hash = '#section';

Methods

toString()

Return href as a string.

new URL('https://example.com/').toString(); // "https://example.com/"
toJSON()

Return href for JSON serialization.

JSON.stringify(new URL('https://example.com/')); // "\"https://example.com/\""