惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
Netflix TechBlog - Medium
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
V
V2EX
IT之家
IT之家
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
GbyAI
GbyAI
D
Docker
S
Secure Thoughts
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
云风的 BLOG
云风的 BLOG
博客园_首页
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Security Archives - TechRepublic
Security Archives - TechRepublic
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News | PayPal Newsroom
S
Security @ Cisco Blogs
I
InfoQ
Last Week in AI
Last Week in AI
SecWiki News
SecWiki News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
W
WeLiveSecurity
T
Troy Hunt's Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Attack and Defense Labs
Attack and Defense Labs
美团技术团队
T
The Blog of Author Tim Ferriss
Google DeepMind News
Google DeepMind News
Martin Fowler
Martin Fowler
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Scott Helme
Scott Helme
T
Tor Project blog
Know Your Adversary
Know Your Adversary
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
Recorded Future
Recorded Future
C
Cyber Attacks, Cyber Crime and Cyber Security
AI
AI
G
Google Developers Blog

Hacker News: Show HN

PurrrrrFocus: Pomodoro Timer App - App Store Workflow Engine — Multi-Step Orchestration for Bun RapidPhoto: Pro Photo Editor App - App Store GitHub - DheerG/swarms: Achieve extraordinary results with claude code across a variety of tasks SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads Show HN: VCoding – A 5 MB native Windows IDE with no dynamic dependencies Show HN: LLMs don't hallucinate because they're bad at math, it's the format GitHub - Agent-FM/agentfm-core: AgentFM is a peer-to-peer network that turns everyday computers into a decentralized AI supercomputer. AgentFM lets you run massive AI workloads directly across a global mesh of idle CPUs and GPUs. Show HN: Tracking Top US Science Olympiad Alumni over Last 25 Years GitHub - Potarix/agent-hub: One place to talk to all your agents Show HN: Runtime security for AI agents(injection,tool abuse, data exfiltration) GitHub - dubeyKartikay/lazyspotify: Terminal Spotify client for macOS and Linux GitHub - the-banana-tool/king-louie: Easy to use GUI Personal AI Assistant. Win/Linux/Mac. Show HN I made my vacation rental bookable by AI agents–no Airbnb, 0% commission GitHub - basteez/jsf-autoreload: maven plugin to enable hot reload on jsf projects uvm32/hosts/host-gdbstub at main · ringtailsoftware/uvm32 GitHub - labsai/EDDI: Config-driven engine that turns JSON into production-grade AI agents. Multi-agent orchestration, 12+ LLM providers, MCP/A2A protocols, RAG, persistent memory, and enterprise compliance (EU AI Act, GDPR, HIPAA). Built on Quarkus. GitHub - glitchnsec/fortyone-oss: AI Executive Assistant Platform Quickstart | Alien GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. GitHub - ocrbase-hq/ocrbase: 📄 PDF/IMG ->.MD/JSON Document OCR API for PaddleOCR and GLMOCR. Self-hostable. GitHub - impactjo/home-memory: MCP server that lets your AI assistant remember everything about your home. GitHub - Sets88/dbcls: DbCls is a powerful terminal database client that supports various databases GitHub - neptun2000/heor-agent-mcp GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh RollQuation: Math Puzzles - Apps on Google Play GitHub - dropbox/witchcraft Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis GitHub - opentalon/opentalon: OpenTalon is an open-source platform built from the ground up in Go as a robust alternative to OpenClaw LinkedIn™ 职位抓取工具 - Chrome 应用商店 GitHub - EdoardoBambini/Agent-Armor-Iaga: AI agents are getting tool access — shell, file system, databases, APIs, secrets. But **nobody is governing what they actually do with it**. Frameworks like LangChain, CrewAI, AutoGen, and Claude Code give agents the power to execute. Agent Armor gives you the power to control, audit, and approve every single action before it happens. HN Vibes — Week 15, Apr 7–13 2026 GitHub - chojs23/ec: Easy terminal-native 3-way git mergetool vim-like workflow GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - JakOb-dotcom/cloud-sandbox-security-analysis: Technical analysis and Proof of Concept (PoC) regarding environment variable exfiltration in containerized cloud sandboxes via side-channel data leaks. Springboards - Flint Alpha Show HN: A simpler coding agent harness GitHub - audiodude/sudomake-friends GitHub - 256thFission/mini-mythos: OSS clone of Anthropic’s Mythos harness to locate C/C++ memory vulnerabilities Show HN: OpenParallax: OS-level privilege separation for AI agent execution Hacker News Sorted - Chrome 应用商店 Show HN: How to Install Docker on Ubuntu 24.04 LTS: Complete 2026 Guide GitHub - himanshudongre/smriti GitHub - sverrirsig/claude-control: macOS desktop dashboard for monitoring and managing multiple Claude Code sessions GitHub - ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work. Chiral - Chrome 应用商店 Show HN: Two Claudes collaborating through shared memory on a $100 mini-PC GitHub - pmichaillat/latex-cv: Minimalist LaTeX template for academic CVs GitHub - oguzbilgic/posse: A web UI for Anthropic Managed Agents. GitHub - sshiraz/depsly: Dependency risk analysis tool for npm packages ABI Add safari/agent-harness — Safari browser automation via safari-mcp by achiya-automation · Pull Request #212 · HKUDS/CLI-Anything GitHub - Halfblood-Prince/trustcheck: Verify PyPI package attestations and improve Python supply-chain security GitHub - oguzbilgic/kern-ai: Agents that do the work and show it. GitHub - bruits/satteri: High-performance Markdown and MDX processing for the JavaScript ecosystem GitHub - tylergibbs1/feedstock: High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright GitHub - Grimm67123/grimmbot: The self-improving sandboxed and open-source AI agent. With persistent memory and scheduling. GitHub - whitevanillaskies/whitebloom: Local whiteboard that blooms. GitHub - hwdsl2/docker-whisper: Docker image for a self-hosted Whisper speech-to-text server with speaker diarization and OpenAI-compatible transcription and translation APIs. Powered by faster-whisper. Supports all Whisper models, NVIDIA GPU (CUDA) acceleration, JSON/SRT/VTT output, SSE streaming, offline mode, and multi-arch (amd64, arm64). GitHub - yisding/reviewwiggum GitHub - MarwanAlsoltany/serrors: Structured errors for Go: sentinel hierarchies, typed data, custom formatting, and slog integration. GitHub - soatok/age-php GitHub - Luthiraa/markitme GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits GitHub - tombedor/excalicharts GitHub - wh1le/excalidraw-edit: Open and edit .excalidraw files from the terminal. Offline, auto-saves to disk. MalExt Sentry - Malicious Extension Scanner - Chrome 应用商店 GitHub - syi0808/asciianimesvg: Generate animated ASCII art SVGs from text. CLI, Rust library, WASM, and web editor. GitHub - zaina-ml/ml_forge: A visual-based graph node editor for training computer vision models. GitHub - anakin87/llm-rl-environments-lil-course: 🌱 A little course on Reinforcement Learning Environments for evaluating and training Language Models GitHub - takaakit/superpowers-uml: Superpowers-UML modifies Superpowers to ensure a software development workflow in which AI agents design through UML modeling. AdriByte Studio - Sviluppo Web e Soluzioni Digitali GitHub - chouligi/angel-copilot: Your personalized Angel Investment Advisor Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 GitHub - agenteractai/lodmem: Level Of Detail Context Management for Agents GitHub - ostefani/subnetlens: A fast, concurrent network scanner with a TUI and plain-text CLI, built in Go. It discovers live hosts on your network, scans their open ports, resolves hostnames, and fingerprints operating systems—delivered. Cyber Pulse: Agentic Intel - Apps on Google Play Whisper API: Self-Hostable Speech to Text Transcription The Agent-Web Protocol Stack: A Research Thesis GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Show HN: Provepy – A Python decorator that proves your code using Lean and LLMs Show HN: Pardonned.com – A searchable database of US Pardons GitHub - patrickdappollonio/dux: Dux is a terminal UI that lets you run multiple AI coding agents side by side, each in its own git worktree, with full companion terminals, macros, commit generation, and a command palette that knows more tricks than you do. kMC Crystal Simulator Show HN: HyperFlow – A self-improving agent framework built on LangGraph GitHub - stef41/vibescore: 🎵 Grade your vibe-coded project. One command, instant letter grade across security, quality, dependencies, and testing. GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. imgur.com GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. GitHub - nowork-studio/toprank: Open-source Claude Code skills for SEO, SEM, Google Ads GitHub - tacomanator/sash: Lightweight macOS menu bar app for reliably cycling through windows of the current application. Appents | Social Media Management for Product-First Teams GitHub - pnhoang/youtube-spam-blocker: Automatically detects and hides spam messages in YouTube Live chat. Set rate limits, keyword filters, and block repeat offenders. GitHub - decisionnode/DecisionNode: CLI + Local MCP - A shared structured memory store across Claude Code, Cursor, Windsurf, Antigravity, and every MCP client. Semantically queryable. GitHub - AvaCodeSolutions/django-email-learning: An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components. The $100K Gap in Kubernetes Security Tooling Function Calling Harness: From 6.75% to 100%
GitHub - hbbio/nodiff: NoDiff: Minimalistic framework that lives alongside your app
hbbio · 2026-05-14 · via Hacker News: Show HN

