@verajs/inserts
v0.2.1
Published
Enables inserts system for injecting functionality in VeraJS.
Maintainers
Readme
@verajs/inserts
The VeraJS extension registry (357 B gzip). Every capability that attaches to VeraJS — renderers, autoloaders, styling, error boundaries, batching — attaches through here. It is the module system's backbone rather than a feature.
You rarely install this directly: @verajs/core and @verajs/router re-export what you need.
import { wire } from '@verajs/core';Everything VeraJS does beyond state and templates is registered here, on the same five points and
the same public function you have. @verajs/renderer is a 'render' insert. @verajs/styles is an
'init' insert. An error boundary is an 'error' insert, and write batching is a 'set-handler'
insert — both are a few dozen lines, and both are worked examples in
examples/cdn-js/src/inserts/.
Take wire from the package that owns the extension point, never from @verajs/inserts
directly. A production .min.js inlines this package into every bundle, so registering through a
separately imported copy writes to a map that package never reads — it works in development and
silently does nothing in production.
Registering
wire({ on: 'error', fn: (error, element) => report(error, element), priority: 40 });wire({ on: name, fn: callback, priority: priority }) — priority is required. Lower runs first. Registering at a
priority that is already taken replaces that entry, which is how a renderer is swapped.
Chains are stored dense and priority-sorted rather than indexed by priority: indexing left holes
(a renderer at 50 produced a 51-element array with 50 of them) and every chain is walked on the hot
path, which cost roughly 238 ns per store read.
Two more exports, for a module that reads chains on its own hot path:
import { inserts, revision } from '@verajs/inserts';
let cached = inserts.get('proxy-handler'); // the registry: Map of name -> ordered chain
let seen = revision; // bumped by every registration
const chain = () => (seen === revision ? cached : ((seen = revision), (cached = inserts.get('proxy-handler'))));revision is a live binding, not a getter — reading it is a variable access, which is the point:
the chains that matter are read on every store read and write, a Map.get there measured at 13%
of a tracked read, and a registration is a once-per-app event, so the cost sits on the
registration side. (Remember the rule below before importing inserts directly: registering
goes through core's wire, always.)
The extension points
| Name | Runs when | Signature |
| --- | --- | --- |
| 'render' | a component renders | (template, element, ...args) |
| 'init' | init() sets an element up — after its shadow root exists, before its first render | (element) |
| 'proxy-handler' | a store property is read | (obj, prop, value, addCallback, runCallbacks) |
| 'set-handler' | a store property is written, before the default propagation. Return false to suppress it — that is how batching, transactions and undo/redo hold changes back | (obj, prop, value, prevValue, runCallbacks) |
| 'error' | a hook callback threw. Core never lets one failing effect stop the others, so this decides what happens to it. With nothing registered it falls back to console.error | (error, element) |
| 'collection' | a method is read off a Map or Set inside a store. Type-keyed, so a plain-object read never reaches it — that is what lets reactive collections live outside core. With nothing registered, a Map in a store is inert and core raises a __DEV__ error naming the package | (obj, prop, propValue, addCallback, runCallbacks) |
| 'slot' | a <slot> in a template being rendered into LIGHT DOM — @verajs/renderer/slots takes it over and distributes the host's own children. Returning null or undefined declines, which is what a shadow root gets (the platform slots there) and what the SSR shim gets (the server distributes in its own pass). One registrant owns it: the renderer takes the highest-priority answer rather than chaining | (slot, root, name) |
| 'loader' | a DIRECTIVE NAME nobody has wired — @verajs/directives asks before rejecting an unknown data-vd-*, and @verajs/autoloader's directiveLoader answers by convention ({base}/{name}.js). First claimer wins: return the import() promise (or any truthy) to claim, false/undefined to decline; the claimed module registers itself through wireDirectives, and the asker re-checks its registry when the promise settles | (name, element) |
| 'settle' | a SERVER render's component tree is final — lifecycle run, frames drained, markup about to be serialized. The only moment a server can offer for reading rendered content, since 'init' fires before the first render and a server has no observer to catch what follows. @verajs/directives evaluates declarative directives here, so reflections are correct before any JavaScript reaches the browser | (element) |
| 'value' | a child-position value the renderer does not already handle — <div>${value}</div>. For types you do not own: a Promise, an Observable, a Temporal.PlainDate. Return true to claim the value and stop the chain. Strings, numbers, null and undefined never reach it — those take a fast path — so this cannot be used to intercept text | (part, value) |
Priority 50 is the convention for a default implementation: register below it to run first, or at it to replace.
Every callback in a chain runs, in priority order. An insert that wants to change what core does —
rather than merely watch — says so through its return value, and only 'set-handler' has one:
returning false suppresses the default propagation, which is what lets a module hold writes back
and flush them itself.
For a whole new kind of hook rather than a new implementation of an existing one, createHook in
@verajs/core is the primitive useEffect and its siblings are built from.
An insert that throws
Nothing catches it, and that is deliberate — but it is not the same as a hook. A useEffect that
throws is isolated and reported through the 'error' insert, because core runs an element's hooks in
one loop and an escaping error would skip every hook after the failing one. An insert is not in that
position:
'set-handler'and'proxy-handler'run inside the store's ownsetandgettraps, so a throw comes out ofstate.count = 1in the caller's own stack, at the line that wrote it. That is the most useful place it could surface, and swallowing it would leave the write in an undefined state — a suppressed handler has already decided whether the value propagates. These are also the hottest paths in the framework, and atry/catchon every property read is not free.'collection'and'value'are the same case as those two, for the same reason: the first runs inside aMaporSetmethod and the second inside a child-position commit, so a throw comes out oftags.add('x')or ofrenderIntoat the line that called it.'init'and'render'run insideinit()and the render, so a throw surfaces there. And the chain stops there: an insert is not isolated from the ones beside it, so every insert after the failing one is skipped.'init'is where per-element setup hooks in, so one throwing module keeps the rest from initialising at all — which is why the practical rule below matters most here.'error'is the one that must not throw. It is already handling a failure, and a throw from it replaces the error being reported with its own.
The practical rule: an insert is framework-level code and is expected not to throw. If yours can, catch inside it and decide what to do — the callback knows what a failure means and core does not.
Two copies is a mistake, not an arrangement
Each standalone .min.js inlines its own copy of this package, so loading vera.min.js and a
module that imported this package directly would yield two separate registries — one written
to, the other read from, in production only.
There is no repair function for that any more. connectInserts, which replayed one registry's
chains into another, was removed once every module took the registry it writes to instead of
carrying its own:
import { wire } from '@verajs/core';
import { renderer } from '@verajs/renderer';
import { router } from '@verajs/router';
import { collections } from '@verajs/store/collections';
wire([renderer, router, collections]);router is a connector — wire hands it this registry, and the router keeps no registry
of its own. That removes the hazard by construction rather than reconciling it afterwards, and it is
why @verajs/router has no dependencies at all. tests/cdn-cross-bundle.test.mjs guards the shape.
Take wire from @verajs/core, never from this package. @verajs/eslint-config has a rule for
exactly that mistake.
For AI assistants — and anyone who wants the whole API on one page
The repository root's llms.txt is the complete, hand-maintained API
reference for every package, written to be pasted into a model's context window: full export
tables, the buildless CDN and JSX recipes, semantics that differ from other frameworks, and the
mistakes that come up most. Its recipes are executed by the test suite, so they stay honest.
License
MIT
