
Research
Malicious npm Packages Impersonate Flashbots SDKs, Targeting Ethereum Wallet Credentials
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
DOM tree microhydration
Sprae is open & minimalistic progressive enhancement framework with preact-signals reactivity.
Useful for SPAs, PWAs, lightweight UI or nextjs / SSR (see JSX).
An alternative to alpine, petite-vue, lucia etc (see why).
<div id="container" :if="user">
Hello <span :text="user.name">there</span>.
</div>
<script type="module">
import sprae from './sprae.js' // https://unpkg.com/sprae/dist/sprae.min.js
// init
const container = document.querySelector('#container');
const state = sprae(container, { user: { name: 'friend' } })
// update
state.user.name = 'love'
</script>
Sprae evaluates :
-directives and evaporates them, returning reactive state for updates.
sprae.umd
enables sprae via CDN, CJS, AMD etc.
<script src="https://unpkg.com/sprae/dist/sprae.umd"></script>
<script>
window.sprae; // global standalone
</script>
sprae.auto
autoinits sprae on document body.
<!-- Optional attr `prefix` (by default ':'). -->
<script src="https://unpkg.com/sprae/dist/sprae.auto" prefix="js-"></script>
:if="condition"
, :else
Control flow of elements.
<span :if="foo">foo</span>
<span :else :if="bar">bar</span>
<span :else>baz</span>
<!-- fragment -->
<template :if="foo">foo <span>bar</span> baz</template>
:each="item, index? in items"
Multiply element.
<ul><li :each="item in items" :text="item" /></ul>
<!-- cases -->
<li :each="item, idx in array" />
<li :each="value, key in object" />
<li :each="count, idx in number" />
<!-- fragment -->
<template :each="item in items">
<dt :text="item.term"/>
<dd :text="item.definition"/>
</template>
:text="value"
Set text content of an element.
Welcome, <span :text="user.name">Guest</span>.
<!-- fragment -->
Welcome, <template :text="user.name"><template>.
:class="value"
Set class value.
<div :class="foo"></div>
<!-- appends to static class -->
<div class="bar" :class="baz"></div>
<!-- array/object, a-la clsx -->
<div :class="['foo', bar && 'bar', { baz }]"></div>
:style="value"
Set style value.
<span style="'display: inline-block'"></span>
<!-- extends static style -->
<div style="foo: bar" :style="'bar-baz: qux'">
<!-- object -->
<div :style="{barBaz: 'qux'}"></div>
<!-- set CSS variable -->
<div :style="{'--bar-baz': qux}"></div>
:value="value"
Set value to/from an input, textarea or select (like alpinejs x-model
).
<input :value="value" />
<textarea :value="value" />
<!-- selects right option & handles selected attr -->
<select :value="selected">
<option :each="i in 5" :value="i" :text="i"></option>
</select>
<!-- handles checked attr -->
<input type="checkbox" :value="item.done" />
:<prop>="value"
, :="values"
Set any attribute(s).
<label :for="name" :text="name" />
<!-- multiple attributes -->
<input :id:name="name" />
<!-- spread attributes -->
<input :="{ id: name, name, type: 'text', value }" />
:with="values"
Define values for a subtree.
<x :with="{ foo: 'bar' }">
<y :with="{ baz: 'qux' }" :text="foo + baz"></y>
</x>
:fx="code"
Run effect, not changing any attribute.
<div :fx="a.value ? foo() : bar()" />
<!-- cleanup function -->
<div :fx="id = setInterval(tick, 1000), () => clearInterval(id)" />
:ref="name"
, :ref="el => (...)"
Expose element in state with name
or get reference to element.
<div :ref="card" :fx="handle(card)"></div>
<!-- local reference -->
<li :each="item in items" :ref="li">
<input :onfocus..onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))"/>
</li>
<!-- set innerHTML -->
<div :ref="el => el.innerHTML = '...'"></div>
<!-- mount / unmount -->
<textarea :ref="el => (/* onmount */, () => (/* onunmount */))" :if="show"></textarea>
:on<event>="handler"
, :on<in>..on<out>="handler"
Attach event(s) listener with optional modifiers.
<input type="checkbox" :onchange="e => isChecked = e.target.value">
<!-- multiple events -->
<input :value="text" :oninput:onchange="e => text = e.target.value">
<!-- sequence of events -->
<button :onfocus..onblur="e => (handleFocus(), e => handleBlur())">
<!-- modifiers -->
<button :onclick.throttle-500="handler">Not too often</button>
.once
, .passive
, .capture
– listener options..prevent
, .stop
(.immediate
) – prevent default or stop (immediate) propagation..window
, .document
, .parent
, .outside
, .self
– specify event target..throttle-<ms>
, .debounce-<ms>
– defer function call with one of the methods..<key>
– filtered by event.key
:
.ctrl
, .shift
, .alt
, .meta
, .enter
, .esc
, .tab
, .space
– direct key.delete
– delete or backspace.arrow
– up, right, down or left arrow.digit
– 0-9.letter
– A-Z, a-z or any unicode letter.char
– any non-space character.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>
– key combinations, eg. .ctrl-alt-delete
or .meta-x
..*
– any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).:data="values"
Set data-*
attributes. CamelCase is converted to dash-case.
<input :data="{foo: 1, barBaz: true}" />
<!-- <input data-foo="1" data-bar-baz /> -->
:aria="values"
Set aria-*
attributes. Boolean values are stringified.
<input role="combobox" :aria="{
controls: 'joketypes',
autocomplete: 'list',
expanded: false,
activeOption: 'item1',
activedescendant: ''
}" />
<!--
<input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
-->
Sprae uses preact-flavored signals for reactivity and can take signal values as inputs.
Signals can be switched to an alternative preact/compatible implementation:
import sprae from 'sprae';
import { signal, computed, effect, batch, untracked } from 'sprae/signal';
import * as signals from '@preact/signals-core';
// switch sprae signals to @preact/signals-core
sprae.use(signals);
// use signal as state value
const name = signal('Kitty')
sprae(el, { name });
// update state
name.value = 'Dolly';
Provider | Size | Feature |
---|---|---|
ulive | 350b | Minimal implementation, basic performance, good for small states. |
@webreflection/signal | 531b | Class-based, better performance, good for small-medium states. |
usignal | 850b | Class-based with optimizations, good for medium states. |
@preact/signals-core | 1.47kb | Best performance, good for any states, industry standard. |
signal-polyfill | 2.5kb | Proposal signals. Use via adapter. |
Expressions use new Function as default evaluator, which is fast & compact way, but violates "unsafe-eval" CSP. To make eval stricter & safer, as well as sandbox expressions, an alternative evaluator can be used, eg. justin:
import sprae from 'sprae'
import justin from 'subscript/justin'
sprae.use({compile: justin}) // set up justin as default compiler
Justin is minimal JS subset that avoids "unsafe-eval" CSP and provides sandboxing.
++ -- ! - + * / % ** && || ??
= < <= > >= == != === !==
<< >> >>> & ^ | ~ ?: . ?. [] ()=>{} in
= += -= *= /= %= **= &&= ||= ??= ... ,
[] {} "" ''
1 2.34 -5e6 0x7a
true false null undefined NaN
Sprae can be tailored to project needs via sprae/core
:
// sprae.custom.js
import sprae, { dir, parse } from 'sprae/core'
import * as signals from '@preact/signals'
import compile from 'subscript'
// standard directives
import 'sprae/directive/default.js'
import 'sprae/directive/if.js'
import 'sprae/directive/text.js'
// custom directive :id="expression"
dir('id', (el, state, expr) => {
// ...init
return value => el.id = value // update
})
sprae.use({
// configure signals
...signals,
// configure compiler
compile,
// custom prefix, default is `:`
prefix: 'js-'
})
Sprae works with JSX via custom prefix.
Case: keep nextjs server components intact by offloading dynamic UI logic (active nav, tabs etc) to sprae instead of client components:
// app/page.jsx - server component
export default function Page() {
return <>
<nav id="nav">
<a href="/" js-class="location.pathname === '/' && 'active'">Home</a>
<a href="/about" js-class="location.pathname === '/about' && 'active'">About</a>
</nav>
...
</>
}
// layout.jsx
import Script from 'next/script'
export default function Layout({ children }) {
return <>
{children}
<Script src="https://unpkg.com/sprae" prefix="js-" />
</>
}
<style>[\:each],[\:if],[\:else] {visibility: hidden}</style>
.<li :each="el in els" :text="el.name"></li>
is not the same as <li :text="el.name" :each="el in els"></li>
.<a :text="item" />
will cause error. Valid self-closing tags are: li
, p
, dt
, dd
, option
, tr
, td
, th
, input
, img
, br
._
are untracked: let state = sprae(el, {_x:2}); state._x++; // no effect
.element[Symbol.dispose]()
.sprae(el, { x:1, get x2(){ return this.x * 2} })
.this
is not used, to get current element use :ref
.event
is not used, :on*
attributes expect a function with event argument :onevt="event => handle()"
, see #46.key
is not used, :each
uses direct list mapping instead of DOM diffing.await
is not supported in attributes, it’s a strong indicator you need to put these methods into state.:ref
comes after :if
for mount/unmount events <div :if="cond" :ref="(init(), ()=>dispose())"></div>
.Modern frontend stack is complex and obese, like overprocessed non-organic food. There are healthy alternatives:
:
, x-
, {}
, @
, $
), encapsulated and not care about size/performance as much.Sprae holds open & minimalistic philosophy:
:
.FAQs
DOM microhydration
The npm package sprae receives a total of 70 weekly downloads. As such, sprae popularity was classified as not popular.
We found that sprae demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers 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.
Research
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.