v0.1.12 - a strict-subset TSX framework

No virtual DOM. No hooks. No surprises.

marisjs is a signals-based full-stack framework that compiles to plain JavaScript. A strict, machine-checked subset of TSX with one state primitive, server prerendering, API routes, and a 5 KB runtime. Errors die at build time, not on your users' screens.

marisjs - quick start
$ npm install marisjs @marisjs/runtime
$ npx marisjs init
wrote package.json (dev / build / serve scripts)
wrote .gitignore (excludes .env)
wrote .env.example (no real values)
$ mkdir -p src/pages
$ npm run dev
-> watching src for changes
-> http://localhost:3000
$ npm run build
validated 12 files, 0 errors
built 3 pages, 2 islands
wrote runtime -> runtime.mjs (5.4 KB)
BEARING 001°
5,450B
compiled runtime, zero-dependency JavaScript
BEARING 002°
0
runtime dependencies in your bundle
BEARING 003°
5.7MB
full toolchain - one npm package, native binary
BEARING 004°
1
state primitive: signal(). One way to hold state.
chart 02 · the compiler

Three decisions.Everything else follows.

marisjs removes the abstractions that cause runtime bugs, then machines-check what is left.

01

Compiled to plain JavaScript

Components compile to direct DOM construction and per-value bindings. Nothing to diff, nothing to reconcile, nothing to unload from the page.

02

One state primitive

signal() for state, computed() for derivation. No dependency arrays, no stale closures, no rules-of-hooks linting.

03

Fails at build time

Every rule is machine-checkable. The validator rejects illegal code with an error code and a fix hint - before it ever reaches a browser.

No virtual DOM

Direct DOM construction with per-value bindings. Nothing to diff, nothing to reconcile.

Islands, not apps

Server pages prerender to static HTML. Islands hydrate where interactivity actually exists.

A 5 KB runtime

Zero-dependency JavaScript embedded in your output at compile time. No framework shipped.

Pure TSX

Every valid marisjs file is valid TypeScript. Your editor and type checker already speak the language.

One way to do it

One canonical form for lists, conditionals, state, and handlers. No competing patterns to argue about.

Full-stack by default

pages/ maps to routes, api/ to HTTP handlers. await data() fetches at build time, env() keeps secrets server-side.

chart 03 · the comparison

Same feature.Half the ceremony.

A counter with shared state, as written in a classic hook-based framework versus marisjs.

react - 30 lines of rules to remember
// App.tsx - with hooks
import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    document.title = 'Count: ' + count;
  }, [count]);

  useEffect(() => {
    setCount((c) => c + step);
  }, [step]);

  const inc = () => setCount((c) => c + step);
  const dec = () => setCount((c) => c - step);

  return (
    <div className="counter">
      <p>Count: {count}</p>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
      <select value={step} onChange={(e) => setStep(Number(e.target.value))}>
        <option value={1}>1</option>
        <option value={2}>2</option>
      </select>
    </div>
  );
}
marisjs - the whole component
// src/islands/Counter.tsx - marisjs
// @runsOn client
import { signal, computed } from '@marisjs/runtime';

type Props = {};

export function Counter(props: Props) {
  const count = signal(0);
  const step = signal(1);
  const double = computed(() => count.value * 2);

  function inc() { count.set(count.value + step.value); }
  function dec() { count.set(count.value - step.value); }
  function setStep(e: any) { step.set(Number(e.target.value)); }

  return (
    <div class="counter">
      <p>Count: {count.value} (double: {double.value})</p>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
      <input type="number" value={step.value} onInput={setStep} />
    </div>
  );
}
chart 04 · the rules

Boring on purpose

Frameworks get dangerous when they let you write code in ten different ways and guess what each one does. marisjs is built on five rules, and every single one is enforced by the compiler - not by a style guide:

  • 01One canonical way to do each thing. No competing patterns.
  • 02No hidden execution order. Nothing runs unless it is written down.
  • 03No action at a distance. Everything a file touches is visible in that file.
  • 04Fail at validation time, not runtime.
  • 05Every rule is machine-checkable - or it is not a rule.

