Skip to main content

Using Exodra from a CDN

Every runtime package is published to npm and mirrored by the public CDNs (jsDelivr, unpkg, esm.sh), so you can use Exodra in a plain HTML file with no build step. There are two ways in, depending on whether you want ES modules or a classic <script>.

The browser-relevant packages are @exodra/core, @exodra/reactivity, @exodra/dom, @exodra/router, @exodra/forms. (@exodra/jsx is a compile-time helper — you don't need it if you write h() by hand; @exodra/ssr and @exodra/string are server-side.)

Approximate gzip sizes of the <script> globals: core 0.4 KB, reactivity 1.6 KB, dom 5 KB, router 4.8 KB, forms 0.25 KB.

Each package has an ESM endpoint on jsDelivr — append /+esm. jsDelivr rewrites the internal @exodra/* imports to pinned /+esm URLs, so a shared dependency (core, reactivity) resolves to one instance no matter how many packages you load — a single reactive runtime, no duplication.

<script type="module">
import { h, text } from 'https://cdn.jsdelivr.net/npm/@exodra/core@0.1/+esm';
import { mount } from 'https://cdn.jsdelivr.net/npm/@exodra/dom@0.1/+esm';
import { bindable, derive } from 'https://cdn.jsdelivr.net/npm/@exodra/reactivity@0.1/+esm';

const count = bindable(0);
mount(h('div', { static: { children: [
h('button', {
static: { children: text('+') },
handlers: { onClick: () => count.setValue(count.getValue() + 1) },
}),
h('span', { bindables: { textContent: derive(count, c => `Count: ${c}`) } }),
]}}), document.body);
</script>

For a bigger app, an import map lets you keep bare specifiers (and pins every version in one place):

<script type="importmap">
{ "imports": {
"@exodra/core": "https://cdn.jsdelivr.net/npm/@exodra/core@0.1/+esm",
"@exodra/reactivity": "https://cdn.jsdelivr.net/npm/@exodra/reactivity@0.1/+esm",
"@exodra/dom": "https://cdn.jsdelivr.net/npm/@exodra/dom@0.1/+esm",
"@exodra/router": "https://cdn.jsdelivr.net/npm/@exodra/router@0.1/+esm",
"@exodra/forms": "https://cdn.jsdelivr.net/npm/@exodra/forms@0.1/+esm"
}}
</script>
<script type="module">
import { mount } from '@exodra/dom';
import { Route, Routes } from '@exodra/router';
// ...
</script>

Classic <script> globals

For no-module pages, each package also ships a self-contained minified IIFE that attaches to a single window.Exodra namespace. jsDelivr serves it from the bare package URL (via the package's jsdelivr field):

<!-- load the shared runtime FIRST, then the pieces that use it -->
<script src="https://cdn.jsdelivr.net/npm/@exodra/core@0.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@exodra/reactivity@0.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@exodra/dom@0.1"></script>
<script>
const { h, text } = Exodra.core;
const { bindable, derive } = Exodra.reactivity;
const { mount } = Exodra.dom;

const count = bindable(0);
mount(h('div', { static: { children: [
h('button', {
static: { children: text('+') },
handlers: { onClick: () => count.setValue(count.getValue() + 1) },
}),
h('span', { bindables: { textContent: derive(count, c => `Count: ${c}`) } }),
]}}), document.body);
</script>

window.Exodra.dom and window.Exodra.router reference window.Exodra.core / window.Exodra.reactivity — they do not re-bundle the runtime, so the reactive instance stays shared. Load order matters: core and reactivity before dom / router / forms.

packageglobal
@exodra/coreExodra.coreh, text, defineComponent, createContextKey
@exodra/reactivityExodra.reactivitybindable, derive, list, persist
@exodra/domExodra.dommount, hydrate
@exodra/routerExodra.routerRoute, Routes, Outlet, Link, RouterProvider
@exodra/formsExodra.formsbindText, bindNumber, bindChecked, bindSelect

Versions

CDNs serve exactly what's published to npm. Pin a version (@exodra/core@0.1.2) for production so a future release can't shift under you; use a range (@0.1) to follow patch/minor. Whatever code is on the CDN is the last published version — to get a change onto the CDN, publish a new version to npm.