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

exobj

v0.6.0

Published

Controlled object mutation with deep read-only proxies and change subscriptions.

Readme

ExObj

ExObj is a dependency-free JavaScript library that uses Proxy to enforce controlled object mutation. Data can be read normally, while direct assignment, deletion, property definition, nested mutation, and mutating array methods are blocked. Changes must go through ExObj.set() and ExObj.unset().

Runtime support

| Environment | Format | Entry point | | --- | --- | --- | | Node.js | CommonJS | dist/exobj.cjs | | Node.js | ESM | dist/exobj.esm.js | | Browser | ESM / module | dist/exobj.esm.js | | Browser | Classic script / global | dist/exobj.umd.js | | Browser | AMD | dist/exobj.umd.js | | Browser | UMD | dist/exobj.umd.js | | Deno | Native ESM | src/index.js or dist/exobj.esm.js |

All builds are generated from the same runtime-neutral src/core.js. The core does not import Node.js APIs and does not depend on a DOM, so behavior remains consistent across runtimes.

Installation

Install the local package archive:

npm install ./exobj-0.5.0.tgz

After publishing the package to a registry, it can be installed by its registered package name. Node.js 18 or newer is recommended.

Node.js

ESM:

import ExObj from 'exobj';

const my_obj = ExObj.create({ name: 'Foo' });

CommonJS:

const ExObj = require('exobj');

const my_obj = ExObj.create({ name: 'Foo' });

The package exports map automatically selects dist/exobj.esm.js for import and dist/exobj.cjs for require.

Browser

ESM or module script

<script type="module">
  import ExObj from './dist/exobj.esm.js';

  const my_obj = ExObj.create({ name: 'Foo' });
  ExObj.set(my_obj, 'version', '1.0');
</script>

The ESM build has both default and named exports:

import ExObj, { ExObj as ex_obj_class } from './dist/exobj.esm.js';

Classic script and global

<script src="./dist/exobj.umd.js"></script>
<script>
  const my_obj = ExObj.create({ name: 'Foo' });
  ExObj.set(my_obj, 'version', '1.0');
</script>

The UMD build exposes the class as globalThis.ExObj when no module loader is present.

AMD

require(['./dist/exobj.umd'], function (ExObj) {
  const my_obj = ExObj.create({ name: 'Foo' });
  ExObj.set(my_obj, 'version', '1.0');
});

The UMD build registers itself as an anonymous AMD module when define.amd is available.

Deno

From an extracted source package:

import ExObj from './src/index.js';

const my_obj = ExObj.parse('{"name":"Foo"}');
ExObj.set(my_obj, 'version', '1.0');

The generated ESM distribution works as well:

import ExObj from './dist/exobj.esm.js';

Once published to npm, Deno can also consume the package through its npm: specifier. No Node.js compatibility flag is required by the ExObj runtime.

Controlled mutation

const my_obj = ExObj.create({ name: 'Foo' });

// Throws TypeError:
my_obj.version = '1.0';

ExObj.set(my_obj, 'version', '1.0');
ExObj.set(my_obj, 'auth', {
  name: 'quocta',
  homepage: 'https://quocta.dev',
});
ExObj.unset(my_obj, 'name');

console.log(my_obj.version); // "1.0"
console.log(my_obj.auth.name); // "quocta"

Every managed object and nested proxy also provides non-enumerable instance methods:

my_obj.__set('version', '2.0');
my_obj.auth.__set('name', 'new name');
my_obj.__unset('name');

const compact_json = my_obj.__json();
const pretty_json = my_obj.__json(null, 2);
const mutable_snapshot = my_obj.toJSON();

__set() returns the same managed instance. __unset() returns whether an own field was removed. toJSON() returns a detached mutable snapshot, while __json() returns serialized JSON.

ExObj.create() and ExObj.set() clone incoming data. Retaining and mutating a reference to the input object cannot mutate the protected ExObj graph.

The following operations are blocked at every depth:

my_obj.name = 'Bar';
delete my_obj.name;
Object.defineProperty(my_obj, 'name', { value: 'Bar' });
Object.setPrototypeOf(my_obj, null);
my_obj.roles.push('admin');
my_obj.auth.name = 'Bar';

Object.freeze(my_obj) is also blocked because it attempts to change the target's extensibility. The proxy already provides mutation protection.

Standard object operations

ExObj proxies work with standard reflection and enumeration APIs:

Object.keys(my_obj);
Object.values(my_obj);
Object.entries(my_obj);
Object.hasOwn(my_obj, 'version');

'version' in my_obj; // true
'__set' in my_obj;   // true

Instance methods behave like prototype methods: they are available through the in operator but are not own properties and never appear in keys, values, entries, object spreads, or JSON output.

Safe JSON compatibility

ExObj.parse() and ExObj.stringify() mirror the native JSON.parse() and JSON.stringify() call signatures:

const account = ExObj.parse(`{
  "name": "Foo",
  "auth": {
    "name": "quocta"
  }
}`);

account.auth.name = 'other'; // TypeError

const compact_json = ExObj.stringify(account);
const pretty_json = ExObj.stringify(account, null, 2);

const instance_json = account.__json();
const automatic_json = JSON.stringify(account); // Uses account.toJSON().

