npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@webqit/url-plus

v0.1.8

Published

Upgraded URL API

Readme

URL+ — Reactive, Object‑Backed URLs

npm version bundle License

URL+ extends the web’s native URL and URLSearchParams primitives with reactivity, bidirectional state convergence, and first‑class object models for both paths and query parameters.


Install

npm i @webqit/url-plus
import { URLPlus, URLSearchParamsPlus, Observer } from '@webqit/url-plus';

CDN Include

<script src="https://unpkg.com/@webqit/url-plus/dist/main.js"></script>
<script>
    const { URLPlus, URLSearchParamsPlus, Observer } = window.webqit;
</script>

Overview

URL+ builds directly on the semantics of the platform URL and URLSearchParams APIs. URLPlus and URLSearchParamsPlus are a drop-in replacement each:

const url = new URLPlus('https://example.com/level1/level2/level3?foo=bar');

console.log(url.pathname);                  // '/level1/level2/level3'
console.log(url.search);                    // '?foo=bar'
console.log(url.searchParams.get('foo'));   // 'bar'

URL+ offers additional capabilities on top of this baseline.

At a high level:

  • URLSearchParamsPlus provides a single, structured source of truth for query parameters, with deterministic synchronization between an object tree, traversal APIs, and serialized query strings.
  • URLPlus is a reactive coordinator over the full URL state, ensuring that mutations to paths, queries, and serialized forms all converge on the same underlying model and remain observable.

1. Path Object Model

console.log(url.segments); // ['level1', 'level2', 'level3']

segments is the authoritative representation of the URL's path.

2. POSIX-Style Derived Paths

console.log(url.dirname);  // '/level1/level2'
console.log(url.basename); // 'level3'

These fields are derived views over the same underlying path.

3. Query Object Model

console.log(url.query);     // { foo: 'bar' }

query is the authoritative representation of the URL's query string.

4. Observability

Every part of the URL state is observable.

Observer.observe(url, 'href', (mutation) => {
    console.log(mutation.value);
});

url.pathname = '/level1/level2/level3/level4';
// console → 'https://example.com/level1/level2/level3/level4?foo=bar'

Observability can be fine‑grained and deep:

Observer.observe(url.query, 'foo', (mutation) => {
    console.log(mutation.value);
});

url.searchParams.set('foo', 'baz');
// console → 'baz'

5. Immutability

URL+ can be constructed into an immutable (read‑only) mode:

const url = new URLPlus(
    'https://example.com/level1/level2?foo=bar',
    undefined,
    { immutable: true }
);

console.log(url.immutable); // true

url.pathname = '/new/path';   // throws
url.query.baz = 'b';          // throws
url.segments.push('level4');  // throws

In immutable mode, the instance behaves as a stable value while still supporting reads and serialization.


URLSearchParamsPlus

URLSearchParamsPlus extends the standard URLSearchParams interface with:

  • a persistent, structured object tree
  • deterministic synchronization between object mutations and serialization
  • full observability across all mutation paths
  • an optional structural mode that exposes the object model to traversal APIs

Internally, parameters are always represented as structured data.

Construction

new URLSearchParamsPlus(init?, options?)
  • init may be a query string, iterable, plain object, or another params instance
  • options controls compatibility mode and serialization behavior
// Strings
const params1 = new URLSearchParamsPlus('a=1&b=2');
// Iterables
const params2 = new URLSearchParamsPlus([['a', 1], ['b', 2]]);
// Object
const params3 = new URLSearchParamsPlus({ a: 1, b: 2 });

// Other params
const params4 = new URLSearchParamsPlus(new URLSearchParams('a=1&b=2'));
const params5 = new URLSearchParamsPlus(new URLSearchParamsPlus('a=1&b=2'));
console.log(params instanceof URLSearchParams); // true

The Internal Object Tree

Each instance maintains a live object tree representing the semantic structure of the query string:

const query = params.json();
const params = new URLSearchParamsPlus('a=1&b=2');
const query = params.json();

console.log(query); // { a: 1, b: 2 }

The object tree is the authoritative source of truth for the query string.

The returned object is by reference and fully mutable.

Mutating the Tree

Being the authoritative source of truth, all mutations to tree are immediately reflected in the serialized query string:

