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

@telilabs/x2

v0.1.0

Published

Minimal zero-dependency component framework: write JSX components, get a bundled SPA with no Webpack or Babel

Downloads

93

Readme

x2

A minimal, zero-dependency component framework for the browser. Write components as JSX functions, define your state in a repo, and the CLI bundles everything into a ready-to-serve SPA — no Webpack, no Babel, no config files.

npm install -g @telilabs/x2
x2 create my-app
cd my-app
x2 serve

Quick start

x2 create my-app   # scaffold a new project
x2 serve  my-app   # build + watch + serve at http://127.0.0.1:3000
x2 build  my-app   # production bundle → my-app/x2Target/

Set PORT=8080 to use a different port.

x2 create produces this structure:

my-app/
  app/
    repo.js          ← state (data, getters, setters)
    router.js        ← route guards
    components/      ← one .jsx file per component
      x2.jsx         ← root component (required)
    styles.css       ← optional global styles
    index.html       ← optional HTML template override

Components

Each file in app/components/ is a component. The filename (camelCase) maps to a custom HTML tag (kebab-case with an x2- prefix):

| File | Tag | |---|---| | x2.jsx | <x2> (root, rendered automatically) | | appHeader.jsx | <x2-app-header> | | noteItem.jsx | <x2-note-item> |

A component exports a render(props) function that returns JSX:

// components/greeting.jsx
exports.render = function (props) {
  return <h2>Hello, {props.name}!</h2>;
};

Use it in another component:

<x2-greeting x2-name="Alice" />

Props are passed as x2-* HTML attributes. All prop values arrive as strings. The special inner prop holds the element's text content.

JSX rules

  • Expression children {expr} are auto-escaped. User content is safe by default.
  • Text content is treated as raw HTML — HTML entities like &#x2715; work as expected.
  • Multiple root elements require a fragment <>...</>.
  • Self-closing works for any tag: <x2-header />.
  • Boolean HTML attributes (checked, disabled, selected, etc.) are emitted correctly from boolean values. All other attributes are stringified, so x2-done={false} emits x2-done="false".
exports.render = function (props) {
  var done = props.done === "true";
  return (
    <li class={"item" + (done ? " done" : "")}>
      <input type="checkbox" checked={done} />
      <span>{props.text}</span>
    </li>
  );
};

The html template tag

For quick one-liners or cases where you need to embed raw HTML, the html tagged template is also available. All ${interpolations} are auto-escaped:

exports.render = function (props) {
  return html`<p>Hello, ${props.name}!</p>`;
};

State (repo)

app/repo.js exports data, getters, and setters. Use this inside getters and setters to access the store — the bundler wires it up automatically.

module.exports = {
  data: { count: 0 },
  getters: {
    getCount: function () { return this.count; },
  },
  setters: {
    increment: function () { this.count++; },
    reset:     function () { this.count = 0; },
  },
};

In render functions — access via repo.getters.*. During a render, the state is a read-only snapshot so mutations can't sneak in:

exports.render = function (props) {
  return <p>Count: {repo.getters.getCount()}</p>;
};

In event handlers — call repo.setters.*. Any exported function that calls a setter (or setRoute) is automatically wrapped to trigger a full re-render after it runs. This works with regular, arrow, and async functions:

exports.increment = function () {
  repo.setters.increment();
};

// async — re-render fires after the await settles
exports.loadData = async function () {
  var data = await fetch("/api").then(r => r.json());
  repo.setters.setItems(data);
};

Reference event handlers by their bare name in JSX — the bundler namespaces them automatically to avoid globals collisions:

exports.render = function (props) {
  return <button onclick="increment()">+1</button>;
  //                      ↑ becomes "counter_increment" in the bundle
};

Routing

app/router.js exports named route guards — functions that return true when the route should be active. getRoute() and setRoute(path) are globals provided by the runtime.

module.exports = {
  home:  function () { return getRoute() === "/"; },
  about: function () { return getRoute() === "/about"; },
};

Wrap content in <x2-route x2-route="name"> to show or hide it based on the current path. Hidden routes skip rendering their children entirely:

exports.render = function (props) {
  return (
    <>
      <x2-route x2-route="home"><x2-home-page /></x2-route>
      <x2-route x2-route="about"><x2-about-page /></x2-route>
    </>
  );
};

Navigate with setRoute — the re-render is triggered automatically. The back/forward browser buttons also trigger a re-render via popstate:

exports.goAbout = function (event) {
  event.preventDefault();
  setRoute("/about");
};

Example app

A working note-taking app is included in example/. It demonstrates components, state, routing, props, and event handlers.

node bin/x2.js serve example
# → http://127.0.0.1:3000  (auto-reloads on save)

Features shown:

  • Root component with fragment and child components
  • Two routes (Notes / About) with lazy rendering of inactive pages
  • Repo with getters (all, count, doneCount) and setters (add, toggle, remove)
  • Props via x2-* attributes — noteItem receives id, text, done
  • Auto-escaped expression children {n.text} — user content is XSS-safe
  • Conditional JSX: count > 0 ? <span>...</span> : null
  • Stats badge in the header driven by live getter values

How the build works

x2 build produces three files in x2Target/:

| File | Contents | |---|---| | app.js | State, getters, setters, and component functions — namespaced with UUID-based names to avoid global collisions | | x2.js | The runtime: BFS renderer, in-place DOM reconciler, snapshot isolation, router | | index.html | Your app/index.html or the default template |

The bundler runs a minimal JSX transform (no Babel, no external dependencies) that converts JSX to h() calls before writing the bundle. Component .jsx files are loaded with a custom require extension handler — you write JSX, the bundler outputs plain JavaScript.

The runtime uses an in-place DOM reconciler (patchDOM) rather than wholesale innerHTML replacement, so input focus, scroll position, and checkbox state are preserved across re-renders.


Running the tests

npm test

Known limitations

  • No SSR. The framework is browser-only.
  • No route parameters. Routes are matched by exact guard functions in router.js, not a pattern like /post/:id.
  • applyRenames only matches function calls. Cross-component references must be called with () — passing a function reference (e.g. arr.map(myFn)) will not be renamed in the bundle.
  • One app per page. The state proxy and renderer registry are globals; multiple independent x2 apps on the same page are not supported.
  • JSX heuristic detection. The JSX transformer uses contextual heuristics rather than a full parser. Unusual patterns (e.g. a tagged template immediately after <) could confuse it. Use .jsx file extension to make intent clear.

License

MIT