A tiny TypeScript framework for rich-client apps that treats TSX as browser syntax. This repo (which was built with LLM assistance) is a work-in-progress!

In the LLM era, you do not need a fat web framework for every browser app, especially when your project grows. You often want a lean runtime that lives in the same monorepo as your app and evolves with it.

That shape is easier for people and coding agents to integrate, inspect, change, and reason about. Fewer hidden layers means fewer bugs, faster feedback, and less time spent reverse-engineering framework behavior.

NoDiff gives you JSX ergonomics, real DOM nodes, explicit store subscriptions, zod-checked data, localStorage caching, token auth, forms, and routing. It does this without React, axios, a virtual DOM, a scheduler, or a framework runtime that owns your app.

The whole idea is small enough to keep in your head:

const counter = createStore(() => ({ count: 0 }));

function App() {
  return (
    <button onClick={() => counter.setState((state) => ({ count: state.count + 1 }))}>
      Count: {text(counter, (state) => state.count)}
    </button>
  );
}

mount("#app", App);

TSX compiles through Vite into calls to @nodiffjs/core/jsx-runtime. Those calls create real DOM nodes (like Svelte). Stores decide when small regions update. Data enters the app through zod schemas. Cache entries carry expiry. Auth is just a bearer header helper over an app-owned token.