query.c = 3;
params.toString(); // 'a=1&b=2&c=3'

delete query.a;
params.toString(); // 'b=2&c=3'

All instance‑level mutation APIs converge on the same tree:

params.set('d', 4);
console.log(query); // { b: 2, c: 3, d: 4 }

Observing the Tree

The tree is observable regardless of how mutations are performed:

Observer.observe(query, (mutations) => {
    console.log(mutations[0].key, mutations[0].value);
});

// Mutation via instance API
params.set('e', 5);      // console → e 5

// Mutation via Observer API
Observer.set(query, 'e', 6); // console → e 6

Addressing the Tree

At mutation, bracket notation is interpreted as a path into the object tree:

const params = new URLSearchParamsPlus();
const query = params.json();

params.set('a[b][c]', 1);
console.log(query); // { a: { b: { c: 1 } } }

query.a.b.c = 2;
console.log(params.toString());                         // 'a%5Bb%5D%5Bc%5D=2'
console.log(params.stringify({ prettyPrint: true }));   // 'a[b][c]=2'
const params = new URLSearchParamsPlus('a[b][c]=1');
const query = params.json();

console.log(query); // { a: { b: { c: 1 } } }

This contrasts with native URLSearchParams, where any bracket notation in keys have no special semantics attached.

Compatibility Mode (Default)

By default, instances are created with compatMode: true. It makes URLSearchParamsPlus work as a drop-in replacement for URLSearchParams.

In this mode, traversal APIs behave exactly like URLSearchParams:

  • keys are literal strings
  • traversal does not interpret structure
const params1 = new URLSearchParams('a[b][]=1&a[b][]=2');
const params2 = new URLSearchParamsPlus('a[b][]=1&a[b][]=2');

console.log(params1.get('a[b][]')); // '1'
console.log(params2.get('a[b][]')); // '1'

console.log(params1.getAll('a[b][]')); // ['1', '2']
console.log(params2.getAll('a[b][]')); // ['1', '2']

console.log(params2.get('a[b]')); // null
console.log(params2.get('a[b][0]')); // null

console.log(params1.get('a[b][0]')); // null
console.log(params2.get('a[b][0]')); // null

console.log(params1.getAll('a[b]')); // []
console.log(params2.getAll('a[b]')); // []

Enumeration and stringification preserve the literal keys exactly as set:

// Enumaration
console.log([...params1.keys()]); // ['a[b][]', 'a[b][]']
console.log([...params2.keys()]); // ['a[b][]', 'a[b][]']

console.log([...params1.entries()]); // [['a[b][]', '1'], ['a[b][]', '2']]
console.log([...params2.entries()]); // [['a[b][]', '1'], ['a[b][]', '2']]

// Stringification
console.log(params1.toString()); // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=2'
console.log(params2.toString()); // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=2'

console.log(params2.stringify({ prettyPrint: true })); // 'a[b][]=1&a[b][]=2'

Structural Mode (Opt-In)

Structural mode is where the full power of URLSearchParamsPlus lives. Opt-in is via: compatMode: false.

In this mode, the instance exposes the object tree directly to its traversal APIs – instead of just its mutation APIs:

const params = new URLSearchParamsPlus(
    'a[b][]=1&a[b][]=2',
    { compatMode: false }
);

// Keys are interpreted structurally
console.log(params.get('a[b][]')); // 1
console.log(params.get('a[b][0]')); // 1
console.log(params.get('a[b][1]')); // 2

// Traverse in and out the structure
console.log(params.get('a[b]')); // [1, 2]
console.log(params.get('a')); // URLSearchParamsPlus { b: [1, 2] }

// Traverse in and out programmatically
console.log(params.get('a').get('b')); // [1, 2]

// Mutate by reference
console.log(params.get('a').get('b').pop()); // 2
console.log(params.get('a[b]')); // [1]

Enumeration and stringification expose fully‑qualified paths:

// Enumaration
console.log([...params.keys()]); // ['a[b][0]', 'a[b][1]']
console.log([...params.entries()]); // [['a[b][0]', 1], ['a[b][1]', 2]]

// Stringification
console.log(params.toString()); // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B1%5D=2'
console.log(params.stringify({ prettyPrint: true })); // 'a[b][0]=1&a[b][1]=2'

