mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Introduced a new endpoint `/api/syncback` for one-click full sync from Roblox Studio to the filesystem, eliminating the need for an active WebSocket connection. - Implemented `SyncbackRequest` and `ServiceChunk` structures to handle service-level properties and child instances during syncback. - Enhanced `encodeService` to include ObjectValue carriers for accurate reference handling and prevent data loss. - Updated the syncback process to utilize Luau-style relative paths for reference attributes, improving path management and consistency. - Integrated session caching in the matching algorithm to optimize performance during syncback operations. These changes significantly improve the live syncback experience, ensuring reliable data synchronization and enhanced performance during development.
399 lines
13 KiB
Text
399 lines
13 KiB
Text
---
|
|
alwaysApply: true
|
|
---
|
|
|
|
# Atlas Project Guide
|
|
|
|
> Atlas is a fork of Rojo. Many internal identifiers, API endpoints, and struct names still reference the original "Rojo" naming.
|
|
|
|
## Quality Standard
|
|
|
|
This is a production-grade synchronization system. The filesystem must represent a 1:1 copy of the Studio place. The core invariant:
|
|
|
|
**Round-trip identity:** Syncback (or two-way sync) writes a directory tree. Building an rbxl from that directory tree and forward-syncing it back must produce a bit-identical instance tree -- same names, same classes, same properties, same hierarchy, same ref targets. Any deviation is a bug.
|
|
|
|
Every audit item must be evaluated against this standard. A "works most of the time" finding is not acceptable. If there is ANY code path where an instance name, property, or hierarchy relationship can be lost, mangled, or silently altered through a syncback→rebuild cycle, flag it as critical.
|
|
|
|
## Code Quality Standard
|
|
|
|
We are building a maintainable codebase. During audits, watch for duplicated logic -- the same slugify/dedup/meta-update pattern copy-pasted across `change_processor.rs`, `api.rs`, `dir.rs`, `project.rs`, etc. If you find duplicated code that could be consolidated into a shared helper:
|
|
|
|
- **Small refactor** (extracting a helper function, consolidating 2-3 call sites): flag it AND fix it as part of the audit.
|
|
- **Major system rewrite** (restructuring data flow, changing function signatures across 10+ call sites, redesigning the snapshot pipeline): flag it clearly in a "Deferred Refactors" section at the bottom of the report. We will evaluate whether it's worth doing after the audit is complete.
|
|
|
|
The goal is clean, DRY code that is easy to reason about and hard to get wrong. If the same "compute meta path, check if name needs slugify, upsert/remove name field" sequence appears in 3+ places, that's a refactor candidate.
|
|
|
|
## What is Atlas?
|
|
|
|
Atlas bridges filesystem and Roblox Studio, enabling professional development tools (VS Code, Git) for Roblox. It's designed for power users building complex games, libraries, and plugins.
|
|
|
|
**Core capabilities:**
|
|
- Filesystem-based development with live sync to Studio
|
|
- Build/upload rbxl/rbxm files from command line
|
|
- Syncback: pull instances from Roblox → filesystem (7.7.0+)
|
|
- Two-way sync (plugin-based): live bidirectional sync during `atlas serve` sessions
|
|
- Live syncback: one-click full sync from Studio to filesystem during `atlas serve` (no WebSocket connection required)
|
|
|
|
### Two-Way Sync Strategy
|
|
|
|
**Long-term goal:** Plugin-based two-way sync will completely replace the `atlas syncback` CLI command, allowing syncback to be deprecated.
|
|
|
|
**Current state:**
|
|
- `atlas syncback` (CLI) is the **ground truth** for Roblox → filesystem syncing. It is battle-tested and handles edge cases reliably.
|
|
- Plugin-based two-way sync (via `atlas serve` + the Studio plugin) is under active development to reach feature parity and match the resilience of CLI syncback.
|
|
- The bar for replacing syncback is high: plugin two-way sync must handle all the same scenarios (slugified names, dedup suffixes, nested projects, property conflicts, large trees, concurrent edits) just as reliably before syncback can be deprecated.
|
|
|
|
**When working on two-way sync:** Plugin-based sync must produce **exactly the same filesystem output** that `atlas syncback` would give for the same input. Byte-for-byte identical files, identical directory structures, identical naming. If the plugin path produces different results, treat syncback as the correct reference unless there's an explicit, documented reason to diverge. Any divergence is a bug until proven otherwise.
|
|
|
|
## Architecture Overview
|
|
|
|
### Three Components
|
|
|
|
1. **Server/CLI (Rust)** - `src/`, commands: serve, build, upload, syncback, sourcemap
|
|
2. **Studio Plugin (Lua)** - `plugin/`, Roact UI, WebSocket sync
|
|
3. **rbx-dom (submodule)** - `rbx-dom/`, Roblox format libraries (binary/XML, types, reflection)
|
|
|
|
### The Snapshot System
|
|
|
|
Atlas's core innovation (`src/snapshot/`):
|
|
1. **Snapshot Generation**: Pure function filesystem → instance tree
|
|
2. **Diffing**: Generate minimal PatchSet
|
|
3. **Patch Application**: Apply changes, return AppliedPatchSet for plugin
|
|
|
|
This avoids manual tree manipulation bugs and handles complex cases (large models, concurrent edits).
|
|
|
|
## Project Files
|
|
|
|
`.project.json5` (preferred) | `.project.json` (legacy):
|
|
|
|
```json5
|
|
{
|
|
name: "MyProject", // Optional if default.project.json5 (inherits folder name)
|
|
tree: {
|
|
$path: "src"
|
|
},
|
|
servePlaceIds: [123456], // Optional: restrict live sync
|
|
globIgnorePaths: ["**/*.md"], // Optional: ignore patterns
|
|
syncRules: [...], // Optional: custom file type handling
|
|
syncbackRules: {...} // Optional: syncback configuration
|
|
}
|
|
```
|
|
|
|
**Key conventions:**
|
|
- Services don't need explicit `$className` (inferred)
|
|
- `default.project.json5` inherits parent folder name
|
|
- Projects can be nested (each has independent sync rules)
|
|
- **Use `.luau` for scripts, `.json5` for config** (`.lua`/`.json` are legacy but supported)
|
|
|
|
## Contributing
|
|
|
|
### Required Tools
|
|
|
|
1. **Rust** (minimum 1.88 - see `Cargo.toml`)
|
|
2. **Rokit** - toolchain manager (installs selene, stylua, run-in-roblox)
|
|
3. **Git submodules** - `git submodule update --init --recursive`
|
|
|
|
For plugin work: Luau LSP, Studio with "Reload plugins on file change"
|
|
|
|
### Initial Setup
|
|
|
|
```bash
|
|
git clone --recursive https://github.com/UserGeneratedLLC/rojo
|
|
cd rojo
|
|
rokit install # Installs tools from rokit.toml
|
|
cargo test # Verify setup
|
|
```
|
|
|
|
### Development Workflow
|
|
|
|
**Server (Rust):**
|
|
```bash
|
|
cargo build
|
|
cargo test
|
|
cargo fmt # Format code
|
|
cargo clippy # Lint
|
|
```
|
|
|
|
**Plugin (Lua):**
|
|
```bash
|
|
# Watch mode (auto-rebuild on save)
|
|
bash scripts/watch-build-plugin.sh # macOS/Linux
|
|
.\scripts\watch-build-plugin.ps1 # Windows
|
|
|
|
# Run tests
|
|
bash scripts/unit-test-plugin.sh
|
|
```
|
|
|
|
**Install plugin:** `atlas plugin install`
|
|
|
|
### Testing
|
|
|
|
**Types:**
|
|
1. Rust unit tests - alongside code (`.rs` + tests module)
|
|
2. Rust integration tests - `tests/` directory
|
|
3. Lua unit tests - `.spec.lua` files (TestEZ)
|
|
4. Snapshot tests - `rojo-test/` with `insta` crate
|
|
|
|
**Snapshot testing workflow:**
|
|
```bash
|
|
cargo test
|
|
cargo insta review # Review snapshot changes
|
|
cargo insta accept # Accept all changes
|
|
```
|
|
|
|
Test fixtures: `rojo-test/build-tests/`, `serve-tests/`, `syncback-tests/`
|
|
|
|
### CI Requirements
|
|
|
|
All PRs must pass (`.github/workflows/ci.yml`):
|
|
- Build on 5 platforms (Ubuntu, Windows, macOS, ARM variants)
|
|
- `cargo test --locked`
|
|
- MSRV check (Rust 1.88)
|
|
- Linting: `cargo fmt --check`, `cargo clippy`, `stylua --check`, `selene`
|
|
|
|
### Changelog
|
|
|
|
Every PR updates `CHANGELOG.md`:
|
|
```markdown
|
|
## Unreleased
|
|
|
|
* Made a change to Atlas ([#1234])
|
|
|
|
[#1234]: https://github.com/UserGeneratedLLC/rojo/pull/1234
|
|
```
|
|
|
|
Keep link definitions in ascending order.
|
|
|
|
### Before Submitting PR
|
|
|
|
- [ ] Code formatted (`cargo fmt`, `stylua plugin/src`)
|
|
- [ ] Linters pass (`cargo clippy`, `selene plugin/src`)
|
|
- [ ] Tests pass (`cargo test`)
|
|
- [ ] New tests added if needed
|
|
- [ ] CHANGELOG.md updated
|
|
- [ ] No warnings in build output
|
|
|
|
### Important: Talk to Maintainers First
|
|
|
|
Open issue/discussion before working on features or bugs. Prevents wasted effort on changes that won't be accepted.
|
|
|
|
## Code Conventions
|
|
|
|
### Rust Style
|
|
|
|
**Formatting:** Use `cargo fmt` (default config, no custom rustfmt.toml)
|
|
|
|
**Error handling:**
|
|
```rust
|
|
use anyhow::{Context, Result};
|
|
|
|
fn do_something() -> Result<()> {
|
|
let file = fs::read_to_string(path)
|
|
.context("Failed to read project file")?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Module organization** (see `src/snapshot/mod.rs`):
|
|
- Module doc at top explaining purpose, data flow
|
|
- Private submodules, then public exports
|
|
- Tests in `#[cfg(test)] mod tests`
|
|
|
|
**Context for middleware:**
|
|
```rust
|
|
pub struct InstanceContext {
|
|
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
|
pub sync_rules: Vec<SyncRule>,
|
|
}
|
|
```
|
|
|
|
**Filename handling:** Instance names with forbidden filesystem chars are slugified (forbidden chars → `_`), deduplicated (`~2`, `~3`, etc. on collision), and the real name is stored in `.meta.json5`/`.model.json5` `name` fields. See `src/syncback/file_names.rs`.
|
|
|
|
**VFS passed separately to snapshot functions:**
|
|
```rust
|
|
snapshot_from_vfs(&context, &vfs, &path)?
|
|
```
|
|
|
|
### Lua Style
|
|
|
|
**Formatting:** Use `stylua` (default config)
|
|
**Linting:** `selene` (see `selene.toml` - roblox+testez standard)
|
|
|
|
**Strict mode required:**
|
|
```lua
|
|
local strict = require(script.Parent.strict)
|
|
|
|
local MyModule = {}
|
|
|
|
function MyModule.doSomething()
|
|
-- implementation
|
|
end
|
|
|
|
return strict("MyModule", {
|
|
doSomething = MyModule.doSomething,
|
|
})
|
|
```
|
|
|
|
**Roact components:**
|
|
```lua
|
|
local MyComponent = Roact.Component:extend("MyComponent")
|
|
|
|
function MyComponent:init()
|
|
self.state = { value = 0 }
|
|
end
|
|
|
|
function MyComponent:render()
|
|
return Roact.createElement("Frame", {
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
})
|
|
end
|
|
|
|
return MyComponent
|
|
```
|
|
|
|
**TestEZ pattern:**
|
|
```lua
|
|
return function()
|
|
describe("MyModule", function()
|
|
it("should do something", function()
|
|
expect(result).to.equal(expected)
|
|
end)
|
|
end)
|
|
end
|
|
```
|
|
|
|
### Naming Conventions
|
|
|
|
**Rust:** `snake_case` for functions/variables, `PascalCase` for types
|
|
**Lua:** `camelCase` for locals/functions, `PascalCase` for modules/classes, `SCREAMING_SNAKE_CASE` for constants
|
|
|
|
**Critical:** Luau arrays are 1-indexed! `local arr = {"first"}; print(arr[1])`
|
|
|
|
### Project Files (JSON5)
|
|
|
|
Prefer JSON5 over JSON for comments and trailing commas:
|
|
|
|
```json5
|
|
{
|
|
// Services inferred automatically
|
|
ReplicatedStorage: {
|
|
$path: "src/shared"
|
|
},
|
|
|
|
ServerScriptService: {
|
|
$path: "src/server",
|
|
$properties: {
|
|
LoadStringEnabled: false // Trailing comma OK
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Loading a Project (Rust)
|
|
```rust
|
|
use librojo::Project;
|
|
let project = Project::load_fuzzy(&vfs, path)?;
|
|
// or: Project::load_exact(path)?;
|
|
```
|
|
|
|
### Building Snapshots (Rust)
|
|
```rust
|
|
use librojo::{InstanceContext, snapshot_from_vfs};
|
|
let snapshot = snapshot_from_vfs(&context, &vfs, &path)?;
|
|
```
|
|
|
|
### Applying Patches (Lua)
|
|
```lua
|
|
local Reconciler = require(script.Reconciler)
|
|
Reconciler.applyPatch(instanceMap, patch)
|
|
```
|
|
|
|
## File Organization
|
|
|
|
```
|
|
src/ - Rust server/CLI
|
|
cli/ - Command implementations
|
|
snapshot/ - Core snapshot system
|
|
matching.rs - Forward sync matching algorithm
|
|
snapshot_middleware/ - File type handlers
|
|
syncback/ - Roblox → filesystem
|
|
matching.rs - Syncback matching algorithm
|
|
dedup_suffix.rs - Dedup cleanup rules
|
|
meta.rs - Meta file read/write helpers
|
|
web/ - HTTP/WebSocket server
|
|
interface.rs - Wire types (patches, syncback requests)
|
|
change_processor.rs - Two-way sync filesystem writer
|
|
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
|
|
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
|
|
variant_eq.rs - Property value comparison
|
|
|
|
plugin/src/ - Lua Studio plugin
|
|
App/ - Roact UI components
|
|
Reconciler/ - Patch application logic
|
|
matching.lua - Lua matching algorithm
|
|
trueEquals.lua - Shared value equality module
|
|
ChangeBatcher/ - Two-way sync patch creation
|
|
encodeService.lua - Service-level property encoding for live syncback
|
|
Config.lua - Version and protocol config
|
|
PatchTree.lua - Patch visualization tree (git-based default selections)
|
|
SHA1.luau - SHA1 hash for git blob format comparison
|
|
XXH32.luau - Hash function for plugin
|
|
|
|
rbx-dom/ - Git submodule (separate repo)
|
|
rbx_binary/ - Binary format (rbxm/rbxl)
|
|
rbx_xml/ - XML format (rbxmx/rbxlx)
|
|
rbx_dom_weak/ - Instance representation
|
|
rbx_reflection/ - Property metadata
|
|
rbx_reflection_database/ - Reflection DB
|
|
|
|
tests/ - Rust integration tests
|
|
rojo-test/ - Test fixtures and utilities
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
**Rust:**
|
|
- Don't use `unwrap()` or `expect()` in library code (use `?`)
|
|
- Don't ignore errors
|
|
- Don't use string concatenation for paths (use `Path::join()`)
|
|
|
|
**Lua:**
|
|
- Don't use 0-based indexing (arrays start at 1!)
|
|
- Don't use deprecated functions (`getfenv`, `setfenv`, `wait()`)
|
|
- Don't mutate props in Roact components
|
|
- Don't forget `strict()` wrapper
|
|
|
|
## Version Information
|
|
|
|
- **Protocol version:** 6 (`plugin/src/Config.lua`)
|
|
- **Server/Plugin must match:** MAJOR.MINOR (e.g., 7.7.x works with 7.7.y)
|
|
- **MSRV:** Rust 1.88
|
|
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
|
|
|
## Resources
|
|
|
|
- **Documentation:** https://rojo.space/docs
|
|
- **rbx-dom docs:** https://dom.rojo.space
|
|
- **CONTRIBUTING.md** - Detailed contribution guide
|
|
- **CHANGELOG.md** - Complete release history with PR links
|
|
- **Snapshot system explanation:** `src/snapshot/mod.rs`
|
|
|
|
## Common Issues
|
|
|
|
**Submodule not initialized:**
|
|
```bash
|
|
git submodule update --init --recursive
|
|
```
|
|
|
|
**Build fails with MSRV error:** Check Rust version matches 1.88+
|
|
|
|
**Snapshot tests fail:** Review and accept changes:
|
|
```bash
|
|
cargo insta review
|
|
```
|
|
|
|
**Plugin not syncing:** Check protocol version matches between server and plugin
|
|
|
|
**Properties not syncing:** Verify property is scriptable in rbx_reflection_database
|