This repo contains both the framework package and a demo app.

nodiff/
  packages/nodiff/   framework package, published name @nodiffjs/core
  apps/demo/         demo rich-client app

Fork-first workflow

NoDiff is meant to be forked as a monorepo. The cleanest development loop is to edit the framework package and the app together, then keep the pieces that fit the app you are building.

The demo app intentionally consumes packages/nodiff/src through Vite and TypeScript aliases for @nodiffjs/core. That keeps HMR and editor navigation on source files while you change the runtime, bindings, router, API client, and app code in one workspace.

The package build is still kept clean. packages/nodiff emits dist, declares package exports, uses publishable dependency ranges, and ships package-local README/LICENSE files. Treat that artifact as a consumer/publishing check, not as the source path used by the demo during normal development.

Why this exists

Modern front-end apps often begin with a large default stack. A JSX renderer. A request client. A router. A global state tool. A form library. A cache layer. Auth glue. Then the browser shows up at the end.

NoDiff starts from the other side.

The browser already has:

  • a mutable DOM tree
  • event listeners
  • custom validity on forms
  • fetch
  • AbortController
  • localStorage
  • history and hashchange
  • native modules

The missing piece is a clean TypeScript surface that makes those primitives pleasant enough for a real app.

NoDiff is that surface. It keeps the familiar parts of a modern app, such as TSX, typed stores, schema-checked data, cached reads, and route components, while making each moving part visible.

The core bet

TSX is useful even when React is absent.

With this config:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@nodiffjs/core"
  }
}

this:

<section class="panel">
  <h1>Hello</h1>
  <button onClick={save}>Save</button>
</section>

compiles to calls into this package:

import { jsx, jsxs } from "@nodiffjs/core/jsx-runtime";

The runtime creates HTMLElement instances directly, applies props, attaches event listeners, runs actions, and appends children. No virtual DOM tree is built. No diff pass runs. No component replay is needed for a button click.

State-driven parts opt in explicitly:

<p>{text(session, (state) => state.user?.name ?? "anonymous")}</p>;

{
  view(
    posts,
    (state) => state.visible,
    (items) => (
      <ul>
        {items.map((item) => (
          <li>{item.title}</li>
        ))}
      </ul>
    ),
  );
}

You can read the code for those pieces in a few files.

What you get

Area Included
TSX runtime jsx, jsxs, jsxDEV, Fragment, intrinsic element typing
DOM mount, append, direct node creation, events, refs, actions
Lifecycle cleanup on unmount and region redraw
State bindings text, view, when, Show, list, For, ResourceView, Await, bind.*
Stores works with zustand/vanilla and any compatible store shape
API client fetch, query params, JSON body handling, zod parsing, auth headers, 401 hook
Cache localStorage envelopes with TTL, tags, stale reads, schema validation
Auth bearer token controller over zustand vanilla with optional persistence
Resource state loading, stale, success, error, abort, refresh, mutate
Router hash or history mode, route params, query params, active links
Forms zod-backed submit action, native validity messages
Security DOM sink hardening, strict request policy, CSRF helpers, safe error fallbacks
Tooling Bun workspaces, Vite 8, TypeScript 7 native preview, oxlint, oxfmt

Run it

Prerequisite: Bun.

bun install
bun run dev

Open the local Vite URL printed in the terminal.

Useful commands:

bun run typecheck
bun run lint
bun run format
bun run check
bun run security:check
bun run build

The root scripts run the framework package first, then the demo app where that ordering matters.

