Expo 开源跨平台 UI 框架 Exact,同一份 Contract 跑在 Web、macOS、iOS 和 Linux
Exact – a new cross platform UI framework
Expo 发布开源跨平台应用运行时 Exact,界面用声明式语言 Contract 描述、数据逻辑用 TypeScript 或 Rust 编写,编译成 plan 后在 Web、macOS、iOS 和 Linux 上原生渲染,不使用 webview。
编写一次应用。它就能在 Web、macOS、iOS 和 Linux 上原生运行,AI 代理可以在每一个平台上构建它、运行它、查看它并测试它。
提示
用编码代理试一试。在安装了 Xcode、Rust、Bun 和 Chrome 的 Mac 上,给 Claude Code 输入以下提示:
Clone https://github.com/expo/exact and follow its README to make a new Exact
app with `exact new`. Run `bun scripts/exact.mjs setup` once first. Make a todo
list where I can add items, check them off, delete them,
and see how many are left. Put the view in Contract and keep the list in `app.ts`.
Write an `app.test.contract`, pass it on web, macOS, and the iOS Simulator with
`scripts/agent.mjs`, then open the app for me on all three.
一个全新的代理在干净的克隆上使用此提示的早期版本(Hermes 已在机器缓存中,Cargo 缓存已预热),大约 57 分钟完成,其测试在三个平台上全部通过。其中约 25 分钟用于首次 macOS 和 iOS 构建,各约 13 分钟;后续构建只需一两分钟。在缺少 Hermes 的机器上,会先进行源码构建,从而增加时间。当前设置改为一次性下载并验证固定版本的 host/iOS 包;普通构建保持离线。
| Web | iOS | macOS |
|---|---|---|
![]() |
![]() |
![]() |
| 浏览器自身的 DOM 和 CSS | UIKit 视图、原生控件和系统标签栏 | AppKit 视图和原生控件 |
一个 app.contract,同一屏幕在三个宿主上的显示,由代理驱动捕获。Softwear 是一个演示商店,你可以从 exact-templates 生成:它展示 Alternative Apparel 的商品目录,与该品牌无关联,也不接受真实订单或付款。
Exact 是一个跨平台应用运行时。你用 Contract(一种小型声明式语言)描述界面,用 TypeScript 或 Rust 编写数据逻辑。Contract 编译器将界面转换为紧凑的计划。在 Web 上,该计划变成一个 ES 模块,运行在约 20 KB 的运行时之上,驱动真实的 DOM。在 macOS、iOS 和 Linux 上,Rust 运行器执行相同的计划,Rust 布局内核用 CSS 规则计算每个盒子,每个平台用自己的工具绘制:AppKit、UIKit 或 GPU 绘制器。UI 中没有 webview,也没有 JavaScript 桥接,第一个像素渲染之前不会运行任何应用 JavaScript。
注意
Exact 处于 1.0 之前版本,每天都有变化。按设计,没有 API 稳定性、没有向后兼容性,也没有迁移指南。
升级说明:Snapback mutation 回复 ok: false 现在会抛出
refused(why) 并暴露 failure(slot);
snapback4 包的 README
中有前后对比和继承应用的命令。
目录
三条原则
1. 代理原生
Exact 假设大部分代码将由 AI 代理编写。代理可以创建应用、运行它、查看它、操作它,并在无需人工介入的情况下证明其正常工作。
- 十个操作,在每个宿主上相同。
tree · screenshot · tap · type · state · layout · logs · clock · prefer · perf通过一个脚本scripts/agent.mjs驱动 Web、macOS、iOS(模拟器或真机 iPhone)和 Linux。刻意只有十个:代理 API 会一次一个合理添加,直到没人能记住它。 - 时钟属于代理。在两个操作之间,一切静止。代理不会等到动画结束,而是说
clock +60000或clock settle并读取结果。没有时序波动,没有等待。 - 测试是这些操作的脚本。一个
app.test.contract文件在任何宿主上原样运行,无需第二个求值器。 - 每个工具都与机器对话。
contract build --json提供带有稳定 id 和源码范围的诊断信息。contract symbols提供导航。开发源映射从屏幕上的任意节点回溯到声明它的那一行。驱动程序拒绝驱动比其源码更旧的构建,并指明需要重新构建。 - 契约刻意保持精简。整个应用能放进一个上下文窗口。每次写入都有声明,每个列表都有键,每个值都有类型。错误是说明该做什么的拒绝,而不是悄无声息的异常行为。
bun scripts/agent.mjs web tree "tap change-station" "type station-search Palo" \ "clock +60000" state "screenshot out.png"
2. 速度至上
应用的速度和循环的速度。rules/RULES.md 中的每项预算都在每次提交时被跟踪,任何回退都是带责任人的 P0 问题。
| 预算 | 目标 |
|---|---|
| 冷启动到可交互首帧 | 100 ms p50 |
| 开发重启,从编辑到呈现 | 100 ms p50 |
| 首像素前执行的应用 JavaScript | 无 |
| 改动一行,重建该 crate | 30 s |
| 整套阻塞性检查 | 65 s |
设计如何保持这些数字:
- 启动路径不执行也不编译任何东西。TypeScript 在构建时被编译为 Hermes 字节码,并在首像素之后加载。首帧来自预先烘焙进计划的数据。
- 只为用到的部分付费。GPU 渲染、Markdown 编辑器、文本流和浏览器 SQLite 都是独立的产物,按需加载。核心 crate 上没有任何功能开关。
- Web 有自己的小型运行时。在 RealWorld(“Conduit”)基准测试中,Exact 的运行时比 React SSR 构建完成 hydration 早了约 300 ms 启动。它使用了 26 KB 代码,而 React 为 81 KB(
apps/realworld中的 RealWorld 基准测试)。 - 编辑约在十分之一秒内生效。在由
exact new新建的应用上,保存的 Contract 或 TypeScript 编辑在 99 ms 内完成重建并进入页面。
3. 编写 Web,交付平台
你写的是 CSS:词汇、布局模型、开发循环。你未言明的是平台的部分:控件、颜色、字体、动效、手势、键盘和可访问性,其呈现效果如同用 UIKit 或 AppKit 手工构建的应用,在 Web 上则如同精心构建的 Web 应用。你写的内容优先。顺序是作者、平台、CSS 默认值,一个测试解决争议:了解该平台的人会注意到吗?
- 布局属于 CSS,因为人看不到布局默认值,只能看到其结果。裸节点是
display: block、box-sizing: content-box、flex-direction: row、flex-shrink: 1,与裸<div>相同。 - 名称属于 CSS,因为名称就是你写的内容:
font-size、object-fit、text-overflow、line-clamp、light-dark()、env(safe-area-inset-top)。编译器拒绝别名,并指出 Web 的拼写。 - 未言明的呈现属于平台。文本使用系统的标签颜色和字号(iPhone 上 17 点,Mac 上 13 点,浏览器中为读者的设置),
button是系统按钮,推入的屏幕带有系统的返回。说得更少的应用看起来更自然。 - 浏览器是布局和你所写内容的对等性基准。给定相同输入的原生布局、Canvas 2D、SVG、渐变和作者动效都会与 Chrome 绘制的结果进行比对。
- Web 也是开发循环。你在以秒计的 Web 循环上工作,原生宿主在你身后被同步更新。
- 任何不可避免的布局偏差都会被声明并附上原因。例如,Taffy 没有
position: static。
编写 Web 并不意味着最低共同标准。当平台能做更多时(Web 上 SVG foreignObject 中的真实 HTML,Apple 上通过原生模块使用 MapKit),Exact 允许这样做。
工作原理
flowchart LR
subgraph author["You write"]
C["app.contract<br/>view · state · actions"]
D["app.ts or a Rust crate<br/>data sources"]
end
C --> K["Contract compiler"] --> P["plan<br/>tables + bytecode"]
P --> W["Web<br/>one ES module, ~20 KB runtime,<br/>the browser's DOM and CSS"]
P --> R["Rust runner<br/>+ layout kernel (Taffy)"]
R --> M["macOS<br/>AppKit + CoreText"]
R --> I["iOS<br/>UIKit + CoreText"]
R --> L["Linux<br/>vello GPU / tiny-skia CPU"]
D -.->|answers, checked against declared shapes| W
D -.-> R
加载中
- Contract(
contract/)分四遍编译:语法、类型、分析和降级。构建时计划被烘焙:常量数据被求值写入其中,因此第一帧不需要其他东西。 - 运行器(
runner/)是计划的虚拟机。它处理带键的组件实例、事件、基于可寻址时钟的定时器,以及数据接缝。 - 内核(
kernel/)是一个列式节点竞技场,通过打过补丁的Taffy 实现 CSS 布局。宿主负责测量文本:Apple 上用 CoreText,Linux 上用 cosmic-text。在 Web 上,浏览器自己布局同样的 CSS,因此 Web 构建不携带布局引擎。 - 宿主(
host/)呈现树。Web 宿主写入 DOM 节点,浏览器运行 CSS 过渡。Apple 宿主是一个静态库,在 AppKit 和 UIKit 呈现器下提供 C ABI。Linux 宿主自己绘制树,绘制到带 evdev 输入的 DRM/KMS,或绘制到离屏缓冲区。 - 数据源位于同一接缝之下。TypeScript 在原生宿主上运行于 Hermes,在 Web 上运行于浏览器自身的引擎。Rust 原生运行,并且可以实时替换(作为共享库,或在 iOS 和 Web 上作为 Wasm)。每个答案都会对照 Contract 声明的形状进行检查。授权控制网络、文件、SQLite 和密钥。
- 可选组件是独立的产物:一个 wgpu GPU 模块(Apple 上为 Metal,浏览器中为 WebGPU)、原生模块(由平台控件支撑的连字符标签,如 MapKit),以及一个游戏引擎附加组件。
快速开始
这些步骤在 Apple Silicon 的 macOS 上运行过。Linux 可用于 Web 循环和 Linux 宿主。
1. 安装工具
-
通过 rustup 安装 Rust。
rust-toolchain.toml中固定的稳定工具链会在 Cargo 首次运行时自行安装。 -
Bun 1.4.2,即
package.json固定的版本。不需要 Node 和 npm。 -
Google Chrome。 代理通过无头 Chrome 驱动 Web。设置
CHROME以使用其他 Chromium。 -
Xcode,用于 macOS 和 iOS 宿主。
-
Hermes,仅用于原生宿主上的 TypeScript 应用。Exact 使用 Ibex 固定的、经过认证的
260318099.0.4发布包;普通的 Cargo 构建是离线的,从不下载或编译引擎。构建前为每个目标安装一次(对于这台 Mac,使用aarch64-apple-darwin):cargo run --manifest-path vendor/ibex/crates/hermes-lean-sys-installer/Cargo.toml -- --target <triple>
安装器会验证该包并将其发布到
~/.cargo/hermes-lean-sys/下。v4 集支持 macOS、Linux、Windows、iOS 设备和通用 iOS 模拟器,以及 tvOS 设备和 arm64 tvOS 模拟器。Linux 和 Windows 选择 Ibex 的英文intl层级;Apple 保留 Hermes 由操作系统支持的 Intl。HERMES_LEAN_SYS_DIR只是显式的开发覆盖,不是常规设置。
要在任何现有安装旁安装固定的 Bun:curl -fsSL https://bun.sh/install | BUN_INSTALL=~/.bun-1.4.2 bash -s bun-v1.4.2。如果它不在你的 PATH 中,请对下面的命令使用 ~/.bun-1.4.2/bin/bun。bun scripts/exact.mjs setup 安装声明的稳定版和 Web nightly Rust 工具链、它们的组件/目标(nightly 的 clippy 会对游戏的 Web 烘焙进行 lint)、匹配的 wasm-bindgen、固定的 Binaryen 和 Bun 依赖,安装固定的 Hermes 宿主包以及这台 Mac 构建的 iOS/tvOS 包,并获取 exact2 的锁文件和游戏 SDK 的锁文件(game/app/shells.lock)中的 crate,因为每次烘焙都离线解析。它把 Binaryen 保存在 ~/.cache/exact/binaryen 中;构建会自动找到它。setup --check 以离线检查模式对同一目标集调用 Ibex 的安装器。解析器会验证规范收据、归档、编译器以及宿主/目标 HBC 配对,而不安装任何东西;其退出状态和诊断信息就是检查的结果。
2. 在浏览器中运行 Caltrain
git clone https://github.com/expo/exact.git cd exact bun scripts/exact.mjs setup bun host/web/dev.mjs # Caltrain, at http://127.0.0.1:8765/
首次运行会编译工具链,需要几分钟。之后,打开
apps/caltrain/app.contract,修改一些文本并保存。
页面会在大约十分之一秒内重新构建并重新加载。添加 --lan 即可从网络中的手机打开
同一页面。服务器只响应它打印出的名称
(DNS 重绑定防护;任何其他 Host 都会收到 421);要通过隧道共享它,请用 --allow-host <name> 指定隧道的主机名
(可重复;name:port 固定端口)。允许的名称可以访问页面及其
重载流,但永远无法访问本地安装器的令牌。
3. 原生运行
bun host/apple/build.mjs --run # macOS bun host/apple/build.mjs --ios --run # an iOS Simulator (--device --run for a connected iPhone) cargo build --profile host-dev -p caltrain-linux # Linux: a DRM/KMS console, or headless anywhere (--release to ship)
首次原生构建在笔记本电脑上需要十到十五分钟;之后的构建会复用它,
只需一两分钟。请观察其输出,而不是盲目等待。iOS 命令使用
已启动的 iPhone 模拟器,或启动最新的 iPhone Pro。要选择其中一个,
将 EXACT_SIM 设为其名称或 UDID(或将 --sim 传给 build.mjs),并为 agent.mjs ios 保持相同
设置。
在 Linux 上,开发构建可以通过将 EXACT_VNC=1 和
EXACT_VNC_PASSWORD 设为 1–8 字节来提供其屏幕。它监听 127.0.0.1:5900;
显式指定 EXACT_VNC=host:port 可选择另一个地址。生产构建
会忽略这两个变量。
4. 像智能体一样驱动它
bun host/web/build.mjs caltrain-web # the build the agent's web host serves bun scripts/agent.mjs web tree "tap change-station" "type station-search Palo" state "screenshot out.png" bun scripts/agent.mjs web --test apps/caltrain/app.test.contract
在该主机构建完成后,将 web 替换为 macos、ios 或 linux。Caltrain 的三个
测试在 Web 主机上大约需要两秒。每个操作都是一个 shell 参数,因此
要加引号:"type new-todo Buy milk" 输入 Buy milk(目标之后的所有内容都是
文本)。"screenshot film.png over 600 every 50"
将运动拍摄为联系表;使用 .apng 名称可获得动画。
5. 制作你自己的应用
bun scripts/exact.mjs new ../hello # or run `bun link` once, then `exact new ../hello` cd ../hello bun exact.mjs contract types app.contract -o app.contract.d.ts # first run builds the compiler bun exact.mjs web # the dev loop, at http://127.0.0.1:8765/ bun exact.mjs test web tests/*.test.contract # the app's *.test.contract, or the files named bun exact.mjs mac --run # this Mac bun exact.mjs ios --run # an iOS Simulator
exact new 创建一个独立应用:app.contract(视图)、app.ts(其数据)、
app.json(清单:名称、bundle id、主机、部署策略),以及小型 web/ 和
apple/ 主机 crate。其 AGENTS.md(和 CLAUDE.md)将编码智能体指向
docs/start-here.md,这是它必须阅读的唯一指南,并指向 bun exact.mjs help 以获取命令,包括 bun exact.mjs contract … 用于编译器,
contract vocab 用于应用写入内容的卡片(--all:Contract 接受的每个标签和属性)。在做任何事之前,
先运行一次 bun scripts/exact.mjs setup;setup --check 会列出这台机器
缺少的所有内容,而不做任何更改。它有自己的 Cargo 工作区,通过路径使用你的 exact2 检出。
要从 exact2 驱动它,请将 EXACT_APP_DIR 指向它:
EXACT_APP_DIR=../hello bun host/web/build.mjs hello-web
EXACT_APP_DIR=../hello bun scripts/agent.mjs web --app hello tree "screenshot hello.png"查看每一张截图,而不只是测试结果:测试可能在布局溢出屏幕的情况下通过。bun exact.mjs agent <host> "screenshot out.png" 会在各自会话中
在每个主机上显示应用;对于你自己启动的副本,在 iOS 上使用
xcrun simctl io <device> screenshot out.png。
bun scripts/exact.mjs 还会将此仓库中的应用作为真正的 Mac 应用运行:
exact run markdown README.md,或使用 exact install markdown 将 mdview 放到你的
PATH 上。exact list 显示这里有什么。
反馈
应用的 AGENTS.md 会要求其智能体用一行记录摩擦,bun exact.mjs feedback note "<what happened>"。一次失败或缓慢的驱动、构建或验证,以及一次通过的
test 或 verify,都会以一行命名它结束,每个项目每天最多一次。一条记录会留在应用的
.exact/ 中,包含 exact2 的提交、最后一条命令的动词、退出码和持续时间,以及
操作系统;绝不包含任何命令输出,或应用的代码或文件。仅当项目的回答允许时,它才会发送到 Expo(https://exact-diaries.ccheever-0f6.workers.dev):always,或在
bun exact.mjs feedback 准确显示将发送的内容后,对 ask(默认值)回答是。bun exact.mjs feedback never
会为某个项目停止它;EXACT_FEEDBACK=0 会在机器上静默提示并拒绝所有发送。
docs/diary.md 有详细信息。
Contract
构建应用的智能体从 start-here.md 开始,这是唯一 必读内容,然后为每个问题查找 recipe。如需更多信息,请阅读完整的 guide for humans, 或在 guide for agents 或 grammar and vocabulary reference 中查找。
Contract 描述一个应用显示什么以及它的状态如何变化。它不获取数据、不读取文件,也不运行任意代码。那是数据源的职责。下面是一个完整的 todo 应用:一个 Contract 文件、一个 TypeScript 文件和一个测试。这个应用正是为 web、macOS 和 iOS Simulator 构建的,其测试在三个平台上都通过了。
要在用 exact new 创建的应用中试用它,请用下面的示例替换 app.contract 和 app.test.contract。在 app.ts 中,保留生成的 export const appId = … 行,并用下面的 TypeScript 替换其余部分。
// app.contract: the view, its state, and what each action changes. It names no colour,
// font or control size: each host draws its own heading, field, button, checkbox and list.
shape Todo
id: string
title: string
done: bool
shape Change
count: number
component Todos
state draft = ""
mutation changed as shape Change refreshes todos
resource todos = todos() as shape Todo[]
derive left = todos.filter(t => not t.done).length
action edit(value: string)
draft = value
action add
if draft.trim() != ""
send changed = addTodo(draft.trim())
draft = ""
action toggle(id: string, done: bool)
send changed = setDone(id, done)
action remove(id: string)
send changed = removeTodo(id)
view
list appearance="auto" height="100%"
section
header
text `${left} left` role="heading" testId="count"
row
input value=draft input=edit submit=add placeholder="What needs doing?" aria-label="New todo" testId="new-todo" flex=1 min-width=0
button press=add buttonStyle="filled" testId="add"
text "Add"
section
for t in todos key=t.id
row testId=`todo-${t.id}`
input type="checkbox" checked=t.done change=toggle(t.id) aria-label=t.title testId=`toggle-${t.id}`
text t.title flex=1
button press=remove(t.id) destructive=true testId=`remove-${t.id}`
text "Delete"
// app.ts: keeps the list, and answers what app.contract asks for import type { Answer, Sources } from './app.contract.d.ts'; export const grants = ''; // e.g. 'net.fetch https://api.example.com' type Todo = { id: string; title: string; done: boolean }; let todos: Todo[] = []; let next = 1; const sources: Sources = { todos: () => todos, addTodo: ([title]) => { todos = [...todos, { id: String(next++), title, done: false }]; return { count: todos.length }; }, setDone: ([id, done]) => { todos = todos.map((t) => (t.id === id ? { ...t, done } : t)); return { count: todos.length }; }, removeTodo: ([id]) => { todos = todos.filter((t) => t.id !== id); return { count: todos.length }; }, }; export const answer: Answer = (source, args, store, storage, native) => sources[source](args, store, storage, native);
// app.test.contract: runs on any host with `scripts/agent.mjs <host> --test`
test "add, finish and delete"
expect text "count" == "0 left"
type "new-todo" "Buy milk"
tap "add"
type "new-todo" "Walk the dog"
type "new-todo" key "Enter"
expect text "count" == "2 left"
tap "toggle-1"
expect text "count" == "1 left"
tap "remove-1"
expect tree missing "todo-1"
expect tree has "todo-2"
| Web | macOS | iOS Simulator |
|---|---|---|
![]() |
![]() |
![]() |
视图从不自行编辑列表。一个 action 会向数据源 send 一个mutation,app.ts 会改变其数据并作出应答,然后 refreshes todos 再次请求该列表。数据只存在于一处,对它的每一次更改都是一次具名、带类型的调用,agent 可以看到。这个列表存在于内存中。要在多次启动之间保留它,请给 app.ts 一个类似 sqlite.open app:/data/todos.db 的授权并使用 storage.sqlite,正如 Fieldnotes 所做的那样。
组成部分
| 概念 | 它是什么 |
|---|---|
shape |
一个封闭的记录类型。字段可以是 number、string、bool、另一种形状、option<T> 或 T[]。 |
component |
一个 UI 单元。文件中的第一个是根。组件接受 props,可以 inject 祖先 provide 的内容,并可以用 children 填充 slot。 |
state |
组件拥有的一个值,其类型可推断或声明(state picked: option<Item> = none)。在 for 下,子状态属于该带键的行。 |
derive |
一个由其他值计算得出的值,当它们变化时重新计算。 |
resource |
来自数据源的数据:resource x = source(args) as shape T。当参数变化时,会再次向数据源请求。 |
mutation / send |
通过数据源进行的更改:send x = source(args)。refreshes r 之后会再次请求资源 r,pending(x) 和 failed(x) 显示进度,failure(x) 说明资源失败的原因。 |
action |
状态发生变化的唯一场所。它写入的内容由其主体推断,let 在其内部绑定一个局部变量。 |
task |
按计划执行的工作:every(1000, tick)、after(ms, a)、every(frame, a)。 |
view |
缩进的元素、if … else、for … key=…(必须提供 key)、对选项的 match,以及对其他组件的调用。 |
style / class= |
一组具名的 CSS 属性。节点自身的属性优先,且没有级联。 |
fn、xs.map / xs.filter / xs.join |
纯的、单表达式辅助函数。拒绝递归。 |
| 内置函数 | web 的字符串和数组方法,按名称在值上调用(xs.length、s.trim()、s.includes(t)、xs.at(-1)),以及函数(first(xs)、formatDate、formatNumber);所有这些都在 plan/tables/format.json 中的 stdlib 下列出及其类型。 |
routes |
一个路由器:每个标签页有一个位置和一个保留的栈,通过 open、push、replace 和 back 移动,并通过 top 读取。 |
font |
声明一个字体族,并在构建时将其绑定到文件。 |
use … from "./x.contract" |
从另一个 Contract 文件导入组件、形状、样式和 fn。 |
testId |
一个稳定的名称,供 agent、测试和辅助功能工具使用。 |
Contract 省略了什么,以及为什么
- 视图中没有 JavaScript,也没有 I/O。表达式没有副作用。副作用只发生在 action 中,作为赋值、
send、refresh,或像focus(…)和share(…)这样的命令。数据只跨越一个接缝:数据源作出应答,运行器检查该应答。 - action 中没有循环,没有
await。列表是视图的带键for,计算是derive或fn(action 可以用let命名一个值),任何更慢的东西都放在数据源中。会变化的列表通过 mutation 改变其数据所在的位置,如上面的示例所示。这正是让计划能够在四个宿主上以相同方式被烘焙、diff、检查和执行的原因。 - 没有逃生舱;只有访问舱口。没有任何东西能脱离计划。当应用需要平台控件时,它使用原生模块:一个带连字符的标签,如
exact-map,由 Swift、Kotlin 或 Rust 支持,由内核像其他任何盒子一样布局。当它需要 Exact 本身构建的平台对象(一个UIView、一个导航控制器、一个 DOM 元素)时,它标记节点hatch="word",其原生代码被交给该对象:一个访问舱口,即构建机器时所用的面板,以便能够触及内部。它配置 Exact 所创建的内容,并且仅通过作用于已编写的节点来改变 Contract 状态,就像人一样。
这些限制服务于原则。代理无法连接它无法编写的数据竞争。不含 JavaScript 的计划启动迅速。用 CSS 自己的语言编写的视图在每个宿主上布局相同,并且在每个宿主上都显得自然。
编译器会回话
$ cargo run -q -p contract -- build app.contract app.contract:4:5 [type-assign] `count` is `number`, cannot assign `string` app.contract:7:23 [lower-unknown-attr] `text` has no attribute `size`; `size` is spelled `font-size` here, the web's name
一次运行最多报告二十个独立错误,而不仅仅是第一个。其他子命令:--json 用于工具,symbols(以 JSON 形式表示的定义和引用),fmt(保留源),types(为 TypeScript 源生成app.contract.d.ts),以及rust(Rust 数据 crate 的形状)。完整语言规范见docs/contract-grammar.md。
示例应用
每个应用都位于apps/<name>/中。在 Web 上运行一个,使用bun host/web/dev.mjs --app <name>;在 macOS 上使用bun host/apple/build.mjs <name>-apple --run;在 iOS 上通过添加--ios。所有这些截图都来自 Web 宿主,由scripts/agent.mjs拍摄。
要学习,请从Shelf这个食谱应用开始。它没有指定任何颜色和字号,而scripts/no-tells.mjs使其遵守这一点,因此它展示了一个将呈现留给平台的应用在每个宿主上的样子。然后阅读以下四个,了解各自教了什么。它们是在 Exact 将呈现留给平台之前编写的,并绘制自己的颜色和尺寸,所以取其结构,而非其风格:
| 应用 | 它教什么 |
|---|---|
| Caltrain | 一个 Rust 数据 crate、编写的测试、路由、一个可选的 GPU 模块 |
| Weatherlight | TypeScript 与fetch和授权 |
| Fieldnotes | 存储:SQLite、文件、备份和恢复 |
| Recorder | 通过@exact/media-capture和@exact/recording使用麦克风 |
![]() |
![]() |
![]() |
![]() |
| Caltrain 定义 v1 的应用:附近车站、实时出发板、搜索、主题、Canvas 2D 线路图,以及极光 GPU 天空。Rust 数据。每个宿主。 |
Weatherlight 来自 Open-Meteo 的天气,位于 GPU 动画天空下,跟随你选择的小时。TypeScript 数据和一个 wgpu 模块。 |
Spark 一个滑动卡组:用手指扔出一张卡片,它会以你的释放速度飞走。每个宿主上的平移手势和弹簧。 |
Expose 一个模拟手机操作系统(锁屏、主屏幕、带 AI 助手的 Messages、Reader),使用自定义字体。TypeScript。助手需要一个 OpenRouter 密钥。 |
![]() |
![]() |
![]() |
![]() |
| Calendar 月页面、可拖拽的表单、在日期之间拖动的事件、壁纸主题、SQLite。它位于 apps/之外,因此设置EXACT_APP_DIR来运行它。 |
Sparkline 一个市场列表,包含绘制并脉动的动画 SVG 图表。 |
Photo Editor 通过原生模块进行旋转、平移和裁剪。 |
Video Player 一个捆绑的片段,当键盘打开时缩小让路。 |
![]() |
![]() |
| Exact Live 一个创意制作工作区:一个通过拖动重新排序的照片网格、一个可以承受 10,000 条消息突发的团队聊天,以及一个 Markdown 运行手册。Rust 数据。 |
Still(交互画廊) 拖动以重新排序、平移和缩放照片,一个可调整大小的面板内含嵌套列表,以及 100 到 25,000 条记录。 |
![]() |
![]() |
| Reflow 七项排版研究,灵感来自 Cheng Lou 的 Pretext 演示:文字环绕移动的球体、一条龙、瀑布流布局和杂志跨页。行高来自构建时读取的字体度量。 |
Textflow 使用 CSS 的 shape-outside 和 wrap-flow 让文字环绕形状:可拖动的圆球、一位舞者、一个编辑排版。一个 Rust walker 被所有宿主共享。 |
![]() |
![]() |
| Markdown 一个 Markdown 阅读器,可从 Finder 或命令行( mdview README.md)打开文件。它的同类 LLP(llpview)读取一个包含编号设计文档的文件夹。 |
Conduit(RealWorld) RealWorld Medium 克隆,对接其托管 API。它是将 Exact 的 Web 构建与 React 19 进行比较的基准测试。仅限 Web。 |
更多应用、实验室和压力测试
| 应用 | 展示内容 | 备注 |
|---|---|---|
| Fieldnotes | 一个本地笔记本,支持固定、搜索、SQLite 存储以及 JSON 备份和恢复 | TypeScript + Rust |
| Messages | Expo 聊天演示的移植版,通过 OpenRouter 与模型对话 | 需要 bun apps/messages/service.ts 和 OpenRouter 密钥 |
| Type Tour | 以手机操作系统屏幕作为 Expose 字体的字体样本 | 完全没有 JavaScript |
| Map Demo | 原生模块:Apple 上使用 MapKit,Web 上使用 OpenStreetMap | |
| Carousel | 一个包含 25,000 张卡片的水平虚拟化列表 | |
| Motion Gallery | 动画 GIF 和 WebP 图像以及 CSS 关键帧,跨宿主进行比较 | |
| Update Lab | 在运行中的应用里实时替换 Contract、TypeScript 和 Rust | 手动测试实验室 |
| Messages Stress、Completion Storm、Markdown Stress | 10 万条消息历史、128 个并行请求和 4 MiB 文档,在负载下 | 可选工作负载 |
| Native Fixture、Auth Fixture | 原生模块接口;针对本地服务器使用 PAR、DPoP 和 PKCE 的 OAuth | 测试夹具 |
| Beacons、Tennis 等 | 基于可选 engine 插件的游戏:Rust 游戏逻辑、Contract 菜单 | bun game/dev.mjs beacons |
目前可用的功能,以及尚不可用的功能
v1 标准:一个真实应用,而非演示,从单一 Contract 源在 Web、macOS、iOS 和 Linux 上运行,并满足 rules/RULES.md 中的时间预算。 Caltrain 就是那个应用,它在这四个平台上都能运行。这里其他的一切之所以被纳入,是因为真实应用需要它。
可用
- 四个宿主。Web、macOS(AppKit,无 SwiftUI)、iOS(UIKit,在模拟器和设备上)以及 Linux(GPU 或 CPU 绘制,DRM/KMS 或无头)。现有的 Swift 应用可以通过 ExactKit 嵌入 Exact 会话。
- 布局与文本。CSS 块、flex 和 grid;
position: static和包含块;aspect-ratio;声明的字体;文字环绕形状。 - 绘制。渐变、阴影、
backdrop-filter、玻璃材质、完整 SVG、每个宿主上的 Canvas 2D,以及首像素后加载的 wgpu GPU 画布。 - 动效。CSS 过渡、弹簧、
@keyframes、带释放速度的跟随释放手势(平移、滑动、捏合)、布局过渡、退出动画,以及滚动或拖动关联的时间线。 - 列表。虚拟化列表,垂直或水平,一层深度。垂直
list的行由平台决定:UIKit 的分隔符和高亮、Mac 上的表格、Web 上行间的一条线,应用本身不绘制任何内容。 - 控件与媒体。 输入、
select、一个在 Web、macOS 和 iOS 上的所见即所得 Markdown 编辑器、包括动图的图像、视频、针对应用声明的文件类型的选择器,以及分享。 - 数据。 TypeScript 或 Rust 源码,可选运行在 worker 上。经授权检查的
fetch和流、文件与 SQLite(原生及浏览器内)、Keychain 密钥,以及 OAuth 登录。路由器为每个标签页维护一个栈。 - 原生模块。 平台控件以连字符标签形式提供。
- Web,服务端。 页面在构建时或按请求由 Rust 渲染器预渲染,然后就地接管而非水合。
- 交付。
scripts/deploy.mjs发布 Web 根目录和签名的更新包,已安装的应用在首像素后获取。Apple 应用在活跃状态下按每种激活策略每十五分钟检查一次;deploy.check.interval以秒为单位覆盖该间隔。检查会暂存候选版本;deploy.activate控制其何时应用。TypeScript 和 Rust 逻辑可在开发中实时替换。每次 Web 构建都会生成安装页,exact release为 Mac 应用签名并公证。 - 代理与测试。 每个主机上的十种操作,包括实体 iPhone;契约测试;source map;录制的截图。
尚未支持,或完全不支持
- Windows。 应用默认在 DirectX 11 合成交换链上绘制墨迹,或在清单声明
game时使用 CPU。host.windows.painter: "ink"或"cpu"选择任一绘制器,EXACT_PAINTER覆盖两者。Skirmish 是用 Exact 可选游戏引擎构建的独立游戏;CPU 默认值保留了其模块自有的窗口呈现。Windows Desk 演示了通过契约和内核之上的原生控件,并配有应用本地的 Win32 呈现器;Windows 控件的整体对齐和交付仍未完成。 - Android。 早期阶段。应用可为 Android 构建并在模拟器或手机上运行:
bun exact.mjs android构建它,bun exact.mjs test android运行其测试,bun exact.mjs agent android …驱动它。需要 Android SDK、Rust 目标aarch64-linux-android,以及 Hermes Android 包(bun scripts/hermes-android.mjs build),直到 Ibex 发布提供。交付和商店打包尚不存在。 - 没有 JSX 或 React 层。 数据接缝之上不运行任何 JavaScript。大门保持敞开,但没人在构建它。
- TypeScript 尚不能导入 npm 包的代码。
app.ts导入自己的本地文件;从包中只导入类型(import type),构建会检查但从不运行。 - 生产环境中的代码更新。 签名交付涵盖计划和资源。TypeScript 或 Rust 模块的签名交付尚未实现,应用商店规则也对此有限制。
- Linux 编辑。 Web 和 Apple 主机可编辑;Linux 只读。Linux 也没有分词器,因此泰语、老挝语、高棉语和缅甸语的流式文本仅在空格处断行。
- v1 有意排除。 相机访问、手势竞技场、通用布局动画、网格或瀑布流虚拟化、
iframe之外的通用 webview,以及开发者工具 UI。 - 没有打包的 CLI。 你从源码检出工作。
exact new应用通过路径引用它。
仓库地图
| 路径 | 内容 |
|---|---|
kernel/ |
exact-kernel:节点竞技场、线框、事务、Taffy 布局和导出。kernel/tables/schema.json 声明每个节点类型、属性和操作码。 |
contract/ |
契约编译器(syntax → types → analyze → lower)、其 CLI 和语料库 |
plan/、runner/ |
计划格式,由单一 JSON 权威生成,以及运行它的 VM |
motion/ |
CSS 过渡、弹簧,以及用于非 Web 主机的可寻址时钟 |
host/ |
web/、web-js/(JS 目标)、apple/(Rust 核心、ExactKit Swift 包、AppKit 与 UIKit)、linux/、render/(服务器页面),以及更新适配器与光栅化器 |
js/、logic/、data/ |
TypeScript 执行器(Hermes,以及 Web 上的浏览器引擎)、Rust 逻辑模块,以及数据接缝 |
gpu/、canvas/、textflow/、markdown/ |
按需加载的能力:wgpu 画布、Canvas 2D、形状环绕文字,以及 Markdown 编辑器 |
route/、update/、bake/、filesystem/ |
路由器、签名更新存储、交付烘焙,以及应用自有文件 |
apps/、examples/、game/ |
上述应用、Calendar 示例,以及可选的游戏引擎 |
scripts/ |
exact.mjs、agent.mjs、smoke.mjs、metrics.mjs、deploy.mjs,以及各项检查 |
docs/ |
面向应用作者的指南:从这里开始、Contract 语法、配方与常见陷阱 |
rules/ |
RULES.md,Exact 上的工作是如何进行的 |
vendor/taffy/ |
Taffy,以及 Exact 的补丁 |
在 Exact 上工作
从 rules/RULES.md 开始,这一页讲这里的工作是如何进行的。智能体
还应阅读 AGENTS.md。简而言之:
-
六项阻塞性检查,总计 65 秒。任何更慢的检查都按提交异步运行。
cargo build --all-targets --keep-going cargo test --tests --no-fail-fast cargo clippy --all-targets --keep-going -- -D warnings && cargo fmt --all -- --check bun scripts/caps.mjs # budgets: 1,500 lines per source file, document caps bun scripts/boot.mjs # counts the module graph before first pixel cargo check --workspace --all-targets --keep-going # every crate compiles
前三项构建并测试 Cargo 的
default-members。最后一项还会编译 所有其他 crate,因此它们之外的主机或应用无法在未被发现的情况下停止编译。它 需要将固定版本的 Bun(package.json)放在PATH的最前面:应用 crate 的构建脚本会运行它。 -
通过运行来验证,而不是通过阅读。构建改动所涉及的应用并驱动它:
bun scripts/smoke.mjs <web|macos|ios|linux>在该主机上驱动整个应用。 -
删除,而非弃用。1.0 之前没有兼容性垫片。生成 文件是构建出来的,绝不提交。
-
智能体可自由移除装置,且不得新增(检查、脚本、注册表、设计 文档),除非有人明确要求。
-
只有在有实现者和日期时才编写规范。
详细的操作说明涵盖工具链、Contract CLI 的 JSON 格式、源
映射、TypeScript 与 Rust 数据模块、存储、实时替换、交付信任、
安装页面,以及压力测试框架。它们位于
docs/reference.md。
该工作区采用 MIT 许可证(Cargo.toml)。
Exact 的 Snapback4 适配器使用已发布的 0.4.33,并对应不可变的
Rust 源码修订 3653cae823145170df3e720f5e321bd1b5b4d79d。
接线与设备重建请参见 snapback4 包的 README。
来源:Hacker News · github.com



