Value Semantics

In structural mode, values retain their actual types.

For strings passed to the constuctor for hydration, numeric values are sensibly cast to numbers during parsing:

const params = new URLSearchParamsPlus('a=39', { compatMode: false });

params.json().a; // 39

Programmatic mutations preserve exact types:

params.set('x', 39);
params.set('y', '39');

params.json().x; // 39
params.json().y; // '39'

Mode Comparison

| Aspect | Default Mode (compatMode: true) | Structural Mode (compatMode: false) | | ------------------ | --------------------------------- | ------------------------------------- | | Key interpretation | Literal strings | Structural paths | | Traversal | Literal | Path‑based | | Enumeration | Literal keys | Fully‑qualified paths | | Stringification | Literal | Fully‑qualified paths | | Value semantics | Strings | Preserved types |

Serialization Options

By default, bracket characters are percent‑encoded as par native behavior:

params.toString(); // 'a%5Bb%5D%5B0%5D=1'

Pretty printing can be enabled for readability:

const params = new URLSearchParamsPlus(null, { prettyPrint: true });

params.toString(); // 'a[b][0]=1'
params.stringify({ prettyPrint: false }); // 'a%5Bb%5D%5B0%5D=1'
  • toString() always returns the canonical representation
  • stringify() allows formatting control
  • constructor options define defaults

URLPlus

URLPlus is a reactive extension of the standard URL interface. It coordinates multiple views over the same underlying URL state and guarantees bidirectional convergence between them.

Construction

new URLPlus(input, base?, options?)
  • input may be a string or another URL instance
  • base may be a string or another URL instance
  • options controls compatibility mode and serialization behavior
const url = new URLPlus('https://example.com/a/b?x=1');
url.protocol;               // 'https:'
url.username;               // ''
url.password;               // ''
url.hostname;               // 'example.com'
url.port;                   // ''
url.host;                   // 'example.com'
url.origin;                 // 'https://example.com'
url.segments;               // ['a', 'b']
url.pathname;               // '/a/b'
url.dirname;                // '/a'
url.basename;               // 'b'
url.searchParams;           // URLSearchParamsPlus { x: '1' }
url.query;                  // { x: '1' }
url.search;                 // '?x=1'
url.hash;                   // ''
url.href;                   // 'https://example.com/a/b?x=1'
console.log(url instanceof URL); // true

Field Convergence

As with native behaviour, mutating one field updates all related fields:

url.pathname = '/a/b/c';

console.log(url.href);     // 'https://example.com/a/b/c?x=1'
console.log(url.dirname);  // '/a/b'
url.href = 'https://example.com/x/y?x=2';

console.log(url.pathname);     // '/x/y'
console.log(url.dirname);      // '/x'
console.log(url.searchParams); // URLSearchParamsPlus { x: 2 }
console.log(url.query);        // { x: 2 }

Path Object Model (segments)

URLPlus maintains its path as a live array, exposed as segments:

url.segments; // ['a', 'b']

This array is the authoritative path for the instance. Mutations propagate to all derived fields:

url.segments.push('c');

url.pathname; // '/a/b/c'
url.dirname;  // '/a/b'
url.basename; // 'c'

All path mutation pathways converge back on segments:

url.dirname = '/a/a/a/b/b/b/c/c/c';

url.segments;  // ['a', 'a', 'a', 'b', 'b', 'b', 'c', 'c', 'c', 'd']
url.pathname; // '/a/a/a/b/b/b/c/c/c/d'
url.basename; // 'd'
url.href;     // 'https://example.com/a/a/a/b/b/b/c/c/c/d'

Query Integration (query)

The instance's searchParams is backed by URLSearchParamsPlus:

url.searchParams instanceof URLSearchParamsPlus; // true

The instance's query field is a direct reference to the underlying object model of searchParams:

url.query === url.searchParams.json(); // true
console.log(url.query); // { x: 1 }

This object is live.

Mutating it updates searchParams, and therefore, the URL:

url.query.a = { b: [1, 2] };

console.log(url.search);            // '?a[b][0]=1&a[b][1]=2'
console.log(url.href);              // 'https://example.com/a/b?a[b][0]=1&a[b][1]=2'

