
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
FyneJS is a tiny, fast, zero‑dependency reactive UI framework for the browser.
.ts component files directly—blazing fast type stripping (~34ms for 1000 lines) with zero build toolsBuilt-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
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'] })
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>
There are two easy ways to use FyneJS with TypeScript:
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>
exports (no triple-slash needed).html helper from fynejs for tagged template literals without TS errors and with IDE HTML highlighting.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.
<div x-data="{ message: 'Hello' }" x-text="message"></div>
<button x-on:click.prevent.stop="submit()">Save</button>
<input x-on:keydown.enter="save()">
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:
<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>
<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
}
});
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){ el, phase, config } where config.duration is the effective time including delaysx-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.
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:
<component source="..."> already exists at registration time, the file is fetched in an idle callback.Defer mode details:
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
]);
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().
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
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 refThree 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>
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>
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:
What gets removed:
!)as syntax)implements clausesImportant notes:
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!
XTool.init();
XTool.createComponent?.({ /* if using programmatic API */ });
// In methods:
this.$nextTick(() => {
// DOM is updated
});
<script>
XTool.init({ delegate: true });
</script>
Delegates click, input, change, keydown, keyup at the container level to reduce listeners.
XTool.init({
sandboxExpressions: true,
allowGlobals: ['setTimeout', 'requestAnimationFrame'] // whitelist if needed
});
When enabled, expressions do not see window/document unless whitelisted.
<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.
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())
});
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, ...
For complete documentation, guides, and interactive examples, visit:
MIT
FAQs
A tiny, fast, zero-dependency reactive UI framework for the browser
The npm package fynejs receives a total of 7 weekly downloads. As such, fynejs popularity was classified as not popular.
We found that fynejs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.