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

@hyperfixi/engine

v4.1.0

Published

A small, typed hyperscript engine, faithful to upstream _hyperscript: its test suite is the acceptance oracle. Ships hyperfixi-hs.js, the script-tag bundle

Downloads

144

Readme

@hyperfixi/engine

A hyperscript engine written against upstream _hyperscript's source as the specification and upstream's own test suite as the acceptance oracle. It is typed (tsc --strict, no any), synchronous until a script really waits on something, and built from modules, so a bundle contains the commands it registers and nothing else.

It replaced the engine in packages/core in 4.0: @hyperfixi/core's root re-exports it, and core's dist/hyperfixi.js is this package's hyperfixi-hs.js. It is also published on its own (since 2026-10-02).

Install

The script-tag bundle, hyperscript and nothing else (34.1 KB gzipped). It installs as window._hyperscript and window.hyperfixi, and reads the document when it is ready:

<script src="https://unpkg.com/@hyperfixi/engine/dist/hyperfixi-hs.js"></script>
<button _="on click toggle .active on me">Toggle</button>

Or, with a bundler, build an engine from the modules a page needs:

npm install @hyperfixi/engine

Use

import { register, boot, on, add, remove, toggle } from '@hyperfixi/engine';

register(on, add, remove, toggle); // this engine knows `on`, `add`, `remove`, `toggle`
boot(); // install as window._hyperscript and initialise the document

A module is a function (g: Grammar) => void that adds its rules to the grammar: a command, a feature, or an optional expression kind. A rule that is not registered does not exist, and a script that uses it fails with a parse error that names the token. src/bundles/full.ts is the list of every module.

A module can live in another package. @hyperfixi/speech is one (upstream's speak): it imports expr and the Grammar / Cmd / Expr types from here, sets g.commands.speak to a rule that reads its operands off the Parser it is handed, and returns a node whose run may return a promise. register(...everything, speak) builds an engine with it. (The script-tag bundle is a fixed list; nothing adds a module to a page's hyperfixi-hs.js after it loads.)

The public object is shaped like upstream's _hyperscript (evaluate, parse, process, config, use(plugin), addBeforeProcessHook), so a plugin written for upstream's public API can be used on it; the multilingual adapter is. It adds one hook upstream lacks:

_hyperscript.addSourceTransform((source, element) => english);

A script is rewritten as it is read. The element keeps the text its author wrote, and a parse error in the rewritten script carries that text (error.written). @lokascript/hyperscript-adapter uses the hook when the host has it.

Layout

