Skip to content

Commit 7f39c07

Browse files
huacnleecodex
andauthored
chore: improve component conventions for AI-assisted development (longbridge#2993)
## Summary Improves the conventions AI consumers can transfer between GPUI Kit components: predictable constructors, semantic value callbacks, and explicit Form composition. Adds executable consumer examples and strengthens Shell/type feedback so incorrect assumptions are caught by repeatable checks. ## Detailed Changes The tables below cover all 50 changed files. Paths are relative to the repository root. ### Rust component conventions | Area | Files | Detailed change | Consumer behavior and compatibility | | --- | --- | --- | --- | | Checkbox events | `crates/component/src/checkbox.rs` | Adds `on_change` receiving the requested `&bool`; documents application-owned state updates and notification. | The same callback shape can be transferred to Switch and Radio. Existing `on_click` delegates to the same handler; the last setter wins. | | Switch events | `crates/component/src/switch.rs` | Adds `on_change` for pointer and keyboard value requests, using the existing callback storage and activation behavior. | Preserves the existing `on_click` signature, disabled behavior, and controlled-state semantics. | | Radio events | `crates/component/src/radio.rs` | Adds `Radio::on_change` receiving `&bool` and `RadioGroup::on_change` receiving the requested `&usize` index. | Existing `on_click` methods remain aliases. Radio selection retains its existing semantics; the group owner stores `Some(*index)`. | | RadioGroup construction | `crates/component/src/radio.rs` | Makes `RadioGroup::new(id)` public. | The natural `new(id)` constructor now compiles. Defaults remain vertical layout with no selected item; horizontal/vertical constructors remain available. | | Form construction | `crates/component/src/form/form.rs` | Makes `Form::new()` public and implements `Default`. | Both create one field column with labels above controls, matching the existing vertical default. | | Form label orientation | `crates/component/src/form/form.rs` | Adds `label_layout(Axis)`; makes `layout(Axis)` delegate to it; clarifies horizontal/vertical constructor documentation. Corrects the documented label width from 100px to 140px. | Label orientation and `columns(count)` are independent decisions. Existing builders retain their meaning and behavior. | | Form actions | `crates/component/src/form/form.rs` | Adds replaceable `footer(element)` content rendered after fields, spanning all columns, with trailing alignment and a zero minimum width. | `child` continues to accept typed Fields; buttons have an explicit composition slot. No footer row is created unless supplied. The application owns action callbacks and submission. | | Control convention tests | `crates/component/tests/controlled_change_callbacks.rs` | Adds two interaction tests: a four-control matrix across both callback names, both setter orders, enabled/disabled states, mouse/Enter/Space activation; and owner state writeback across redraw and subsequent activation. | Checks one callback per enabled activation, no disabled callbacks, replacement of earlier handlers, public RadioGroup construction, and controlled-state persistence. | | Form contract tests | `crates/component/src/form/tests.rs` | Adds four tests for builder defaults, label direction independent of two field columns, footer geometry, and legacy geometry without a footer. Layout bounds are checked at 360px and 800px. | Verifies labels above/beside controls, row placement, footer full-span/trailing alignment, and unchanged no-footer geometry. | | Table constructor documentation | `crates/component/src/table/state.rs` | Corrects two rustdoc examples to call `TableState::new(delegate, window, cx)`. | Documentation-only signature correction; no Table runtime behavior changes. | ### Shell runtime and generated TypeScript | Area | Files | Detailed change | Consumer behavior and compatibility | | --- | --- | --- | --- | | Single-snapshot checking | `crates/shell/src/plugin.rs` | `ShellRuntime::check` builds one snapshot, materializes its eager native elements, and returns that snapshot's debug description. Keeps the application and callback-owning snapshot alive through materialization. | Registered-component construction failures now reach the check result. `--print-spec` describes the same snapshot that was checked. Adds assertions for exactly one script render and one materialization. | | Structured materialization errors | `crates/shell/src/materialize.rs` | Adds internal `try_materialize`, wrapping the existing materializer in an error frame. | Source checks receive registered-component failures instead of only an error fallback element. Normal rendering continues to use the existing materializer. | | Hidden-window initialization | `crates/shell/src/host.rs` | Completes the catalog-provided window opener before calling runtime checks through the opened window. | Component materializers can access the installed Root and other window facilities. | | CLI failure status and help | `crates/shell/src/host.rs` | Centralizes reporting and process exit in `CheckOutcome::exit`, including failures before window creation; rewrites `check` help to state its actual stages. | Invalid metadata, load/render errors, and materialization failures exit 1. The help explicitly excludes layout, paint, deferred slots, nested view renders, and later interactions. | | Test-only description helper | `crates/shell/src/engine/quickjs/mod.rs` | Gates `render_to_spec` with `#[cfg(test)]` after production checking moves to snapshots. | Internal cleanup; production checks use the same snapshot for inspection and materialization. | | Renderable and native types | `crates/shell/src/typings.rs` | Introduces `NativeElement` for native/Base builders and makes `Element` a union of native and registered component builders. Updates generated factory, host-module, TextView, and FPS declarations accordingly. | Registered components remain usable as children and view results while retaining component-specific signatures such as semantic size. Helpers requiring native-only signatures should use `NativeElement`. | | Fluent type preservation | `crates/shell/src/typings.rs` | Uses explicit receiver generics across fluent methods, parameterized/nullary styles, child/slot composition, state styles, and conditionals. `map` preserves its actual result type. Registered builders derive from `NativeElement` with descriptor overrides and withheld behaviors. | Styling or composition no longer widens a registered builder into an unrestricted element. Unsupported handlers and invalid size values remain rejected after chaining. Generated registered-component types also withhold native `role` and `transition` unless the descriptor provides them; native builders retain these methods. | | Declaration regression assertions | `crates/shell/src/typings.rs`, `crates/shell/src/tests/render.rs` | Updates declaration expectations for native factories, receiver-preserving methods, registered descriptors, imports, and specialized element behavior. | Keeps generated declarations aligned with the revised fluent type structure and existing runtime descriptors. | | CLI integration tests | `crates/component-shell/tests/check.rs` | Adds six tests covering valid typed children and printed descriptions; invalid ordinary Form children; styles on data-only components; load/render failures without hanging; invalid metadata before window creation; and structured error propagation/reset between checks. | Exercises real CLI exit behavior and ensures a failed check does not contaminate the next successful check. | | TypeScript contract fixtures | `crates/component-shell/tests/types/fluent.ts` | Adds a complete positive application exercising typed fluent composition, native roles/transitions, typed Form children, component-specific methods, and `map` return values. Its emitted JavaScript is the actual runtime fixture. | The same positive source must pass strict TypeScript checking and the actual CLI check; there is no independent runtime copy that can omit failing calls. | | Negative TypeScript fixture | `crates/component-shell/tests/types/rejected.ts` | Separates expected compiler failures from executable positive examples, including direct and chained unsupported `role`/`transition` calls. | Fails if a forbidden call becomes accepted; negative expressions are never executed as the valid application. | | Reproducible type test runner | `crates/component-shell/tests/types/run.mjs`, `crates/component-shell/tests/types/package.json`, `crates/component-shell/tests/types/package-lock.json`, `crates/component-shell/tests/types/README.md` | Pins TypeScript 5.9.3; generates declarations in a temporary directory; emits and executes the strict positive TypeScript fixture, checks separate negative fixtures and the shipped JavaScript gallery, and runs six expected CLI rejection cases; cleans up temporary files. Supports an explicit host-binary override. | Tests use the existing editor configuration's `skipLibCheck`; they validate consumer contracts rather than all ambient library declarations. | | Shipped gallery typing | `examples/js_story/stories/registered.js` | Adds a JSDoc array type for MessageScroller messages, preserving literal alignment and variant values. | The shipped JavaScript example passes generated-type checking without changing runtime behavior. | ### Executable examples and acceptance checks | Area | Files | Detailed change | Consumer behavior and verification | | --- | --- | --- | --- | | Isolated consumer package | `examples/ai_recipes/Cargo.toml`, `examples/ai_recipes/Cargo.lock` | Adds an independent workspace depending on the `gpui-kit` facade and a committed lockfile. | Production compilation does not rely on unrelated workspace members supplying missing dependencies or features. The standalone lockfile accounts for most added lines. | | Complete Settings view | `examples/ai_recipes/src/lib.rs` | Retains Input state and its subscription; updates preview text; uses Checkbox/Switch/RadioGroup `on_change` callbacks with owner writeback; composes typed Form fields and an About-button footer; renders dialog, sheet, and notification layers. | Provides one complete source example of state ownership, transferable callbacks, form composition, and overlays. | | Application entry point | `examples/ai_recipes/src/main.rs` | Adds application initialization, assets, window creation, and Root setup around the Settings view. | The recipe has a runnable entry point as well as a compilable view. | | Retained-input regression | `examples/ai_recipes/tests/settings.rs` | Exercises typing after unrelated redraws and checks owner state updates. | Detects lost subscription/state lifetime behavior in the published recipe. | | Recipe inventory and guidance | `examples/ai_recipes/recipes.json`, `examples/ai_recipes/README.md` | Maps source to three published fragments and documents run/sync commands, task-specific acceptance profiles, and verification limits. | Consumers can select the appropriate check and understand what a passing result proves. | | Source-to-doc synchronization | `script/check-ai-recipes` | Adds fragment freshness checks and `--sync`; rejects missing/duplicate markers and empty coverage; checks isolated recipe formatting, production-binary compilation, all targets, and tests. Provides an explicitly limited `--docs-only` mode. | Published recipes must match compiled source. Documentation-only checks cannot be mistaken for compilation or interaction verification. | | Drift-detection tests | `script/tests/test_ai_recipes.py` | Adds three tests for stale source and synchronization, removed/duplicate markers, and empty inventories. | Prevents recipe coverage from silently disappearing or reporting a false pass. | | Acceptance entry point | `script/check-ai` | Adds `docs`, `rust`, `shell`, and `all` profiles with failure propagation and a shared target directory. Rust includes consumer checks, control callback tests, existing legacy-control compatibility tests, and Form geometry tests. Shell includes declaration/render tests, component-host tests, and pinned TypeScript/CLI fixtures. | Provides repeatable task-scoped commands instead of relying on manual interpretation of separate checks. | | CI integration | `.github/workflows/ci.yml` | Runs docs/Rust acceptance profiles on macOS and installs/runs pinned generated TypeScript contracts in the component-shell job. | Recipe drift and the new component conventions are checked continuously alongside component-host type contracts. | ### Documentation and AI discovery | Area | Files | Detailed change | Effect on AI consumers | | --- | --- | --- | --- | | Dependency/facade guidance | `README.md`, `README.zh-CN.md`, `crates/kit/Cargo.toml` | Corrects the facade description to GPUI, Base, Component, and assets; identifies Shell as a separate host dependency and Component Shell as the styled catalog. The English README links executable recipes and acceptance checks. | Avoids generating imports or dependencies based on a nonexistent Shell re-export. | | Shell capability guidance | `crates/shell/README.md` | Fixes the missing `div` import; documents the existing styled host/gallery and component inventory; replaces outdated unavailable-component claims; explains materialization checks and their limits. | Guides consumers to the correct host and actual supported surfaces without implying full Rust API parity. | | Skill entry point | `skills/gpui-kit/SKILL.md` | Links family conventions and tested recipes early; prefers `llms.txt` plus a targeted page, retaining `llms-full.txt` for broader reference. | Makes constructor/state/event/layout decisions discoverable before loading the full catalog. | | Component-family reference | `skills/gpui-kit/references/conventions.md` | Adds a task-to-family table, constructor and ownership rules, semantic callback patterns, independent Form layout decisions, capability-trait imports, and verification guidance. Distinguishes Rust conventions from the JavaScript host catalog. | Lets a model transfer known patterns while recognizing family-specific constructor and state models. | | Usage reference corrections | `skills/gpui-kit/references/usage.md` | Corrects facade/import examples; retains Input/List subscriptions on the owner; shows Checkbox/Switch state writeback through `on_change`; places Form actions in `footer`; replaces the blanket `new(id)` claim with family-specific guidance. | Removes examples that encourage dropped subscriptions, invalid imports, invalid Form children, or overgeneralized constructors. | | Published application recipe | `skills/gpui-kit/references/recipes.md`, `website/docs/getting-started.md`, `website/zh-CN/docs/getting-started.md` | Publishes synchronized copies of the complete Settings view and associated setup/verification guidance; corrects getting-started guidance around the executable consumer. | The three marked source fragments are checked against the same compilable implementation. | | Checkbox documentation | `website/docs/components/checkbox.md`, `website/zh-CN/docs/components/checkbox.md` | Makes `on_change` the documented value callback; explains owner writeback, notification, and `on_click` alias replacement behavior; updates examples. | Establishes one reusable controlled-value pattern. | | Switch documentation | `website/docs/components/switch.md`, `website/zh-CN/docs/components/switch.md` | Updates callback examples/reference to `on_change` and documents the shared alias/ownership contract. | Transfers the Checkbox pattern to Switch without rediscovering event naming. | | Radio documentation | `website/docs/components/radio.md`, `website/zh-CN/docs/components/radio.md` | Documents semantic callbacks, their payloads, compatibility aliases, and the public `RadioGroup::new(id)` default. | Clarifies boolean Radio requests versus indexed RadioGroup requests and predictable group construction. | | Form documentation | `website/docs/components/form.md`, `website/zh-CN/docs/components/form.md` | Adds explicit examples of `Form::new`, `label_layout`, independent `columns`, and `footer`; states that the application owns values, validation, submission, and responsive column choices. | Reduces ambiguity between label orientation and field placement, and shows where action buttons belong. | ## Compatibility | Surface | Breaking change? | Migration or effect | | --- | --- | --- | | Rust component builders | No | Adds methods/constructors; preserves `on_click`, existing Form constructors/layout builders, and default behavior. | | TypeScript `Element` | Yes, at the type level | Changes from an interface to a renderable union. Interface extension/declaration merging against `Element` is no longer supported. Use `NativeElement` for native-specific interfaces/helpers; use `Element` for general render results. | | Registered TypeScript capabilities | Stricter validation | Previously accepted unsupported `role`/`transition` calls now fail compilation, including after chaining. Such calls already failed at runtime. Use a native wrapper for native behavior where appropriate. | | Shell `check` | Stricter failure behavior | Previously missed eager materialization and metadata errors now return exit 1. Passing checks still do not prove layout, paint, or later interactions. | ## Test Plan | Verification | Coverage | Recorded result | | --- | --- | --- | | `script/check-ai docs` | Three synchronized published fragments and three drift-detection tests. | Passed locally. | | `script/check-ai rust` | Isolated production/test-target compilation; one retained-input interaction test; two control callback/state tests; four existing legacy compatibility tests; four Form contract tests. | Passed locally; 11 Rust tests across these suites. | | Control activation matrix | Four controls, mouse/Enter/Space, enabled/disabled, both callback names, and both alias setter orders. | Passed locally. | | Form layout bounds | 360px and 800px widths; independent label direction/field columns; full-span and trailing footer placement; legacy no-footer geometry. | Passed locally. | | `script/check-ai shell` | Generated declarations, Shell rendering, component-host targets, pinned TypeScript positive/negative contracts, shipped JavaScript checking, and real CLI acceptance/rejection. | Passed locally. | | Single-snapshot regression | `cargo test -p gpui-shell --lib runtime_check_returns_the_rendered_description` checks one script render and one materialization. | Passed locally. | | Component Clippy | `cargo clippy -p gpui-component --lib --tests --no-default-features -- -D warnings`. | Passed locally. | | Shell Clippy | `cargo clippy -p gpui-shell -p gpui-component-shell --all-targets -- -D warnings`. | Passed locally. | | Formatting and documentation hygiene | Workspace formatting, changed-file spelling, and diff whitespace checks. | Passed locally. | Local validation was performed on macOS; these results do not assert the current remote CI status. This is an initial component-family improvement, not coverage of every component. Deterministic contract tests do not measure AI first-attempt success rates. Native visual/accessibility review and model benchmarks were not performed. Shell checks cover eager materialization, not layout, paint, deferred slots, nested view renders, or later interactions. Type fixtures use `skipLibCheck` and do not certify all ambient declarations. Authored with Codex. --------- Co-authored-by: Codex <codex@openai.com>
1 parent 9796bb7 commit 7f39c07

50 files changed

Lines changed: 11075 additions & 360 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ jobs:
6262
exit 1
6363
fi
6464
done
65+
- name: Verify executable application recipes
66+
if: ${{ matrix.run_on == 'macos-latest' }}
67+
run: |
68+
python3 script/check-ai docs
69+
python3 script/check-ai rust
6570
- name: Lint
6671
if: ${{ matrix.run_on == 'macos-latest' }}
6772
run: |
@@ -142,6 +147,10 @@ jobs:
142147
key: component-shell-cargo-x86_64-linux-gnu-${{ hashFiles('**/Cargo.lock') }}
143148
- name: Test component bindings
144149
run: cargo test -p gpui-component-shell --all-targets
150+
- name: Verify generated TypeScript contracts
151+
run: |
152+
npm --prefix crates/component-shell/tests/types ci
153+
npm --prefix crates/component-shell/tests/types test
145154
- name: Lint component bindings
146155
run: cargo clippy -p gpui-component-shell --all-targets -- -D warnings
147156
# The inventory test proves every public component is registered or

‎README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ Documentation: <https://gpui-kit.com>
2020
```text
2121
gpui-kit The one crate applications depend on
2222
├── gpui-base Unstyled behavior, state, and infrastructure
23-
├── gpui-shell JavaScript extensions for a Rust host
2423
└── gpui-component GPUI Component: the complete styled UI system
2524
```
2625

27-
`gpui-kit` pins the matching GPUI release and re-exports every layer, so an
28-
application lists a single dependency and never GPUI itself.
26+
`gpui-kit` pins the matching GPUI release and re-exports GPUI, base, component,
27+
and assets, so a Rust application lists a single dependency. JavaScript extension
28+
hosts add `gpui-shell` separately; `gpui-component-shell` supplies the styled catalog.
29+
30+
See the [executable application recipe and AI-assisted development acceptance checks](examples/ai_recipes/README.md) for a tested starting point and verification commands.
2931

3032
## Features
3133

‎README.zh-CN.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ GPUI Kit 是一个综合性的 Rust 桌面应用开发框架。它将生产级 U
1515
```text
1616
gpui-kit 应用唯一需要依赖的 crate
1717
├── gpui-base 无样式的行为、状态与基础设施
18-
├── gpui-shell 为 Rust 宿主提供 JavaScript 扩展能力
1918
└── gpui-component GPUI Component:完整的带样式 UI 系统
2019
```
2120

22-
`gpui-kit` 会固定配套的 GPUI 版本并重新导出全部三层,应用只需声明这一个依赖,无需接触 GPUI 本身。
21+
`gpui-kit` 会固定配套的 GPUI 版本并导出 GPUI、base、component 和 assets,Rust 应用只需声明这一个依赖。JavaScript 扩展宿主另行依赖 `gpui-shell`;`gpui-component-shell` 提供带样式的组件目录。
2322

2423
## 特性
2524

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
use std::{
2+
fs,
3+
path::PathBuf,
4+
process::{Child, Command, ExitStatus, Stdio},
5+
sync::atomic::{AtomicU64, Ordering},
6+
thread,
7+
time::{Duration, Instant},
8+
};
9+
10+
use gpui::AppContext as _;
11+
12+
static NEXT_APP: AtomicU64 = AtomicU64::new(0);
13+
14+
struct CheckApp(PathBuf);
15+
16+
impl CheckApp {
17+
fn new(source: &str) -> Self {
18+
let path = std::env::temp_dir().join(format!(
19+
"gpui-component-shell-check-{}-{}",
20+
std::process::id(),
21+
NEXT_APP.fetch_add(1, Ordering::Relaxed)
22+
));
23+
fs::create_dir(&path).unwrap();
24+
fs::write(path.join("main.js"), source).unwrap();
25+
Self(path)
26+
}
27+
28+
fn run(&self) -> (ExitStatus, String, String) {
29+
let stdout = self.0.join("stdout");
30+
let stderr = self.0.join("stderr");
31+
let mut child = CheckChild(
32+
Command::new(env!("CARGO_BIN_EXE_gpui-component-shell"))
33+
.env("XDG_DATA_HOME", self.0.join("data"))
34+
.arg("check")
35+
.arg(&self.0)
36+
.arg("--print-spec")
37+
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
38+
.stderr(Stdio::from(fs::File::create(&stderr).unwrap()))
39+
.spawn()
40+
.unwrap(),
41+
);
42+
let deadline = Instant::now() + Duration::from_secs(30);
43+
let status = loop {
44+
if let Some(status) = child.0.try_wait().unwrap() {
45+
break status;
46+
}
47+
assert!(
48+
Instant::now() < deadline,
49+
"check did not exit within 30 seconds: {}",
50+
fs::read_to_string(&stderr).unwrap()
51+
);
52+
thread::sleep(Duration::from_millis(20));
53+
};
54+
(
55+
status,
56+
fs::read_to_string(stdout).unwrap(),
57+
fs::read_to_string(stderr).unwrap(),
58+
)
59+
}
60+
}
61+
62+
impl Drop for CheckApp {
63+
fn drop(&mut self) {
64+
fs::remove_dir_all(&self.0).unwrap();
65+
}
66+
}
67+
68+
struct CheckChild(Child);
69+
70+
impl Drop for CheckChild {
71+
fn drop(&mut self) {
72+
let _ = self.0.kill();
73+
let _ = self.0.wait();
74+
}
75+
}
76+
77+
#[test]
78+
fn check_materializes_valid_typed_children_and_preserves_print_spec() {
79+
let app = CheckApp::new(
80+
r#"
81+
import { View, div } from "gpui-kit";
82+
import { HForm, Field } from "gpui-component";
83+
export default class App extends View {
84+
render() {
85+
return new HForm().child(new Field().label("Name").child(div().child("Ada")));
86+
}
87+
}
88+
"#,
89+
);
90+
let (status, stdout, stderr) = app.run();
91+
assert!(status.success(), "{stdout}\n{stderr}");
92+
assert!(
93+
stdout.contains("Field") && stdout.contains("Ada"),
94+
"{stdout}"
95+
);
96+
assert!(stdout.contains("check passed:"), "{stdout}");
97+
}
98+
99+
#[test]
100+
fn check_rejects_an_ordinary_child_in_a_typed_form() {
101+
let app = CheckApp::new(
102+
r#"
103+
import { View, div } from "gpui-kit";
104+
import { HForm } from "gpui-component";
105+
export default class App extends View {
106+
render() { return div().child(new HForm().child(div())); }
107+
}
108+
"#,
109+
);
110+
let (status, stdout, stderr) = app.run();
111+
assert_eq!(status.code(), Some(1), "{stdout}\n{stderr}");
112+
assert!(stderr.contains("failed to materialize `Form`"), "{stderr}");
113+
assert!(
114+
stderr.contains("Field children; received an ordinary element"),
115+
"{stderr}"
116+
);
117+
assert!(!stdout.contains("check passed:"), "{stdout}");
118+
}
119+
120+
#[test]
121+
fn check_rejects_style_on_a_data_only_component() {
122+
let app = CheckApp::new(
123+
r#"
124+
import { View, div } from "gpui-kit";
125+
import { MenuItem } from "gpui-component";
126+
export default class App extends View {
127+
render() { return div().child(new MenuItem("Open", "open").p(2)); }
128+
}
129+
"#,
130+
);
131+
let (status, stdout, stderr) = app.run();
132+
assert_eq!(status.code(), Some(1), "{stdout}\n{stderr}");
133+
assert!(
134+
stderr.contains("failed to materialize `MenuItem`"),
135+
"{stderr}"
136+
);
137+
assert!(stderr.contains("does not implement Styled"), "{stderr}");
138+
assert!(!stdout.contains("check passed:"), "{stdout}");
139+
}
140+
141+
#[test]
142+
fn check_reports_load_and_render_failures_without_hanging() {
143+
for (source, expected) in [
144+
("this is not javascript", "main.js"),
145+
(
146+
r#"import { View } from "gpui-kit";
147+
export default class App extends View {
148+
render() { throw new Error("render failed deliberately"); }
149+
}"#,
150+
"render failed deliberately",
151+
),
152+
(
153+
r#"import { View } from "gpui-kit";
154+
export default class App extends View {
155+
render() { while (true) {} }
156+
}"#,
157+
"interrupted",
158+
),
159+
] {
160+
let (status, stdout, stderr) = CheckApp::new(source).run();
161+
assert_eq!(status.code(), Some(1), "{stdout}\n{stderr}");
162+
assert!(stderr.contains(expected), "{stderr}");
163+
assert!(!stdout.contains("check passed:"), "{stdout}");
164+
}
165+
}
166+
167+
#[test]
168+
fn check_reports_invalid_metadata_before_opening_a_window() {
169+
let app = CheckApp::new("export default 1;");
170+
fs::write(app.0.join("gpui-shell.json"), "{}").unwrap();
171+
let (status, stdout, stderr) = app.run();
172+
assert_eq!(status.code(), Some(1), "{stdout}\n{stderr}");
173+
assert!(stderr.contains("gpui-shell.json"), "{stderr}");
174+
assert!(stderr.contains("check failed:"), "{stderr}");
175+
assert!(!stdout.contains("check passed:"), "{stdout}");
176+
}
177+
178+
#[gpui::test]
179+
fn runtime_check_preserves_errors_and_clears_them_before_the_next_check(
180+
cx: &mut gpui::TestAppContext,
181+
) {
182+
use std::ops::Deref as _;
183+
184+
cx.update(gpui_component_shell::init);
185+
let runtime = gpui_component_shell::new_isolated_runtime().unwrap();
186+
let window = cx.add_window(|window, cx| {
187+
let empty = cx.new(|_| gpui::Empty);
188+
gpui_component::Root::new(empty, window, cx)
189+
});
190+
let mut context = gpui::VisualTestContext::from_window(*window.deref(), cx);
191+
let invalid = CheckApp::new(
192+
r#"import { View, div } from "gpui-kit";
193+
import { HForm } from "gpui-component";
194+
export default class App extends View {
195+
render() { return new HForm().child(div()); }
196+
}"#,
197+
);
198+
let error = context
199+
.update(|window, cx| runtime.check(&invalid.0, window, cx))
200+
.expect_err("the Rust facade must return materialization failures");
201+
assert!(format!("{error:#}").contains("Form accepts only registered Field children"));
202+
203+
let valid = CheckApp::new(
204+
r#"import { View } from "gpui-kit";
205+
import { HForm, Field } from "gpui-component";
206+
export default class App extends View {
207+
render() {
208+
if (this.rendered) throw new Error("check rendered twice");
209+
this.rendered = true;
210+
return new HForm().child(new Field().child("checked once"));
211+
}
212+
}"#,
213+
);
214+
let description = context
215+
.update(|window, cx| runtime.check(&valid.0, window, cx))
216+
.expect("the next check must not inherit the previous materialization error");
217+
assert!(description.contains("checked once"), "{description}");
218+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated type contracts
2+
3+
Run from the repository root:
4+
5+
```sh
6+
npm --prefix crates/component-shell/tests/types ci
7+
npm --prefix crates/component-shell/tests/types test
8+
```
9+
10+
The harness generates fresh declarations with the styled host, compiles the strict positive TypeScript fixture and executes its emitted JavaScript, checks a separate strict negative fixture, checks the shipped registered-component JavaScript gallery, and runs valid and invalid scripts through the actual CLI. Temporary declarations and applications are removed after each run. Set `GPUI_COMPONENT_SHELL_BIN` to use an already built host; otherwise the harness uses `cargo run --locked`.
11+
12+
Fluent methods retain the receiver's component contract. For example, `new Spinner().p(4).size('small')` remains valid, while `.on_click(...)` remains unavailable after styling or conditional chaining. `Spinner.role(...)` and `Spinner.transition(...)` are also rejected, including after styling; these behaviors remain supported on native `div()` builders. There is no separate hand-maintained runtime copy of the positive fixture. Native factories return `NativeElement`; `Element` is the union of renderable native and registered builders. Use `NativeElement` when a helper specifically needs native signatures such as numeric size, and `Element` for general render results and children.
13+
14+
Consumer checking uses `skipLibCheck`, matching the generated editor configuration. It does not validate all ambient runtime library declarations. `@ts-expect-error` fixtures fail if a forbidden call becomes accepted; they are not casts that suppress unrelated fixture failures.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { div, View, type Element, type NativeElement, type Context } from 'gpui-kit';
2+
import { Spinner, Separator, Skeleton, HForm, Field, type SpinnerElement } from 'gpui-component';
3+
import { TextView } from 'gpui-base';
4+
5+
function padded(element: Element): Element {
6+
return element.p(2).when(true, current => current.p(4));
7+
}
8+
9+
export default class FluentContracts extends View {
10+
render(_cx: Context): Element {
11+
const spinner: SpinnerElement = new Spinner()
12+
.size('medium').p(4).flex()
13+
.hover(style => style.p(2))
14+
.active(style => style.p(1))
15+
.focus(style => style.p(2))
16+
.bg('#fff')
17+
.when(true, element => element.size('small').p(2))
18+
.map(element => element.size('large')).size('small');
19+
const element: Element = spinner;
20+
const answer: number = new Spinner().map(current => {
21+
current.size('small');
22+
return 42;
23+
});
24+
if (answer !== 42) throw new Error('map must preserve its return value');
25+
const native: NativeElement = div();
26+
return native.p(2).size(240).role('status').transition('opacity', 120).children([
27+
padded(element),
28+
new Separator().p(2).label('Section'),
29+
new Skeleton().flex().secondary(),
30+
new HForm().child(new Field().label('Name').child('Ada')).children([]).columns(2),
31+
TextView.markdown('text', '# Hello').p(2).selectable().flex().scrollable(),
32+
]);
33+
}
34+
}

‎crates/component-shell/tests/types/package-lock.json‎

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "gpui-component-shell-type-contracts",
3+
"private": true,
4+
"scripts": { "test": "node run.mjs" },
5+
"devDependencies": { "typescript": "5.9.3" }
6+
}

0 commit comments

Comments
 (0)