During bun run dev, the demo resolves @nodiffjs/core to packages/nodiff/src instead of dist. This is deliberate: framework edits and app edits should update together in the fork.

Current toolchain

The repo is intentionally modern:

  • Bun workspaces with dependency versions centralized in the root catalog
  • Vite 8 for the demo app
  • TypeScript 7 native preview through @typescript/native-preview and tsgo
  • oxlint with type-aware linting through oxlint-tsgolint
  • oxfmt for formatting
  • ES2022 browser target

bun run check runs strict typechecking, type-aware linting, format checking, and tests. bun run security:check adds the dependency audit and production build on top.

Security defaults

NoDiff tries to make the safe path the short path for rich-client apps:

  • DOM text is escaped by construction. The innerHTML prop is rejected, raw HTML must be wrapped with trustedHTML(...) or passed through sanitizeHTML(...), dangerous tags are blocked, URL attributes are scheme/origin checked, CSS execution sinks are rejected, event props must be functions, and _blank links get noopener noreferrer.
  • configureSecurityPolicy(...) installs a process-wide policy for strict origin checks, allowed URL schemes, HTTPS enforcement, cache TTL/schema rules, CSRF mode, and security event reporting.
  • createApi(...) resolves requests against a configured baseUrl, strips managed auth headers from other origins, supports required auth, zod response schemas, cache partitioning by auth, strict cached-response schemas, bounded cache TTLs, request timeouts, external-origin opt in, and CSRF headers.
  • createAuth(...) keeps tokens in memory by default. localStorage persistence is explicit, and refresh-token persistence requires a second explicit opt in.
  • catchRender and router error/onError hooks render redacted failures by default, so route exceptions do not become stack traces or secret-bearing messages in the UI.
  • contentSecurityPolicy(...) and securityHeaders(...) produce strict server headers that match the same policy model.

The demo app opts into strict mode with one policy:

export const securityPolicy = configureSecurityPolicy({
  mode: "strict",
  allowedOrigins: ["self", "https://jsonplaceholder.typicode.com"],
  allowedUrlSchemes: ["http:", "https:"],
  enforceHttps: true,
  cache: { requireSchema: true, maxTtl: 5 * 60 * 1000 },
  csrf: "double-submit-cookie",
});

export const publicApi = createApi({
  baseUrl: "https://jsonplaceholder.typicode.com",
  security: securityPolicy,
});

export const sessionApi = createApi({
  baseUrl: "/api",
  security: securityPolicy,
  getAuthHeaders: auth.authHeaders,
  csrf: { getToken: readCsrfToken, required: "state-changing" },
});

The demo keeps public JSONPlaceholder reads on publicApi, without auth headers, and reserves sessionApi for first-party authenticated requests. apps/demo/public/_headers applies the matching CSP and security headers for static deployments. For server-rendered shells or deployments that generate headers differently, start from:

const headers = securityHeaders({
  policy: securityPolicy,
  hsts: true,
});

The demo app

The demo is intentionally plain. It shows the framework surface without hiding it behind another abstraction.

Route What it demonstrates
/ direct DOM TSX, persisted zustand state, live text binding, theme preference
/posts zod-validated API data, cached GET request, resource state, search binding
/auth fake token login, bearer token headers, zod form submit
/cache cache key inspection, localStorage clearing, auth reset

The API data comes from JSONPlaceholder. The app parses every post through zod before it enters UI state.

Tutorial: build the mental model

This section walks through the app from zero to a useful screen.

1. Create a store

NoDiff does not ship its own state container. It expects a simple vanilla store shape:

interface ReadableStore<T> {
  getState(): T;
  subscribe(listener: (state: T, previous: T) => void): () => void;
}

That is the shape exposed by zustand/vanilla.

import { createStore } from "zustand/vanilla";

const preferences = createStore(() => ({
  count: 0,
  search: "",
  theme: "system" as "system" | "light" | "dark",
}));

2. Render real DOM with TSX

A component is just a function that returns a child value. A child can be a node, text, a fragment, an array, null, or another component result.

function HomePage() {
  return (
    <section class="panel">
      <h1>Direct DOM TSX</h1>
      <button onClick={() => preferences.setState((state) => ({ count: state.count + 1 }))}>
        Increment
      </button>
    </section>
  );
}

onClick becomes addEventListener("click", ...). When the node is removed by mount, view, or another cleanup-aware path, the listener is removed too.

3. Bind text to state

Use text when only a text node needs to change.

<p>Count: {text(preferences, (state) => state.count)}</p>

This creates one Text node and subscribes it to the store. When count changes, the node data changes. The surrounding paragraph stays in place.

4. Bind form controls

bind.value keeps an input and a store field in sync.

<input
  placeholder="Search"
  use={bind.value(
    preferences,
    (state) => state.search,
    (search) => ({ search }),
  )}