Parsing always uses the native JSON.parse() implementation and never uses eval. Parsed objects and arrays are cloned and wrapped in deep read-only proxies. Top-level JSON primitives are returned unchanged. A __proto__ field remains a normal own data property and cannot change the object's prototype.

Native JSON APIs also work directly:

const json = JSON.stringify(account); // No internal proxy metadata is exposed.
const plain_object = JSON.parse(json); // A normal mutable object.

The reviver, replacer, and space parameters are supported:

const data = ExObj.parse('{"version":1}', (key, value) =>
  key === 'version' ? `v${value}` : value,
);

const json = ExObj.stringify(data, ['version'], 2);

Native rules are preserved: malformed JSON throws SyntaxError, undefined object fields are omitted, undefined array entries become null, and BigInt throws TypeError during serialization.

Strict TypeScript support

The package provides format-correct declarations for ESM (index.d.ts), CommonJS (index.d.cts), and the browser UMD global (index.umd.d.cts). Deno discovers the ESM declarations through @ts-self-types.

import ExObj, {
  type ExObjInstance,
  type DeepMutable,
} from 'exobj';

interface Account {
  name: string;
  auth: {
    enabled: boolean;
  };
  roles: string[];
}

const my_obj: ExObjInstance<Account> = ExObj.create<Account>({
  name: 'Foo',
  auth: { enabled: true },
  roles: ['admin'],
});

my_obj.name = 'Other';       // TypeScript error: deeply readonly.
my_obj.roles.push('editor'); // TypeScript error: readonly array.

const extended_obj = my_obj.__set('version', '1.0' as const);
extended_obj.version; // Typed as the literal "1.0".

const snapshot: DeepMutable<Account> = my_obj.toJSON();
snapshot.auth.enabled = false; // Valid: snapshots are mutable.

Managed values carry a private type brand, so static mutation methods reject ordinary objects at compile time. Nested objects are typed as nested ExObj instances and expose their own instance methods. Subscriptions preserve the root schema through generic ChangeEvent<T> and ExObjSubscription<T> types.

Change subscriptions

const subscriber = ExObj.create_subscription(my_obj);

subscriber.on('changed', ({ field, old_value, new_value }) => {
  console.log(
    `Value of field ${String(field)} changed ` +
    `from ${String(old_value)} to ${String(new_value)}.`,
  );
});

ExObj.set(my_obj, 'version', '2.0');
subscriber.unsubscribe();

Events run synchronously in registration order. A changed event contains:

{
  type: 'set' | 'unset';
  target: object;
  field: string | symbol;
  path: readonly PropertyKey[];
  had_field: boolean;
  old_value: unknown;
  new_value: unknown;
}

A subscription created for the root receives mutations performed through nested proxies:

const account = ExObj.create({ auth: { name: 'old' } });
const subscriber = ExObj.create_subscription(account);

subscriber.on('changed', ({ path }) => console.log(path));
ExObj.set(account.auth, 'name', 'new'); // ["auth", "name"]

Use off('changed', handler) to remove one handler. unsubscribe() removes all handlers, detaches the subscription from the object, and is safe to call more than once.

API reference

ExObj.create(initial_value?)

Creates a deep read-only proxy from a plain object or array.

ExObj.parse(text, reviver?)

Parses with native JSON.parse(). Objects and arrays become ExObj proxies; top-level primitives remain primitives. Data is cloned after the reviver runs, so a reference retained by the reviver cannot mutate the resulting ExObj.

ExObj.stringify(value, replacer?, space?)

Serializes according to native JSON.stringify() rules. Accepts ExObj values, plain values, replacer functions, property lists, and pretty-print spacing.

ExObj.set(target, field, value)

Adds or replaces an own field and returns target. The field can be a string or symbol. A nested ExObj proxy can be used as the target.

target.__set(field, value)

Instance equivalent of ExObj.set(). Returns the same proxy with a refined TypeScript result type that includes the assigned field.

ExObj.unset(target, field)

Deletes an own field. Returns true when a field was deleted and false when it did not exist.

target.__unset(field)

Instance equivalent of ExObj.unset().

target.__json(replacer?, space?)

Serializes the managed object with native JSON rules.

target.toJSON()

Returns a detached deep mutable snapshot. JSON.stringify() invokes this method automatically.

ExObj.create_subscription(target)

Returns { on, off, unsubscribe, active }. The supported event is changed.

ExObj.is_ex_obj(value)

Returns whether a value is a proxy managed by ExObj.

ExObj.to_object(target)

Returns a deep mutable clone suitable for snapshots or integration with code that expects an ordinary object.

Supported data

ExObj accepts plain objects, arrays, and primitive values. Functions, class instances, and circular references are rejected explicitly so they cannot provide hidden mutation paths.

The field names __set, __unset, __json, and toJSON are reserved at every object depth for instance methods. Creating, parsing, or assigning data with one of these own field names throws TypeError.

Development

npm run build
npm run typecheck
npm test
npm run pack:check

npm run build produces all three distribution files from src/core.js. The test suite checks the source API, Node.js CJS and ESM, browser global scripts, AMD, UMD, JSON behavior, and runtime-neutral Deno compatibility.

Run the native Deno test when Deno is installed:

deno test ./test/deno.test.js

License

MIT