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.3

Published

Upgraded URL API

Downloads

375

Readme

URL+ — Reactive URLs with Object‑Backed Query Parameters

npm version bundle License

URL+ extends the web’s URL and URLSearchParams primitives with reactivity and a real object model.


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

At its core, URL+ provides two things:

  1. Reactive URLs — a URL whose fields can be observed for changes
  2. Object‑backed query parameterssearchParams with a real object model behind it

All of this builds directly on the native URL and URLSearchParams semantics.

For example, URL+ works like the native URL and URLSearchParams by default:

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

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

But it lets you do more:

// A new ".ancestorPathname" field
console.log(url.ancestorPathname); // '/level1'

// A new ".query" field – an object model of url's query params
console.log(url.query); // { foo: 'bar' }

// Observability
Observer.observe(url, 'href', (mutation) => {
    console.log(mutation.value); // 'https://example.com/level1/level2/level3?foo=bar'
});
url.pathname = '/level1/level2/level3';

// Deep, fine-grained observation
Observer.observe(url.query, 'foo', (mutation) => {
    console.log(mutation.value); // 'bar'
});
url.searchParams.set('foo', 'baz');

URL+'s capabilities start with URLSearchParamsPlus.


URLSearchParamsPlus

URLSearchParamsPlus extends the standard URLSearchParams interface with a persistent object model and deterministic synchronization between that object tree and the serialized query string.

Internally, parameters are always represented as structured data. This internal representation exists regardless of how the instance is configured or mutated.

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'));

The Internal Object Tree

Every URLSearchParamsPlus instance maintains a live object tree representing the semantic structure of the query.

This tree can be accessed via:

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

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

This tree is returned by reference and is mutable.

Mutating the Tree

This tree is the authoritative source of truth for the query string. Mutations to the tree are reflected in the query string:

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

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

The instance mutation APIs all converge on the tree:

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

Observing the Tree

The tree is fully observable across all modes of mutation:

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

// Reactive mutation via instance API
params.set('e', 5); // Console: "e", 5

// Reactive mutation via the Observer API
Observer.set(tree, 'e', 6); // Console: "e", 6

Addressing the Tree

URLSearchParamsPlus lets you address this model deeply using paths:

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

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

tree.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 tree = params.json();

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

By comparing, the URLSearchParams API does accept the bracket notation on key names but not with any specific semantics attached. It's just a string.

URLSearchParamsPlus lets that address the underlying object model, while aligning with the surface behavior of the URLSearchParams API:

Traversal is by literal string identifiers, not by path:

const params1 = new URLSearchParams('a[b][]=1&a[b][]=2');
const params2 = new URLSearchParamsPlus('a[b][]=1&a[b][]=2');

// Keys are just strings that must match strictly
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']

// ...not interpreted structurally
console.log(params1.get('a[b]')); // null
console.log(params2.get('a[b]')); // 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 expose exact strings 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'

But this alignment with the URLSearchParams API is only one of two modes with the URLSearchParamsPlus API – and the default. URLSearchParamsPlus lets you opt out of this "compatibility" with URLSearchParams into full structural mode:

const params = new URLSearchParamsPlus(null, { compatMode: false });

In this mode, URLSearchParamsPlus exposes its internal tree to its traversal APIs – not just its mutation APIs:

Query keys are interpreted as paths into that tree:

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

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

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

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

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

Enumeration and stringification expose fully-qualified paths:

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

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

Observing the Tree Deeply

The tree can be observed to any depth:

// Observe a key
Observer.observe(tree, 'a', (mutation) => {
    console.log(mutation.key, mutation.value);
});

// Observe 2-levels deep
Observer.observe(tree, Observer.path('a', 'b'), (mutation) => {
    console.log(mutation.path, mutation.key, mutation.value);
});

// Observe full depth
Observer.observe(tree, Observer.subtree(), (mutations) => {
    console.log(mutations.map((m) => m.path, m.key, m.value));
});

// Reactive mutation via instance API
params.set('a[b][c]', 5);

// Reactive mutation via the Observer API
Observer.set(tree.a.b, 'c', 6);

Value Semantics

In advanced mode, values retain their actual types in the tree.

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 sets preserve exact value types:

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

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

The Default vs Advanced Mode Comparison

| Aspect | Default Mode (compatMode: true) | Advanced Mode (compatMode: false) | | --- | --- | --- | | Key Interpretation | Literal strings | Structural paths | | Traversal | By literal strings | By paths | | Enumeration | By literal strings | By fully-qualified paths | | Stringification | By literal strings | By fully-qualified paths | | Value Semantics | Always strings | Actual types as set |

Default mode provides exact URLSearchParams behavior.

Serialization Options

Bracket Encoding

By default, bracket characters are percent‑encoded to match native behavior.

const params = new URLSearchParamsPlus('a[b][0]=1');

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

For readability, this can be disabled:

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, spec-aligned representation.
  • stringify() allows formatting control.
  • The constructor lets you set a default for prettyPrint.

URLPlus

URLPlus is a reactive extension of the standard URL interface.

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.pathname;               // '/a/b'
url.ancestorPathname;       // '/a'
url.searchParams;           // URLSearchParamsPlus { x: 1 }
url.query;                  // { x: 1 }
url.search;                 // '?x=1'
url.hash;                   // ''
url.href;                   // 'https://example.com/a/b?x=1'

The Existing Update Model

Mutating one field updates the others:

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

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

console.log(url.pathname);          // '/x/y'
console.log(url.ancestorPathname);  // '/x'
console.log(url.searchParams);      // URLSearchParamsPlus { x: 2 }
console.log(url.query);             // { x: 2 }
console.log(url.search);            // '?x=2'

...With 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

Observers react to the resulting state in each case.

Query Parameters

The Search Params (.searchParams) is backed by URLSearchParamsPlus:

url.searchParams instanceof URLSearchParamsPlus; // true

The special .query field is a direct reference to the underlying object model of the search params:

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

This object is live.

Mutating it updates the Search Params, 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.

Observing the Full Structure

Because the query object is part of the URL’s state, deep observers work across all mutation paths.

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);

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: false,
    prettyPrint: true
});

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