/>

The use prop accepts an action. An action receives an element and may return cleanup.

const focusOnMount: Action<HTMLInputElement> = (input) => {
  input.focus();
};
<input use={focusOnMount} />

Actions are the main escape hatch. Use them for observers, subscriptions, custom events, animations, third-party widgets, and anything else that attaches behavior to a node.

5. Redraw a region

Use view when a region depends on state.

{
  view(
    postsVm,
    (state) => state.visible,
    (posts) => (
      <div class="post-list">
        {posts.map((post) => (
          <article class="post-card">
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </article>
        ))}
      </div>
    ),
  );
}

view places two comment markers in the DOM. When the selected value changes, it removes the nodes between those markers, runs cleanup for them, and inserts freshly rendered nodes.

This is intentionally simple. For small and medium regions it is easy to reason about. For keyed lists that should preserve stable rows across insertions, removals, and reorders, use For.

{
  For({
    store: postsVm,
    each: (state) => state.visible,
    by: (post) => post.id,
    fallback: <p>No posts match this filter.</p>,
    children: (post) => (
      <article class="post-card">
        <h2>{post.title}</h2>
        <p>{post.body}</p>
      </article>
    ),
  });
}

The by function is used instead of a key prop because JSX treats key as special metadata. Rows with the same key and the same item identity are moved in place. Rows with changed item identity are redrawn and cleaned up.

6. Parse API data at the edge

The API client wraps fetch and accepts a zod schema.

import { z } from "zod";

const PostSchema = z.object({
  userId: z.number(),
  id: z.number(),
  title: z.string(),
  body: z.string(),
});

const PostsSchema = z.array(PostSchema);

const api = createApi({
  baseUrl: "https://jsonplaceholder.typicode.com",
});

const posts = await api.get("/posts", {
  schema: PostsSchema,
});

Parsed data is typed. Bad data fails before it reaches your UI.

7. Add cache with TTL

Create a cache once:

const apiCache = createLocalCache("demo:http:");

const api = createApi({
  baseUrl: "https://jsonplaceholder.typicode.com",
  cache: apiCache,
});

Then cache a request:

const posts = await api.get("/posts", {
  schema: PostsSchema,
  cache: {
    key: "posts",
    ttl: 5 * 60 * 1000,
    swr: true,
    tags: ["posts"],
  },
});

Cache entries look like this:

type CacheEnvelope<T> = {
  value: T;
  updatedAt: number;
  expiresAt: number | null;
  tags: string[];
};

When a schema is passed on read, cached data is parsed again. Corrupt or outdated cache shapes are removed.

8. Wrap loading and errors in a resource

createResource gives you a zustand store around an async function.

const posts = createResource({
  immediate: true,
  load: (_args, { signal }) =>
    api.get("/posts", {
      signal,
      schema: PostsSchema,
      cache: { key: "posts", ttl: 300_000, swr: true },
    }),
});

When a resource has required args, immediate: true must include initialArgs. refresh() reuses the last successful load(...) args or initialArgs; before any args are known it resolves to undefined without calling your loader.

The resource state is explicit:

type ResourceState<T> = {
  status: "idle" | "loading" | "success" | "error";
  data: T | null;
  error: Error | null;
  updatedAt: number | null;
  loading: boolean;
  stale: boolean;
};

Render it with view:

{
  view(
    posts.store,
    (state) => state,
    (state) => {
      if (state.loading && !state.data) return <p>Loading...</p>;
      if (state.error && !state.data) return <pre>{state.error.message}</pre>;
      return <PostList posts={state.data ?? []} />;
    },
  );
}

9. Add token auth

createAuth keeps a token in memory by default and exposes helpers for the API client.

const auth = createAuth<{ email: string; name: string }>();

const api = createApi({
  baseUrl: "/api",
  getAuthHeaders: auth.authHeaders,
  onUnauthorized: () => auth.logout(),
});

Set a token after login:

auth.setToken(
  {
    accessToken: "token-from-server",
    expiresAt: Date.now() + 60 * 60 * 1000,
  },
  { email: "demo@example.com", name: "demo" },
);

Make a protected request:

await api.get("/me", {
  auth: "required",
  schema: UserSchema,
});

The auth helper is intentionally narrow. It handles bearer token state and headers. Your app still owns login, refresh policy, secure cookie decisions, and server protocol. CSRF helpers are available for cookie-backed requests.

If an app deliberately accepts the localStorage tradeoff, persistence is explicit:

const auth = createAuth<User>({
  persist: true,
  storageKey: "auth",
});

10. Add a router

Routes are data:

const router = createRouter(
  [
    { path: "/", title: "Home", component: HomePage },
    { path: "/posts", title: "Posts", component: PostsPage },
    { path: "/posts/:id", title: "Post", component: PostPage },
  ],
  { mode: "hash" },
);