Invalid code is rejected with a specific error code and a fix hint. Your build is the compiler's verdict on your code, and it is a harsh judge.

marisjs validate ./src/components/Cart.tsx build failed
checking ./src/components/Cart.tsx
error[PROP_UNWRAPPED_SIGNAL]Cart.tsx:14:11
a bare signal was passed as a component prop
fix: · read .value where used
1 error - build failed

Ship something small

Two commands, a pages/ folder, and one state primitive. Install marisjs and build your first island today.

$ npm install marisjs
chart 05 · the live run

Compiled to plain JavaScript.Running on this page.

The dashboard below is a marisjs client island - the same code you see here, compiled to direct DOM calls. No framework is loaded to run it. Drag the sliders.

// @runsOn client
import { signal, computed } from '@marisjs/runtime';

type Props = {};

export function Playground(props: Props) {
  const energy = signal(72);
  const focus = signal(55);
  const depth = signal(88);
  const clicks = signal(0);

  const maxVal = computed(() => Math.max(energy.value, focus.value, depth.value, 1));
  const total = computed(() => energy.value + focus.value + depth.value);
  const avg = computed(() => Math.round(total.value / 3));
  const grade = computed(() => {
    const v = avg.value;
    return v >= 80 ? 'smooth' : v >= 55 ? 'steady' : 'rough';
  });

  const items = [
    { id: 'energy', label: 'energy', sig: energy },
    { id: 'focus', label: 'focus', sig: focus },
    { id: 'depth', label: 'depth', sig: depth },
  ];

  function setEnergy(e: any) { energy.set(Number(e.target.value)); }
  function setFocus(e: any) { focus.set(Number(e.target.value)); }
  function setDepth(e: any) { depth.set(Number(e.target.value)); }
  function bump() { clicks.set(clicks.value + 1); }
  function resetAll() {
    energy.set(72);
    focus.set(55);
    depth.set(88);
    clicks.set(0);
  }

  return (
    <div class="play-card">
      <div class="play-card-head">
        <span class="play-card-title">playground.tsx</span>
        <span class="play-live">island hydrated</span>
      </div>
      <div class="play-body">
        <div>
          <div class="slider-row">
            <span class="slider-label">energy</span>
            <input type="range" min="0" max="100" value={energy.value} onInput={setEnergy}></input>
            <span class="slider-val">{energy.value}</span>
          </div>
          <div class="slider-row">
            <span class="slider-label">focus</span>
            <input type="range" min="0" max="100" value={focus.value} onInput={setFocus}></input>
            <span class="slider-val">{focus.value}</span>
          </div>
          <div class="slider-row">
            <span class="slider-label">depth</span>
            <input type="range" min="0" max="100" value={depth.value} onInput={setDepth}></input>
            <span class="slider-val">{depth.value}</span>
          </div>
        </div>
        <div class="bars">
          <For each={items} key={(item) => item.id}>
            {(item) => (
              <div class="bar-track">
                <div class="bar-fill" style={'width:' + Math.round((item.sig.value / maxVal.value) * 100) + '%'}>
                  {item.label}
                </div>
              </div>
            )}
          </For>
        </div>
        <div class="play-totals">
          <div class="total-chip">
            <div class="t-label">total</div>
            <div class="t-value"><em>{total.value}</em></div>
          </div>
          <div class="total-chip">
            <div class="t-label">average</div>
            <div class="t-value">{avg.value}</div>
          </div>
          <div class="total-chip">
            <div class="t-label">condition</div>
            <div class="t-value">{grade.value}</div>
          </div>
        </div>
        <div style="display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap">
          <div style="display:flex;gap:10px;align-items:center">
            <button class="counter-btn" style="width:auto;padding:0 18px;font-size:15px" onClick={bump}>+1</button>
            <span class="slider-label">clicks: <span class="slider-val">{clicks.value}</span></span>
          </div>
          <button class="counter-reset" onClick={resetAll}>reset</button>
        </div>
      </div>
    </div>
  );
}

The card above is hydrated from exactly this source: three signals, two computed values, one loop, and a handful of event handlers. Open DevTools and inspect the bundle - it contains the island and nothing else.