跳到正文
Hacker News· vmg12·· 6 小时前AI 评分60

Expo 开源跨平台 UI 框架 Exact,同一份 Contract 跑在 Web、macOS、iOS 和 Linux

Exact – a new cross platform UI framework

AI 导读

Expo 发布开源跨平台应用运行时 Exact,界面用声明式语言 Contract 描述、数据逻辑用 TypeScript 或 Rust 编写,编译成 plan 后在 Web、macOS、iOS 和 Linux 上原生渲染,不使用 webview。

正文 · AI 翻译

编写一次应用。它就能在 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
Softwear's hoodies page in Chrome Softwear's hoodies page as a native iOS app Softwear's hoodies page as a native macOS app
浏览器自身的 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"
WebmacOSiOS Simulator
The todo app on the web host The todo app on macOS The todo app on the 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 Weatherlight Spark Expose
Caltrain
定义 v1 的应用:附近车站、实时出发板、搜索、主题、Canvas 2D 线路图,以及极光 GPU 天空。Rust 数据。每个宿主。
Weatherlight
来自 Open-Meteo 的天气,位于 GPU 动画天空下,跟随你选择的小时。TypeScript 数据和一个 wgpu 模块。
Spark
一个滑动卡组:用手指扔出一张卡片,它会以你的释放速度飞走。每个宿主上的平移手势和弹簧。
Expose
一个模拟手机操作系统(锁屏、主屏幕、带 AI 助手的 Messages、Reader),使用自定义字体。TypeScript。助手需要一个 OpenRouter 密钥。
Calendar Sparkline Photo Editor Video Player
Calendar
月页面、可拖拽的表单、在日期之间拖动的事件、壁纸主题、SQLite。它位于apps/之外,因此设置EXACT_APP_DIR来运行它。
Sparkline
一个市场列表,包含绘制并脉动的动画 SVG 图表。
Photo Editor
通过原生模块进行旋转、平移和裁剪。
Video Player
一个捆绑的片段,当键盘打开时缩小让路。
Exact Live Still, the interaction gallery
Exact Live
一个创意制作工作区:一个通过拖动重新排序的照片网格、一个可以承受 10,000 条消息突发的团队聊天,以及一个 Markdown 运行手册。Rust 数据。
Still(交互画廊)
拖动以重新排序、平移和缩放照片,一个可调整大小的面板内含嵌套列表,以及 100 到 25,000 条记录。
Reflow Textflow
Reflow
七项排版研究,灵感来自 Cheng Lou 的 Pretext 演示:文字环绕移动的球体、一条龙、瀑布流布局和杂志跨页。行高来自构建时读取的字体度量。
Textflow
使用 CSS 的 shape-outside 和 wrap-flow 让文字环绕形状:可拖动的圆球、一位舞者、一个编辑排版。一个 Rust walker 被所有宿主共享。
Markdown reader Conduit, the RealWorld app
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