Render the outlet:

function App() {
  const Link = (props: Parameters<typeof router.Link>[0]) => router.Link(props);

  return (
    <main>
      <nav>
        <Link to="/" exact activeClass="active">
          Home
        </Link>
        <Link to="/posts" activeClass="active">
          Posts
        </Link>
      </nav>
      {router.outlet()}
    </main>
  );
}

router.start();
mount("#app", App);

Route params and query params arrive in the component context:

function PostPage(ctx: RouteContext) {
  return <h1>Post {ctx.params.id}</h1>;
}

11. Handle forms with zod

zodSubmit converts FormData to an object, parses it, and sends zod issues into native form validity where possible.

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

function LoginForm() {
  return (
    <form use={zodSubmit(LoginSchema, login)}>
      <input name="email" type="email" autocomplete="email" />
      <input name="password" type="password" autocomplete="current-password" />
      <button type="submit">Sign in</button>
    </form>
  );
}

For numeric fields, use zod coercion because browser form values are strings:

const ProfileSchema = z.object({
  age: z.coerce.number().int().min(13),
});

The framework API

DOM and JSX

jsx(type, props);
jsxs(type, props);
jsxDEV(type, props);
Fragment(props);
catchRender({ render, fallback?, onError? });
ErrorBoundary(props);
mount(host, componentOrNode, props?);
append(parent, child);
fragment(children?);
on(type, handler, options?);
setText(value);
trustedHTML(html);
sanitizeHTML(html);

Common props:

<div
  class={{ active: true, muted: false }}
  style={{ display: "grid", gap: "1rem" }}
  dataset={{ id: 123 }}
  aria={{ busy: false }}
  unsafeHTML={sanitizeHTML("<strong>trusted markup only</strong>")}
  ref={(node) => console.log(node)}
  use={(node) => () => console.log("cleanup", node)}
  onClick={(event) => console.log(event.currentTarget)}
/>

Raw HTML must use unsafeHTML with trustedHTML(...) or sanitizeHTML(...). The ordinary innerHTML prop is rejected so HTML injection is visible at the call site.

Events are inferred from onX prop names. onClick maps to click, onInput maps to input, and so on.

Use catchRender({ render }) around app shells or isolated render callbacks:

mount("#app", catchRender({ render: App }));

It only catches exceptions thrown while the render callback runs. Already-created JSX children are evaluated before a boundary function receives them, so ErrorBoundary({ children: <App /> }) cannot catch App render errors. ErrorBoundary({ render: App }) and function children are kept for compatibility, but new code should prefer catchRender.

Store bindings

text(store, selector, format?, equality?);
view(store, selector, render, options?);
when(store, predicate, yes, no?);
Show({ store, when, children, fallback?, equality? });
list(store, selector, render, options?);
For({ store, each, by, children, fallback?, equality? });
ResourceView({ resource, children, pending?, error?, empty?, equality? });
Await({ resource, children, pending?, error?, empty?, equality? });
derivedStore(stores, derive, options?);
effect(store, selector, run, equality?);
subscribeSelector(store, selector, listener, equality?);

Use derivedStore when UI state depends on multiple explicit stores:

const postsVm = derivedStore([posts.store, preferences], () => readPostsVm());

It is a read-only store. Dependencies stay visible in the call site; there is no hidden dependency tracking.

bind helpers:

bind.text(store, selector, format?, equality?);
bind.attr(name, store, selector, equality?);
bind.class(name, store, selector, equality?);
bind.classes(store, selector, equality?);
bind.style(store, selector, equality?);
bind.prop(name, store, selector, equality?);
bind.props(store, selector, equality?);
bind.dataset(name, store, selector, equality?);
bind.aria(name, store, selector, equality?);
bind.value(store, selector, commit, options?);
bind.checked(store, selector, commit, equality?);
bind.number(store, selector, commit, options?);
bind.checkedGroup(store, selector, commit, options?);
bind.radio(store, selector, commit, options?);
bind.selected(store, selector, commit, equality?);
bind.files(store, commit);

Use text for inline text nodes. Use bind.text when the element already exists and should receive textContent. Use view for regions. Use effect for side effects tied to a node lifecycle.

Use bind.props when several element states should move together:

<button
  use={bind.props(postsVm, (state) => ({
    disabled: state.loading,
    aria: { busy: state.loading },
    class: { "btn-disabled": state.loading },
    title: state.loading ? "Refreshing posts" : "Refresh posts",
  }))}
/>

Cache

const cache = createLocalCache("app:");

cache.set("preferences", value, {
  ttl: 24 * 60 * 60 * 1000,
  tags: ["preferences"],
});

const entry = cache.get("preferences", PreferencesSchema, {
  allowStale: true,
});

