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 🙏

© 2025 – Pkg Stats / Ryan Hefner

fynejs

v1.2.0

Published

A tiny, fast, zero-dependency reactive UI framework for the browser

Readme

FyneJS

npm version npm downloads (weekly) npm downloads (monthly) npm downloads (total)

FyneJS is a tiny, fast, zero‑dependency reactive UI framework for the browser.

Why FyneJS

  • Tiny & fast: zero dependencies, ~18.6KB gzipped, optimized for performance
  • Engineered reactivity: lean, cached evaluations; fine‑grained computed invalidation
  • Smart GC: precise cleanup for timers, observers, events, and component trees
  • Built-in SPA router: client-side navigation with view transitions, prefetching, and lifecycle hooks
  • Browser-native TypeScript: load .ts component files directly—blazing fast type stripping (~34ms for 1000 lines) with zero build tools
  • Declarative power: rich directives and component composition without a build step
  • SEO‑friendly by design: progressive enhancement on existing HTML; no client‑side rendering takeover, so your server content remains crawlable
  • Signals API: lightweight child → parent messaging with payloads and bubbling

Capabilities (with examples)

  • Built-in SPA router with view transitions, prefetching, and navigation hooks (see Built-in SPA Router section)

  • Browser-native TypeScript: Load .ts component files directly without build tools—blazing fast type stripping in ~34ms (see Browser-Native TypeScript section)

  • Declarative directives: text, HTML, show/hide, if/else, loops, model binding, events, styles, classes, transitions

    <div x-data="{ n: 0, items: [1,2,3], open: true }">
      <button x-on:click="n++">+1</button>
      <span x-text="n"></span>
    
      <template x-if="open"><p>Shown</p></template>
      <!-- Transition example (class phases + end hook) -->
      <div
        x-show="open"
        x-transition:enter="transition ease-out duration-200"
        x-transition:enter-from="opacity-0 -translate-y-2"
        x-transition:enter-to="opacity-100 translate-y-0"
        x-transition:leave="transition ease-in duration-150"
        x-transition:leave-from="opacity-100 translate-y-0"
        x-transition:leave-to="opacity-0 -translate-y-2"
        x-transition:enter.after="({ el, phase, config }) => console.log('entered', config.duration)"
      >Animated block</div>
      <ul>
        <template x-for="(v,i) in items"><li>#<span x-text="i"></span>: <span x-text="v"></span></li></template>
      </ul>
    </div>
  • Powerful events & modifiers: keys, buttons, touch, outside, once, passive, capture, combos, defer

    <input x-on:keydown.enter.ctrl="save()">
    <button x-on:click.once="init()">Init once</button>
    <div x-on:click.outside="open=false">Panel</div>
    <input x-on:input.defer="recompute()"> <!-- handler runs in a microtask -->
  • Model binding: inputs, checkboxes (arrays), radios, selects (multiple)

    <input type="text" x-model="form.name">
    <input type="checkbox" value="admin" x-model="roles"> <!-- toggles 'admin' in roles -->
    <select multiple x-model="selected"> ... </select>
  • Computed & watch: derived state and change observers

    <div x-data="{ a: 2, b: 3 }" x-text="a*b"></div>
    <!-- via component API, you can also define computed and watch -->
  • Lifecycle hooks: beforeMount, mounted, updated, beforeUnmount, unmounted

Transitions (x-transition)

Animate enter/leave for x-show and x-if:

<div x-data="{ open: false }">
  <button x-on:click="open = !open">Toggle</button>
  <div x-show="open" x-transition="opacity-0 transition-opacity duration-200" class="panel">Fade panel</div>
</div>

Granular class phases + end callback:

<div
  x-data="{ open: true }"
  x-show="open"
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-from="opacity-0 scale-95"
  x-transition:enter-to="opacity-100 scale-100"
  x-transition:leave="transition ease-in duration-200"
  x-transition:leave-from="opacity-100 scale-100"
  x-transition:leave-to="opacity-0 scale-95"
  x-transition:enter.after="({ el, config }) => console.log(config.duration)"
>Modal</div>

