{"id":51073645,"url":"https://github.com/m9tdev/verrex","last_synced_at":"2026-06-23T12:33:31.119Z","repository":{"id":360555755,"uuid":"1247441018","full_name":"m9tdev/verrex","owner":"m9tdev","description":"an Effect-native UI framework where E/R channels survive to compile-time — forget a service Layer and it's a compile error that names it","archived":false,"fork":false,"pushed_at":"2026-06-20T12:50:56.000Z","size":1099,"stargazers_count":41,"open_issues_count":39,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-20T13:18:59.417Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://m9tdev.github.io/verrex/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/m9tdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-23T10:17:24.000Z","updated_at":"2026-06-17T22:50:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/m9tdev/verrex","commit_stats":null,"previous_names":["m9tdev/efx","m9tdev/verrex"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/m9tdev/verrex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m9tdev%2Fverrex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m9tdev%2Fverrex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m9tdev%2Fverrex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m9tdev%2Fverrex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/m9tdev","download_url":"https://codeload.github.com/m9tdev/verrex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/m9tdev%2Fverrex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34688146,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-23T02:00:07.161Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-23T12:33:29.823Z","updated_at":"2026-06-23T12:33:31.108Z","avatar_url":"https://github.com/m9tdev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# verrex\n\nAn experimental TypeScript UI framework where Effect's `\u003cA, E, R\u003e` channels\npropagate from every leaf of the view tree to the root. Forgetting to provide\na service `Layer` becomes a **compile-time error that names the missing\nservice**.\n\n\u003e **Why \"verrex\"?** The name is built from the channels of an\n\u003e `Effect\u003cView, E, R\u003e` — **V** (View — the `A`, which here is always the\n\u003e `View`), **E** (Error), **R** (Requirements) — plus **X**, because the JSX/TSX\n\u003e syntax it borrows adds an X too. `V + E + R + X` spells **verx**, stylized to\n\u003e **verrex** (and the source extension is `.vx`).\n\nStatus: proof-of-concept. Not for production. Architecture, invariants, and\nper-package contracts live in [AGENTS.md](./AGENTS.md) and the per-subsystem\nAGENTS.md tree.\n\n**▶ [Live demo](https://m9tdev.github.io/verrex/)** — a guided tour through each\nprimitive: the source on the left, the inferred `Effect\u003cView, E, R\u003e` type, and\nthe running component (with a reset button) on the right.\n\n## What you get\n\n- **Channels survive the tree.** A `\u003cUserPage userId=\"42\" /\u003e` whose internals\n  call `Http.getUser(id)` propagates `HttpError` and `Http | Theme` up to the\n  root through every intervening `\u003cdiv\u003e`, `\u003cFragment\u003e`, conditional, list,\n  and component. The root must provide a `Layer` covering the entire `R`, or\n  it fails to compile.\n- **Reactive values in JSX expressions.** `{loading.value ? \u003cSpinner /\u003e : \u003cContent /\u003e}`\n  works against `loading: AtomRef\u003cboolean\u003e` — you write `.value` explicitly and\n  the compiler rewrites that read into a tracked one, wrapping the surrounding\n  expression in a tracking scope so it re-renders when `loading` changes.\n- **Effect-native async boundary.** `Async(() =\u003e http.getUser(id), { initial, failure, success })`\n  runs an effect and renders initial → success/failure, folding the effect's `R`\n  into the component (a forgotten `Layer` is still a compile error). The thunk\n  auto-tracks any `.value` it reads, so it refetches when they change.\n- **Effect-native error boundary.** `Catch(child, (cause, reset) =\u003e fallback)` (or\n  `Catch(child, { HttpError: … })` for tag-selective) recovers the failure side of\n  a view subtree, mirroring Effect's `catch*`. `mount` requires every error\n  discharged, so a forgotten boundary is a compile error that names the unhandled\n  error — the runtime counterpart of a forgotten `Layer`.\n- **Effect v4 primitives all the way down.** `AtomRef`/`Atom`/`AtomRegistry`\n  from `effect/unstable/reactivity` are the reactivity layer; we don't build\n  our own. `AsyncResult` is the loading/success/failure shape.\n- **Keyed reactive lists.** `{todos.value.map(item =\u003e \u003cRow item={item} /\u003e)}`\n  is compiled to a keyed list that reconciles by `AtomRef` identity — adding,\n  removing, or toggling one item never tears down the others.\n- **Custom file extension.** `.vx` files are compiled by Babel to plain\n  TypeScript before `tsc` ever sees them, so TypeScript's JSX type checker\n  is never engaged — that's how channels survive instead of collapsing to\n  `JSX.Element`.\n\n## Quick start\n\n```bash\ngit clone … verrex\ncd verrex\npnpm install\npnpm dev\n# open http://localhost:5173\n```\n\nThe demo is a guided tour that exercises every primitive — reactive counter,\nblocking and `Async`-boundary data fetches, auto-tracking refetch, keyed\nreactive list, and per-component lifecycle — each with a reset button. It's\nalso deployed at [m9tdev.github.io/verrex](https://m9tdev.github.io/verrex/).\n\nOn Nix, `nix develop` drops you into a shell with Node, Corepack (for\n`pnpm` via the `packageManager` field), and Chromium (with `VERREX_CHROMIUM`\npre-exported for the probe scripts).\n\n## Install\n\n```bash\npnpm add @verrex/core effect            # the framework (effect is a peer dependency)\npnpm add -D @verrex/ts-plugin     # editor support for .vx files (see below)\n```\n\n`@verrex/core` ships its compiled `dist` alongside the original `src` with declaration\nmaps, so go-to-definition jumps straight into the framework's TypeScript source.\nReleases are cut from conventional commits (release-please) and published to npm\nwith provenance. It's `0.x` and experimental — expect breaking changes between\nminor versions.\n\n## Smallest possible example\n\n```tsx\n// Counter.vx\nimport { AtomRef } from \"effect/unstable/reactivity\"\nimport { Component } from \"@verrex/core\"\n\nexport const Counter = Component.make(function* () {\n  const count = AtomRef.make(0)\n  return yield* (\n    \u003cdiv\u003e\n      \u003cbutton onclick={() =\u003e count.update((n) =\u003e n + 1)}\u003e+\u003c/button\u003e\n      \u003cspan\u003e {count} clicks \u003c/span\u003e\n      \u003cbutton onclick={() =\u003e count.set(0)}\u003ereset\u003c/button\u003e\n    \u003c/div\u003e\n  )\n})\n```\n\n```ts\n// main.vx\nimport { Effect, Layer } from \"effect\"\nimport { VerrexLive, mount } from \"@verrex/core\"\nimport { Counter } from \"./Counter.vx\"\n\nconst program = Effect.gen(function* () {\n  yield* mount(\u003cCounter /\u003e, document.getElementById(\"root\")!)\n  yield* Effect.never\n}).pipe(\n  Effect.scoped,\n  Effect.provide(VerrexLive),\n)\n\nEffect.runFork(program)\n```\n\n## Layout\n\n```\npackages/\n  core/               one package (`@verrex/core`), subpath exports:\n    src/runtime/        export `@verrex/core`     — View IR, h(), mount(), Async(), list()\n    src/compiler/       export `@verrex/core/compiler` — .vx → plain TypeScript (Babel)\n    src/language/       export `@verrex/core/language` — Volar language plugin\n    src/check/          export `@verrex/core/check`, bin `verrex-check`\n    src/vite-plugin/    export `@verrex/core/vite`  — Vite integration\n    src/testing/        export `@verrex/core/testing`\n  ts-plugin/   publishes as `@verrex/ts-plugin` — TS Language Service plugin (editor)\napps/\n  demo/               Counter, UserPage, AsyncUserPage, LiveUser, Todos, Lifecycle, CatchDemo, AsyncEscalate\n```\n\n## The primitives\n\n| You import from      | What you get                                                                              |\n|----------------------|-------------------------------------------------------------------------------------------|\n| `@verrex/core`      | `h`, `mount`, `Component`, `Async`, `asyncRef`, `AsyncHandle`, `Catch`, `list`, `Fragment`, `View`, `VerrexLive`                             |\n| `effect`             | `Effect`, `Layer`, `Context.Service`, `Data.TaggedError`, `Cause`, `Option`, `Result`, …  |\n| `effect/unstable/reactivity` | `AtomRef`, `Atom`, `AtomRegistry`, `AsyncResult`                                  |\n\n`h.track` / `h.read` are compiler-emitted; you generally never write them\nby hand. Reactive reads are always explicit through `.value` — the compiler\nrewrites those calls into tracked reads under the hood, and the surrounding\nJSX expression is automatically wrapped in a tracking scope.\n\n## Workflow\n\n| Command            | What it does                                                  |\n|--------------------|---------------------------------------------------------------|\n| `pnpm dev`         | Vite dev server with HMR on `.vx` files                      |\n| `pnpm typecheck`   | Per-package `tsc --noEmit`; apps/demo uses `@verrex/core/check` (.vx-aware) |\n| `pnpm build`       | Production build via Vite (`@verrex/core/vite` owns the transform) |\n| `pnpm test`        | All package suites — compiler, runtime, language, vite-plugin, testing, ts-plugin (incl. its tsserver integration probe) + `@verrex/core/check` |\n\n## Bundle size\n\n`pnpm build` on the demo produces:\n\n| Asset                 | Raw      | Gzipped  |\n|-----------------------|----------|----------|\n| `dist/index.html`     | 11.66 kB |  3.06 kB |\n| `dist/assets/index-*.js` | 117.45 kB | **39.64 kB** |\n\nThe JS bundle contains: `effect@4.0.0-beta.78` runtime (~6 kB gzipped per\nupstream docs), `effect/unstable/reactivity` (`AtomRef`, `Atom`,\n`AtomRegistry`, `AsyncResult`), the `verrex` runtime (~600 LOC,\ncontributes single-digit kB), plus all eight demo components (`Counter`,\n`UserPage`, `AsyncUserPage`, `LiveUser`, `Todos`, `Lifecycle`, `CatchDemo`,\n`AsyncEscalate`), the guided-tour\nshell (a small dependency-free TSX highlighter + reactivity-flash visualizer),\nand their mock services. Verified interactive after build — Counter increments,\nthe `Async` boundaries load then resolve, Todos add/remove/toggle, Lifecycle's\nper-row scope fires releases on row removal.\n\nVite serves `.vx` files directly through `@verrex/core/vite` at dev time;\ntype-checking goes through `@verrex/core/check`, which feeds `.vx` to tsc as virtual\nTypeScript via the shared Volar language plugin. No sibling `.ts` files are\nemitted to disk.\n\n## Editor setup\n\nA TypeScript Language Service Plugin (`@verrex/ts-plugin`) ships with the\nworkspace and is wired into `apps/demo/tsconfig.json`'s `plugins` array.\nThe plugin uses Volar's language plugin framework to provide full IDE\nsupport for `.vx` files.\n\n**What works:** Diagnostics, hover, go-to-definition, find-references,\ninlay hints, and document highlights (including JSX tag pair matching).\n\nOn GitHub, `.vx` files render with TSX syntax highlighting (via a\n`linguist-language=TSX` override in `.gitattributes`).\n\n### Neovim\n\n```vim\n\" Treat .vx as TSX so your LSP attaches and treesitter highlights it\nautocmd BufRead,BufNewFile *.vx setfiletype typescriptreact\n```\n\nThat plus `tsserver` already configured for `typescriptreact` is enough.\nFirst time opening the workspace you may want to ensure\n`packages/ts-plugin/dist/index.cjs` exists — run `pnpm install` from the\nrepo root or `pnpm --filter @verrex/ts-plugin build` directly.\n\n### VS Code\n\n`@verrex/ts-plugin` is referenced in `apps/demo/tsconfig.json`. Use\n\"TypeScript: Select TypeScript Version → Use Workspace Version\" to make\nsure VS Code's TS extension picks up the plugin. `.vx` files get treated\nas TypeScript once the plugin loads.\n\n## See also\n\n- [AGENTS.md](./AGENTS.md) — architecture, per-package contracts, invariants, anti-patterns.\n- [`apps/demo/src/channels.test-d.ts`](./apps/demo/src/channels.test-d.ts) — compile-time proof that channels propagate and typed props catch misuse.\n- [`packages/core/src/runtime/types/Fold.test-d.ts`](./packages/core/src/runtime/types/Fold.test-d.ts) — channel-fold conditional-type test matrix.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm9tdev%2Fverrex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fm9tdev%2Fverrex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fm9tdev%2Fverrex/lists"}