As with URLSearchParamsPlus, operations over searchParams converge on the same underlying model:

url.searchParams.append('a[b][]', 3);

console.log(url.query);             // { a: { b: [1, 2, 3] } }

Updates to search and href also converge on the same underlying model:

url.search = '?a[b][0]=10';

console.log(url.query);             // { a: { b: [10] } }
url.href = 'https://example.com/?x[y][z]=9';

console.log(url.query);             // { x: { y: { z: 9 } } }

All mutation paths converge on the same underlying state.

POSIX-Style Derived Paths (basename, dirname)

In addition to pathname, URLPlus exposes two POSIX-style path accessors: dirname and basename.

These are derived views over the underlying segments array and behave consistently with familiar filesystem semantics.

const url = new URLPlus('https://example.com/a/b/c');

url.pathname; // '/a/b/c'
url.dirname;  // '/a/b'
url.basename; // 'c'

basename

basename represents the final path segment – that is, the last entry in segments. If the original path is empty (or the root path: /), basename is an empty string.

Setting basename replaces that same segment. If the original path is empty (or the root path: /), setting basename is no-op.

dirname

dirname represents the parent path — all segments except the final one. It's essentially the result of segments.slice(0, -1). If the original path is empty (or the root path: /), dirname is an empty string.

Setting dirname replaces the leading portion of the path, with existing basename preserved. If the original path is empty (or the root path: /), setting dirname is no-op.

Observability

Each URL field can be observed via the Observer API:

Observer.observe(url, 'href', mutation => {
    console.log('href →', mutation.value);
});

url.pathname = '/p/q';
// href → https://example.com/p/q?x=1

Observation works symmetrically:

Observer.observe(url, 'pathname', mutation => {
    console.log('pathname →', mutation.value);
});

url.href = 'https://example.com/m/n?x=1';
// Console: pathname → /m/n

query and segments are observable down to the element level:

Observer.observe(url.segments, mutations => {
    console.log(mutations[0].key, mutations[0].value);
});

url.href = 'https://example.com/m/n?x=1';
// console → 0 'm'

For both query and segments, direct mutations are observable when made reactively:

Observer.set(url.query, 'a', 'bar');
Observer.set(url.segments, 0, 'n');
Observer.proxy(url.segments).push('m');
Observer.proxy(url.segments).splice();

Because the entire URL is reactive, deep observers can track all changes:

Observer.observe(url, Observer.subtree(), mutations => {
    console.log(mutations.map(m => [m.path, m.key, m.value]));
});

The above will react to changes to any part of the URL's state:

url.searchParams.set('a[b][0]', 20);
url.search = '?a[b][1]=30';
url.href = 'https://example.com/?a[b][2]=40';
// Reactive array mutation via Observer.proxy()
Observer.proxy(url.query.a.b).push(4);

The instance can be disposed of its live bindings via dispose():

url.dispose();
url.disposed; // true

On disposal, further mutations are no longer annouced to observers.

Immutability

URLPlus can be constructed into an immutable mode:

const url = new URLPlus('https://example.com/a/b?x=1', null, {
    immutable: true
});
url.immutable; // true

In this mode, the instance becomes read-only and mutations are forbidden:

  • All mutating setters throw
  • segments is frozen
  • query is deeply frozen
  • searchParams mutation APIs are blocked
// Forbids writes
url.segments.push('c'); // throws
url.query.x = 2;        // throws
url.searchParams.set('x', 2); // throws
url.pathname = '/x';    // throws
// Supports reads
url.pathname; // '/a/b'
url.dirname;  // '/a'
url.query;    // { x: '1' }
// Supports sorting
url.searchParams.stringify({ sort: true }); // 'x=1'

Essentially, the instance works as a stable value across time.

Mode Switch and Serialization Options

URLPlus options object can be used to configure the compatibility mode and serialization behavior of its search params.

const url = new URLPlus('https://example.com?a[b]=1', null, {
    compatMode: true, // default
    immutable: false // default
    prettyPrint: false // default
});

console.log(url.searchParams.toString()); // a[b]=1
console.log(url.searchParams.stringify({ prettyPrint: false })); // a%5Bb%5D=1

console.log(url.stringify({ prettyPrint: false })); // https://example.com?a%5Bb%5D=1

License

MIT