.after / .end modifiers run once per completed phase (never on cancellation) and receive { el, phase, config } where config.duration is the effective transition time including delays. For x-if branches, the original display value is preserved (including display: contents for template wrappers).

  • Slot & props: lightweight component composition

  • Seal and freeze: temporarily pause interactivity or lock state

    <!-- Freeze via readonly attribute (no state changes, timers/listeners/observers paused, no renders) -->
    <component source="stats-card" readonly></component>
    
    <!-- Seal programmatically: allow internal state but suppress renders and side-effects -->
    <div x-data="{ paused:false, toggle(){ this.$seal(!(this.$isSealed)); this.paused = this.$isSealed; } }">
      <button x-on:click="toggle()" x-text="$isSealed ? 'Resume' : 'Pause'"></button>
    </div>

    Freeze is fully read‑only (no state changes, no renders). Remove the readonly attribute (or call $seal(false) for sealed) to resume interactivity when appropriate.

  • No build required: works directly in the browser; enhanced builds are optional

  • $nextTick(): run code after DOM updates

    this.$nextTick(()=>{/* DOM updated */})
  • Event delegation: scale to large UIs

    <script> XTool.init({ delegate: true }); </script>
  • Security sandbox: restrict globals in expressions

    XTool.init({ sandboxExpressions: true, allowGlobals: ['setTimeout'] })

Quick start (CDN)

Include the minified build from jsDelivr or unpkg:

<script src="https://cdn.jsdelivr.net/npm/fynejs@latest/dist/x-tool.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/fynejs@latest/dist/x-tool.min.js"></script>
<script>
  XTool.init({ 
    debug: false,
    router: { enabled: true }  // Optional: enable SPA routing
  });
  
  // Optional: Load external components (supports .js and .ts files!)
  XTool.loadComponents([
    'components/header.js',
    'components/user-card.ts'  // TypeScript works directly in browser!
  ]);
</script>

<div x-data="{ count: 0 }">
  <button x-on:click="count++">+1</button>
  <span x-text="count"></span>
</div>

TypeScript usage

There are two easy ways to use FyneJS with TypeScript:

1) Bundlers (recommended)

Install the package and import the API. The package exposes clean ESM/CJS entry points and ships its own types.

// main.ts
import XTool, { html } from 'fynejs';

XTool.init({ debug: false, delegate: true });

XTool.registerComponent({
  name: 'hello-world',
  data: { msg: 'Hello FyneJS' },
  // Use the tagged template for editor highlighting in TS
  template: html`<div x-text="msg"></div>`
});

// Somewhere in your HTML template or via DOM APIs:
// <component source="hello-world"></component>
  • Works with Vite/Rollup/Webpack/ts-node/tsx without extra config.
  • Types are resolved automatically via the package exports (no triple-slash needed).
  • Tip: Import the html helper from fynejs for tagged template literals without TS errors and with IDE HTML highlighting.

2) CDN + types

If you’re using the CDN build in the browser and still want editor types, reference the declarations manually:

/// <reference path="./types/x-tool/types.d.ts" />

You can copy the file into your repo and point typeRoots to it, or vendor the shipped types.d.ts.

Core concepts

Auto-discovery with x-data

<div x-data="{ message: 'Hello' }" x-text="message"></div>

Events and modifiers

<button x-on:click.prevent.stop="submit()">Save</button>
<input x-on:keydown.enter="save()">

Two-way binding

Signals (component messaging)

Signals are lightweight, component-scoped broadcasts for child → parent notifications and other local messaging. Emissions start at the current component and bubble up to ancestors; any ancestor that connected a handler for the signal name will receive the event. Bubbling can be stopped via evt.stopPropagation().

Core API (available on the component context):

  • this.Signals.emit(name, payload?): emit and bubble upward.
  • this.Signals.connect(name, handler): register a handler on the current component.
  • this.Signals.disconnect(name, handler): unregister the handler.

Handlers receive { name, payload, stopPropagation } and run with the component method context, so this has access to data/computed/methods.

Connect just-in-time, emit, then disconnect:

<div x-data="{ log: [],
  handler(evt){ this.log = [...this.log, 'got:'+evt.payload]; },
  run(){ this.Signals.connect('hello', this.handler); this.Signals.emit('hello','x'); this.Signals.disconnect('hello', this.handler); }
}">
  <button x-on:click="run()">fire</button>
  <span x-text="log.join(',')"></span>
</div>

Bubbling to ancestors (no stopPropagation):

<section x-data="{ heard: [], mounted(){ this.Signals.connect('hello', (evt)=>{ this.heard = [...this.heard, evt.payload]; }); } }">
  <div x-data>
    <button x-on:click="Signals.emit('hello','X')">emit</button>
  </div>
  <span x-text="heard.join(',')"></span>
