Minimalist, strongly typed, immutable plugin system.
- Zero runtime dependencies
- ESM only (
"type": "module") - Thoroughly type‑safe API and guards
- Deterministic, attribution‑preserving entity discovery
Links:
- Specification: docs/spec.md
- API Reference: docs/api
- Example: examples/events
- Requirements: Node.js ≥22, pnpm ≥10.
pnpm add immutable-plugin-system- Plugins provide typed read‑only entities via
ImmutablePlugin<C>. - Host aggregates plugins and exposes centralized, typed discovery via
ImmutableHost<P>. - Entities are grouped per “entity type” into iterable collections with helpers.
- Library guarantees structural immutability and preserves provenance (which plugin provided an entity). Values are treated as immutable by convention.
ImmutableEntities<K, V>: inner entity map (Readonly<Record<K, NonNullable<V>>>).ImmutableEntitiesRecord: mapping of entity types to inner maps.ImmutablePlugin<C>: plugin withname: PluginURNandentities: C.ImmutableHost<P>: host that buildsentitiescollections across all plugins.ImmutableEntityCollection<K, E>: iterable view withget,entries,flat,map,flatMap.
Key rules enforced by runtime guards:
- Entity keys: symbols or non‑empty, non‑numeric‑like strings (e.g.
"0","-1","1.5"are rejected). - Values must be defined (
undefinedis forbidden). Omit the key instead. - Plugin
namemust be non‑empty, and equal the URN key in the plugins record.
import {
ImmutableHost,
type ImmutablePlugin,
type ImmutableEntities,
} from 'immutable-plugin-system';
// 1) Define your integration’s entities schema
type Command = (...args: string[]) => string;
type Entities = {
assets: ImmutableEntities<string, string>;
commands: ImmutableEntities<string, Command>;
};
// 2) Model your plugin
interface Plugin extends ImmutablePlugin<Entities> {
description: string;
}
const pluginA: Plugin = {
name: 'pluginA',
description: 'this is plugin A',
entities: {
assets: { foo: 'this is `foo`', duplicate: 'duplicate from A' },
commands: { hello: (...args) => `hello(${args.join(',')})` },
},
};
const pluginB: Plugin = {
name: 'pluginB',
description: 'this is plugin B',
entities: {
assets: { bar: 'this is `bar`', duplicate: 'duplicate from B' },
commands: { world: (...args) => `world(${args.join(',')})` },
},
};
// 3) Create the host; entity completeness is enforced automatically
const host = new ImmutableHost<Plugin>({ pluginA, pluginB });
// 4) Discover entities
for (const [value, key, plugin] of host.entities.assets) {
console.log(`asset ${String(key)} from ${plugin}: ${value}`);
}
// Get by key (returns all contributors)
const dup = host.entities.assets.get('duplicate'); // ['duplicate from A','duplicate from B']
// Call a command (integration decides on uniqueness/selection semantics)
const results = host.entities.commands.get('hello').map((fn) => fn('arg'));get(key: K): E[]— returns all entities for a key. Returns a fresh array copy; mutating it does not affect the collection.size: number— returns the number of unique keys in the collection.keys(): Iterator<K>— returns an iterator over unique keys.values(): Iterator<E[]>— returns an iterator over entity arrays per key.entries(): Iterator<[K, E[]]>— per‑key grouped arrays.flat(): [E, K, PluginURN][]— flattened entities with provenance.map(fn)/flatMap(fn)— transform grouped or individual items.- Iterable:
for (const [e, k, p] of collection) { … }.
Importable helpers validate shapes and invariants when needed by your integration:
isPlainObject,assertPlainObjectisEntityRecord,assertEntityRecordisEntitiesRecord,assertEntitiesRecordisImmutablePlugin,assertImmutablePluginisImmutablePlugins,assertImmutablePlugins
The host uses the same rules internally.
Notes:
- Primary contract is TypeScript; runtime validation is optional and integration‑driven.
- The host already enforces presence of every entity type; guards help validate dynamic inputs before constructing the host or in bespoke tooling.
Guards return booleans (is*) or throw on violation (assert*). Host
constructor performs the same validations and throws TypeError with
descriptive messages on failure.
Validate a single plugin or a plugin record before constructing the host:
import {
assertImmutablePlugin,
assertImmutablePlugins,
type ImmutableEntities,
type ImmutablePlugin,
} from 'immutable-plugin-system';
type P = ImmutablePlugin<{
assets: ImmutableEntities<string, string>;
commands: ImmutableEntities<string, string>;
}>;
const candidate: unknown = {
name: 'p',
entities: {
assets: { foo: 'bar' },
commands: {},
},
};
assertImmutablePlugin(candidate); // narrows to ImmutablePlugin<P>
assertImmutablePlugins({ p: candidate });Missing entity types trigger TypeError during guard checks or host
construction. Supply empty maps for entity types that have nothing to
contribute.
- Install deps:
pnpm -s install - Fast iteration:
pnpm -s test:fix(auto‑fix + build + type + tests) - Full pipeline (CI‑equivalent):
pnpm -s test - Lint only:
pnpm -s lint - Build everything (lib, examples, docs):
pnpm -s build
Conventions:
- TypeScript, strict types;
anyis forbidden. Minimal, defensibleunknownonly with guards. - TSDoc everywhere; validated by ESLint (
tsdoc/syntax). - No runtime mutations; library never deep‑clones entity values.
- Plugin discovery. Use e.g.
installed-node-modules. - Plugin ordering and dependency resolution.
- Plugin/host configuration.
These are integration concerns; see the specification for discussion and recommendations.
-
Every entity type is required; represent “no entities” with empty
ImmutableEntitiesmaps. The host enforces this invariant at runtime and throws on omissions. -
Entity keys exclude numbers and numeric‑like strings to avoid JS coercion ambiguity. Prefer textual identifiers or symbols.
-
Performance: Host construction groups inner maps by entity type with straightforward iteration. Complexity is linear in the number of plugins and entity maps; large sets are covered by tests.
-
Collections are plain JavaScript iterables. Iteration order follows the standard
Mapinsertion order semantics and is not a stability guarantee across different plugin sets.
- Install:
pnpm -s install - Iterate:
pnpm -s test:fix - Full checks:
pnpm -s test - Pre‑commit hook runs the equivalent of
pnpm -s lint. - Coding standards:
- TypeScript strict types;
anyis forbidden,unknownonly with proper guards. - TSDoc everywhere.
- Use conventional commits for clarity and tooling compatibility.
- TypeScript strict types;
This package is pre‑1.0 (0.x). While the specification is stable (v1.1.0), the
public API may evolve before 1.0.0. Follow semver and consult the changelog
once released.
MIT — see LICENSE