Exact – Expoチームによる新しいクロスプラットフォームUIフレームワーク
Exact – a new cross platform UI framework by the Expo team (github.com)
要約
Expoチームが開発した新しいクロスプラットフォームUIフレームワーク「Exact」が発表されました。このフレームワークは、Contractという宣言型言語とTypeScript/Rustで記述されたロジックを用いて、Web、macOS、iOS、Linuxでネイティブに動作するアプリケーションを一度の記述で開発できます。主な原則は、AIエージェントによる開発の容易さ、速度、そしてWebを基準にしながらも各プラットフォームの特性を活かすことです。
全文翻訳
Exact
一度アプリを書けば、Web、macOS、iOS、Linuxでネイティブに動作し、AIエージェントがそれらを構築、実行、確認、テストできます。
ヒント
コーディングエージェントで試してみてください。 Xcode、Rust、Bun、ChromeがインストールされたMacで、Claude Codeに次のプロンプトを与えてください。
https://github.com/expo/exact をクローンし、そのREADMEに従って新しいExactアプリを`exact new`で作成してください。最初に`bun scripts/exact.mjs setup`を実行してください。アイテムを追加、チェックオフ、削除でき、残りの数を表示できるToDoリストを作成してください。ビューはContractに配置し、リストは`app.ts`に配置してください。
`app.test.contract`を書き、`scripts/agent.mjs`を使ってWeb、macOS、iOSシミュレーターで実行し、その後すべてのプラットフォームでアプリを開いてください。
このプロンプトをクリーンなクローン(Hermesは既にマシンキャッシュにある、Cargoのキャッシュはウォーム)で与えられた初期のエージェントは、約57分で完了し、そのテストは3つのプラットフォームすべてでパスしました。そのうち約25分は最初のmacOSとiOSのビルド(それぞれ約13分)で、その後のビルドは1〜2分で完了します。Hermesがインストールされていないマシンでは、ソースビルドが最初に行われ、時間が追加されました。現在のセットアップでは、ホスト/iOSバンドルを一度ダウンロードして検証します。通常のビルドはオフラインのままです。
| Web | iOS | macOS |
|---|---|---|
| [画像: ChromeでのSoftwearのフーディーページ] | [画像: ネイティブiOSアプリとしてのSoftwearのフーディーページ] | [画像: ネイティブmacOSアプリとしてのSoftwearのフーディーページ] |
| ブラウザ独自のDOMとCSS | UIKitビュー、ネイティブコントロール、システムのタブバー | AppKitビューとネイティブコントロール |
エージェントドライバーによってキャプチャされた、3つのホストで同じ画面である1つのapp.contract。Softwearはexact-templatesから生成できるデモストアです。Alternative Apparelのカタログを表示し、そのブランドとは提携しておらず、実際の注文や支払いを受け付けません。
Exactはクロスプラットフォームアプリケーションランタイムです。インターフェースは小さな宣言型言語であるContractで記述し、データロジックはTypeScriptまたはRustで記述します。Contractコンパイラはインターフェースをコンパクトなプランに変換します。Webでは、そのプランは~20 KBのランタイムを持つ1つのESモジュールとなり、実際のDOMを駆動します。macOS、iOS、Linuxでは、Rustランナーが同じプランを実行し、RustレイアウトカーネルがCSSルールで各ボックスを計算し、各プラットフォームは独自のツール(AppKit、UIKit、またはGPUペインター)で描画します。UIにはWebviewやJavaScriptブリッジはなく、最初のピクセルが表示される前にアプリのJavaScriptは実行されません。
注意
Exactは1.0未満であり、毎日変更されます。APIの安定性、後方互換性、移行ガイドはありません。これは意図したものです。
アップグレードノート:Snapbackミューテーションの返信でok: falseが返された場合、現在はrefused(why)をスローし、failure(slot)を公開します。snapback4パッケージのREADMEに、変更前後の情報とinherited-appコマンドがあります。
目次
3つの原則
1. エージェントネイティブ
Exactは、コードの多くがAIエージェントによって書かれることを想定しています。エージェントは、人が介在することなく、アプリを作成し、実行し、確認し、操作し、それが機能することを証明できます。
- 10の操作、すべてのホストで同じ。
tree · screenshot · tap · type · state · layout · logs · clock · prefer · perfは、1つのスクリプトscripts/agent.mjsを通じてWeb、macOS、iOS(シミュレーターまたは実機iPhone)、Linuxを駆動します。10個であるのは意図的です。エージェントAPIは、誰もが記憶できる範囲になるまで、合理的な追加が1つずつ行われることで成長します。 - クロックはエージェントのもの。 2つの操作の間には何も動きません。アニメーションが終了するまで待機するのではなく、エージェントは
clock +60000またはclock settleと指示し、結果を読み取ります。タイミングの不具合や待機はありません。 - テストはその操作のスクリプトです。
app.test.contractファイルは、2番目の評価子なしで、どのホストでも変更なく実行されます。 - すべてのツールは機械と対話します。
contract build --jsonは、安定したIDとソース範囲を持つ診断情報を提供します。contract symbolsはナビゲーションを提供します。開発用ソースマップは、画面上の任意のノードからそれを宣言した行にまで戻ります。ドライバーは、ソースよりも古いビルドを駆動することを拒否し、再ビルドを要求します。 - Contractは意図的に小さい。 アプリ全体がコンテキストウィンドウに収まります。すべての書き込みは宣言され、すべてのリストはキー付けされ、すべての値は型付けされます。間違いは、静かな誤動作ではなく、何をすべきかを伝える拒否です。
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 | なし |
| 1行変更、そのクレートを再ビルド | 30 s |
| 全てのブロッキングチェックスイート | 65 s |
設計がこれらの数値を維持する方法:
- ブートパスは何も実行・コンパイルしません。 TypeScriptはビルド時にHermesバイトコードにコンパイルされ、最初のピクセル後にロードされます。最初のフレームはプランに焼き込まれたデータから生成されます。
- 使用したものだけを支払う。 GPUレンダリング、Markdownエディタ、テキストフロー、ブラウザSQLiteは別々のアーティファクトであり、オンデマンドでロードされます。コアクレートの機能フラグはありません。
- Webには独自の小さなランタイムがあります。 RealWorld("Conduit")ベンチマークでは、React SSRビルドがハイドレートされる前に、Exactのランタイムは約300 ms早く起動しました。Reactの81 KBに対して26 KBのコードを使用しました(
apps/realworldのRealWorldベンチマーク)。 - 編集は約0.1秒で反映されます。
exact newで作成された新しいアプリでは、ContractまたはTypeScriptの編集を保存すると、約99 msで再ビルドされ、ページに表示されました。
3. Webを書き、プラットフォームをシップする
あなたが書くのはCSSです:語彙、レイアウトモデル、開発ループ。あなたが言わないことはプラットフォームのものです:コントロール、色、タイプ、モーション、ジェスチャー、キーボード、アクセシビリティは、UIKitまたはAppKitで手作業で構築されたアプリと同じように、Webでは注意深く構築されたWebアプリと同じように表示されます。あなたが書いたものが勝ちます。順序は、作成者、次にプラットフォーム、次にCSSのデフォルト、そして1つのテストが論争を解決します:プラットフォームを知っている人は気づくでしょうか?
- レイアウトは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はシステムのボタン、プッシュされた画面にはシステムのBackがあります。言葉数の少ないアプリは、よりホームに馴染みます。 - ブラウザは、レイアウトとあなたが書いたもののためのパリティオラクルです。同じ入力、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/)は4つのパス(構文、型、分析、低下)でコンパイルされます。ビルド時にプランは焼き込まれます:定数データはプランに評価されるため、最初のフレームには他に何も必要ありません。 - ランナー (
runner/)はプランの仮想マシンです。キー付けされたコンポーネントインスタンス、イベント、シーク可能なクロック上のタイマー、データシームを処理します。 - カーネル (
kernel/)は、パッチ適用されたTaffyによるCSSレイアウトを持つカラム状ノードアリーナです。ホストはテキストを測定します:AppleではCoreText、Linuxではcosmic-text。Webでは、ブラウザが同じCSSを自分でレイアウトするため、Webビルドにはレイアウトエンジンは含まれません。 - ホスト (
host/)はツリーを表示します。WebホストはDOMノードを書き込み、ブラウザはCSSトランジションを実行します。Appleホストは、AppKitおよびUIKitプレゼンターの下にあるC ABIを持つ静的ライブラリです。Linuxホストは、DRM/KMSとevdev入力に、またはオフスクリーンのバッファにツリー自体を描画します。 - データソースは1つのシームの下にあります。TypeScriptはネイティブホストのHermes上およびWeb上のブラウザ独自のエンジンで実行されます。Rustはネイティブで実行され、ライブで置き換え可能です(共有ライブラリとして、またはiOSとWeb上のWasmとして)。すべての回答は、Contractが宣言した形状と比較されます。ネットワーク、ファイル、SQLite、およびシークレットへのアクセスを許可します。
- オプションのピースは別々のアーティファクトです:wgpu GPUモジュール(AppleではMetal、ブラウザではWebGPU)、ネイティブモジュール(プラットフォームウィジェットに裏打ちされたハイフン付きタグ、例:MapKit)、およびゲームエンジンアドオン。
クイックスタート
これらの手順はmacOS(Apple Silicon)で実行されました。LinuxはWebループとLinuxホストで動作します。
1. ツールのインストール
-
Rust はrustup経由でインストールします。
rust-toolchain.tomlでピン留めされた安定版ツールチェーンは、Cargoが最初に実行されるときにインストールされます。 -
Bun 1.4.2、
package.jsonがピン留めしているバージョンです。Nodeとnpmは必要ありません。 -
Google Chrome。 エージェントはヘッドレスChromeを通じてWebを駆動します。別のChromiumを使用するには
CHROMEを設定してください。 -
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のOSバックエンド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ナイトリー版Rustツールチェーン、それらのコンポーネント/ターゲット(ナイトリー版のclippyはゲームのWebベイクをリンティングします)、一致するwasm-bindgen、ピン留めされたBinaryenおよびBun依存関係をインストールし、ピン留めされたHermesホストバンドルとこのMacがビルドするiOS/tvOSバンドルを取得し、exact2のロックとゲームSDKのクレート(game/app/shells.lock)を取得します。これは、すべてのベイクがオフラインで解決されるためです。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, http://127.0.0.1:8765/ で表示
最初の実行はツールチェーンをコンパイルし、数分かかります。その後、apps/caltrain/app.contractを開き、テキストを変更して保存します。ページは約0.1秒で再ビルドされ、リロードされます。電話で同じページを開くには--lanを追加します。サーバーは印刷された名前(DNSリバインディングガード、他のHostは421を返す)にのみ応答します。トンネル経由で共有するには、トンネルのホスト名を--allow-host <name>(繰り返し可能、name:portはポートをピン留め)で指定します。許可された名前はページとそのリロードストリームに到達しますが、ローカルインストーラーのトークンには到達しません。
3. ネイティブで実行する
bun host/apple/build.mjs --run # macOS
bun host/apple/build.mjs --ios --run # iOSシミュレーター (--device --run で接続されたiPhone用)
cargo build --profile host-dev -p caltrain-linux # Linux: DRM/KMSコンソール、またはどこでもヘッドレス (--release で出荷)
最初のネイティブビルドはラップトップで10〜15分かかります。その後のビルドはそれを再利用し、1〜2分で完了します。盲目的に待つのではなく、その出力を監視してください。iOSコマンドは、既に起動しているiPhoneシミュレーターを使用するか、最新のiPhone Proを起動します。選択するには、EXACT_SIMをその名前またはUDIDに設定します(またはbuild.mjsに--simを渡します)。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 # エージェントのWebホストが提供するビルド
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の3つのテストは、Webホストで約2秒かかります。各操作は1つのシェル引数なので、引用符で囲んでください:"type new-todo Buy milk"はBuy milkと入力します(ターゲット以降はすべてテキストです)。"screenshot film.png over 600 every 50"は、コンタクトシートとしてモーションを撮影します。アニメーションを取得するには.apng名を使用してください。
5. 独自のアプリを作成する
bun scripts/exact.mjs new ../hello # または`bun link`を一度実行し、その後`exact new ../hello`を実行
cd ../hello
bun exact.mjs contract types app.contract -o app.contract.d.ts # 最初の実行でコンパイラがビルドされます
bun exact.mjs web # 開発ループ、http://127.0.0.1:8765/ で表示
bun exact.mjs test web tests/*.test.contract # アプリの*.test.contract、または指定されたファイル
bun exact.mjs mac --run # このMac
bun exact.mjs ios --run # iOSシミュレーター
exact newはスタンドアロンアプリを作成します:app.contract(ビュー)、app.ts(データ)、app.json(マニフェスト:名前、バンドルID、ホスト、デプロイポリシー)、および小さなweb/とapple/ホストクレート。そのAGENTS.md(およびCLAUDE.md)は、コーディングエージェントをdocs/start-here.md(読むべき唯一のガイド)およびbun exact.mjs help(コマンド用、コンパイラ用のbun exact.mjs contract …、カードの語彙用のcontract vocabを含む)に向けます。その前に、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は、エージェントに1行で問題点を記録するように要求します:bun exact.mjs feedback note "<what happened>"。失敗または遅延したドライブ、ビルド、検証、およびパスしたtestまたはverifyは、1行の名前で終了します(プロジェクトごとに1日1回まで)。ノートはアプリの.exact/に、exact2のコミット、最後のコマンドの動詞、終了コードと期間、OSとともに保存されます。コマンド出力やアプリのコードやファイルは含まれません。プロジェクトの回答が許可する場合のみExpo(https://exact-diaries.ccheever-0f6.workers.dev)に送信されます:always、またはbun exact.mjs feedbackが送信される内容を正確に表示した後のask(デフォルト)へのyes。プロジェクトに対してはbun exact.mjs feedback neverで送信を停止します。EXACT_FEEDBACK=0は通知をミュートし、マシン全体でのすべての送信を拒否します。docs/diary.mdに詳細があります。
Contract
アプリを構築するエージェントは、start-here.mdから開始します。これは唯一必須の読み物であり、その後、各質問のレシピを参照します。さらに詳しい情報は、完全な人間向けガイド、エージェント向けガイド、または文法と語彙リファレンスを参照してください。
Contractは、アプリが表示するものと状態がどのように変化するかを記述します。ファイルを取得したり、ファイルを読み取ったり、任意のコードを実行したりしません。それはデータソースの役割です。ここに完全なToDoアプリがあります:Contractファイル、TypeScriptファイル、およびテストです。このExactアプリは、Web、macOS、およびiOSシミュレーター用に構築され、そのテストは3つすべてでパスしました。
exact newで作成したアプリで試すには、app.contractとapp.test.contractを以下の例に置き換えてください。app.tsでは、生成されたexport const appId = …行を保持し、残りを以下のTypeScriptに置き換えてください。
// app.contract: ビュー、その状態、および各アクションが変更するもの。色、フォント、コントロールサイズは指定しません。各ホストが独自のヘッダー、フィールド、ボタン、チェックボックス、リストを描画します。
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: リストを保持し、app.contractの要求に応答します
import type { Answer, Sources } from './app.contract.d.ts';
export const grants = ''; // 例: '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: `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 |
|---|---|---|
| [画像: Webホスト上のToDoアプリ] | [画像: macOS上のToDoアプリ] | [画像: iOSシミュレーター上のToDoアプリ] |
ビューはリスト自体を編集しません。アクションはミューテーションをソースに送信し、app.tsはデータを変更して応答し、refreshes todosはリストを再度要求します。データは1つの場所に存在し、それへのすべての変更は、エージェントが確認できる名前付きの型付き呼び出しです。このリストはメモリ内にあります。起動間で保持するには、app.tsにsqlite.open app:/data/todos.dbのようなグラントを与え、storage.sqliteを使用します。これはFieldnotesが行っていることです。
コンポーネント
| コンセプト | 内容 |
|---|---|
shape | 閉じられたレコード型。フィールドはnumber、string、bool、別のshape、option<T>、またはT[]です。 |
component | UIの単位。ファイル内の最初のものがルートです。コンポーネントはpropsを受け取り、祖先がprovideするものをinjectでき、slotをchildrenで埋めることができます。 |
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=…(キーは必須)、オプションの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ファイルからコンポーネント、shape、スタイル、fnをインポートします。 |
testId | エージェント、テスト、アクセシビリティツール用の安定した名前。 |
Contractが除外するもの、その理由
- ビューにJavaScriptがなく、I/Oもありません。 式には効果がありません。効果はアクションでのみ発生します。代入、
send、refresh、またはfocus(…)やshare(…)のようなコマンドとしてです。データは1つのシームを通過します:ソースが応答し、ランナーはその応答をチェックします。 - アクションにループがなく、
awaitもありません。 リストはビューのキー付きforであり、計算はderiveまたはfn(アクションはletで値を名前付けできます)であり、それより遅いものはソースにあります。変更されるリストは、上記の例のようにミューテーションを通じて、データの保存場所を変更します。これにより、プランを焼き込み、差分を取り、検査し、4つのホストで同じように実行できます。 - エスケープハッチがなく、アクセスハッチがあります。 何もプランの外に出ません。アプリがプラットフォームウィジェットを必要とする場合、ネイティブモジュールを使用します:Swift、Kotlin、またはRustに裏打ちされたハイフン付きタグ(例:
exact-map)で、カーネルによって他のボックスと同じようにレイアウトされます。プラットフォームオブジェクトExact自体(UIView、ナビゲーションコントローラー、DOM要素)を構築したものを必要とする場合、ノードにhatch="word"とマークし、そのネイティブコードにそのオブジェクトが渡されます:アクセスハッチ、機械が内部にアクセスできるように構築されたパネルです。作成されたものを設定し、作成されたノードに対してアクションを実行することによってのみContractステートを変更します。これは人間が行うようにです。
これらの制限は原則に役立ちます。エージェントは、書けないデータ競合を配線できません。JavaScriptを含まないプランは高速に起動します。CSS独自の言葉で書かれたビューは、すべてのホストで同じようにレイアウトされ、それぞれでホームのように見えます。
コンパイラが応答する
$ cargo run -q -p contract -- build app.contract
app.contract:4:5 [type-assign] `count` は `number` ですが、`string` を代入できません
app.contract:7:23 [lower-unknown-attr] `text` には `size` 属性がありません。`size` はここでは `font-size`、つまりWebの名前でスペルされています。
1回の実行で最大20個の独立したエラーが報告されます。最初のものだけではありません。その他のサブコマンド:ツール用の--json、定義と参照をJSON形式で取得するsymbols、ソースを保持するfmt、TypeScriptソース用のapp.contract.d.tsを生成するtypes、およびRustデータクレート用のrust。完全な言語は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がそれを強制するため、プラットフォームにプレゼンテーションを任せるアプリが各ホストでどのように見えるかを示します。次に、それぞれが何を教えてくれるかを示すために、次の4つを読んでください。これらはExactがプラットフォームにプレゼンテーションを任せる前に書かれたもので、独自の色とサイズでペイントされているため、スタイルではなく構造を取ってください:
| アプリ | 教えること |
|---|---|
| Caltrain | Rustデータクレート、作成されたテスト、ルート、オプションのGPUモジュール |
| Weatherlight | fetchとグラントを持つTypeScript |
| Fieldnotes | ストレージ:SQLite、ファイル、バックアップと復元 |
| Recorder | @exact/media-captureおよび@exact/recordingを介したマイク |
| [画像: Caltrain] | [画像: Weatherlight] | [画像: Spark] | [画像: Expose] |
| Caltrain 近隣駅、ライブ出発ボード、検索、テーマ設定、Canvas 2Dラインマップ、およびオーロラGPU空を定義するアプリ。Rustデータ。全ホスト対応。 | Weatherlight 選択した時間に合わせたGPUアニメーション空の下でのOpen-Meteoからの天気。TypeScriptデータとwgpuモジュール。 | Spark スワイプデッキ:指でカードを投げると、リリース速度で飛び去ります。全ホストでのパンジェスチャーとスプリング。 | Expose カスタムフォントで設定された偽の電話OS(ロック画面、ホーム、AIアシスタント付きメッセージ、リーダー)。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デモに続く7つのタイポグラフィスタディ:ボールが移動する周りのテキストフロー、ドラゴン、メイソンリー、雑誌のレイアウト。行の高さはビルド時に読み取られたフォントメトリクスから取得されます。 | Textflow CSSのshape-outsideとwrap-flowによる形状の周りのテキスト:ドラッグ可能なオーブ、ダンサー、編集レイアウト。全ホストで共有される1つのRustウォーカー。 | ||
| [画像: Markdown reader] | [画像: Conduit, the RealWorld app] | ||
Markdown Finderまたはコマンドラインからファイルを開くMarkdownリーダー(mdview README.md)。その兄弟であるLLP(llpview)は、番号付きデザインドキュメントのフォルダを読み取ります。 | Conduit (RealWorld) ホストされたAPIに対するMediumクローン。ExactのWebビルドとReact 19を比較したベンチマークです。Webのみ。 |
その他のアプリ、ラボ、ストレステスト
| アプリ | 内容 | 注記 |
|---|---|---|
| Fieldnotes | ピン、検索、SQLiteストレージ、JSONバックアップと復元を備えたローカルノートブック | TypeScript + Rust |
| Messages | OpenRouter経由でモデルと通信するExpoチャットデモのポート | bun apps/messages/service.tsとOpenRouterキーが必要 |
| Type Tour | Exposeフォントのタイプ見本としての電話OS画面 | 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 | 100kメッセージ履歴、128並列リクエスト、4MiBドキュメント、負荷下で | オプトインワークロード |
| Native Fixture, Auth Fixture | ネイティブモジュールインターフェース。OAuthとPAR、DPoP、PKCEをローカルサーバーに対して | テストフィクスチャ |
| Beacons, Tennis, … | オプションのゲームエンジンアドオン上のゲーム:Rustゲームプレイ、Contractメニュー | bun game/dev.mjs beacons |
今日動作すること、まだ動作しないこと
v1の目標:1つの実際のアプリケーションが、単一のContractソースからWeb、macOS、iOS、Linuxで、rules/RULES.mdの予算時間内に動作すること。Caltrainはそのアプリケーションであり、4つすべてで動作します。ここにある他のすべては、実際のアプリが必要としたため、追加されました。
動作するもの
- 4つのホスト。 Web、macOS(AppKit、SwiftUIなし)、iOS(UIKit、シミュレーターおよびデバイス上)、Linux(GPUまたはCPUペイント、DRM/KMSまたはヘッドレス)。既存のSwiftアプリはExactKitを通じてExactセッションを埋め込むことができます。
- レイアウトとテキスト。 CSSブロック、フレックス、グリッド;
position: staticと包含ブロック;aspect-ratio;宣言されたフォント;形状の周りのテキスト。 - ペイント。 グラデーション、シャドウ、
backdrop-filter、ガラス素材、完全なSVG、Canvas 2D(全ホスト)、および最初のピクセル後にロードされるwgpu GPUキャンバス。 - モーション。 CSSトランジション、スプリング、
@keyframes、リリース速度を持つフォロー&リリースジェスチャー(パン、スワイプ、ピンチ)、レイアウトトランジション、終了アニメーション、スクロールまたはドラッグにリンクされたタイムライン。 - リスト。 仮想化リスト、垂直または水平、1レベルの深さ。垂直
listの行は、そのプラットフォームのものです:UIKitのセパレーターとハイライト、Macのテーブル、Web上の行間の1行、アプリによって何も描画されません。 - コントロールとメディア。 入力、
select、Web、macOS、iOSでのWYSIWYG Markdownエディタ、アニメーション画像を含む画像、ビデオ、アプリの宣言されたファイルタイプ用のピッカー、および共有。 - データ。 TypeScriptまたはRustソース、オプションでワーカー上。グラントチェック付き
fetchとストリーム、ファイルとSQLite(ネイティブおよびブラウザ)、Keychainシークレット、OAuthサインイン。ルーターはタブごとにスタックを保持します。 - ネイティブモジュール。 ハイフン付きタグとしてのプラットフォームウィジェット。
- Web、サーバーサイド。 ビルド時にプリレンダリングされたページ、またはRustレンダラーによってリクエストごとにレンダリングされ、ハイドレーションの代わりにインプレースで採用されます。
- デリバリー。
scripts/deploy.mjsはWebルートと署名付きアップデートバンドルを公開し、インストールされたアプリは最初のピクセル後に取得します。Appleアプリはアクティブな間、すべての起動ポリシーの下で15分ごとにチェックします。deploy.check.intervalは秒単位の間隔をオーバーライドします。チェックは候補をステージングし、deploy.activateが適用時期を制御します。TypeScriptとRustのロジックは開発中にライブで置き換えることができます。すべてのWebビルドにはインストールページがあり、exact releaseはMacアプリに署名して公証します。 - エージェントとテスト。 すべてのホスト(物理iPhoneを含む)での10の操作、Contractテスト、ソースマップ、撮影されたスクリーンショット。
まだ、または全くない
- Windows。 アプリはデフォルトでInk on a DirectX 11 composition swap chain、またはマニフェストが
gameを宣言している場合はCPUで動作します。host.windows.painter: "ink"または"cpu"でどちらかのペインターを選択し、EXACT_PAINTERで両方をオーバーライドします。SkirmishはExactのオプションゲームエンジンで構築されたスタンドアロンゲームです。CPUデフォルトは、そのモジュール所有のウィンドウプレゼンテーションを維持します。Windows Deskは、Contractとカーネル上のネイティブコントロールを、アプリローカルのWin32プレゼンターでデモンストレーションします。一般的なWindowsコントロールのパリティとデリバリーはまだ完了していません。 - Android。 初期段階です。アプリはAndroid用にビルドされ、エミュレーターまたは電話で実行されます:
bun exact.mjs androidでビルドし、bun exact.mjs test androidでテストを実行し、bun exact.mjs agent android …で駆動します。Android SDK、Rustターゲットaarch64-linux-android、およびIbexリリースがバンドルをリリースするまでHermes Androidバンドル(bun scripts/hermes-android.mjs build)が必要です。デリバリーとストアパッケージングはまだ存在しません。 - 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/ | 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から始めてください。これは作業方法に関する1ページのドキュメントです。エージェントはAGENTS.mdも読むべきです。要約すると:
- 6つのブロッキングチェック、合計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 # バジェット:ソースファイルあたり1,500行、ドキュメントキャップ
bun scripts/boot.mjs # 最初のピクセル前のモジュールグラフをカウントします
cargo check --workspace --all-targets --keep-going # 全てのクレートをコンパイルします
最初の3つはCargoのdefault-membersをビルドおよびテストします。最後は他のすべてのクレートもコンパイルするため、それ以外のホストまたはアプリがコンパイルを停止させることはありません。最初にPATHにピン留めされたBun(package.json)が必要です。アプリクレートのビルドスクリプトはそれを実行します。
-
読むのではなく、実行して検証する。 変更が触れたアプリをビルドし、それを駆動します:
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を参照してください。