| File | What it holds | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | src/index.ts | The library entry: the engine functions, the node types, every module | | src/tokenizer.ts, src/parser.ts | Lexer, token stream, grammar registry, parse errors | | src/expressions.ts | The expression chain and the core expression kinds | | src/expressions-extra.ts, src/conversions.ts | Optional modules: positional, closest, collection operators, type checks; the less common as conversions | | src/statements.ts | Commands, command lists, programs | | src/runtime.ts, src/engine.ts | Contexts, scopes, collections, block execution; DOM init, cleanup, public API | | src/on.ts, src/features.ts | Features: on; def, init, behavior, install, top-level set | | src/reactivity.ts, src/templates.ts, src/live-templates.ts | Optional: when / live / bind; render and template text; live templates | | src/commands/* | 52 command keywords, one module export each | | src/additions.ts | The two forms upstream does not have: new X() and toggle <element> | | src/bundles/* | Browser bundle entries. hyperfixi-hs is the script-tag bundle; full is the same modules, for the gate; core, minimal, common exist to measure size | | upstream-suite/ | The acceptance gate: upstream's Playwright suite (vendored) against a bundle | | tests/ | This package's own tests (src/additions.ts, and regressions upstream's tests did not show), in the form of upstream's and on the same fixtures | | tools/probe.mts | One source on this engine and on upstream, side by side, on the value matrix's fixture |

A node is typed data plus the closure that runs it, bound when the node is parsed: an expression has ev, a command has run, a feature has install. Control flow is a value a command returns (return, break, continue), not an exception.

Commands

npm run build --prefix packages/engine          # dist/: library, bundles, declarations; prints sizes
npm run typecheck --prefix packages/engine
npm run test:upstream --prefix packages/engine  # the gate (needs a build; about 30 s)
npm run test:own --prefix packages/engine       # additions and regressions (needs a build; 2 s)
npm run cost --prefix packages/engine           # what each module costs in the full bundle

# Any engine bundle can be scored on the same suite:
node packages/engine/upstream-suite/run.mjs --bundle <bundle.js> --fails

# One source on this engine and on upstream, side by side:
npx tsx packages/engine/tools/probe.mts '<source>'

# What this engine reads of the sources this repository ships, beside core and upstream:
npx tsx packages/engine/tools/shipped-sources.mts

The gates

Upstream's suite. test:upstream runs upstream's own tests (upstream-suite/vendor/, release 0.9.93, the one this repository's other gates use) against dist/full.js. The tests that fail must be exactly the ones listed in upstream-suite/known-failures.json. A new failure fails the gate. So does a listed test that now passes: prune it with npm run test:upstream:update in the same change, so the list stays what does not pass. CI runs it in the browser-tests job.

The package's own tests. test:own runs tests/ against the same bundle, for the two forms upstream has no tests for. Every one must pass. On upstream's own bundle 2 of the 21 pass (the two that check an upstream form still reads as it did), which is what shows the rest test the additions. tests/regressions.js holds the other kind: places where this engine differed from upstream and upstream's tests did not show it (each of those passes on upstream). CI runs both after the upstream suite.

The multilingual gates, in the packages that own them:

  • The value matrix (packages/testing-framework, value-matrix.ts) has an eng lane, the English source on this engine, and a <lang>/eng lane per language: the adapter's English, the same string the /up lane runs on upstream, on this engine.
  • engine-parser-parity.test.ts (same package) puts every string the canonical-validity gates ask upstream's parser about to this engine's parser; the two must agree.
  • packages/hyperscript-adapter/test/engine-host.test.ts hosts the real plugin on this engine.

The shipped-sources gates, also in packages/testing-framework:

  • shipped-sources-engine.test.ts lists the English sources this repository ships (examples/, the doc trees) that this engine rejects (baselines/shipped-sources-engine.json). A new one fails; so does a listed one that now parses. It is empty. (Until Phase C4 it counted only the sources packages/core compiled clean.) A source written in another language is shipped-sources-localized.test.ts's: it must translate to English both engines parse.
  • shipped-examples-execution.test.ts: every example handler upstream accepts is run on upstream and on this engine, in jsdom, and the DOM effects must match. Measured 2026-10-04: 128 handlers, none different, 44 with no effect on either. (packages/core's lane, which differed on 31 at first, left with its engine in Phase C4.)

Measured (2026-10-01, upstream 0.9.93)

| | this engine (full bundle) | hyperfixi.js | upstream | | ---------------------- | --------------------------- | ----------------------- | -------- | | Whole suite (1,467) | 1,401 | 826 | 1,466 | | Command tests (516) | 516 | 306 | 516 | | Expression tests (459) | 457 | 371 | 459 | | Feature tests (254) | 237 | 100 | 253 | | Core tests (190) | 143 | 49 | 190 | | Template tests (48) | 48 | 0 | 48 | | Size, gzipped | 34.1 KB with every module | 92 KB engine-only build | 45.8 KB |

Bundle sizes, gzipped: minimal (on + add / remove / toggle) 16.3 KB, common (15 everyday commands) 18.4 KB, full 34.1 KB, core (no commands) 13.8 KB.

The script-tag bundle: hyperfixi-hs.js

dist/hyperfixi-hs.js (minified, 34.1 KB gzipped; hyperfixi-hs.dev.js is the readable build) is the first product on this engine: hyperscript and nothing else, every module, no htmx attributes, English only. It installs window._hyperscript, as upstream does, and the same object as window.hyperfixi. The name paired with core's hyperfixi-hx.js (hyperscript plus htmx, retired in Phase C3). Since C-R4b @hyperfixi/core ships this same file as dist/hyperfixi.js (copied by core's scripts/copy-engine-bundle.mjs), where core's own everything-bundle used to be.

Every tracked gallery page loads it instead of packages/core/dist/hyperfixi.js (2026-10-03: forty-seven pages; the error-path page for core's htmx layer was deleted with that layer). Thirty-two by a script tag; fifteen through the examples' loader, data-default="hs", so that ?bundle= still switches them. Core's Playwright suites run them, and the bundle is a column of the bundle-compatibility matrix (?bundle=hs in the examples' loader). No page needed an API shim: processNode is upstream's name too, and the other hyperfixi.* calls were core debugging code. The multilingual demo pages are among them: their handlers are English, and what they translate for display comes from the i18n or semantic bundle, loaded beside the engine. The behavior pages load @hyperfixi/behaviors' bundle beside it, which defines its eleven behaviors on the engine with evaluate (the package peers on this engine since 2026-10-03; its sources are in upstream's idioms and run on upstream too). The intent-element pages load the semantic English bundle beside it: <lse-intent> renders its LSE JSON to hyperscript text and hands it to the host's evaluate, the lse_to_hyperscript direction (core's evalLSENode was the AST path). The history pages write call history.pushState(null, '', X) / replaceState (upstream's spelling; the owner decided against a push url addition) and define their HistorySwap behavior in the page. The htmx pages (examples/hx-v4/, hx-v4-i18n/) are the two stacks that replace core's embedded htmx layer (retired by owner decision, 2026-10-03): the engine's own live / bind blocks where no request is involved, and REAL htmx 4 (vendored under examples/vendor/) with its hx-sse / hx-ws extensions and @lokascript/htmx-adapter for localized attribute names beside the engine where one is. Core's own debug pages left the gallery for packages/core/test-pages/. One behavior of core's is not in this engine, by decision (2026-10-02): increment #count on core counts in the element's text, where upstream and this engine want increment #count's textContent. The examples write the second, which every bundle runs to the same count (the bundle matrix's Counter row).

The 66 known failures: upstream's internal-API surface (internals.tokenizer, evalStatically, source info, error collection: 47), sockets and workers (17, upstream extensions that are not built), and two collection-expression tests that need upstream's component extension. Upstream's one failure is a test that expects its worker extension to be absent.

As a host for the multilingual text path (semantic renders a translation, the adapter's preprocess turns it back into English, the engine runs the English):

  • Value matrix. 4,205 cells. English on this engine gives the oracle's value in every cell. Through the adapter in 23 languages, 96,640 of 96,643 (cell, language) pairs do, and the two hosts agree on every pair: the three that miss are the accepted Italian di ambiguity, and they miss on upstream too. (packages/core missed 8 cells in English; its lanes retired in Phases C2 and C4.)
  • Parser parity. Of the 273 distinct English strings the canonical-validity gates put to upstream's parser, the two parsers disagree on none.
  • Of the 164 corpus rows, 153 parse on both engines and 11 on neither, measured on each row's raw source (2026-10-04, and swap-view-transition 2026-10-05); none parses on one engine only. Ten of the 11 are markup or extension rows (five components; live-with-handler, markup whose _ sources parse on both; sockets, workers, event sources, intercept), and one is valid nowhere (async-block). Twelve more were core-only until the rows were rewritten in upstream's spelling: ten on 2026-10-01, fetch-formdata (new FormData(…) in a js block) on 2026-10-03, and swap-view-transition (start view transition … end, which semantic now reads) on 2026-10-05; five rows of core's htmx v4 attributes left with hyperfixi-hx-v4.js (Phase C3).
  • The adapter's plugin in six languages: the script runs and the attribute stays as written. On upstream the plugin has to rewrite the attribute.

Against what this repository ships

npx tsx packages/engine/tools/shipped-sources.mts puts every English hyperscript source in examples/ and the doc trees (the shipped-sources gate's collection) to upstream's parser and this engine's. Measured 2026-10-04: 315 distinct, upstream accepts 281, this engine all 315; the 34 they disagree on are this engine's two additions (new X(), toggle <element>).

Until Phase C4 packages/core was a third parser, and the question was what replacing it would break. Measured 2026-10-01 (386 sources, 336 distinct): upstream accepted 272, this engine 309, packages/core 320;

  • 12 sources that core accepted were rejected here: the eleven handlers that used core's history commands (push url, replace url), rewritten as call history.pushState(…) on 2026-10-03, and the hyperfixi-hx.js example in docs/BROWSER_BUNDLES.md, written in the hybrid parser's own dialect (on click.debounce(300), me has .loading), which went with that bundle in Phase C3 (2026-10-04). None is left.

It was 78 when first measured. The examples and docs were moved to upstream's spellings where upstream has one (put … into for core's swap strategies, debounced at 300ms, start view transition … end, morph … to, toggle *display of, descending, …: each rewrite was run on core, upstream and this engine, to the same DOM), and two forms were added here (below).

A parse-level count understates a difference: a source can parse and run differently. set t to new Date().getDay() parses on upstream (new is a variable there, the call a second command) and fails when it runs; swap afterBegin of #list with html is upstream's exchange of two values and inserts nothing. The comparison that counts is the DOM a handler leaves (shipped-examples-execution in packages/testing-framework).

Differences from upstream

Three, all recorded here and nowhere silent.

Two additions (src/additions.ts, decided 2026-10-01), forms packages/core has and the shipped examples use. Each is its own module, and neither changes how an upstream script reads: upstream's suite passes the same tests with them registered as without.

  • new Date(), new Intl.NumberFormat('en'), (new Date()).getDay(): a constructor call as an expression. new is still an ordinary name unless a constructor call follows it. Upstream has the make a Date command and no expression. 0.20 KB.
  • toggle #dialog, toggle #dialog modal (or as modal), toggle #details: open what is closed and close what is open, for a dialog, a details element (or its summary), a popover and a select. Upstream rejects toggle <expression> unless between follows; that form is unchanged. 0.21 KB.

One looser reading, found by the measurement above: my @id as String (an attribute read through my / its / your, then a conversion or any further access). Upstream rejects it, because its attribute-access rule does not continue the expression chain as every other access does; the possessive form #a's @title as String works on both. This engine continues the chain. It is kept because the stricter reading looks like an upstream slip and a shipped example uses the form (its @data-stock as Number > 0).

Considered and not added: core's swap strategies (upstream's swap means something else), copy, a ? : conditional, .debounce(n), has, prefix unless, set @a to v on <target>, sorted by … desc. push url / replace url are undecided.

Where the bytes are

npm run cost rebuilds the full bundle without each registered module and prints the difference. Of the 34.1 KB: the fixed core (tokenizer, parser, the core expression kinds, runtime, engine) is 12.2 KB; on is 1.6; the optional modules add up to 15.1; code that several modules share is 5.2. The largest modules: expressionsExtra 1.5, reactivity 1.3, render 1.0, repeat 0.8, toggle 0.8, fetch 0.7, pick 0.7. A simple command costs 20 to 190 bytes (throw 18, get 53, log 73, send 101, append 189).

Three ideas for the command layer were measured. None is worth doing for size:

| Idea | Minified | Gzipped | | ----------------------------------------------------------------- | --------- | --------------------------------------------------------------------------- | | Share the class / attribute handling of add, remove, toggle | −188 | −4 (minimal), +6 (all) | | Drop the descriptive fields from node literals (65 of them) | −1,459 | −566 (all), −177 (minimal) | | A table-driven grammar for the simple commands | not built | an estimate: under 200, since the eight simplest commands cost 700 in total |

Gzip already removes repeated text, so removing duplication between commands shrinks the minified file and not the download. The first row is kept because it is less code. The second saves 1.7 % and would cost the typed half of each node. The third would have to parse those commands for nothing to break even.

The fixed core is three quarters of a small bundle. The comparison operators (is, matches, is greater than, starts with, …) are 1.25 KB of it and the math operators 0.15 KB, measured by stubbing them out. They stay in the core by decision (2026-10-01): a bundle without them would reject when it matches .x, and simpler bundles were preferred to smaller ones.

Source: 8,100 lines, one documented type assertion (num in src/util.ts).