
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.
A clever and mischievous framework that makes your code dance with joy! As cunning as it is lightweight - because being smart doesn't mean being heavy!
Farinel is a lightweight and reactive UI framework for creating user interfaces in JavaScript/TypeScript. It provides a declarative and functional approach to state management and component rendering, built on top of Ciaplu for pattern matching and state management.
.when(), .with(), .withType() are all reactive (v2.5.0+).extracting().test()npm install farinel
import { farinel } from 'farinel';
import { Div, Button } from './html';
const Counter = () => {
const component = farinel()
.stating(() => ({
count: 0
}))
.otherwise(() =>
Div({},
Button({}, `Count: ${component.state.count}`)
.on("click", async () => {
await component.dispatch({
count: component.state.count + 1
});
})
)
);
return component;
}
Farinel uses Ciaplu's pattern matching for state management. The state is managed through the stating() method:
const component = farinel()
.stating(() => ({
user: null,
loading: false
}));
State updates are handled through the dispatch() method:
await component.dispatch({
user: { id: 1, name: 'John' },
loading: true
});
Farinel exports all Ciaplu's pattern matching functions:
component
.with({ type: 'success' }, () =>
Div({}, "Success!")
)
.withType(Error, () =>
Div({}, "Error occurred")
)
.when(state => state.count > 10, () =>
Div({}, "Count is high!")
)
.otherwise(() =>
Div({}, "Default view")
);
Transform state before rendering:
component
.stating(() => ({
firstName: 'John',
lastName: 'Doe'
}))
.extracting(state => ({
...state,
fullName: `${state.firstName} ${state.lastName}`
}))
.otherwise(() =>
Div({}, `Hello ${component.state.fullName}!`)
);
Components can be composed and nested:
const UserProfile = ({ user }) => {
const profile = farinel()
.stating(() => ({
user,
editing: false
}))
.otherwise(() =>
Div({},
UserHeader({ user: profile.state.user }),
UserDetails({
user: profile.state.user,
editing: profile.state.editing,
onEdit: () => profile.dispatch({ editing: true })
})
)
);
return profile;
};
Events are handled with the on() method:
Button({}, "Submit")
.on("click", async (e) => {
e.preventDefault();
await component.dispatch({
loading: true
});
// ... handle submission
});
Farinel provides form components with state management:
const LoginForm = () => {
const form = farinel()
.stating(() => ({
email: '',
password: '',
loading: false
}))
.otherwise(() =>
Form({},
Input({
type: 'email',
value: form.state.email,
disabled: form.state.loading
})
.on("input", (e) => {
// Update state directly to preserve focus
form.state.email = e.target.value;
}),
Input({
type: 'password',
value: form.state.password,
disabled: form.state.loading
})
.on("input", (e) => {
// Update state directly to preserve focus
form.state.password = e.target.value;
}),
Button({
disabled: form.state.loading
}, "Login")
.on("click", async () => {
// Dispatch only on submit, not on every keystroke
await form.dispatch({
...form.state,
loading: true
});
// ... handle login
})
)
);
return form;
};
Create a root component and mount it to the DOM:
const app = farinel();
await app.createRoot(document.body, App);
Observe state changes with the spy() method:
const stateChange = component.spy();
await component.dispatch({ count: 1 });
const newState = await stateChange;
import { farinel } from 'farinel';
import { Div, Button, Input, Form } from './html';
const LoginPage = () => {
const loginPage = farinel()
.stating(() => ({
email: '',
password: '',
loading: false,
error: null
}))
.when(state => state.error, () =>
Div({},
Form({},
Div({}, `Error: ${loginPage.state.error}`),
Input({
type: 'email',
value: loginPage.state.email,
disabled: loginPage.state.loading
})
.on("input", (e) => {
loginPage.state.email = e.target.value;
loginPage.state.error = null;
}),
Input({
type: 'password',
value: loginPage.state.password,
disabled: loginPage.state.loading
})
.on("input", (e) => {
loginPage.state.password = e.target.value;
loginPage.state.error = null;
}),
Button({
disabled: loginPage.state.loading
}, "Login")
.on("click", async () => {
await loginPage.dispatch({
...loginPage.state,
loading: true
});
try {
// ... handle login
await loginPage.dispatch({
...loginPage.state,
loading: false,
error: null
});
} catch (error) {
await loginPage.dispatch({
...loginPage.state,
loading: false,
error: error.message
});
}
})
)
)
)
.otherwise(() =>
Div({},
Form({},
Input({
type: 'email',
value: loginPage.state.email,
disabled: loginPage.state.loading
})
.on("input", (e) => {
loginPage.state.email = e.target.value;
loginPage.state.error = null;
}),
Input({
type: 'password',
value: loginPage.state.password,
disabled: loginPage.state.loading
})
.on("input", (e) => {
loginPage.state.password = e.target.value;
loginPage.state.error = null;
}),
Button({
disabled: loginPage.state.loading
}, "Login")
.on("click", async () => {
await loginPage.dispatch({
...loginPage.state,
loading: true
});
try {
// ... handle login
await loginPage.dispatch({
...loginPage.state,
loading: false,
error: null
});
} catch (error) {
await loginPage.dispatch({
...loginPage.state,
loading: false,
error: error.message
});
}
})
)
)
);
return loginPage;
};
const App = async () => {
const app = farinel();
await app.createRoot(document.body, LoginPage);
};
App();
stating(getState): Initialize component statedispatch(newState): Update component statecreateRoot(container, component): Mount component to DOMspy(): Observe state changesresolve(): Resolve component to final elementwith(value, handler): Match exact valuewithType(type, handler): Match by typewhen(matcher, handler): Match by conditionotherwise(handler): Default handlerextracting(handler): Transform statetest(matcher): Test state conditionDiv(attributes, children)Button(attributes, children)Input(attributes)Form(attributes, children)Select(attributes, children)Option(attributes, children)and more...
Always use ternary operators with null for conditional rendering, not logical AND (&&) operators:
// ✅ CORRECT - Use ternary operator
state.showModal ? Div({}, "Modal content") : null
// ❌ WRONG - Using && can cause rendering issues
state.showModal && Div({}, "Modal content") // Returns false when condition is false
Why? When using &&, JavaScript returns false when the condition is false. Farinel normalizes false to an empty string '', which corrupts the children array structure and breaks the diffing algorithm. Using ternary operators with : null ensures proper handling of conditional elements.
For text inputs, avoid calling dispatch() on every keystroke. Instead, update state directly:
// ✅ CORRECT - Direct state update preserves focus
Input({ value: component.state.text })
.on("input", (e) => {
component.state.text = e.target.value; // No re-render
})
// ❌ WRONG - Dispatch on every keystroke loses focus
Input({ value: component.state.text })
.on("input", async (e) => {
await component.dispatch({ text: e.target.value }); // Re-renders, loses focus
})
Why? Calling dispatch() triggers a full re-render and diff/patch cycle. Farinel's PropsPatch automatically preserves input focus, but only if the input element is patched, not replaced. Direct state updates avoid unnecessary re-renders while typing.
Use unique key attributes for conditional elements to help Farinel's diffing algorithm:
state.step === 1 ? Div({ key: 'step-1' },
// Step 1 content
) : null,
state.step === 2 ? Div({ key: 'step-2' },
// Step 2 content
) : null
Why? Keys enable identity-based diffing, allowing Farinel to correctly track which elements are added, removed, or moved, rather than relying on positional matching.
MIT
Major Feature:
.when(), .with(), .withType(): All Ciaplu pattern matching methods are now reactive! When you call dispatch(), Farinel re-evaluates all patterns and automatically switches the rendered view based on the new state.What This Means:
Before v2.5.0, only .otherwise() was reactive. Now you can use declarative pattern matching for state-based views:
const app = farinel()
.stating(() => ({ status: 'loading' }))
.when(state => state.status === 'loading', () =>
Div({}, 'Loading...')
)
.when(state => state.status === 'success', () =>
Div({}, 'Success!')
)
.when(state => state.status === 'error', () =>
Div({}, 'Error!')
)
.otherwise(() => Div({}, 'Unknown'));
// This now works reactively!
await app.dispatch({ status: 'success' }); // UI updates to "Success!"
await app.dispatch({ status: 'error' }); // UI updates to "Error!"
Technical Details:
.otherwise() if no pattern matchesTest Coverage:
Breaking Changes:
Major Improvements:
null/undefined now uses comment placeholders (document.createComment("placeholder")) to maintain consistent DOM structure and prevent index mismatches during patchingPropsPatch to automatically preserve input focus and cursor position during patches, eliminating focus loss issuesid and key attributes, enabling efficient updates for lists, conditional elements, and multi-step formscomponent.state directly without triggering re-renders, useful for form inputs and frequent state changesnull children in the virtual DOMBreaking Changes:
Best Practices Added:
: null) instead of && for conditional renderingkey attributes for conditional/dynamic elementsKey-Based Diffing:
id and key attributesPropsPatchAuto-Wait Mechanism:
dispatch() now automatically waits for component mount before executingFixed a bug where nested arrays of children were not properly handled during rendering and patching. The virtual DOM now correctly flattens nested array structures in children:
// This now works correctly:
Div({},
[[Button({}, 'A'), Button({}, 'B')],
[Button({}, 'C')]]
);
// Renders as: <div><button>A</button><button>B</button><button>C</button></div>
true/false) are converted to empty text nodes, consistent with render behaviorEvent handlers passed as props (e.g., onClick) that are removed during patch are now properly tracked and removed. The attachListener and detachPropListeners methods on Element ensure listeners don't leak.
To verify the fixes and run the test suite:
npm test
Test files covering these fixes:
src/__tests__/nested-events.test.ts - Nested array rendering and event handlingsrc/__tests__/events-update.test.ts - Event handler prop removalsrc/__tests__/list-reordering.test.ts - List and array handling edge casesFAQs
A clever and mischievous framework that makes your code dance with joy! As cunning as it is lightweight - because being smart doesn't mean being heavy!
We found that farinel 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.