</section>

Notes:

  • Signals handlers are cleared on component destroy to avoid leaks.
  • You can also connect in lifecycle to listen continuously, or connect/disconnect around a single operation.
<input type="text" x-model="form.name">
<input type="checkbox" value="admin" x-model="roles"> <!-- adds/removes 'admin' in roles array -->
<select multiple x-model="selected"> ... </select>

Lists and conditionals

<template x-if="items.length === 0">
  <p>No items</p>
</template>

<ul>
  <template x-for="(todo, i) in todos">
    <li>
      <span x-text="todo.title"></span>
    </li>
  </template>

### Built-in SPA Router

FyneJS includes a lightweight client-side router with view transitions and navigation hooks:

```js
XTool.init({
  router: {
    enabled: true,
    transtionName: 'slide',        // CSS view transition name
    before: (to, from, info) => {
      // Check auth, analytics, etc.
      // Return false to cancel navigation
      return true;
    },
    after: (to, from, info) => {
      // Update UI, scroll, analytics
      console.log(`Navigated from ${from} to ${to}`);
    },
    error: (error, to, from) => {
      console.error('Navigation error:', error);
    },
    prefetchOnHover: true          // Smart link prefetching
  }
});

Transitions (x-transition)

Animate enter/leave for x-show and x-if.

Simple toggle form:

<div x-show="open" x-transition="opacity-0 transition-opacity duration-200">Fade</div>

Configuration object form:

<div
  x-show="open"
  x-transition="{ duration: 300, easing: 'ease-out', enter: 'transition', enterFrom: 'opacity-0 scale-95', enterTo: 'opacity-100 scale-100', leave: 'transition', leaveFrom: 'opacity-100 scale-100', leaveTo: 'opacity-0 scale-95' }"
  x-transition:enter.after="({ config }) => console.log(config.duration)"
>Dialog</div>

Granular phases + end hook:

<div
  x-show="open"
  x-transition:enter="transition ease-out duration-300"
  x-transition:enter-from="opacity-0 scale-95"
  x-transition:enter-to="opacity-100 scale-100"
  x-transition:leave="transition ease-in duration-200"
  x-transition:leave-from="opacity-100 scale-100"
  x-transition:leave-to="opacity-0 scale-95"
  x-transition:enter.after="({ el, phase, config }) => console.log(phase, config.duration, el)"
>Modal</div>

Notes:

  • .after / .end modifiers run once per completed phase (never on cancellation)
  • Handlers receive { el, phase, config } where config.duration is the effective time including delays
  • With x-if, original display is preserved (including display: contents for template wrappers)

Use x-link directive for SPA navigation:

<nav>
  <a href="/index.html" x-link>Home</a>
  <a href="/about.html" x-link>About</a>
  <a href="/contact.html" x-link>Contact</a>
</nav>

<!-- With prefetching -->
<a href="/dashboard.html" x-link x-prefetch="hover">Dashboard</a>

The router intercepts link clicks, updates the URL, and loads new pages without full refreshes—perfect for multi-page apps that feel like SPAs.

Dynamic component file loading

Load external component files (.js or .ts) with flexible loading strategies:

// Preload immediately (default for string entries)
XTool.loadComponents([
  'components/stats-card.js',
  { path: 'components/chat-panel.js', mode: 'preload' }
]);

// Defer: ensure file loads before initial auto-discovery (blocks first scan)
XTool.loadComponents([
  { path: 'components/large-dashboard.js', mode: 'defer' }
]);

// Lazy: only fetch when a <component source="name"> appears in the DOM
XTool.loadComponents([
  { path: 'components/order-book.js', mode: 'lazy', name: 'order-book' },
  // name can be omitted; filename (without extension) is used
  { path: 'components/advanced-calculator.js', mode: 'lazy' }
]);

// Later in HTML (triggers lazy fetch on first encounter)
// <component source="order-book"></component>

Lazy mode details:

  • Registration does not fetch the file until the component is actually used.
  • If a matching <component source="..."> already exists at registration time, the file is fetched in an idle callback.
  • After load, auto-discovery re-runs so the component mounts automatically.

Defer mode details:

  • Files are fetched before the framework performs the initial DOM scan, guaranteeing definitions are available when components are first discovered.

Return value: loadComponents resolves with { settled, failed } counting only immediate (preload + defer) operations; lazy entries are not counted until they actually load.

TypeScript components work too! FyneJS automatically strips TypeScript type annotations from .ts files:

XTool.loadComponents([
  'components/user-card.ts',      // TypeScript file
  'components/data-chart.ts',     // TypeScript file
  'components/modal.js'           // Regular JavaScript
]);

HTML Component Files

Write components in native .html files with full IDE syntax highlighting—no template strings needed!

<!-- components/greeting.html -->
<template>
  <div class="greeting">
    <h2 x-text="message"></h2>
    <button x-on:click="greet()">Say Hello</button>
  </div>
</template>

<script setup>
const message = data('Hello World');

function greet() {
  message.value = 'Hello, FyneJS!';
}

expose({ message, greet });
onMounted(() => console.log('Mounted!'));
</script>

Multiple components in one file:

<!-- components/widgets.html -->
<template name="widget-header">
  <header x-text="title"></header>
</template>
<script setup name="widget-header">
const title = data('Header');
expose({ title });
</script>

<template name="widget-footer">
  <footer x-text="copyright"></footer>
</template>
<script setup name="widget-footer">
const copyright = data('© 2025');
expose({ copyright });
</script>

Load like any other component:

XTool.loadComponents([
  'components/greeting.html',
  'components/widgets.html'  // loads both widget-header & widget-footer
]);

Available helpers in HTML components: data(), computed(), watch(), expose(), onMounted(), onBeforeMount(), onUnmounted(), onBeforeUnmount().

Programmatic Component Mounting

Mount registered components on any DOM element programmatically:

// Register a component
XTool.registerComponent({
  name: 'user-card',
  template: '<div><h3 x-text="name"></h3></div>',
  data: { name: 'Guest' }
});

// Mount on any element with optional props
const container = document.getElementById('dynamic-area');
const instance = XTool.mountComponent('user-card', container, {
  name: 'John Doe'
});

// Later: instance.$destroy() to unmount

Element References (x-ref)

Create named references to DOM elements accessible via $refs:

<div x-data="{ focusInput() { $refs.myInput.focus(); } }">
  <input x-ref="myInput" type="text">
  <button x-on:click="focusInput()">Focus</button>
</div>
  • $refs.name returns the element (or array if multiple elements share the name)
  • $ref(name) function form to get a ref
  • $ref(name, value) to register any value as a ref
  • Refs bubble up the component tree—child components can access parent refs

Shorthand Attribute Binding

Three equivalent syntaxes for dynamic attributes:

<div x-data="{ url: '/image.png', active: true }">
  <!-- Full syntax -->
  <img x-bind:src="url">
  
  <!-- x: shorthand -->
  <img x:src="url">
  
  <!-- : shorthand (shortest, Vue-style) -->
  <img :src="url">
  <button :disabled="!active">Click</button>
</div>

DOM Helpers ($attr, $css)

Programmatically set attributes and CSS on the target element:

<div x-data>
  <button x-on:click="$attr('data-clicked', true)">Mark Clicked</button>
  <div x-on:click="$css('background', 'red')">Click to color</div>
  
  <!-- Object syntax for multiple -->
  <button x-on:click="$attr({ 'data-x': 1, 'data-y': 2 })">Set Both</button>
  <div x-on:click="$css({ color: 'white', background: 'blue' })">Style Me</div>
  
  <!-- Glob pattern -->
  <div x-on:click="$attr('[width,height]', 100)">Set width & height</div>
</div>

Browser-Native TypeScript (Zero Build)

Unique to FyneJS: Load TypeScript component files directly in the browser without any build step or compilation!

// Load TypeScript components just like JavaScript
XTool.loadComponents([
  { path: 'components/user-dashboard.ts', mode: 'preload' },
  { path: 'components/analytics-chart.ts', mode: 'lazy' }
]);
<!-- Use TypeScript components seamlessly -->
<component source="user-dashboard"></component>
<component source="analytics-chart"></component>

How it works:

  • Token-based type stripping using a single-pass scanner
  • Blazingly fast: ~34ms for 1000 lines of TypeScript
  • Handles interfaces, types, generics, enums, access modifiers, and more
  • No compilation, no waiting, no build process
  • Works with simple and complex TypeScript patterns

What gets removed:

  • Type annotations (variables, parameters, return types)
  • Interface, type, namespace, and declare declarations
  • Generics in functions and classes
  • Import/export statements (including type-only imports)
  • Non-null assertions (!)
  • Access modifiers (public, private, protected, readonly)
  • Type assertions (as syntax)
  • Enum declarations
  • implements clauses

Important notes:

  • This is type stripping, not type checking—use an IDE (VS Code, WebStorm) for type safety during development
  • Best suited for well-formed TypeScript code
  • Simpler types work better than very complex type constructs
  • Consider file size for large codebases (performance is excellent for typical component files)

Example TypeScript component:

// components/user-card.ts
interface User {
  id: number;
  name: string;
  email: string;
}

XTool.registerComponent<{ user: User }>({
  name: 'user-card',
  data: {
    user: null as User | null,
    loading: false
  },
  methods: {
    async loadUser(id: number): Promise<void> {
      this.loading = true;
      const response = await fetch(`/api/users/${id}`);
      this.user = await response.json();
      this.loading = false;
    }
  },
  template: html`
    <div class="card">
      <template x-if="loading">Loading...</template>
      <template x-if="!loading && user">
        <h3 x-text="user.name"></h3>
        <p x-text="user.email"></p>
      </template>
    </div>
  `
});

The TypeScript code above is automatically stripped to valid JavaScript and executed in the browser—no build step required!

Next tick

XTool.init();
XTool.createComponent?.({ /* if using programmatic API */ });
// In methods:
this.$nextTick(() => {
  // DOM is updated
});

Event delegation (large lists)

<script>
XTool.init({ delegate: true });
</script>

Delegates click, input, change, keydown, keyup at the container level to reduce listeners.

Expression sandbox (optional)

XTool.init({
  sandboxExpressions: true,
  allowGlobals: ['setTimeout', 'requestAnimationFrame'] // whitelist if needed
});

When enabled, expressions do not see window/document unless whitelisted.

Async component templates

<component source="async-card" x-prop="{ id: 42 }"></component>
<script>
XTool.registerComponent({
  name: 'async-card',
  template: () => fetch('/card.html').then(r => r.text()),
  mounted() { /* ... */ }
});
</script>

Until the Promise resolves, the component element stays empty; when it resolves, the template is applied and directives are parsed.

Components: registration, props, slots, dynamic mounting

Register once, reuse anywhere:

<component source="fancy-card" x-prop="{ title: 'Hi' }"></component>

<script>
XTool.registerComponent({
  name: 'fancy-card',
  template: `
    <div class="card">
      <h3 x-text="title"></h3>
      <slot></slot>
    </div>
  `,
  data: { title: '' }
});
</script>

Dynamic mounting: change the source attribute and the framework mounts the new component and cleans up the old one automatically.

<div x-data="{ src: 'fancy-card' }">
  <button x-on:click="src = src==='fancy-card' ? 'simple-card' : 'fancy-card'">Swap</button>
  <component x:source="src"></component>
</div>

Props are reactive; slots distribute original child nodes to <slot>/<slot name="...">.

Async templates are supported: template can be a string, a Promise, or a function returning a Promise.

XTool.registerComponent({
  name: 'delayed-card',
  template: () => fetch('/fragments/card.html').then(r=>r.text())
});

Global API (window.XTool)

XTool.init(config?: {
  container?: string;           // default: 'body'
  debug?: boolean;              // enable debug logging
  staticDirectives?: boolean;   // optimize static directives
  prefix?: string;              // default: 'x'
  delegate?: boolean;           // event delegation for performance
  sandboxExpressions?: boolean; // restrict globals in expressions
  allowGlobals?: string[];      // whitelist globals when sandboxed
  router?: {                    // SPA routing configuration
    enabled: boolean;
    transtionName?: string;     // CSS view transition name
    before?: (to: string, from: string, info: {source: string}) => boolean | Promise<boolean>;
    after?: (to: string, from: string, info: {source: string}) => void;
    error?: (error: unknown, to: string, from: string) => void;
    prefetchOnHover?: boolean;  // smart link prefetching
  }
});

XTool.directive(name: string, impl: { bind?, update?, unbind? }): void;
XTool.registerComponent({ name, data, methods, computed, propEffects, template, ... }): void;
XTool.loadComponents(sources: Array<string | { path: string; mode?: 'preload' | 'defer' | 'lazy'; name?: string }>): Promise<{ settled: number; failed: number }>;

// Optional: custom directive prefix (not hardcoded to "x")
XTool.init({ prefix: 'u' }); // use u-data, u-text, u-on:click, ...

Documentation

For complete documentation, guides, and interactive examples, visit:

https://fynejs.com

License

MIT