
Security News
Federal Audit Finds NIST Wasted Funds With No Plan to Clear NVD Backlog
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.
@ecopages/jsx
Advanced tools
Radiant JSX is the JSX authoring layer for the Radiant ecosystem.
Use @ecopages/jsx when you want TSX syntax, intrinsic element typing, SSR serialization, hydration support, and direct DOM mounting. Keep using @ecopages/radiant when you need component classes, decorators, lifecycle, and reactive host state.
The shortest accurate mental model is:
The package surface is intentionally split by use case:
@ecopages/jsx is the full authoring surface. Use it when a file needs both JSX primitives and rendering helpers.@ecopages/jsx/client is the browser mounting surface. Use it when code should stay client-only.@ecopages/jsx/server is the SSR surface. Use it when code should stay server-only.@ecopages/jsx/jsx-runtime and @ecopages/jsx/jsx-dev-runtime are the automatic runtime entry points used by TypeScript and bundlers.@ecopages/jsx provides:
@ecopages/jsx/jsx-runtime and @ecopages/jsx/jsx-dev-runtimeon:*on-native:*prop:*data, aria, class, className, classes, and style normalizationcreateRoot(...)renderToString(...)get() and subscribe(...)createSubscribableJsxValue(...)Use @ecopages/jsx/server for server-only helpers such as renderToString(...), server custom-element render hooks, and SSR hydration binding scope helpers for framework adapters.
@ecopages/jsx does not provide component state, hooks, decorators, or a standalone component model. Those stay in @ecopages/radiant.
Signal-like values that expose get() and subscribe(...) can be passed directly as child bindings. createSubscribableJsxValue(...) is the adapter case: use it when an external store, emitter, or subscription source does not already speak the signal-like shape but still needs fine-grained child updates. The package consumes reactive values here; it does not define its own full signal system.
@ecopages/jsx owns the rendering primitives for JSX authoring and output.
Choose the narrowest entrypoint that matches the environment you are writing for.
| Entrypoint | Use it for | Includes |
|---|---|---|
@ecopages/jsx | shared library code, examples, and app code that wants one import path | JSX primitives, DOM mounting, hydration, SSR rendering, advanced SSR hooks, and shared types |
@ecopages/jsx/client | browser entry files and DOM-only helpers | JSX primitives, DOM mounting, hydration, and shared renderable types |
@ecopages/jsx/server | SSR adapters, Node or Bun HTML rendering, and custom server-element hooks | renderToString(...), custom-element render hooks, and hydration binding scope helpers |
@ecopages/jsx/jsx-runtime | automatic JSX runtime wiring | jsx, jsxs, Fragment, and runtime JSX types |
@ecopages/jsx/jsx-dev-runtime | dev-mode automatic JSX runtime wiring | development runtime alias for toolchains that emit jsxDEV(...) |
If a module is environment-specific, prefer the subpath import even when the root barrel would also work. That keeps browser-only and server-only code obvious at the import site.
renderToString(...) emits hydration markers by consuming one hydrate binding sequence. Most app code should let the renderer own that sequence implicitly.
Framework adapters are the exception. If an integration composes one page from multiple sibling renderToString(...) calls, those calls must share one binding namespace so the client sees one continuous marker stream for that hydration root.
Use createServerHydrationBindingState() and withServerHydrationBindingState(...) from @ecopages/jsx/server when you need that explicit control:
At nested custom-element boundaries, the parent hydration root still owns the custom-element host itself. The nested root starts at the host's rendered internal subtree, not at the host tag.
If you only call renderToString(...) once for a root, you do not need these helpers.
Install both packages:
npm install @ecopages/radiant @ecopages/jsx
Minimum TypeScript setup:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@ecopages/jsx"
}
}
Or enable it per file:
/** @jsxImportSource @ecopages/jsx */
The most common path is a RadiantElement that returns JSX from render().
/** @jsxImportSource @ecopages/jsx */
import { RadiantElement, customElement, prop } from '@ecopages/radiant';
const CounterButton = ({ label, onPress }: { label: string; onPress: (event: MouseEvent) => void }) => (
<button type="button" on:click={onPress} aria={{ label }}>
{label}
</button>
);
@customElement('radiant-counter')
export class RadiantCounter extends RadiantElement {
@prop({ type: Number, reflect: true, defaultValue: 0 }) count!: number;
private readonly increment = () => {
this.count += 1;
};
private readonly decrement = () => {
this.count -= 1;
};
override render() {
return (
<section class="counter" data={{ state: this.count > 0 ? 'active' : 'idle' }}>
<h2>Count: {this.count}</h2>
<div class="controls">
<CounterButton label="Decrement" onPress={this.decrement} />
<CounterButton label="Increment" onPress={this.increment} />
</div>
</section>
);
}
}
There is one authoring output and two rendering targets.
flowchart TD
View["JSX view"] --> Template["Template result"]
Template --> Client["createRoot(...).render(...) or hydrate(...)"]
Template --> Server["renderToString(...)"]
Server --> Html["HTML string"]
Html --> HydrateHtml["Optional hydration markers"]
HydrateHtml --> Client
This is the key simplification:
The public entrypoints stay small because the implementation is split by responsibility instead of bundling DOM and SSR behavior into one renderer file.
flowchart LR
Runtime["jsx-runtime"] --> Values["template results and renderable values"]
Values --> Client["dom-render"]
Values --> Server["server-render"]
Client --> DomInternals["dom-render/* reconciliation, hydration, namespaces, event delegation"]
Server --> Html["HTML string output"]
Read it like this:
jsx-runtime owns authoring output, child-slot semantics, attribute normalization, and runtime value wrappers.dom-render owns DOM mounting, reconciliation, and hydration.server-render owns HTML serialization of the same renderable values.dom-render/* holds the browser-only internals so the public surface can stay small.If you need app-level mounting outside a RadiantElement, use the DOM root helper.
/** @jsxImportSource @ecopages/jsx */
import { createRoot } from '@ecopages/jsx/client';
function DirectHandlers() {
function handleClick() {
console.log('Click');
}
const handleInput = (event: Event) => {
console.log((event.currentTarget as HTMLInputElement).value);
};
return (
<>
<button on:click={handleClick}>Log click</button>
<button on-native:click={handleClick}>Always native click</button>
<input on:input={handleInput} />
</>
);
}
const container = document.querySelector('#app');
if (container instanceof HTMLElement) {
const root = createRoot(container);
root.render(<DirectHandlers />);
}
Browser events are simpler than they first look. Something happens, the browser creates an Event object, and that event starts on the node where the interaction actually occurred. That original node is event.target.
From there, the event may travel through the DOM tree. Most UI events you care about, like click, input, and keydown, bubble upward. When a handler runs, event.currentTarget is the element whose listener is currently executing. That difference matters: target answers "where did this start?" and currentTarget answers "which listener is running right now?"
Event delegation is just using that travel on purpose. Instead of attaching one listener to every matching element, you attach fewer listeners higher in the tree and react based on where the event started. That is often a good trade for repeated interactive UI such as lists, tables, menus, and boards. The catch is that delegation only works for events that bubble, and it changes where the real DOM listener lives.
Radiant keeps those choices explicit instead of hiding them behind a synthetic event system.
| Use this | When you want | Runtime shape |
|---|---|---|
on:* | The normal event API | Radiant delegates a fixed allowlist of bubbling events and falls back to direct listeners otherwise |
on-native:* | Exact element-level browser semantics even for delegated events | Radiant calls addEventListener(...) on the element itself |
on:* is the default. For the delegated allowlist, Radiant attaches one listener per event type on the render root and dispatches to matched elements. For everything else, on:* attaches directly on the element. In both cases the handler receives the native browser event object.
The delegated allowlist is fixed and documented: beforeinput, click, contextmenu, dblclick, focusin, focusout, input, keydown, keyup, mousedown, mouseout, mouseover, mouseup, pointerdown, pointerout, pointerover, pointerup, touchend, touchmove, and touchstart.
Use on-native:* when exact element-level attachment semantics matter, such as when an ancestor stops bubbling before the root listener runs or when you want to opt out of delegation for a supported bubbling event. Events outside the allowlist, such as focus, blur, scroll, load, invalid, or dragstart, already attach directly when authored with on:*.
The main non-obvious part of the package is that it already supports child-range subscriptions. A parent tree does not need to rerender when one bound child value changes.
sequenceDiagram
participant App
participant Root
participant Renderer
participant Binding as Subscribable child
participant Dom as DOM range
App->>Root: render(view)
Root->>Renderer: mount template
Renderer->>Binding: subscribe(notify)
Binding-->>Renderer: getValue()
Renderer->>Dom: mount initial child content
Binding-->>Renderer: notify(nextValue)
Renderer->>Dom: patch owned child range only
Pass a signal-like child value directly when it already exposes get() and subscribe(...). Use createSubscribableJsxValue(...) when a child value has its own update source but does not already match that shape. Think of it as a small renderer adapter, not a separate state model.
/** @jsxImportSource @ecopages/jsx */
import { createRoot, createSubscribableJsxValue } from '@ecopages/jsx/client';
let count = 0;
const subscribers = new Set<(value: number) => void>();
const boundCount = createSubscribableJsxValue({
getValue: () => count,
subscribe: (notify) => {
subscribers.add(notify);
return () => {
subscribers.delete(notify);
};
},
});
const root = createRoot(document.querySelector('#app') as HTMLElement);
root.render(<p>Count: {boundCount}</p>);
count += 1;
for (const subscriber of subscribers) {
subscriber(count);
}
That contract is intentionally small. The package does not impose a single state container, computed graph, or scheduler. It just gives the renderer a stable subscription surface for either signal-like values or explicit subscribable wrappers.
Most code should use normal JavaScript values for empty output and removal semantics.
Use this rule of thumb:
null, undefined, and false render no child contentnull and undefined remove normal attributesfalse removes boolean attributes such as hidden or disablednull removes delegated and native event handlers by omitting the next listenerundefined clears deferred property bindings by writing undefinedFor child content:
/** @jsxImportSource @ecopages/jsx */
const nextLabel = shouldShowLabel ? 'Ready' : null;
return <p>{nextLabel}</p>;
For attributes and bindings:
/** @jsxImportSource @ecopages/jsx */
return (
<button
class={shouldResetClass ? null : 'toolbar-action'}
hidden={isVisible ? false : true}
on:click={isInteractive ? handleClick : null}
prop:payload={hasPayload ? payload : undefined}
/>
);
Important consequence: removing a binding by switching to null, undefined, or false follows normal template update semantics. If that changes the template shape, the renderer may replace the affected DOM node instead of preserving the previously committed instance.
The runtime warnings are intentionally defensive around hydration markers and renderer-owned DOM anchors because those failures are otherwise silent and hard to debug.
They are already off in production by default. In development, you can force them on or off globally:
import { setDevWarningsEnabled } from '@ecopages/jsx/jsx-dev-runtime';
setDevWarningsEnabled(false);
setDevWarningsEnabled(true);
setDevWarningsEnabled(undefined);
Pass undefined to return to the default behavior, which is "on in development, off in production".
renderToString(...) now has two explicit output modes:
mode: 'plain' emits plain HTML without hydration markersmode: 'hydrate' emits hydratable HTML with binding markers/** @jsxImportSource @ecopages/jsx */
import { renderToString } from '@ecopages/jsx/server';
const view = (
<button class="action" hidden={false} aria={{ label: 'Ship order' }}>
Ship
</button>
);
const html = renderToString(view, { mode: 'plain' });
const hydratedHtml = renderToString(view, { mode: 'hydrate' });
Hydrated SSR adds binding markers so hydrate(...) can attach listeners and dynamic parts without rebuilding the existing DOM tree.
Within the JSX SSR pipeline, any registered intrinsic tag containing - is treated as a custom-element candidate.
There are three practical outcomes during SSR:
renderHostToString(...) and can be serialized directly by @ecopages/jsx/serverRadiantElement, are adapted through the server custom-element render hook so JSX does not need framework-specific branches in its core rendererThe generic contract is intentionally small:
import type { RenderToStringOptions } from '@ecopages/jsx/server';
type ServerRenderableCustomElement = {
renderHostToString(options?: RenderToStringOptions): string;
};
That means JSX itself does not distinguish between "Radiant custom element" and "standard custom element" as first-class renderer categories. Instead, it understands one generic SSR-capable shape plus one hook seam for frameworks that need richer host rendering.
Use withServerCustomElementRenderHook(...) from @ecopages/jsx/server when a framework wants to intercept a registered custom-element instance and replace the default generic SSR path with framework-aware host rendering.
const view = (
<section>
<h2>Status</h2>
<svg viewBox="0 0 24 24" aria={{ hidden: true }}>
<circle cx="12" cy="12" r="10" />
</svg>
</section>
);
When jsxImportSource points at @ecopages/jsx, custom elements should augment the runtime module instead of the global JSX namespace.
import type { JsxCustomElementAttributes } from '@ecopages/jsx';
type UserCardProps = {
name: string;
isAdmin: boolean;
};
declare module '@ecopages/jsx/jsx-runtime' {
interface JsxCustomIntrinsicElements {
'user-card': JsxCustomElementAttributes<HTMLElement, UserCardProps>;
}
}
Custom elements default to property bindings for unprefixed names, with a small attribute-default set for obvious HTML semantics: id, class, style, title, role, slot, part, tabindex, hidden, lang, dir, plus expanded data-* and aria-*. Use attr:* when a non-default name must serialize to markup, and prop:* when you want to override the default explicitly.
Typing follows the same ergonomic split. Put public unprefixed JSX props on Props, and use the element instance type for explicit prop:* bindings. Props keeps its own required and optional fields, so required public JSX props stay required. That means items={rows} is typed from Props, while prop:api={gridApi} is typed from the custom element class property.
<user-grid id="people" class="panel" items={rows} selection={currentRow} attr:status="ready" prop:api={gridApi} />
import type { JsxCustomElementAttributes } from '@ecopages/jsx';
type UserGridRow = {
id: string;
};
type UserGridProps = {
items: UserGridRow[];
selection?: UserGridRow;
};
class UserGridElement extends HTMLElement {
api?: UserGridApi;
}
type UserGridApi = {
focusRow(id: string): void;
};
declare module '@ecopages/jsx/jsx-runtime' {
interface JsxCustomIntrinsicElements {
'user-grid': JsxCustomElementAttributes<UserGridElement, UserGridProps>;
}
}
const rows: UserGridRow[] = [{ id: '1' }];
const currentRow = rows[0];
const gridApi: UserGridApi = {
focusRow: (_id) => undefined,
};
<user-grid items={rows} selection={currentRow} prop:api={gridApi} />;
type CardProps = {
title: string;
children?: import('@ecopages/jsx').JsxRenderable;
};
const Card = ({ title, children }: CardProps) => (
<>
<article class="card">
<h2>{title}</h2>
{children}
</article>
</>
);
<button on:click={this.handleClick}>Save</button>
<button on-native:click={this.handleNativeClick}>Save with native attachment</button>
on:* is the normal event API. Radiant automatically delegates compatible bubbling events for efficiency and attaches directly for everything else. on-native:* is the escape hatch when the handler must behave exactly like a browser listener attached on that element. Neither mode wraps the browser event in a React-style synthetic object.
<custom-editor prop:value={draft} prop:config={editorConfig} />
Use prop:* when the target must receive a real property value instead of a serialized attribute, or when you want to be explicit even though custom elements already default most unprefixed names to properties.
data, aria, class, and style<section
class={['panel', isActive && 'panel--active']}
classes={['surface', { interactive: true }]}
style={{ backgroundColor: 'white', fontSize: '14px' }}
data={{ tid: 'panel', state: 'ready' }}
aria={{ live: 'polite' }}
/>
jsx() and jsxs() return a template result object that contains:
jsx() is emitted when JSX produces one logical child value
jsxs() is emitted when JSX produces multiple sibling child values
static string segments
dynamic values
a stable marker used by the Radiant renderers to recognize the object shape
That distinction is not cosmetic. It preserves child-slot structure from the automatic JSX transform.
const single = <div>{items.map(renderItem)}</div>;
const siblings = (
<div>
<span>A</span>
<span>B</span>
</div>
);
The first case compiles to jsx(...) because the div has one logical child expression. Radiant keeps that iterable child grouped as one binding.
The second case compiles to jsxs(...) because the div has multiple sibling children. Radiant expands those siblings into separate positional template slots.
Why keep both instead of one export:
children should stay as one grouped value or be split into sibling slots.strings and values shape less predictable.That object is an internal contract between the JSX runtime and the Radiant renderers. It is not positioned as a generic third-party virtual DOM format.
@ecopages/jsx escapes normal text and attribute values by default.
If you already have final, trusted HTML and need to hand it to the runtime as markup, use unsafeHtml(...).
import { unsafeHtml } from '@ecopages/jsx';
const trustedSnippet = unsafeHtml('<strong>Trusted</strong>');
const view = <p>{trustedSnippet}</p>;
Important:
The runtime treats trusted markup as opaque HTML content. It is inserted as markup for DOM mounting and emitted as-is during SSR, but it does not become a live JSX template or a hydratable binding boundary.
Main exports:
FragmentunsafeHtmljsxjsxscreateRootrenderhydratehasHydrationMarkerscreateSubscribableJsxValueisKeyedJsxValueisSubscribableJsxValueKey types:
JsxComponentJsxFragmentJsxIntrinsicAttributesJsxNodeLikeJsxPrimitiveJsxPropsWithChildrenJsxRenderableJsxRootSubscribableJsxValueTemplateResultLikeThe automatic development runtime also exports jsxDEV from @ecopages/jsx/jsx-dev-runtime.
Server-only exports from @ecopages/jsx/server include:
renderToStringRenderToStringOptionswithServerCustomElementRenderHookisServerRenderHydrationActive@ecopages/jsx handles authoring and rendering primitives. @ecopages/radiant handles component lifecycle and host reactivity.renderToString(...) and createRoot(...) directly when you need lower-level control outside a Radiant host.Use @ecopages/jsx if you want to:
FAQs
Unknown package
We found that @ecopages/jsx 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
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.