mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
- Updated the `audit.md` documentation to clarify the syncback process and the structure of the `FsSnapshot`. - Revised the `atlas-api.mdc` to reflect changes in the Git metadata handling, including the introduction of a separate endpoint for fetching Git metadata. - Improved the `atlas-dedup.mdc` to include handling for the reserved name "init" during slugification. - Enhanced the `atlas-syncback.mdc` to document the new `SyncbackResult` structure and its integration with the syncback process. - Updated various sections across multiple documentation files to ensure consistency and clarity regarding the project's architecture and API endpoints. These changes aim to improve the overall understanding of the system and its components, facilitating better usage and integration.
436 lines
15 KiB
Text
436 lines
15 KiB
Text
---
|
|
globs: src/**,crates/**
|
|
alwaysApply: false
|
|
---
|
|
|
|
# Rojo Server/CLI (Rust)
|
|
|
|
Core implementation: CLI, HTTP/WebSocket server, snapshot system, filesystem transformations.
|
|
|
|
**Entry:** `src/main.rs` (CLI) | `src/lib.rs` (public API)
|
|
|
|
## Core Systems
|
|
|
|
### 1. Snapshot System (`src/snapshot/`)
|
|
|
|
Pure functions: filesystem → instance tree → minimal patches → apply changes
|
|
|
|
**Key files:**
|
|
- `mod.rs` - System overview with detailed explanation
|
|
- `patch_compute.rs` - Generate diffs (uses matching algorithm)
|
|
- `patch_apply.rs` - Apply changes (resolves `Rojo_Ref_*` and `Rojo_Target_*` refs)
|
|
- `matching.rs` - Forward sync matching algorithm (recursive change-count scoring, `MatchingSession` caching)
|
|
- `instance_snapshot.rs` - Core snapshot type
|
|
|
|
```rust
|
|
pub struct InstanceSnapshot {
|
|
pub snapshot_id: Ref,
|
|
pub metadata: InstanceMetadata,
|
|
pub name: Cow<'static, str>,
|
|
pub class_name: Ustr,
|
|
pub properties: UstrMap<Variant>,
|
|
pub children: Vec<InstanceSnapshot>, // Nested snapshots, not Refs!
|
|
}
|
|
```
|
|
|
|
### 2. Snapshot Middleware (`src/snapshot_middleware/`)
|
|
|
|
Transforms files → `InstanceSnapshot`. Main entry: `snapshot_from_vfs()`
|
|
|
|
| File | Handler | Result | Note |
|
|
|------|---------|--------|------|
|
|
| `*.luau` | `lua.rs` | ModuleScript | Preferred |
|
|
| `*.server.luau` | `lua.rs` | Script (RunContext.Server) | Preferred |
|
|
| `*.client.luau` | `lua.rs` | Script (RunContext.Client) | Preferred |
|
|
| `*.plugin.luau` | `lua.rs` | Script (RunContext.Plugin) | |
|
|
| `*.local.luau` | `lua.rs` | LocalScript | |
|
|
| `*.legacy.luau` | `lua.rs` | Script (RunContext.Legacy) | |
|
|
| `*.lua` | `lua.rs` | ModuleScript | Legacy |
|
|
| `*.server.lua` | `lua.rs` | Script (RunContext.Legacy) | Legacy |
|
|
| `*.client.lua` | `lua.rs` | LocalScript | Legacy |
|
|
| `*.plugin.lua` | `lua.rs` | Script (RunContext.Plugin) | Legacy |
|
|
| `*.rbxm` / `*.rbxmx` | `rbxm.rs` / `rbxmx.rs` | Binary/XML model tree | |
|
|
| `*.model.json5` | `json_model.rs` | JSON instance | Preferred |
|
|
| `*.model.json` | `json_model.rs` | JSON instance | Legacy |
|
|
| `*.project.json5` | `project.rs` | Nested project | Preferred |
|
|
| `*.project.json` | `project.rs` | Nested project | Legacy |
|
|
| `*.csv` / `*.txt` | `csv.rs` / `txt.rs` | LocalizationTable / StringValue | |
|
|
| `*.toml` / `*.json5` / `*.yaml` | Data → ModuleScript | |
|
|
| Directory | `dir.rs` | Folder or inferred service | |
|
|
| `*.meta.json5` | `meta_file.rs` | Metadata overlay | Preferred |
|
|
| `*.meta.json` | `meta_file.rs` | Metadata overlay | Legacy |
|
|
|
|
**Init files:** `init.luau`, `init.server.luau`, etc. → Instance named after parent directory (`.lua` legacy supported)
|
|
|
|
**Context passed to middleware:**
|
|
|
|
```rust
|
|
pub struct InstanceContext {
|
|
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
|
pub sync_rules: Vec<SyncRule>,
|
|
}
|
|
```
|
|
|
|
**Filename handling (syncback/two-way sync):**
|
|
|
|
Instance names with forbidden filesystem chars (`<>:"/\|?*~`) are handled by:
|
|
1. `slugify_name()` -- replaces forbidden chars with `_` (pure, stateless)
|
|
2. `deduplicate_name_with_ext()` -- dedup key = full filesystem name (slug + extension), case-insensitive; appends `~2`, `~3`, etc. on collision (stateful)
|
|
3. Metadata `name` field -- stores the real instance name in `.meta.json5`/`.model.json5` when the slug differs
|
|
|
|
### 3. Commands (`src/cli/`)
|
|
|
|
- `serve.rs` - Live sync with WebSocket; live syncback via `run_live_syncback()` + `build_dom_from_chunks()` (server restarts after syncback)
|
|
- `build.rs` - Generate rbxl/rbxm/rbxmx/rbxlx
|
|
- `upload.rs` - Upload to Roblox.com
|
|
- `sourcemap.rs` - Generate sourcemaps
|
|
- `syncback.rs` - Roblox → filesystem
|
|
- `clone.rs` - Create project from Roblox place (init + syncback --download)
|
|
- `completions.rs` - Shell completions (bash, zsh, fish, PowerShell, elvish) via `clap_complete`
|
|
- `cursor.rs` - Open project directory in Cursor IDE
|
|
- `studio.rs` - Open project in Roblox Studio via URL
|
|
- `init.rs` / `fmt_project.rs` / `doc.rs` / `plugin.rs`
|
|
|
|
### 4. Syncback (`src/syncback/`)
|
|
|
|
Reverse: Roblox file → filesystem
|
|
|
|
- `snapshot.rs` - Convert instances to snapshots
|
|
- `fs_snapshot.rs` - Filesystem representation
|
|
- `matching.rs` - Syncback matching algorithm (recursive change-count scoring, `MatchingSession` caching)
|
|
- `dedup_suffix.rs` - Dedup cleanup rules (gap-tolerant, base-name promotion, group-to-1)
|
|
- `meta.rs` - Meta file read/write helpers (upsert/remove name, update ref paths)
|
|
- `ref_properties.rs` - Ref property path linking (`Rojo_Ref_*`, tentative_fs_path, placeholder system)
|
|
- `hash/` - Content hashing for change detection
|
|
- `fs_snapshot.rs` - Filesystem representation, parallel writes (`write_to_vfs_parallel`), ref path post-processing (`fix_ref_paths`)
|
|
- `property_filter.rs` - Apply syncback rules (default: `sync_unscriptable = false`)
|
|
|
|
### 5. Web Server (`src/web/`)
|
|
|
|
- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback`, `GET /api/git-metadata`, MCP stream/syncback handlers
|
|
- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason`, `GitMetadata`
|
|
- `mod.rs` - `SyncbackSignal` (Mutex + Notify pattern), `LiveServer` (uses `tokio::select!` to wait for connections or syncback signal)
|
|
- `mcp.rs` - MCP JSON-RPC endpoint (`POST /mcp`), `McpState`, tool definitions and handlers
|
|
- `mcp_docs/` - MCP tool description markdown files
|
|
- Protocol version enforcement
|
|
|
|
### 6. Project System (`src/project.rs`)
|
|
|
|
Parse/validate `.project.json5` (or legacy `.project.json`), handle nested projects, manage sync rules.
|
|
|
|
## Command Implementations
|
|
|
|
### `rojo serve`
|
|
|
|
1. Load project
|
|
2. Build initial snapshot tree
|
|
3. Start HTTP server (Hyper)
|
|
4. Open WebSocket at `/api/rojo`
|
|
5. Watch filesystem (memofs VFS)
|
|
6. Generate patches on changes
|
|
7. Send to connected plugins
|
|
|
|
**Key:** `src/serve_session.rs` manages active sessions
|
|
|
|
**Live syncback integration:** The serve loop checks for `ServerExitReason::SyncbackRequested(payload)`. When received, calls `run_live_syncback()` which reconstructs the DOM from the plugin's rbxm blob + service chunks via `build_dom_from_chunks()`, runs `syncback_loop(incremental=false)` (clean mode), writes files via `write_to_vfs_parallel()`, refreshes git index, then restarts the serve loop.
|
|
|
|
### `rojo build`
|
|
|
|
1. Load project
|
|
2. Build snapshot tree
|
|
3. Convert to `WeakDom` (rbx_dom_weak)
|
|
4. Serialize with `rbx_binary` or `rbx_xml`
|
|
5. Write output
|
|
|
|
**Flags:** `--output`, `--plugin`, `--watch`
|
|
|
|
### `rojo syncback`
|
|
|
|
1. Load project
|
|
2. Read Roblox file (rbxl/rbxm)
|
|
3. Parse with `rbx_binary`
|
|
4. Match instances to project
|
|
5. Write files per syncback rules
|
|
|
|
**Rules:**
|
|
```rust
|
|
pub struct SyncbackRules {
|
|
ignore_trees: Vec<String>, // Roblox tree paths to ignore
|
|
ignore_paths: Vec<String>, // Filesystem path patterns
|
|
ignore_properties: IndexMap<Ustr, Vec<Ustr>>, // Class → properties
|
|
ignore_classes: Vec<String>, // Classes to skip entirely
|
|
sync_current_camera: Option<bool>, // Default: false
|
|
sync_unscriptable: Option<bool>, // Default: false
|
|
ignore_referents: Option<bool>, // Skip Ref properties, default: false
|
|
create_ignore_dir_paths: Option<bool>, // Default: true
|
|
ignore_hidden_services: Option<bool>, // Override root-level ignoreHiddenServices
|
|
warn_duplicate_names: Option<bool>, // Warn on duplicate child names
|
|
}
|
|
```
|
|
|
|
## Error Handling Patterns
|
|
|
|
**CLI (anyhow):**
|
|
```rust
|
|
use anyhow::{Context, Result};
|
|
|
|
pub fn load_project(path: &Path) -> Result<Project> {
|
|
fs::read_to_string(path)
|
|
.context("Failed to read project file")?;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**Library (thiserror):**
|
|
```rust
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum SnapshotError {
|
|
#[error("Instance {0} not found")]
|
|
InstanceNotFound(Ref),
|
|
}
|
|
```
|
|
|
|
**Context chaining:**
|
|
```rust
|
|
.with_context(|| format!("Failed for {}", path.display()))?
|
|
```
|
|
|
|
## Testing
|
|
|
|
**Unit tests:** Co-located with code
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_something() {
|
|
assert_eq!(result, expected);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Integration tests:** `tests/` directory with `gen_build_tests!` macro
|
|
|
|
**Snapshot tests:** `insta` crate
|
|
```rust
|
|
use insta::assert_snapshot;
|
|
|
|
#[test]
|
|
fn test_output() {
|
|
let result = build_something();
|
|
assert_snapshot!(result);
|
|
}
|
|
```
|
|
|
|
Review: `cargo insta review`
|
|
|
|
**Test utilities:** `tests/rojo_test/` - io_util, roundtrip_util, serve_util, syncback_util
|
|
|
|
## Web API
|
|
|
|
**Default port:** 34873
|
|
|
|
**HTTP:**
|
|
- `GET /` - Browser UI
|
|
- `GET /api/rojo` - Server info (version, project name, protocol)
|
|
- `GET /api/git-metadata` - Git metadata (changedIds, scriptCommittedHashes, newFileIds)
|
|
- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync
|
|
- `POST /api/mcp/syncback` - MCP syncback (inline, returns stats)
|
|
- `POST /mcp` - MCP JSON-RPC endpoint
|
|
|
|
**WebSocket:** `ws://host:port/api/socket/:cursor` (live patches), `ws://host:port/api/mcp/stream` (MCP)
|
|
|
|
**Protocol v6 (MessagePack):**
|
|
```json
|
|
{
|
|
"sessionId": "<uuid>",
|
|
"packetType": "messages",
|
|
"body": {
|
|
"messageCursor": 5,
|
|
"messages": [
|
|
{ "removed": [], "added": {}, "updated": [] }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Session:** `src/serve_session.rs` manages state, VFS, message queue
|
|
|
|
## Project File Structure
|
|
|
|
**ProjectNode:**
|
|
```rust
|
|
pub struct ProjectNode {
|
|
pub class_name: Option<Ustr>, // $className
|
|
pub id: Option<String>, // $id for Ref linking
|
|
pub children: BTreeMap<String, ProjectNode>, // Child nodes (flattened)
|
|
pub properties: BTreeMap<Ustr, UnresolvedValue>, // $properties
|
|
pub attributes: BTreeMap<String, UnresolvedValue>, // $attributes
|
|
pub ignore_unknown_instances: Option<bool>, // $ignoreUnknownInstances
|
|
pub path: Option<PathNode>, // $path
|
|
}
|
|
```
|
|
|
|
**SyncRule:**
|
|
```rust
|
|
pub struct SyncRule {
|
|
pub include: Glob, // Serialized as "pattern"
|
|
pub exclude: Option<Glob>, // Optional exclusion pattern
|
|
pub middleware: Middleware, // Serialized as "use"
|
|
pub suffix: Option<String>, // Optional suffix to trim
|
|
pub base_path: PathBuf, // Internal: glob base path
|
|
}
|
|
```
|
|
|
|
## Common Operations
|
|
|
|
**Load project:**
|
|
```rust
|
|
use librojo::Project;
|
|
let project = Project::load_fuzzy(&vfs, path)?;
|
|
// or: Project::load_exact(&vfs, path, fallback_name)?;
|
|
```
|
|
|
|
**Build snapshot:**
|
|
```rust
|
|
use librojo::{InstanceContext, snapshot_from_vfs};
|
|
let snapshot = snapshot_from_vfs(&context, &vfs, &path)?;
|
|
```
|
|
|
|
**Generate patch:**
|
|
```rust
|
|
use librojo::snapshot::compute_patch_set;
|
|
let patch = compute_patch_set(&old_snap, &new_snap, &old_tree, &new_tree);
|
|
```
|
|
|
|
**Serialize to binary:**
|
|
```rust
|
|
use rbx_binary::to_writer;
|
|
use rbx_dom_weak::WeakDom;
|
|
|
|
let dom = WeakDom::new(root);
|
|
to_writer(&mut buffer, &dom, dom.root())?;
|
|
```
|
|
|
|
## Performance
|
|
|
|
**Profiling:** Enable Tracy with `--features profile-with-tracy`
|
|
|
|
**Optimization:** Use `#[profiling::function]` on hot paths
|
|
|
|
**Parallelization:** Use `rayon` for parallel operations (sparingly - I/O often bottlenecks)
|
|
|
|
**VFS:** `memofs::Vfs` caches file contents, invalidates on watch events
|
|
|
|
## Key Dependencies
|
|
|
|
**Core:** rbx_binary, rbx_xml, rbx_dom_weak, rbx_reflection, rbx_reflection_database, memofs (local crate)
|
|
**Async:** tokio, hyper, hyper-util, http-body-util, hyper-tungstenite, reqwest, futures
|
|
**Serialization:** serde, serde_json, json5, toml, yaml-rust2, csv, rmp-serde, serde_bytes, bincode
|
|
**Hashing:** blake3, sha1
|
|
**Logging:** log, tracing, tracing-appender, tracing-subscriber, time, dirs, flate2
|
|
**CLI:** clap (with derive, env), clap_complete
|
|
**Schema:** schemars
|
|
**Float precision:** lexical-write-float (for roundtrip-safe f32/f64 formatting in JSON)
|
|
**Utilities:** anyhow, thiserror, crossbeam-channel, rayon, globset, uuid, walkdir, tempfile, pathdiff
|
|
|
|
## Module Organization
|
|
|
|
See `src/snapshot/mod.rs` for canonical example:
|
|
- Module doc explaining purpose, data flow
|
|
- Private submodules, public exports
|
|
- Tests in `#[cfg(test)] mod tests`
|
|
|
|
## Contributing Guidelines
|
|
|
|
### Adding File Type Support
|
|
|
|
1. Create middleware in `src/snapshot_middleware/`
|
|
2. Add to `Middleware` enum
|
|
3. Register in `snapshot_from_vfs()`
|
|
4. Add syncback support if applicable
|
|
5. Add tests in `rojo-test/build-tests/`
|
|
|
|
### Adding Command
|
|
|
|
1. Create file in `src/cli/`
|
|
2. Add to `Subcommand` enum
|
|
3. Implement with clap derives
|
|
4. Add `run()` method
|
|
|
|
### Modifying Snapshots
|
|
|
|
**Critical:** Add extensive tests first, consider migration path, test with real projects
|
|
|
|
## Debugging
|
|
|
|
**Verbose logging:**
|
|
```bash
|
|
RUST_LOG=trace rojo serve
|
|
RUST_LOG=rojo=debug rojo build
|
|
```
|
|
|
|
**Inspect snapshots:** Add `eprintln!("Snapshot: {:#?}", snapshot);`
|
|
|
|
**Test specific middleware:** Create minimal test in `rojo-test/build-tests/`
|
|
|
|
**Watch events:** `RUST_LOG=memofs=trace`
|
|
|
|
**Protocol issues:** Browser dev tools on `http://localhost:34873`
|
|
|
|
## Common Pitfalls
|
|
|
|
**Path separators:** Always use `Path::join()`, never string concatenation
|
|
**Watch events:** Can fire multiple times - use `change_processor` to dedupe
|
|
**Property types:** Use rbx_reflection to validate expected types
|
|
**Protocol version:** Bump in both plugin AND server when changing communication
|
|
**Backwards compat:** Support legacy formats where possible
|
|
|
|
## File Locations
|
|
|
|
```
|
|
src/
|
|
cli/ - Commands (serve, build, upload, syncback, sourcemap,
|
|
clone, completions, cursor, studio, init, fmt_project,
|
|
doc, plugin)
|
|
snapshot/ - Core system
|
|
matching.rs - Forward sync matching algorithm
|
|
tree.rs - RojoTree (script_refs, known_paths(), path_to_ids)
|
|
snapshot_middleware/ - File handlers
|
|
syncback/ - Reverse sync
|
|
matching.rs - Syncback matching algorithm
|
|
dedup_suffix.rs - Dedup cleanup rules
|
|
meta.rs - Meta file helpers
|
|
web/ - HTTP/WebSocket server
|
|
api.rs - HTTP/WebSocket endpoints
|
|
interface.rs - Wire types (patches, syncback requests, GitMetadata)
|
|
mcp.rs - MCP JSON-RPC, McpState, tool handlers
|
|
mcp_docs/ - MCP tool descriptions
|
|
change_processor.rs - Two-way sync filesystem writer
|
|
git.rs - Git integration (CLI-based, changed files, SHA1, auto-staging)
|
|
logging.rs - Structured logging, file output to ~/.atlas/logs/
|
|
message_queue.rs - Patch message batching and cursor system
|
|
roblox_api.rs - Roblox auth, place download, experience name lookup
|
|
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex, Luau-style relative paths)
|
|
variant_eq.rs - Property value comparison
|
|
lib.rs - Public API
|
|
main.rs - CLI entry
|
|
|
|
crates/
|
|
memofs/ - Virtual filesystem
|
|
|
|
tests/
|
|
end_to_end.rs - Integration test crate root
|
|
rojo_test/ - Test utilities
|
|
tests/ - Integration tests
|
|
```
|
|
|
|
## Additional Resources
|
|
|
|
- **Snapshot system:** `src/snapshot/mod.rs` (comprehensive explanation)
|
|
- **Middleware examples:** `src/snapshot_middleware/lua.rs`, `dir.rs`
|
|
- **Test examples:** `tests/tests/build.rs`, `serve.rs`
|
|
- **Project parsing:** `src/project.rs`
|