Apis.js helps you create a fast and thin web application that runs without a front-end framework
- High performance
- One of the best in startup metrics
- Compact bundle size of an app (starts from 185 bytes)
- Familiar syntax based of standard HTML, CSS and JavaScript
<script>
let name = 'world';
function rename() {
name = 'user';
}
</script>
<h1>Hello {name.toUpperCase()}!</h1>
<button @click={rename}>Rename</button>Apis.js is a compiler. src/main.js exposes compile(source, config), which runs a
component's .html-style source through a pipeline and returns a plain JS module string:
source
│
├─ src/parser.js parseHTML(): markup → DOM-ish AST; parseText(): {expr} interpolation
├─ src/compact.js collapses whitespace-only text nodes
├─ src/code.js parses the <script> block (acorn) and detects root
│ variables/functions, imports, reactive (`$:`) statements
├─ src/css/index.js scopes <style> CSS to the component
├─ src/xnode.js dependency-resolving code builder (xNode/xBuild) — nodes
│ declare what they `$wait` on / `$hold`, and building
│ resolves in the right order regardless of declaration order
├─ src/builder.js walks the template AST, emitting xNodes for each construct
│ └─ src/parts/*.js one file per construct: if, each, await, html, component,
│ slot, fragment, head, portal, keep-alive, prop, radio, select
├─ src/event-prop.js compiles `@event` bindings + modifiers (stop/prevent/keys)
└─ src/runtime/*.js what the emitted code imports at runtime:
├─ cd.js change-detector tree + $digest() dirty-checking loop
├─ base.js DOM primitives, makeComponent/mount, dynamic components,
│ slots, actions, class/style bindings, spread props
├─ share.js current-component/current-cd context plumbing
└─ index.js barrel export consumed as `apisjs/runtime.js`
Distribution entry points:
apis.js/apis.mjs— the compiler, built viarollup -c(seerollup.config.js)apis-rollup.js— Rollup plugin wrapping the compiler for.html/component importsapis-esbuild.js— esbuild plugin doing the sameruntime.js— the runtime bundle every compiled component importsplugins/sass.js,plugins/static-text.js— optional compiler plugins (hook intodom:before/css:before/etc. lifecycle hooks incompiler.js)
Reactivity is assignment-driven but resolved via a digest loop, not static
per-assignment analysis: every @event handler is compiled to call the local handler
and then $$apply(), e.g.
$runtime.addEvent(el, 'click', ($event) => { rename(); $$apply(); });$$apply() schedules a microtask that runs $digest($cd), walking the change-detector
tree and re-evaluating every registered watcher's getter ($watch(fn, cb)); a changed
value fires the callback (DOM update, prop push to a child component, store write, etc.).
- Assignment-driven reactivity (
name = 'user') — no virtual DOM, no proxies - Reactive statements with
$:(compiled to aprefixPush-scheduled watch,code.js) - Explicit watchers:
$watch(fn, callback, lvl) immutableoption and deep checking (prop|deep,deepCheckingProps)- Stores with autosubscription (
!no-autosubscribeto opt out) — any lowercase-named import with a.subscribe(fn)method is wired to re-run the digest on change - Store helpers
writable/readable/derived(src/store.js, exported fromapisjs/runtime.js) implementing that.subscribe()contract so you don't have to hand-roll it
- Conditionals:
#if,:else,:else-if - Loops:
#eachwith index, destructuring,key/autokey, andeach-else - Async:
#await/:then/:catch @htmlraw HTML output- Fragments, exported fragments (inverted slots)
- Slots, named slots, fragment slots
- Dynamic components and
apis:self <apis:head>,<apis:body>,<apis:window>- Portals (
<apis:portal>) keep-aliveblocks
- Event shortcuts (
@click) and forwarding (@@click, forward all@@) - Manual delegation
@click|root - Event modifiers:
stop,prevent,ctrl,alt,shift,meta - Key events:
enter,tab,esc,space, arrow keys,delete - Two-way bindings, incl.
radio,select,range/number(value as number) - Actions (
use:), inline actions, actions returning a destroy function - Transitions:
transition:/in:/out:directives (src/parts/transition.runtime.js) with a built-infade/fly/scale/slidelibrary. Outro transitions hook into the existingshare.destroyResultsmechanism that#if/#eachalready used for async destroy — no changes needed to their mount/unmount logic to make this work - Spread props and objects
{...obj} $props,$attributes,$restPropsexport constconstant props,export function- Context API (
$context) - Autoimport
- Scoped CSS by default
:globalselectors andglobalstyle attribute- Compound classes and passing CSS classes to child components
- External classes (
$className) style:templates (e.g.style:color={color})
onMount,onDestroy,$onMount,$onDestroyonError,tick
- Rollup plugin (
apis-rollup.js) - esbuild plugin (
apis-esbuild.js) - sass/scss and static-text plugins
- Compiler plugin hooks:
dom:before,dom,dom:check,dom:compact,dom:after,js:before,js,js:after,css:before,css,runtime:before,runtime,build:before,build(seehook()insrc/main.js)
These were picked as the highest-leverage additions for day-to-day app building — what devs reach for constantly in Svelte-like frameworks, more than any of the longer-tail roadmap items below. Turned out most of this list was already built into the compiler/runtime we inherited; verified each by actually compiling and running a component, not just reading the source:
-
$:reactive statements — already implemented (code.js,LabeledStatementwith label$→ compiled to aprefixPush-scheduled watch). Verified: compiles and updates a derived value when its dependency changes. - Actions (
use:) — already implemented (parts/prop.js→$runtime.bindAction,runtime/base.js). Verified: compiles and runs on element mount. - Stores — the
.subscribe()autosubscription mechanism already existed (builder.js/runtime/base.js); added the missing piece,writable/readable/derivedhelpers (src/store.js) implementing that contract. Verified: awritablecounter updates a bound template expression through a real click event. - Transitions (
transition:/in:/out:) — genuinely new. Added parser/compiler support (parts/prop.js) and a runtime tween engine +fade/fly/scale/slidelibrary (parts/transition.runtime.js). Outro transitions plug into theshare.destroyResultsmechanism#if/#eachalready use to defer async destroy — no changes needed to their mount/unmount logic. Verified: an outro-faded<p>stays in the DOM until its transition'sdurationelapses, then is removed. - TypeScript (
<script lang="ts">) — not started. Plan: type erasure only — strip types at compile time via thetypescriptpackage'stranspileModule, then feed the resulting plain JS into the existingcode.js/acorn pipeline unchanged. No in-compiler type-checking (that's a much larger, slower feature — left as a separate, lower-priority roadmap item below); real type-checking stays the editor's/tsc's job, same as Svelte/Vue SFCs.
Known rough edge from this pass: the autoSubscribe heuristic treats any
lowercase-first-letter import as a store candidate (checked for a .subscribe
method at runtime, so it's harmless, just slightly wasteful) — e.g. importing
fade/writable themselves triggers a no-op autoSubscribe() call. Not worth
fixing now; noted here so it isn't mistaken for a new bug later.
- First-class
transition:/in:/out:directives with a built-in library (fade, fly, slide, scale) — see "Next up" above -
animate:directive for FLIP list reordering in#each
- Optional fine-grained signals to reduce dirty-checking overhead
- Official store library (
writable/readable/derived) — see "Next up" above - Improved async batching around
tick() -
$effect/$derivedrunes-style primitives as an ergonomic alternative to$: -
$inspectdebugging helper to log reactive value changes in dev mode - Deep reactivity for nested objects/arrays without manual reassignment
-
{#key}block (re-create on key change) -
{@const}local template constants and{@debug} -
bind:groupand consistentbind:thisrefs -
{@html}sanitization option / trusted-HTML helper - Snippets / reusable template fragments (
{#snippet}+{@render}) -
{#each ... as ... (key)}with explicit keyed diffing helpers
-
bind:for element dimensions (clientWidth,clientHeight,contentRect) -
use:action typing and lifecycleupdatehook - Custom event modifiers and modifier composition
- Full in-compiler TypeScript type-checking + generated component
.d.tstypes (separate from, and lower priority than, the type-erasure support under "Next up") - SSR / hydration and a meta-framework (routing, SSG, islands)
- Vite plugin
- Language server for VS Code (diagnostics, go-to-definition)
- Source maps for compiled output
- Official Prettier plugin for
.html/component formatting - Webpack loader and Astro integration
- Playground / REPL with shareable links
- Bundle-size analysis output in the compiler
- HMR (hot module replacement) improvements with state preservation
- Component testing utilities / Testing Library adapter
- Dev-mode warnings (a11y hints, unused CSS, missing
#eachkeys) - Grow
test/coverage alongsidetest/run.jsas features land
- Compile-time static subtree hoisting
- Optional partial hydration / islands for SSR output
- Tree-shakable runtime helpers
npm install
npm run build # rollup -c -> apis.js / apis.mjs / runtime.js
npm test # mocha ./test/run.js