## Quick Start
**Prerequisites:** Xcode (macOS only) - everything else is automatic!
```bash
# Install Valdi CLI
npm install -g @snap/valdi
# One-command setup (installs all dependencies)
valdi dev_setup
# Create your first project
mkdir my_project && cd my_project
valdi bootstrap
valdi install ios # or android
```
> [!TIP]
> **Editor Extensions:** For the best development experience, install the [Valdi VSCode/Cursor extensions](./docs/INSTALL.md#vscodecursor-setup-optional-but-recommended) for syntax highlighting, debugging, and device logs during hot reload.
## Quick Links
- [Getting Started Guide](./docs/INSTALL.md)
- [Documentation](./docs/README.md)
- [Codelabs](./docs/docs/start-code-lab.md)
- [API Reference](./docs/api/api-quick-reference.md)
- [Frequently Asked Questions](./docs/docs/faq.md)
- [Component Library](https://github.com/Snapchat/Valdi_Widgets)
## Why Choose Valdi?
Valdi is a cross-platform UI framework designed to solve the fundamental problem of cross-platform development: velocity vs. runtime performance. For 8 years, it has powered a large portion of Snap's production apps.
### True Native Performance
Unlike frameworks that rely on web views or JavaScript bridges, Valdi compiles declaratively rendered TypeScript components into platform-native views. Valdi also includes several other performance advantages:
- **[Automatic view recycling](./docs/docs/performance-view-recycling.md)** - Global view pooling system reuses native views across all screens, dramatically reducing inflation latency
- **Optimized component rendering** - Components re-render independently without triggering parent re-renders, enabling fast incremental updates
- **Optimized layout engine** - C++ layout engine runs on the main thread with minimal marshalling overhead
- **Viewport-aware rendering** - Only visible views are inflated, making infinite scrolling performant by default
Learn more in our [Performance Optimization Guide](./docs/docs/performance-optimization.md).
### Developer Experience Built for Speed
Valdi eliminates the traditional compile-test-debug cycle that slows native development:
- **Instant hot reload** - See changes in milliseconds on iOS, Android, or desktop without recompiling
- **[Full VSCode debugging](./docs/docs/workflow-hermes-debugger.md)** - Set breakpoints, inspect variables, profile performance, and capture heap dumps directly in VSCode
- **Familiar syntax** - TSX components with TypeScript for type safety
### Flexible Adoption Model
Valdi integrates easily into existing apps - start small and scale as needed:
- **[Embed Valdi in native](./docs/docs/native-bindings.md)** - Drop Valdi components into existing UIKit or Android view hierarchies
- **[Embed native in Valdi](./docs/docs/native-customviews.md)** - Use platform-specific views within Valdi layouts via ``
- **[Polyglot modules](./docs/docs/native-polyglot.md)** - Write performance-critical code in C++, Swift, Kotlin, or Objective-C with type-safe bindings to TypeScript
- **[Full-stack architecture](./docs/docs/advanced-full-stack.md)** - Build entire features in Valdi with worker threads for background processing, eliminating platform-specific bridge code
### Deep Native Integration
Valdi generates type-safe bindings between TypeScript and native platforms:
- **[Automatic code generation](./docs/docs/native-annotations.md)** - TypeScript interfaces compile to Kotlin, Objective-C, and Swift bindings
- **[Native API access](./docs/docs/native-polyglot.md)** - Direct access to platform APIs and third-party native libraries through polyglot modules
- **Bidirectional communication** - Pass complex data structures and callbacks between TypeScript and native code safely
- **[Native protobuf support](./docs/docs/advanced-protobuf.md)** - Seamless integration with protobuf for efficient data serialization
### Proven at Scale
- Powers critical features in production Snap apps.
- Supports [advanced animations](./docs/docs/advanced-animations.md), real-time rendering, and [complex gesture systems](./docs/docs/core-touches.md)
### Feature Highlights
- **[Flexbox layout system](./docs/docs/core-flexbox.md)** with automatic RTL support
- **[Worker threads](./docs/docs/advanced-worker-service.md)** for multi-threaded JavaScript execution
- **[Native animations](./docs/docs/advanced-animations.md)** for native look and feel
- **[Advanced gesture recognition](./docs/docs/core-touches.md)** with platform-native handling
- **[Built-in testing framework](./docs/docs/workflow-testing.md)** with component-level unit tests
- **[Bazel integration](./docs/docs/workflow-bazel.md)** for reproducible, incremental builds
## Need Help?
Join our [Discord](https://discord.gg/uJyNEeYX2U) for support.
## Contributing
Please follow the [contributing](./CONTRIBUTING.md) guidelines.
## License
Valdi is made available under the MIT [License](./LICENSE.md).
================================================
FILE: SECURITY.md
================================================
# Security Protocol
If you believe you've found a security vulnerability in this project, please report it to us via: [https://hackerone.com/snapchat](https://hackerone.com/snapchat)
================================================
FILE: WORKSPACE
================================================
workspace(name = "valdi")
load("//bzl:workspace_prepare.bzl", "valdi_prepare_workspace")
valdi_prepare_workspace(__workspace_dir__)
load("//bzl:workspace_preinit.bzl", "valdi_preinitialize_workspace")
valdi_preinitialize_workspace()
load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains", "register_yq_toolchains")
register_yq_toolchains()
# Required bazel-lib dependencies
aspect_bazel_lib_dependencies()
# Required rules_shell dependencies
load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains")
rules_shell_dependencies()
rules_shell_toolchains()
# Register bazel-lib toolchains
aspect_bazel_lib_register_toolchains()
# Create the host platform repository transitively required by bazel-lib
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
load("@platforms//host:extension.bzl", "host_platform_repo")
maybe(
host_platform_repo,
name = "host_platform",
)
load("//bzl:workspace_init.bzl", "platform_dependency_rule", "valdi_initialize_workspace")
# repo rule generates target_platform.bzl
platform_dependency_rule(name = "platform_check")
load("@platform_check//:target_platform.bzl", "VALDI_PLATFORM_DEPENDENCIES")
valdi_initialize_workspace(VALDI_PLATFORM_DEPENDENCIES)
load("@valdi_npm//:repositories.bzl", "npm_repositories")
npm_repositories()
load("//bzl:workspace_postinit.bzl", "valdi_post_initialize_workspace")
valdi_post_initialize_workspace()
================================================
FILE: WORKSPACE.bzlmod
================================================
android_sdk_repository(
name = "androidsdk",
api_level = 36, # The API version for Android compileSdk
build_tools_version = "34.0.0",
)
================================================
FILE: ai-skills/CONTRIBUTING_SKILLS.md
================================================
# Contributing Skills
Skills are neutral, agent-agnostic markdown files that teach AI assistants how to work with the Valdi framework. They are installed into your AI tool of choice using `valdi skills install`.
## What is a skill?
A skill is a single `skill.md` file containing focused guidance for a specific area of Valdi development. Skills are:
- **Agent-agnostic** — plain markdown with no tool-specific frontmatter or directives
- **Self-contained** — each skill covers one topic without assuming other skills are loaded
- **Code-first** — concrete examples with correct and incorrect patterns labeled clearly
- **Accurate** — verified against the actual Valdi source and documentation
## skill.md format
```markdown
# Skill Title
Brief description of what this skill covers.
## When to use
Optional section: describe the file types or scenarios where this skill applies.
## Key concepts
Prose or list explaining important ideas.
## Correct patterns
\`\`\`typescript
// ✅ Correct example
\`\`\`
## Common mistakes
\`\`\`typescript
// ❌ Wrong example
\`\`\`
```
Rules:
- Start with an H1 title
- Include code examples — skills without examples are less useful
- Label examples as `// ✅` (correct) or `// ❌` (wrong)
- No agent-specific frontmatter (no `alwaysApply`, no YAML blocks)
- No secrets, API keys, or proprietary Snap information
## Scaffolding a new skill
Run the interactive scaffold command from within the Valdi framework checkout:
```bash
valdi skills create
```
You will be prompted for a name, description, and category. The command creates `ai-skills/skills//skill.md` in the correct location and registers the entry in `ai-skills/registry.json` automatically.
## Submitting a skill to the public registry
1. Fork [github.com/Snapchat/Valdi](https://github.com/Snapchat/Valdi)
2. Run `valdi skills create` from the repo root — it creates the skill file and registers it in `registry.json` automatically
3. Fill in the `skill.md` content
4. Open a pull request — title it `[skill] Add `
## Review criteria
Pull requests adding or updating skills are reviewed for:
- **Accuracy** — does the code compile and run correctly against current Valdi?
- **Code examples** — are there concrete `✅` / `❌` examples?
- **Breadth** — does it cover the common cases a developer would encounter?
- **No hallucinations** — especially for TSX skills, ensure no React patterns are presented as valid Valdi
- **Conciseness** — focused guidance is more useful than exhaustive reference dumps
================================================
FILE: ai-skills/registry.json
================================================
{
"version": "1.0.0",
"skills": [
{ "name": "valdi-tsx", "description": "Valdi TSX component patterns, lifecycle, styling — NOT React", "tags": ["tsx", "components", "patterns", "styling"], "path": "skills/valdi-tsx/skill.md", "category": ["client"] },
{ "name": "valdi-bazel", "description": "Valdi Bazel build rules, valdi_module, platform builds", "tags": ["bazel", "build"], "path": "skills/valdi-bazel/skill.md", "category": ["framework", "client"] },
{ "name": "valdi-testing", "description": "Valdi unit and integration testing patterns", "tags": ["testing"], "path": "skills/valdi-testing/skill.md", "category": ["client"] },
{ "name": "valdi-ios", "description": "Valdi iOS platform integration, Swift/ObjC bridging", "tags": ["ios", "swift", "objc"], "path": "skills/valdi-ios/skill.md", "category": ["framework", "client"] },
{ "name": "valdi-android", "description": "Valdi Android platform integration, Kotlin/Java bridging", "tags": ["android", "kotlin"], "path": "skills/valdi-android/skill.md", "category": ["framework", "client"] },
{ "name": "valdi-cpp-runtime", "description": "Valdi C++ runtime internals, renderer, event system", "tags": ["cpp", "runtime"], "path": "skills/valdi-cpp-runtime/skill.md", "category": ["framework"] },
{ "name": "valdi-compiler", "description": "Valdi compiler pipeline, TSX→native compilation", "tags": ["compiler"], "path": "skills/valdi-compiler/skill.md", "category": ["framework"] },
{ "name": "valdi-polyglot-module", "description": "Valdi polyglot module system: cross-platform APIs, web polyglot entry pattern, webPolyglotViews", "tags": ["polyglot", "modules", "web"], "path": "skills/valdi-polyglot-module/skill.md", "category": ["client"] },
{ "name": "valdi-custom-view", "description": "Valdi custom native view integration", "tags": ["custom-view", "native"], "path": "skills/valdi-custom-view/skill.md", "category": ["client"] },
{ "name": "valdi-overview", "description": "Valdi framework overview and project conventions", "tags": ["overview", "conventions"], "path": "skills/valdi-overview/skill.md", "category": ["client"] },
{ "name": "valdi-async", "description": "Valdi async lifecycle safety: CancelablePromise, registerDisposable, isDestroyed guards", "tags": ["async", "lifecycle", "http"], "path": "skills/valdi-async/skill.md", "category": ["client"] },
{ "name": "valdi-perf", "description": "Valdi performance: viewModel identity stability, createReusableCallback, layout vs view, Style interning", "tags": ["performance", "rendering"], "path": "skills/valdi-perf/skill.md", "category": ["client"] },
{ "name": "valdi-component-tests", "description": "Valdi component test patterns: elementKeyFind, componentTypeFind, tapNodeWithKey, discriminated unions, arrays", "tags": ["testing", "components", "jasmine"], "path": "skills/valdi-component-tests/skill.md", "category": ["client"] },
{ "name": "valdi-setup", "description": "Valdi module setup: BUILD.bazel, valdi_module(), tsconfig, deps, ios_module_name, hot reload", "tags": ["setup", "bazel", "build"], "path": "skills/valdi-setup/skill.md", "category": ["client"] },
{ "name": "valdi-migrate", "description": "Migrate Flutter, React, or Jetpack Compose code to Valdi — component model, lifecycle, element mapping tables", "tags": ["migration", "flutter", "react", "compose"], "path": "skills/valdi-migrate/skill.md", "category": ["client"] }
]
}
================================================
FILE: ai-skills/skills/valdi-android/skill.md
================================================
# Android Runtime Rules
**Applies to**: Kotlin/Java files in `/valdi/src/valdi/android/` and related Android runtime code
## Overview
The Valdi Android runtime bridges TypeScript/Valdi components to native Android views. It's implemented in Kotlin and C++ (via JNI).
## Code Style
- **4-space indentation** for Kotlin/Java
- Follow Kotlin coding conventions
- Use Kotlin features (data classes, extension functions, etc.)
## Key Concepts
### View Rendering
- Valdi components map to Android Views
- View recycling for performance
- Efficient view updates
### Platform Bridge
- JNI bridge to C++ runtime
- Kotlin/Java to native code communication
- Performance-critical paths
## Common Patterns
### Bazel Android Targets
```python
android_library(
name = "valdi_android",
srcs = glob(["**/*.kt", "**/*.java"]),
)
```
### View Binding
- Custom view implementations
- Attribute binding for Valdi properties
- Event handling
## Testing
```bash
# Run Android runtime tests
bazel test //valdi:test_java
# Run with coverage
bazel coverage //valdi:test_java
```
Test files are in `/valdi/test/java/`
## Building
```bash
# Build Android runtime library
bazel build //valdi:valdi_android
# Test with hello world app
valdi install android
cd apps/helloworld
valdi install android
```
## Platform-Specific Notes
1. **API Level** - Be mindful of minimum API level
2. **Android NDK** - C++ integration via NDK
3. **Permissions** - Handle runtime permissions appropriately
4. **Lifecycle** - Android Activity/Fragment lifecycle
## Important
- **Performance** - View creation and updates are critical
- **Memory** - Be careful with leaks (Activity context)
- **Threading** - UI thread vs background threads
- **Compatibility** - Support multiple Android versions
## More Information
- Runtime source: `/valdi/src/valdi/android/`
- Runtime tests: `/valdi/test/java/`
- Build config: `/valdi/BUILD.bazel`
- Framework docs: `/AGENTS.md`
================================================
FILE: ai-skills/skills/valdi-async/skill.md
================================================
# Valdi Async & Lifecycle Safety
Async operations that complete after a component is destroyed will call `setState()`
on a dead component — the framework throws an error. The fix is always to cancel or
guard before the callback fires.
## HTTPClient: Store, Cancel, Repeat
`HTTPClient` methods return `CancelablePromise`. Store it in a typed
field so you can cancel it in `onDestroy()` and before starting a new request in
`onViewModelUpdate()`.
```typescript
import { Component, StatefulComponent } from 'valdi_core/src/Component';
import { CancelablePromise } from 'valdi_core/src/CancelablePromise';
import { HTTPClient } from 'valdi_http/src/HTTPClient';
import { HTTPResponse } from 'valdi_http/src/HTTPTypes';
interface UserState {
user: { name: string } | null;
loading: boolean;
}
class UserProfile extends StatefulComponent {
state: UserState = { user: null, loading: true };
private client = new HTTPClient('https://api.example.com');
private request?: CancelablePromise;
onCreate(): void {
this.fetchUser(this.viewModel.userId);
}
onViewModelUpdate(previous?: UserProfileViewModel): void {
if (this.viewModel.userId !== previous?.userId) {
this.request?.cancel?.(); // Cancel in-flight before starting new one
this.fetchUser(this.viewModel.userId);
}
}
onDestroy(): void {
this.request?.cancel?.(); // Always clean up
}
private fetchUser(id: string): void {
this.request = this.client.get(`/users/${id}`);
this.request.then(response => {
const user = JSON.parse(new TextDecoder().decode(response.body));
this.setState({ user, loading: false });
});
}
onRender(): void {
if (this.state.loading) {
;
return;
}
;
}
}
```
**Important:** `cancel` is an optional method on `CancelablePromise` — always use
`?.cancel?.()`, not `?.cancel()`.
## registerDisposable: Timers and Subscriptions
For anything that emits over time (timers, event emitters, observables), use
`registerDisposable()`. The framework calls `cancel()` on all registered disposables
in `onDestroy()` automatically — you don't need to override `onDestroy()` for these.
```typescript
class LiveClock extends Component<{}> {
onCreate(): void {
// ❌ Timer leaks if component is destroyed
setInterval(() => this.tick(), 1000);
// ✅ Automatically cancelled on destroy
const id = setInterval(() => this.tick(), 1000);
this.registerDisposable({ cancel: () => clearInterval(id) });
}
private tick = () => { /* ... */ };
}
```
Same pattern for any subscription-style API:
```typescript
onCreate(): void {
const unsubscribe = eventEmitter.on('change', this.handleChange);
this.registerDisposable({ cancel: unsubscribe });
}
```
## isDestroyed() Guard for Plain Promises
When using `async/await` or plain `Promise` chains that can't be cancelled, guard
`setState()` with `isDestroyed()` before calling it:
```typescript
private async loadData(): Promise {
const result = await someAsyncOperation();
if (this.isDestroyed()) return; // Component unmounted while awaiting
this.setState({ data: result });
}
```
Prefer `CancelablePromise` + `registerDisposable` over this pattern where possible —
they make cancellation explicit and don't rely on the check being remembered.
## onViewModelUpdate: Cancel Before Restart
When a viewModel update requires fetching new data, always cancel the previous
request before starting a new one. Skipping this means two requests can be in flight
simultaneously and the earlier one can resolve last, overwriting newer data.
```typescript
onViewModelUpdate(previous?: MyViewModel): void {
if (this.viewModel.query !== previous?.query) {
this.searchRequest?.cancel?.(); // ← required
this.searchRequest = this.client.get(`/search?q=${this.viewModel.query}`);
this.searchRequest.then(response => {
this.setState({ results: parse(response) });
});
}
}
```
## Observable Subscriptions
For RxJS or observable-based APIs, prefer `registerDisposable` for one-off subscriptions
or the `Subscription` class for managing multiple subscriptions as a group:
```typescript
import { Subscription } from 'rxjs';
class FeedComponent extends StatefulComponent {
private subscription = new Subscription();
onCreate(): void {
// Add multiple subscriptions to the group
this.subscription.add(
this.viewModel.feedItems$.subscribe({ next: items => this.setState({ items }) })
);
this.subscription.add(
this.viewModel.loading$.subscribe({ next: loading => this.setState({ loading }) })
);
}
onDestroy(): void {
this.subscription?.unsubscribe(); // Cancels all at once
}
}
```
For a single subscription, `registerDisposable` is simpler:
```typescript
onCreate(): void {
this.registerDisposable(
this.viewModel.counter$.subscribe({ next: count => this.setState({ count }) })
);
// No onDestroy() override needed
}
```
### Preventing redundant re-renders with distinctUntilChanged
Subscribe with `distinctUntilChanged()` to skip emissions where the value hasn't
actually changed — avoids a `setState` call and a re-render for each identical value:
```typescript
import { distinctUntilChanged } from 'rxjs/operators';
this.subscription.add(
this.viewModel.title$.pipe(distinctUntilChanged()).subscribe({
next: title => this.setState({ title }),
})
);
```
## setTimeoutInterruptible for Debounce / Race Conditions
When a delayed action must be cancelled if conditions change (e.g. search debounce,
retry delay), use `setTimeoutInterruptible` rather than a bare `setTimeout`:
```typescript
import { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';
// setTimeoutInterruptible returns a number (timer ID) — cancel with clearTimeout()
class SearchBar extends StatefulComponent {
private debounceId?: number;
onViewModelUpdate(previous?: SearchViewModel): void {
if (this.viewModel.query !== previous?.query) {
clearTimeout(this.debounceId); // Cancel any pending debounce
this.debounceId = setTimeoutInterruptible(() => {
this.fetchResults(this.viewModel.query);
}, 300);
}
}
}
```
## promiseToCancelablePromise
When you have a plain `Promise` (e.g. from a third-party library) that needs to participate in Valdi's cancellation system, wrap it with `promiseToCancelablePromise`:
```typescript
import { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';
class MyComponent extends StatefulComponent {
private request?: CancelablePromise;
onCreate(): void {
const rawPromise: Promise = thirdPartyApi.fetchData();
this.request = promiseToCancelablePromise(rawPromise, () => {
// optional cleanup on cancel
});
this.request.then(data => {
if (this.isDestroyed()) return;
this.setState({ data });
});
}
onDestroy(): void {
this.request?.cancel?.();
}
}
```
Prefer `CancelablePromise` directly (e.g. `HTTPClient`) when available — `promiseToCancelablePromise` is the bridge for external APIs.
## Import Paths
```typescript
import { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';
import { HTTPClient } from 'valdi_http/src/HTTPClient';
import { HTTPResponse } from 'valdi_http/src/HTTPTypes';
import { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';
import { Subscription } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
```
================================================
FILE: ai-skills/skills/valdi-async/tests/BUILD.bazel
================================================
load("//bzl/valdi:valdi_module.bzl", "valdi_module")
valdi_module(
name = "tests",
srcs = glob([
"src/**/*.ts",
"src/**/*.tsx",
]) + [
"tsconfig.json",
],
android_output_target = "release",
ios_module_name = "SCCValdiAsyncRef",
ios_output_target = "release",
visibility = ["//visibility:public"],
deps = [
"//src/valdi_modules/src/valdi/valdi_core",
"//src/valdi_modules/src/valdi/valdi_http",
],
)
================================================
FILE: ai-skills/skills/valdi-async/tests/README.md
================================================
# valdi-async skill tests
Compile check for the async lifecycle safety patterns taught in `skill.md`.
## What's tested
`src/reference.tsx` exercises:
- `HTTPClient` + `CancelablePromise`: store in field, cancel in `onDestroy`, cancel before restart in `onViewModelUpdate`
- `registerDisposable` with `setInterval` for auto-cleanup subscriptions
- `isDestroyed()` guard for plain `Promise` / `async/await`
- `setTimeoutInterruptible` for debounce — returns `number`, cancel with `clearTimeout()`
- `promiseToCancelablePromise` wrapping a third-party `Promise`
## Compile check
```bash
cd client/src/open_source
bazel build //ai-skills/skills/valdi-async/tests:tests
```
Expected: `Build completed successfully`
## Updating
When Valdi APIs change (import paths, type signatures), update `src/reference.tsx` to match and keep it compiling. Also update `../skill.md` with any corrected patterns.
================================================
FILE: ai-skills/skills/valdi-async/tests/src/reference.tsx
================================================
// valdi-async skill reference — compile check for async lifecycle safety patterns.
// This file must compile cleanly against the Valdi TypeScript compiler.
// Run: bzl build //ai-skills/skills/valdi-async/tests:tests
import { StatefulComponent } from 'valdi_core/src/Component';
import { CancelablePromise, promiseToCancelablePromise } from 'valdi_core/src/CancelablePromise';
import { setTimeoutInterruptible } from 'valdi_core/src/SetTimeout';
import { HTTPClient } from 'valdi_http/src/HTTPClient';
import { HTTPResponse } from 'valdi_http/src/HTTPTypes';
// ─── HTTPClient: store, cancel in onDestroy, cancel before restart ────────────
interface UserProfileViewModel {
userId: string;
}
interface UserProfileState {
user: { name: string } | null;
loading: boolean;
}
class UserProfile extends StatefulComponent {
state: UserProfileState = { user: null, loading: true };
private client = new HTTPClient('https://api.example.com');
private request?: CancelablePromise;
onCreate(): void {
this.fetchUser(this.viewModel.userId);
}
onViewModelUpdate(previous?: UserProfileViewModel): void {
if (this.viewModel.userId !== previous?.userId) {
this.request?.cancel?.(); // Cancel in-flight before starting new one
this.fetchUser(this.viewModel.userId);
}
}
onDestroy(): void {
this.request?.cancel?.(); // Always clean up
}
private fetchUser(id: string): void {
this.request = this.client.get(`/users/${id}`);
this.request.then(response => {
const user = JSON.parse(new TextDecoder().decode(response.body)) as { name: string };
this.setState({ user, loading: false });
});
}
onRender(): void {
if (this.state.loading) {
;
return;
}
;
}
}
// ─── registerDisposable: timers and subscriptions ────────────────────────────
interface ClockViewModel {
format: string;
}
interface ClockState {
ticks: number;
}
class LiveClock extends StatefulComponent {
state: ClockState = { ticks: 0 };
onCreate(): void {
// ✅ Automatically cancelled on destroy — no onDestroy() needed
const id = setInterval(() => this.tick(), 1000);
this.registerDisposable({ cancel: () => clearInterval(id) });
}
private tick = (): void => {
this.setState({ ticks: this.state.ticks + 1 });
};
onRender(): void {
;
}
}
// ─── isDestroyed() guard for plain Promises ──────────────────────────────────
declare function expensiveOperation(): Promise;
interface DataState {
data: string | null;
}
interface DataViewModel {
query: string;
}
class DataLoader extends StatefulComponent {
state: DataState = { data: null };
onCreate(): void {
this.loadData().catch(() => {});
}
private async loadData(): Promise {
const result = await expensiveOperation();
if (this.isDestroyed()) return; // Guard before setState
this.setState({ data: result });
}
onRender(): void {
;
}
}
// ─── setTimeoutInterruptible for debounce ────────────────────────────────────
// setTimeoutInterruptible returns number (timer ID) — cancel with clearTimeout
interface SearchViewModel {
query: string;
}
interface SearchState {
results: string[];
}
class SearchBar extends StatefulComponent {
state: SearchState = { results: [] };
private debounceId?: number;
private searchRequest?: CancelablePromise;
private client = new HTTPClient('https://api.example.com');
onViewModelUpdate(previous?: SearchViewModel): void {
if (this.viewModel.query !== previous?.query) {
clearTimeout(this.debounceId); // Cancel pending debounce
this.debounceId = setTimeoutInterruptible(() => {
this.fetchResults(this.viewModel.query);
}, 300);
}
}
onDestroy(): void {
this.searchRequest?.cancel?.();
}
private fetchResults(query: string): void {
this.searchRequest?.cancel?.();
this.searchRequest = this.client.get(`/search?q=${encodeURIComponent(query)}`);
this.searchRequest.then(response => {
const data = JSON.parse(new TextDecoder().decode(response.body)) as { results: string[] };
this.setState({ results: data.results });
});
}
onRender(): void {
{this.state.results.forEach(result => {
;
})}
;
}
}
// ─── promiseToCancelablePromise ──────────────────────────────────────────────
declare function thirdPartyFetch(url: string): Promise;
interface FetchState {
content: string | null;
}
interface FetchViewModel {
url: string;
}
class ThirdPartyFetcher extends StatefulComponent {
state: FetchState = { content: null };
private request?: CancelablePromise;
onCreate(): void {
const rawPromise = thirdPartyFetch(this.viewModel.url);
this.request = promiseToCancelablePromise(rawPromise, () => {
// optional cleanup on cancel
});
this.request.then(content => {
if (this.isDestroyed()) return;
this.setState({ content });
});
}
onDestroy(): void {
this.request?.cancel?.();
}
onRender(): void {
;
}
}
void UserProfile;
void LiveClock;
void DataLoader;
void SearchBar;
void ThirdPartyFetcher;
================================================
FILE: ai-skills/skills/valdi-async/tests/tsconfig.json
================================================
{"extends": "../../../../src/valdi_modules/src/valdi/_configs/base.tsconfig.json"}
================================================
FILE: ai-skills/skills/valdi-bazel/skill.md
================================================
# Bazel Build System Rules
**Applies to**: `BUILD.bazel`, `*.bzl` files in `/bzl/`, `WORKSPACE`, `MODULE.bazel`
## Overview
Valdi uses Bazel as its build system. Bazel provides reproducible, incremental builds across all platforms.
## Key Commands
```bash
# Build everything
bazel build //...
# Build specific target
bazel build //apps/helloworld:helloworld
# Run tests
bazel test //...
# Clean (use sparingly - cache is valuable!)
bazel clean
```
## Important Notes
1. **Use `bazel`** for all build commands
2. **The CLI wraps Bazel** - `valdi` commands use bazel under the hood
3. **Cache is important** - Don't suggest `bazel clean` unless necessary
## Build Rules
### Valdi-Specific Rules
- `/bzl/valdi/` - Valdi build rules and macros
- Custom rules for compiling .tsx to .valdimodule
- Platform-specific build transitions
### Common Targets
```python
# Valdi application
valdi_application(
name = "my_app",
root_component_path = "App@my_app/src/MyApp",
title = "My App",
version = "1.0.0",
deps = ["//apps/my_app/src/valdi/my_app"],
)
# Valdi module
valdi_module(
name = "my_module",
srcs = glob(["src/**/*.ts", "src/**/*.tsx"]),
deps = [
"//src/valdi_modules/src/valdi/valdi_core",
],
)
```
## Conventions
### File Naming
- `BUILD.bazel` not `BUILD` (explicit extension)
- `.bzl` for Starlark macros and rules
### Targets
- Use descriptive target names
- One main target per BUILD file usually matches directory name
### Dependencies
- Be explicit about dependencies
- Don't rely on transitive deps implicitly
- Use visibility to control access
## Platform Builds
```bash
# Build and install iOS app
valdi install ios
# Build and install Android app
valdi install android
# Or use bazel directly with configs
bazel build //apps/helloworld:hello_world --config=ios
bazel build //apps/helloworld:hello_world --config=android
```
## Configuration
- `.bazelrc` - Build flags and configurations
- `MODULE.bazel` - Bazel module dependencies
- `WORKSPACE` - Legacy workspace configuration (being migrated to MODULE.bazel)
## Common Issues
1. **Missing dependencies** - Add to `deps` in BUILD.bazel
2. **Cache issues** - Try `bazel clean --expunge` (last resort)
3. **Platform transitions** - Use correct config flags
## Testing
```bash
# Run all tests
bazel test //...
# Run specific test
bazel test //valdi/test:renderer_test
# Run with coverage
bazel coverage //...
```
## More Information
- Bazel docs: https://bazel.build
- Valdi build rules: `/bzl/valdi/README.md`
- Framework docs: `/AGENTS.md`
================================================
FILE: ai-skills/skills/valdi-compiler/skill.md
================================================
# Valdi Compiler Rules
**Applies to**: Swift files in `/compiler/compiler/`
## Overview
The Valdi compiler consists of two parts:
1. **Compiler (Swift)**: Transforms TypeScript/TSX into `.valdimodule` files
2. **Companion (TypeScript/Node.js)**: Handles TypeScript compilation, type checking, and provides debugging support
## Key Conventions
### Code Style
- **4-space indentation** for Swift
- Follow Swift naming conventions (camelCase for methods, PascalCase for types)
- Use Swift type inference where appropriate
- Prefer `let` over `var` when possible
### Architecture
- Compiler is a multi-pass system
- AST transformation pipeline
- Type checking and validation
- Code generation for each platform
### Important Files
- `/compiler/compiler/` - Main Swift compiler implementation
- Output: `.valdimodule` files (binary format read by runtime)
## Common Patterns
### AST Traversal
```swift
// Follow existing visitor patterns
class MyASTVisitor: ASTVisitor {
func visit(_ node: Node) -> Result {
// Visit logic
}
}
```
### Error Handling
```swift
// Use proper error types
enum CompilerError: Error {
case invalidSyntax(String)
case typeError(String)
}
```
## Testing
Tests are critical - add tests for new features and error cases. The `update_compiler.sh` script runs tests automatically.
## Build
### Using the update script (recommended):
```bash
cd compiler/compiler
./scripts/update_compiler.sh ../../bin/compiler
```
This script:
- Runs `swift test` automatically
- Builds the compiler for the correct architecture
- Copies the binary to `bin/compiler/macos/valdi_compiler` (or `linux/valdi_compiler`)
- Handles platform differences (macOS vs Linux)
### Alternative: Using Xcode
Open `compiler/compiler/Compiler/Package.swift` in Xcode, let it resolve dependencies, then build.
## Companion (TypeScript)
The companion is a TypeScript service that works alongside the Swift compiler. It handles TypeScript compilation, type checking, and provides debugging support.
### Build the companion:
```bash
cd compiler/companion
./scripts/update_companion.sh ../../bin/compiler_companion
```
This script:
- Runs `npm install`
- Runs `npm run test`
- Builds with `bazel build //compiler/companion:bundle`
- Copies output to `bin/compiler_companion`
**Note**: Once built, the companion is automatically invoked by the compiler during compilation. You don't need to run it manually - it's part of the compiler process.
## Important Notes
1. **Performance matters** - Compiler speed affects developer experience
2. **Error messages** - Make them helpful and actionable
3. **Backward compatibility** - Don't break existing .valdimodule files
4. **Cross-platform** - Consider iOS, Android, macOS targets
## More Information
- Compiler architecture: `/compiler/compiler/README.md`
- Framework docs: `/AGENTS.md`
================================================
FILE: ai-skills/skills/valdi-component-tests/skill.md
================================================
# Write Valdi Component Tests
Write unit tests for a Valdi component using standard Valdi test suite patterns.
## Steps
### 1. Read the source component
Read the component source file to understand:
- What view model properties it accepts
- Which JSX elements have `key` attributes
- What changes based on view model props vs. what's always static
- How "hidden" state is implemented (translationY, opacity, or child component prop)
- Whether the view model contains any **discriminated unions** (type/kind fields)
- Whether any props are **arrays** of items to render
### 2. Add `key` attributes to source elements (only if needed)
Add `key` attributes **only** to elements whose rendering depends on view model properties. Never add keys to static content.
Good candidates for keys:
- The outer container when `hidden` controls `translationY` or `opacity`
- An `` element whose `src` comes from a view model URL prop
- An element inside a `when(condition, ...)` block (to assert presence/absence)
- A badge/overlay that toggles based on a boolean prop
- A spinner shown in a loading/saving state of a union type
Do NOT add keys to:
- Elements with hardcoded asset values (e.g., `src={SIGIcon.xSignStroke}`)
- Label elements with always-present localized strings
- Elements that are always rendered the same regardless of view model
### 3. Determine the test file path
Test files **must mirror the source file hierarchy**. For example:
- Source: `src/categories/CollectionComponent.tsx` → Test: `test/categories/CollectionComponentTest.spec.tsx`
- Source: `src/home_page/OptionPreviewView.tsx` → Test: `test/home_page/OptionPreviewViewTest.spec.tsx`
- Source: `src/MyComponent.tsx` → Test: `test/MyComponentTest.spec.tsx`
### 4. Write the test file
**Imports:**
```tsx
import { MyComponent } from 'my_module/src/MyComponent';
import { componentGetElements } from 'foundation/test/util/componentGetElements';
import { componentTypeFind } from 'foundation/test/util/componentTypeFind';
import { elementKeyFind } from 'foundation/test/util/elementKeyFind';
import { elementTypeFind } from 'foundation/test/util/elementTypeFind';
import { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';
import { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';
import 'jasmine/src/jasmine';
import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';
import { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';
import { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';
```
Only import what you actually use. `View` is needed when you call `getAttribute` on a non-image element (e.g., for `onTap`, `onVisibilityChanged`). `ImageView` is needed for `src` attribute access.
**Factory function pattern (always add explicit return type):**
```tsx
const makeViewModel = (): MyComponentViewModel => ({
imageUrl: 'https://example.com/image.png',
isVisible: true,
onTap: fail.bind(null, 'onTap should not be called'),
});
```
Use `fail.bind` for callbacks in the factory default — tests that need to assert on a callback should declare their own spy and pass it explicitly, rather than relying on the factory.
**Render pattern:**
```tsx
const nodes = driver.render(() => {
;
});
```
### 5. Test patterns by assertion type
#### Finding elements
```tsx
// Single-level: component renders native elements directly
elementKeyFind(componentGetElements(nodes[0].component!), 'my-key')[0]
// Cross-boundary: component renders a child component that renders the elements
const inner = componentTypeFind(nodes[0].component as MyComponent, InnerComponent)[0];
elementKeyFind(componentGetElements(inner), 'my-key')[0]
// Typed (for typed attribute access without casting):
elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0]
elementKeyFind(componentGetElements(nodes[0].component!), 'image')[0]
// By element type (e.g. find all Label elements):
// Can pass componentGetElements() result OR an IComponent directly:
const labels = elementTypeFind(componentGetElements(nodes[0].component!), IRenderedElementViewClass.Label);
const labels2 = elementTypeFind(nodes[0].component!, IRenderedElementViewClass.Label); // equivalent
expect(labels[0]?.getAttribute('value')).toBe('Expected text');
```
Use the generic type param to get typed `getAttribute()` results and avoid `@typescript-eslint/no-unsafe-call` errors — prefer this over casting the `getAttribute()` return value.
`elementTypeFind` is useful when elements don't have `key` attributes but you know their type (Label, Image, View, etc.). It returns all elements of that type in render order.
#### Hidden via `translationY`
```tsx
// hidden=false
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(0);
// hidden=true
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('translationY')).toBe(850);
```
#### Hidden via `opacity` on native view
```tsx
// visible
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(1);
// hidden
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-view')[0]?.getAttribute('opacity')).toBe(0);
```
#### Hidden via `opacity` prop passed to a child Component (component boundary)
```tsx
import { CoreButton } from 'coreui/src/components/button/CoreButton';
const component = nodes[0].component as MyComponent;
const buttons = componentTypeFind(component, CoreButton);
expect(buttons[0].viewModel.opacity).toBe(1); // or 0
```
#### View model URL bound to image `src`
```tsx
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');
```
#### Text from view model
```tsx
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-label')[0]?.getAttribute('value')).toBe('Expected Text');
```
#### `when()`-conditional element (boolean presence)
```tsx
// condition=true → element exists
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeDefined();
// condition=false → element absent
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0]).toBeUndefined();
```
#### `when()`-conditional Component (boolean presence via componentTypeFind)
```tsx
import { BadgeComponent } from 'my_module/src/BadgeComponent';
const component = nodes[0].component as MyComponent;
// condition=true
expect(componentTypeFind(component, BadgeComponent).length).toBe(1);
// condition=false
expect(componentTypeFind(component, BadgeComponent).length).toBe(0);
```
#### Callbacks not expected to be invoked
For callbacks that should never fire in a given test, use `fail.bind(null, '...')` instead of `jasmine.createSpy`. This causes the test to immediately fail with a clear message if the callback is accidentally triggered:
```tsx
```
`fail.bind(null, 'message')` is a plain function call (not an inline lambda), so it satisfies `jsx-no-lambda`. It is assignable to any callback type since TypeScript allows functions with fewer parameters.
If the same fail callback is used across many tests in the file, extract it to a module-level const to avoid repetition:
```tsx
const failOnSelect = (): void => fail('onSelect should not be called');
```
#### Tap callback (use `tapNodeWithKey` — it's async, always `await` it)
`tapNodeWithKey(component, key, timeoutMs?, intervalMs?)` accepts `IComponent | IRenderedElement`.
```tsx
const onTap = jasmine.createSpy('onTap');
const nodes = driver.render(() => {
;
});
await tapNodeWithKey(nodes[0].component!, 'my-button');
expect(onTap).toHaveBeenCalled();
```
When you need to find a node without tapping it, use `findNodeWithKey`:
```tsx
import { findNodeWithKey } from 'foundation/test/util/findNodeWithKey';
const node = findNodeWithKey(nodes[0].component!, 'my-button')[0];
expect(node).toBeDefined();
```
When the callback receives arguments (e.g., an index), always assert the exact arguments with `toHaveBeenCalledWith`:
```tsx
const onSelect = jasmine.createSpy('onSelect');
// ... render and trigger ...
expect(onSelect).toHaveBeenCalledWith(1); // not just toHaveBeenCalled()
```
For callbacks invoked via a child component's view model (e.g., through `componentTypeFind`), call the view model method directly:
```tsx
const component = nodes[0].component as MyComponent;
componentTypeFind(component, ItemComponent)[1].viewModel.onTap();
expect(onSelect).toHaveBeenCalledWith(1);
```
#### `onTap` retrieved via `getAttribute` (non-tappable element pattern)
```tsx
// Use elementKeyFind so getAttribute('onTap') is typed — no cast needed
const el = elementKeyFind(componentGetElements(nodes[0].component!), 'my-element')[0];
el?.getAttribute('onTap')?.();
expect(onTap).toHaveBeenCalled();
```
#### `onVisibilityChanged` callback
`View.onVisibilityChanged` signature is `(isVisible: boolean, eventTime: EventTime)` where `EventTime = number`. Always pass both args when invoking:
```tsx
const el = elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0];
el?.getAttribute('onVisibilityChanged')?.(true, 0);
```
**Asserting the callback:** depends on how the component wires the handler:
- If the component **wraps** the viewModel callback (e.g., `onVisibilityChanged={(v) => vm.onVisibilityChanged(v)}`), `toHaveBeenCalledWith(true)` works:
```tsx
expect(onVisibilityChanged).toHaveBeenCalledWith(true);
```
- If the component **directly assigns** the viewModel callback (e.g., `onVisibilityChanged={this.viewModel.onVisibilityChanged}`), the spy receives both args. If the spy is typed as `(isVisible: boolean) => void`, `toHaveBeenCalledWith(true, ...)` is a TypeScript error. Check the first arg via `calls`:
```tsx
const spy = viewModel.onVisibilityChanged as jasmine.Spy;
expect(spy).toHaveBeenCalled();
expect(spy.calls.mostRecent().args[0]).toBe(true);
```
If the spy is an untyped `jasmine.createSpy()`, you can use `toHaveBeenCalledWith(true, 0)` directly.
### 6. Discriminated union state testing
When a view model contains a discriminated union (e.g., `type: 'LOADING' | 'CONTENT' | 'ERROR'`), **test every branch**. For each state:
1. Assert which elements/components ARE rendered
2. Assert which elements/components from OTHER states are NOT rendered
```tsx
// LOADING state: spinner present, action button absent
valdiIt('Verify spinner is shown in loading state', async driver => {
const nodes = driver.render(() => {
;
});
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeDefined();
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeUndefined();
});
// CONTENT state: action button present, no spinner
valdiIt('Verify action button is shown in content state', async driver => {
const nodes = driver.render(() => {
;
});
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'action-button')[0]).toBeDefined();
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'loading-spinner')[0]).toBeUndefined();
});
```
### 7. Array view model testing
When a component renders a list from an array prop, test three cases:
1. **Empty array** — assert 0 items render
2. **Single item** — assert 1 item renders
3. **Multiple items** — assert item count matches array length
Use `componentTypeFind(component, ItemComponent)` or `elementKeyFind` with indexed keys (e.g., `tile-0`, `tile-1`) to count rendered items.
```tsx
valdiIt('Verify no items render when array is empty', async driver => {
const emptyOptions: OptionViewModel[] = [];
const nodes = driver.render(() => {
;
});
const component = nodes[0].component as MyComponent;
expect(componentTypeFind(component, ItemComponent).length).toBe(0);
});
valdiIt('Verify item count matches array length', async driver => {
const threeItems: OptionViewModel[] = [makeItem('a'), makeItem('b'), makeItem('c')];
const nodes = driver.render(() => {
;
});
const component = nodes[0].component as MyComponent;
expect(componentTypeFind(component, ItemComponent).length).toBe(3);
});
```
Note: Extract array literals to local `const` variables before using in JSX (required by `@snapchat/valdi/jsx-no-lambda`).
### 8. Component boundary traversal
When a component's `onRender()` only renders child components (not native elements), `componentGetElements(component)` returns `[]`. You must get the child component first:
```tsx
import { componentGetElements } from 'foundation/test/util/componentGetElements';
import { componentTypeFind } from 'foundation/test/util/componentTypeFind';
import { elementKeyFind } from 'foundation/test/util/elementKeyFind';
const component = nodes[0].component as OuterComponent;
const inner = componentTypeFind(component, InnerComponent)[0];
const container = elementKeyFind(componentGetElements(inner), 'container')[0];
expect(container?.getAttribute('translationY')).toBe(0);
```
### 9. Extract render and find helpers for readability
For larger test files, extract repeated render + find logic into helper functions. This keeps individual tests focused:
```tsx
// Extract rendering
const renderComponent = (driver: IComponentTestDriver, overrides?: Partial) => {
const vm = { ...makeViewModel(), ...overrides };
return driver.render(() => { ; })[0].component as MyComponent;
};
// Extract finding
const getImage = (component: MyComponent) =>
elementKeyFind(componentGetElements(component), 'image')[0];
// Tests become clean:
valdiIt('Verify imageUrl is bound', async driver => {
expect(getImage(renderComponent(driver))?.getAttribute('src')).toBe('https://example.com/image.png');
});
```
### 10. Lint rules to follow
- **`explicit-function-return-type`**: Always add explicit return types to factory functions: `const makeViewModel = (): MyViewModel => ({...})`
- **`jsx-no-lambda`**: Never assign inline array literals directly in JSX props. Extract to a local `const` first:
```tsx
// WRONG
;
// CORRECT
const items: ItemViewModel[] = [makeItem('a')];
;
```
- **`no-unsafe-call`**: Use the generic type parameter on `elementKeyFind` to get typed `getAttribute()` results rather than casting: `elementKeyFind(...)` gives typed access to `onTap`, `onVisibilityChanged`, etc.
- **`import/order`**: Keep imports sorted alphabetically by path.
### 11. Key principle
**Only assert on things that change based on view model props.** Every test should have a clear "when X is Y, then Z" story. If the UI looks the same regardless of the prop, skip it.
For union types, a test that only verifies "the component renders without error" in a given state is not sufficient — assert the meaningful structural difference that state introduces.
## Example test file structure
```tsx
import { MyComponent } from 'my_module/src/MyComponent';
import { MyComponentViewModel } from 'my_module/src/MyComponentViewModel';
import { ChildComponent } from 'my_module/src/ChildComponent';
import { componentGetElements } from 'foundation/test/util/componentGetElements';
import { componentTypeFind } from 'foundation/test/util/componentTypeFind';
import { elementKeyFind } from 'foundation/test/util/elementKeyFind';
import { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';
import 'jasmine/src/jasmine';
import { IComponentTestDriver, valdiIt } from 'valdi_test/test/JSXTestUtils';
import { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';
// elementTypeFind + IRenderedElementViewClass for finding elements by type (no key needed):
// import { elementTypeFind } from 'foundation/test/util/elementTypeFind';
// import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';
const makeViewModel = (): MyComponentViewModel => ({
imageUrl: 'https://example.com/image.png',
isVisible: true,
onTap: fail.bind(null, 'onTap should not be called'),
});
describe('MyComponentTest', () => {
valdiIt('Verify visible when isVisible is true', async driver => {
const nodes = driver.render(() => {
;
});
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(1);
});
valdiIt('Verify hidden when isVisible is false', async driver => {
const nodes = driver.render(() => {
;
});
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'container')[0]?.getAttribute('opacity')).toBe(0);
});
valdiIt('Verify imageUrl is bound to image src', async driver => {
const nodes = driver.render(() => {
;
});
expect(elementKeyFind(componentGetElements(nodes[0].component!), 'image')[0]?.getAttribute('src')).toBe('https://example.com/image.png');
});
valdiIt('Verify onTap is called when tapped', async driver => {
const onTap = jasmine.createSpy('onTap');
const nodes = driver.render(() => {
;
});
await tapNodeWithKey(nodes[0].component!, 'button');
expect(onTap).toHaveBeenCalled();
});
valdiIt('Verify ChildComponent is present when condition is true', async driver => {
const nodes = driver.render(() => {
;
});
const component = nodes[0].component as MyComponent;
expect(componentTypeFind(component, ChildComponent).length).toBe(1);
});
valdiIt('Verify ChildComponent is absent when condition is false', async driver => {
const nodes = driver.render(() => {
;
});
const component = nodes[0].component as MyComponent;
expect(componentTypeFind(component, ChildComponent).length).toBe(0);
});
});
```
================================================
FILE: ai-skills/skills/valdi-component-tests/tests/BUILD.bazel
================================================
load("//bzl/valdi:valdi_module.bzl", "valdi_module")
valdi_module(
name = "tests",
srcs = glob([
"src/**/*.ts",
"src/**/*.tsx",
"test/**/*.ts",
"test/**/*.tsx",
]) + [
"tsconfig.json",
],
android_output_target = "debug",
ios_module_name = "SCCValdiCompTestRef",
ios_output_target = "debug",
visibility = ["//visibility:public"],
deps = [
"//src/valdi_modules/src/valdi/foundation",
"//src/valdi_modules/src/valdi/jasmine",
"//src/valdi_modules/src/valdi/valdi_core",
"//src/valdi_modules/src/valdi/valdi_test",
"//src/valdi_modules/src/valdi/valdi_tsx",
],
)
================================================
FILE: ai-skills/skills/valdi-component-tests/tests/README.md
================================================
# valdi-component-tests skill tests
Reference spec file demonstrating all test utility patterns from `skill.md`.
## Files
| File | Description |
|------|-------------|
| `src/ProfileCard.tsx` | Component under test — has an image, labels, conditional child, tap handler |
| `test/ProfileCardTest.spec.tsx` | Spec file exercising all test utilities |
## What's demonstrated
`test/ProfileCardTest.spec.tsx` exercises:
- `elementKeyFind` — typed `getAttribute('src')` on image element
- `elementKeyFind` — untyped string attribute access
- `elementKeyFind` — typed `getAttribute('onTap')` for direct invocation
- `elementTypeFind` — find elements by type (`IRenderedElementViewClass.Label`)
- `componentTypeFind` — conditional child component presence/absence
- `tapNodeWithKey` — async tap triggering a spy callback
- `fail.bind(null, '...')` — factory function default for callbacks that must not fire
- `valdiIt` — test runner with driver
## Run tests
```bash
cd client/src/open_source
bazel test //ai-skills/skills/valdi-component-tests/tests:tests
```
Expected: all jasmine specs pass.
## Updating
When test utility APIs change, update `test/ProfileCardTest.spec.tsx` to match and keep it passing. Also update `../skill.md` with any corrected patterns.
================================================
FILE: ai-skills/skills/valdi-component-tests/tests/src/ProfileCard.tsx
================================================
// Simple component used by the component-tests skill reference spec.
// Exercises: elementKeyFind, elementTypeFind, componentTypeFind, tapNodeWithKey.
import { Component } from 'valdi_core/src/Component';
// ─── FollowBadge: child component (used for componentTypeFind) ────────────────
export interface FollowBadgeViewModel {
label: string;
}
export class FollowBadge extends Component {
onRender(): void {
;
}
}
// ─── ProfileCard ──────────────────────────────────────────────────────────────
export interface ProfileCardViewModel {
avatarUrl: string;
name: string;
isFollowing: boolean;
onFollowTap: () => void;
}
export class ProfileCard extends Component {
onRender(): void {
;
;
{this.viewModel.isFollowing && }
;
;
;
}
}
================================================
FILE: ai-skills/skills/valdi-component-tests/tests/test/ProfileCardTest.spec.tsx
================================================
// valdi-component-tests skill reference — demonstrates all test utility patterns.
// Run: bzl test //ai-skills/skills/valdi-component-tests/tests:tests
import { componentGetElements } from 'foundation/test/util/componentGetElements';
import { componentTypeFind } from 'foundation/test/util/componentTypeFind';
import { elementKeyFind } from 'foundation/test/util/elementKeyFind';
import { elementTypeFind } from 'foundation/test/util/elementTypeFind';
import { tapNodeWithKey } from 'foundation/test/util/tapNodeWithKey';
import 'jasmine/src/jasmine';
import { IRenderedElementViewClass } from 'valdi_test/test/IRenderedElementViewClass';
import { valdiIt } from 'valdi_test/test/JSXTestUtils';
import { ImageView, View } from 'valdi_tsx/src/NativeTemplateElements';
import { FollowBadge, ProfileCard, ProfileCardViewModel } from '../src/ProfileCard';
// ─── Factory function with explicit return type ───────────────────────────────
const makeViewModel = (): ProfileCardViewModel => ({
avatarUrl: 'https://example.com/avatar.png',
name: 'Alice',
isFollowing: false,
onFollowTap: fail.bind(null, 'onFollowTap should not be called'),
});
// ─── Tests ───────────────────────────────────────────────────────────────────
describe('ProfileCardTest', () => {
// elementKeyFind: typed access to src attribute
valdiIt('Verify avatar src is bound from viewModel', async driver => {
const nodes = driver.render(() => {
;
});
expect(
elementKeyFind(componentGetElements(nodes[0].component!), 'avatar')[0]
?.getAttribute('src'),
).toBe('https://example.com/avatar.png');
});
// elementKeyFind: untyped access to string attribute
valdiIt('Verify name label is bound from viewModel', async driver => {
const nodes = driver.render(() => {
;
});
expect(
elementKeyFind(componentGetElements(nodes[0].component!), 'name-label')[0]
?.getAttribute('value'),
).toBe('Alice');
});
// elementTypeFind: find all Label elements without key attributes
valdiIt('Verify label count when not following', async driver => {
const nodes = driver.render(() => {
;
});
const labels = elementTypeFind(
componentGetElements(nodes[0].component!),
IRenderedElementViewClass.Label,
);
// name-label + follow button label = 2 (FollowBadge not rendered)
expect(labels.length).toBe(2);
});
// componentTypeFind: conditional child component absent
valdiIt('Verify FollowBadge is absent when isFollowing is false', async driver => {
const nodes = driver.render(() => {
;
});
expect(
componentTypeFind(nodes[0].component as ProfileCard, FollowBadge).length,
).toBe(0);
});
// componentTypeFind: conditional child component present
valdiIt('Verify FollowBadge is present when isFollowing is true', async driver => {
const nodes = driver.render(() => {
;
});
expect(
componentTypeFind(nodes[0].component as ProfileCard, FollowBadge).length,
).toBe(1);
});
// tapNodeWithKey: async tap triggers callback
valdiIt('Verify onFollowTap fires when follow button is tapped', async driver => {
const onFollowTap = jasmine.createSpy('onFollowTap');
const nodes = driver.render(() => {
;
});
await tapNodeWithKey(nodes[0].component!, 'follow-button');
expect(onFollowTap).toHaveBeenCalled();
});
// elementKeyFind: typed access to onTap via getAttribute
valdiIt('Verify onTap is set on follow button', async driver => {
const onFollowTap = jasmine.createSpy('onFollowTap');
const nodes = driver.render(() => {
;
});
const button = elementKeyFind(
componentGetElements(nodes[0].component!),
'follow-button',
)[0];
button?.getAttribute('onTap')?.();
expect(onFollowTap).toHaveBeenCalled();
});
});
================================================
FILE: ai-skills/skills/valdi-component-tests/tests/tsconfig.json
================================================
{"extends": "../../../../src/valdi_modules/src/valdi/_configs/base.tsconfig.json"}
================================================
FILE: ai-skills/skills/valdi-cpp-runtime/skill.md
================================================
# C++ Runtime Rules
**Applies to**: C++ files in `/valdi/`, `/valdi_core/`, `/snap_drawing/`
## Overview
Valdi's runtime and layout engine are implemented in C++ for cross-platform performance. This code runs on iOS, Android, macOS, but not web.
**Note**: `/libs/` contains shared utility libraries (crypto, logging, image processing) used across the codebase, but not the core runtime itself.
## Code Style
- **4-space indentation**
- Follow existing C++ style in the codebase
- Use smart pointers appropriately
- Prefer const correctness
## Key Concepts
### Layout Engine
- Uses **Yoga** (Facebook's Flexbox implementation) for layout
- Cross-platform layout calculations
- Performance-critical code
- RTL (right-to-left) support built-in
- Yoga source: `/third-party/yoga/`
### Memory Management
- Be careful with memory ownership
- Use RAII patterns
- Consider platform-specific memory constraints (mobile)
### Platform Abstractions
- Code must work on iOS, Android, macOS
- Use platform-agnostic APIs where possible
- Platform-specific code goes in appropriate subdirectories
## Common Patterns
### Djinni Generated Code
- Some C++ code is generated from `.djinni` interface files
- **Don't modify generated code** - change the .djinni file instead
- Generated files typically in `generated-src/` directories
### Performance
- This is performance-critical code
- Profile before optimizing
- Consider cache locality
- Be mindful of allocations in hot paths
## Testing
```bash
# Run all C++ runtime tests
bazel test //valdi:test
# Run specific test suites
bazel test //valdi:test_runtime # Runtime tests
bazel test //valdi:test_integration # Integration tests
bazel test //valdi:test_snap_drawing # Snap drawing tests
```
## Platform-Specific Notes
### iOS
- Objective-C++ bridge in `/valdi/src/valdi/ios/`
- Metal for GPU rendering
- UIKit view integration
### Android
- JNI bridge in `/valdi/src/valdi/android/`
- NDK integration
- Native view rendering
### macOS
- Platform-specific code in `/valdi/src/valdi/macos/`
- Desktop runtime support
### Build
```bash
# Build runtime for specific platform
bazel build //valdi:valdi_ios
bazel build //valdi:valdi_android
bazel build //valdi:valdi_macos
```
## Important
1. **Cross-platform first** - Code must work on all platforms
2. **Performance critical** - UI rendering and layout
3. **Memory efficiency** - Mobile devices have constraints
4. **Thread safety** - Consider concurrency
## More Information
- Runtime overview: `/valdi/README.md`
- Core bindings: `/valdi_core/README.md`
- Framework docs: `/AGENTS.md`
================================================
FILE: ai-skills/skills/valdi-custom-view/skill.md
================================================
# Custom View Rules
**Applies to**: `**/*.tsx` files using `` elements.
## `` Element
`` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name.
```typescript
```
## Class Attributes
| Attribute | Platform | Resolution |
|-----------|----------|------------|
| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` |
| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` |
| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` |
| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export |
## Platform Discovery
- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime.
- **iOS**: The ObjC class must be an `NSView`/`UIView` subclass linked into the binary.
- **macOS**: Same as iOS but with `NSView` subclass.
- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a web polyglot entry file (see `web-polyglot.md` rule).
## viewFactory Pattern
Instead of resolving by class name, you can pass a `ViewFactory` object directly. This is useful when the factory is provided by a parent component or constructed dynamically:
```typescript
import { ViewFactory } from 'valdi_tsx/src/ViewFactory';
interface MyViewModel {
viewFactory?: ViewFactory;
}
class MyComponent extends Component {
onRender(): void {
if (this.viewModel.viewFactory) {
;
}
}
}
```
`viewFactory` and `*Class` attributes are mutually exclusive — use one or the other. `viewFactory` takes precedence when both are provided.
## Common Mistakes
- Using `` without checking the platform — wrap in `Device.isAndroid()` / `Device.isIOS()` etc. if not all platforms are supported
- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel
- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly
================================================
FILE: ai-skills/skills/valdi-ios/skill.md
================================================
# iOS Runtime Rules
**Applies to**: Objective-C/C++ files in `/valdi/src/valdi/ios/` and related iOS runtime code
## Overview
The Valdi iOS runtime bridges TypeScript/Valdi components to native UIKit views. It's implemented in Objective-C, Objective-C++, and Swift.
## Code Style
- **4-space indentation** for Objective-C
- Follow Apple's Objective-C conventions
- Use modern Objective-C features (properties, blocks, etc.)
## Key Concepts
### View Rendering
- Valdi components map to UIViews
- View recycling for performance
- UIKit integration
### Platform Bridge
- Objective-C++ bridge to C++ runtime
- Memory management (ARC)
- iOS-specific APIs
## Common Patterns
### Bazel iOS Targets
```python
objc_library(
name = "valdi_ios",
srcs = glob(["**/*.m", "**/*.mm"]),
hdrs = glob(["**/*.h"]),
)
```
### View Implementation
- Custom UIView subclasses
- CALayer for advanced rendering
- Metal for GPU acceleration
## Testing
```bash
# Run iOS runtime tests (Objective-C)
bazel test //valdi:valdi_ios_objc_test
# Run iOS runtime tests (Swift)
bazel test //valdi:valdi_ios_swift_test
# Run all iOS tests
bazel test //valdi:valdi_ios_objc_test //valdi:valdi_ios_swift_test
```
Test files are in `/valdi/test/ios/` and `/valdi/test/ios_swift/`
## Building
```bash
# Build iOS runtime library
bazel build //valdi:valdi_ios
# Test with hello world app
cd apps/helloworld
valdi install ios
```
## Platform-Specific Notes
1. **iOS Version** - Be mindful of minimum iOS version
2. **ARC** - Automatic Reference Counting (memory management)
3. **Auto Layout** - Valdi uses flexbox, not Auto Layout
4. **Metal** - GPU rendering for advanced graphics
## Important
- **Performance** - UIView creation and layout are critical
- **Memory** - Understand retain cycles and weak references
- **Threading** - Main thread for UI, background for heavy work
- **Lifecycle** - UIViewController lifecycle
## More Information
- Runtime source: `/valdi/src/valdi/ios/`
- Runtime tests: `/valdi/test/ios/` and `/valdi/test/ios_swift/`
- Core iOS code: `/valdi_core/src/valdi_core/ios/`
- Build config: `/valdi/BUILD.bazel`
- Framework docs: `/AGENTS.md`
================================================
FILE: ai-skills/skills/valdi-migrate/skill.md
================================================
# Valdi Migration Assistant
Guidance for migrating code from Flutter, React, or Jetpack Compose to Valdi.
## When to use
Use this skill when converting Flutter widgets, React components, or Compose `@Composable` functions to Valdi components, or when translating framework-specific patterns (hooks, widgets, Navigator, setState, remember, LaunchedEffect, Modifier, Provider, styled-components, FlatList, LazyColumn, etc.) to Valdi equivalents.
## Critical: Never suggest these patterns
```typescript
// ❌ React hooks — DO NOT EXIST in Valdi
useState / useEffect / useContext / useMemo / useCallback / useRef
// ❌ Compose APIs — DO NOT EXIST in Valdi
@Composable annotation
remember { mutableStateOf() } / remember { }
derivedStateOf / collectAsState / LaunchedEffect / DisposableEffect
Modifier chain (Modifier.padding().background().clickable())
CompositionLocalProvider / LocalXxx.current
// ❌ Functional components — DO NOT EXIST
const MyComp = () => ;
function MyComp(props) { return ; }
// ❌ Wrong naming
this.props // → this.viewModel
onMount/onUnmount // → onCreate/onDestroy
markNeedsRender() // → this.setState({})
scheduleRender() // → deprecated, use setState
// ❌ Returning JSX — onRender() returns void
onRender() { return ; } // no return statement
// ❌ Inline lambdas in JSX props — causes re-renders
this.doThing()} /> // use class arrow fn
// ❌ map() in render — does not work (JSX is side-effect, not return value)
{items.map(i => )} // use forEach
// ❌ JSX as a prop value
} /> // use render prop () => void
// ❌ new Style() inside onRender() — style interning requires module-level init
onRender() { const s = new Style({...}); ... } // wrong
```
## Correct patterns
```typescript
import { Component, StatefulComponent } from 'valdi_core/src/Component';
// ✅ Stateless component
class MyComp extends Component {
onRender() {
;
;
}
}
// ✅ Stateful component
class Counter extends StatefulComponent {
state = { count: 0 };
// Class arrow function — never inline lambda in JSX
private handleTap = () => {
this.setState({ count: this.state.count + 1 });
};
onRender() {
;
;
}
}
// ✅ Lists — forEach, not map()
onRender() {
{this.viewModel.items.forEach(item => {
;
})}
;
}
// ✅ Style — created at module level, not inside onRender()
const cardStyle = new Style({ backgroundColor: '#fff', borderRadius: 8 });
```
## Lifecycle mapping
| Flutter | Jetpack Compose | React | Valdi |
|---------|-----------------|-------|-------|
| `initState()` | `LaunchedEffect(Unit) { }` | `componentDidMount` / `useEffect(fn, [])` | `onCreate()` |
| `dispose()` | `DisposableEffect { onDispose { } }` | `componentWillUnmount` / `useEffect(() => fn, [])` | `onDestroy()` |
| `didUpdateWidget(old)` | `LaunchedEffect(key) { }` | `componentDidUpdate(prev)` / `useEffect(fn, [dep])` | `onViewModelUpdate(previous?)` |
| `build(context)` | `@Composable fun` recomposition | `render(): JSX.Element` | `onRender(): void` |
| `setState(() {...})` | `mutableStateOf` + state write | `this.setState({...})` | `this.setState({...})` |
## Component and element mapping
| Flutter | Jetpack Compose | React | Valdi |
|---------|-----------------|-------|-------|
| `StatelessWidget` | `@Composable fun` (stateless) | Function component | `class X extends Component` |
| `StatefulWidget` + `State` | `@Composable fun` + `remember { mutableStateOf() }` | Class component + state | `class X extends StatefulComponent` |
| `Container` / `SizedBox` (visual) | `Box` / `Surface` | `