cache.remove("preferences");
cache.clearTag("preferences");
cache.clear();
cache.keys();

The cache is for client-side convenience, not durable storage. It is best for preferences, response snapshots, and quick reloads.

API

const api = createApi({
  baseUrl: "/api",
  headers: { "X-App": "demo" },
  cache,
  security: securityPolicy,
  getAuthHeaders: auth.authHeaders,
  csrf: { getToken: readCsrfToken, required: "state-changing" },
  timeout: 10_000,
  refreshAuth: refreshToken,
  onUnauthorized: () => auth.logout(),
});

Request helpers:

api.get(path, options);
api.post(path, body, options);
api.put(path, body, options);
api.patch(path, body, options);
api.delete(path, options);
api.request(path, options);

Request options:

type ApiRequestOptions<T> = {
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  query?: Record<string, string | number | boolean | null | undefined>;
  body?: unknown;
  headers?: HeadersInit;
  signal?: AbortSignal;
  schema?: z.ZodType<T>;
  cache?:
    | false
    | {
        key?: string;
        ttl?: number;
        tags?: string[];
        swr?: boolean;
      };
  auth?: false | "optional" | "required";
  csrf?: boolean | CsrfRequestOptions;
  external?: boolean;
  timeout?: number;
  credentials?: RequestCredentials;
};

The client serializes plain bodies as JSON, leaves FormData, URLSearchParams, Blob, and buffers intact, parses JSON responses by content type, and throws ApiError for failed HTTP responses.

Auth

const auth = createAuth<User>();

auth.setToken(token, user);
auth.setUser(user);
auth.logout();
auth.getToken();
auth.authHeaders();
auth.isExpired();
auth.isAuthenticated();

The auth store is a zustand vanilla store:

auth.store.getState();
auth.store.subscribe((state) => {
  console.log(state.status);
});

Resources

const users = createResource<User[], { search: string }>({
  initialArgs: { search: "" },
  initialData: [],
  load: ({ search }, { signal }) =>
    api.get("/users", {
      query: { search },
      signal,
      schema: UsersSchema,
    }),
});

users.refresh(); // uses initialArgs until load(...) provides new args
users.load({ search: "ada" });
users.refresh();
users.mutate((current) => [...(current ?? []), newUser]);
users.abort();
users.reset();

Router

const router = createRouter(routes, {
  mode: "history",
  fallback: () => <NotFound />,
  error: () => <p role="alert">Something went wrong.</p>,
  onError: (error) => reportError(error),
});

router.href("/posts");
router.navigate("/posts", { replace: true });
router.start();
router.stop();
router.outlet();
router.Link({ to: "/posts", children: "Posts" });

Route paths support static segments, params, and a wildcard:

/
/posts
/posts/:id
/docs/*

Forms

formValues(form);
clearFormValidity(form);
applyZodValidity(form, error);
zodSubmit(schema, handler, options?);

Use zodSubmit as a form action:

<form use={zodSubmit(Schema, save, { resetOnSuccess: true })}>...</form>

Recipes

Persist a zustand store

const cache = createLocalCache("app:");

const PreferencesSchema = z.object({
  theme: z.enum(["system", "light", "dark"]),
  sidebarOpen: z.boolean(),
});

const saved = cache.get("preferences", PreferencesSchema, { allowStale: true })?.value;

const preferences = createStore(() => ({
  theme: saved?.theme ?? "system",
  sidebarOpen: saved?.sidebarOpen ?? true,
}));

preferences.subscribe((state) => {
  cache.set("preferences", state, { tags: ["preferences"] });
});

Add an active class from state

<button use={bind.class("selected", tabs, (state) => state.current === "settings")}>
  Settings
</button>

Disable a button while saving

<button use={bind.attr("disabled", saveState, (state) => state.loading)}>Save</button>

Run a custom subscription for one element

const pageTitle = effect(
  session,
  (state) => state.user?.name ?? "anonymous",
  (name) => {
    document.title = `${name} | Admin`;
  },
);
<div use={pageTitle} />

The subscription is removed when the element is cleaned up.

Read stale cache manually

const hit = cache.get("posts", PostsSchema, { allowStale: true });

if (hit?.stale) {
  console.log("show old data, refresh soon");
}

Clear every cached response for a tag

api.cache.clearTag("posts");

Use request auth only for selected calls

await api.get("/public", { auth: false });
await api.get("/me", { auth: "required", schema: UserSchema });

Use history mode

const router = createRouter(routes, { mode: "history" });

For production history mode, configure your server to return index.html for app routes.

Design notes

TSX is only syntax

There is no component instance model. A component call creates nodes. Persistent updates live in stores, actions, and resources.

DOM ownership is explicit

If code creates a node, code can also clean it. The lifecycle module stores cleanup callbacks in a WeakMap<Node, Cleanup[]>. Redrawing a view or unmounting a root walks the nodes and runs those callbacks.

Reactivity is opt-in

A static subtree remains static. A text node subscribed with text updates itself. A view redraws only its own marker-bounded region. An action may subscribe to anything and return cleanup.

Data is trusted after parsing

The API client and cache both accept zod schemas. The goal is to reject bad network data and stale cache shapes at the boundary.

localStorage is treated as a cache

Every write stores an envelope with updatedAt, expiresAt, and tags. A read can reject stale entries, allow stale entries, or remove invalid entries after schema parsing fails.

The router is a store

Routing state is a zustand vanilla store. The outlet is a view over the current path. Links are anchors with click handling and optional active class binding.

What this deliberately skips

This project is small by choice. Some things are better added per app than baked into the core.

Skipped Reason
Virtual DOM diffing The runtime creates DOM nodes directly and updates explicit regions.
Keyed list reconciliation Useful for very large lists, but not needed for the demo. Add it as a focused helper.
SSR and hydration The target is a modern browser rich client.
Server components Outside the scope of this client runtime.
Suspense semantics Resource state is explicit and ordinary.
Built-in OAuth flow Servers and threat models vary too much.
IndexedDB cache localStorage keeps the example small. Use IndexedDB for larger payloads.
Component context system Plain modules and stores cover the current app.

When to use this shape

This architecture fits apps where:

  • the browser is the main runtime
  • TSX syntax is desired
  • the app wants strong network parsing without a large UI runtime
  • data and auth flows are custom
  • the team prefers direct ownership of DOM behavior
  • the codebase should be readable from top to bottom
  • coding agents should be able to reason across app and framework code in one repository

Choose a larger UI framework when you need a mature component ecosystem, server rendering, hydration, a11y component kits, animation systems, or many third-party UI widgets with framework adapters.

Reading path

To understand the framework, read these files in order:

  1. packages/nodiff/src/dom.ts for TSX, props, events, refs, actions, and mount
  2. packages/nodiff/src/lifecycle.ts for cleanup
  3. packages/nodiff/src/store.ts for text, view, list, and form control bindings
  4. packages/nodiff/src/cache.ts for localStorage envelopes
  5. packages/nodiff/src/api.ts for zod-backed fetch and auth headers
  6. packages/nodiff/src/resource.ts for async state
  7. packages/nodiff/src/router.ts for navigation
  8. apps/demo/src/main.tsx for the whole app in one place

Package layout

packages/nodiff/src/
  api.ts              fetch client with zod, auth, cache
  auth.ts             bearer token store with optional persistence
  cache.ts            localStorage cache with TTL and tags
  csrf.ts             CSRF token readers and double-submit helpers
  dom.ts              direct DOM TSX runtime
  errors.ts           redacted error messages for UI fallbacks
  forms.ts            zod form action and validity helpers
  index.ts            public exports
  jsx-dev-runtime.ts  Vite/dev automatic JSX runtime entry
  jsx-runtime.ts      TypeScript automatic JSX runtime entry
  lifecycle.ts        cleanup registry
  resource.ts         async resource store
  router.ts           hash/history router
  security.ts         policy, CSP, and security header helpers
  store.ts            zustand-compatible bindings

Modern browser target

The repo targets modern browsers only:

{
  "target": "ES2022",
  "lib": ["ES2022", "DOM", "DOM.Iterable"],
  "moduleResolution": "Bundler",
  "strict": true,
  "noUncheckedIndexedAccess": true,
  "exactOptionalPropertyTypes": true
}

The demo Vite build target is also es2022.

Publishing notes

@nodiffjs/core has package exports for the framework and JSX runtimes:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./dom": {
      "types": "./dist/dom.d.ts",
      "import": "./dist/dom.js"
    },
    "./api": {
      "types": "./dist/api.d.ts",
      "import": "./dist/api.js"
    },
    "./jsx-runtime": {
      "types": "./dist/jsx-runtime.d.ts",
      "import": "./dist/jsx-runtime.js"
    },
    "./jsx-dev-runtime": {
      "types": "./dist/jsx-dev-runtime.d.ts",
      "import": "./dist/jsx-dev-runtime.js"
    }
  }
}

The package source keeps extensionless local imports for the monorepo development path. The package build rewrites only the emitted dist specifiers so standard ESM consumers can import the packed artifact directly. Package metadata avoids workspace-only dependency protocols, so npm pack output can be installed outside this workspace.

The one-file promise

Open apps/demo/src/main.tsx and you can see the whole client app: preferences, auth, cache, API client, resource state, routes, pages, and mount call.

That is the point of the project. A rich-client app should be possible to understand without following a renderer through a black box.