Compare commits

..

21 Commits

Author SHA1 Message Date
Lucien Greathouse
cafb547894 v0.2.3 2017-12-04 00:19:17 -08:00
Lucien Greathouse
35543c2790 Structured test-folder project 2017-12-04 00:17:46 -08:00
Lucien Greathouse
88efdb5ba4 Make tests compatible with TestEZ, provide 'runTests' script 2017-12-04 00:09:30 -08:00
Lucien Greathouse
eeff7cfd92 Add dependencies to rojo.json for development 2017-12-03 23:57:23 -08:00
Lucien Greathouse
f66cbe0049 Add dependencies:
* Roact
* Rodux
* RoactRodux
* TestEZ
2017-12-03 23:50:54 -08:00
Lucien Greathouse
d0c6f2a470 Clean up development a little bit -- when 'dev' is set to true, port 8001 is used 2017-12-03 19:20:54 -08:00
Lucien Greathouse
34d5de9f2c Tighten init file handling, fixes some buggy edge cases by not supporting them 2017-12-03 19:02:58 -08:00
Lucien Greathouse
16676ebfa1 Add Studio Bridge to README, forgot it! 2017-12-03 13:35:18 -08:00
Lucien Greathouse
bf9be6ccae Fix reconciler with init files, v0.2.2 2017-12-01 15:18:36 -08:00
Lucien Greathouse
974ebc33c2 Major documentation facelift, should be usable now 2017-12-01 14:07:06 -08:00
Lucien Greathouse
4b03a79cfe Change config to work with plugin version v0.2.1 2017-12-01 02:49:49 -08:00
Lucien Greathouse
43cc350b7a 0.2.1 2017-12-01 02:48:43 -08:00
Lucien Greathouse
5685619c3a Switch to using the latest Rojo release to sync itself 2017-12-01 02:40:08 -08:00
Lucien Greathouse
f3483ee2e0 0.2.0 2017-12-01 02:02:39 -08:00
Lucien Greathouse
60a9135452 Robust init.lua support 2017-12-01 01:55:34 -08:00
Lucien Greathouse
c3d6dc0e2c First past at implementing init.lua support 2017-12-01 01:28:23 -08:00
Lucien Greathouse
2681972976 Much more robust reconciliation implementation 2017-12-01 00:53:41 -08:00
Lucien Greathouse
5e64773784 Improve plugin accuracy 2017-12-01 00:18:11 -08:00
Lucien Greathouse
c7171ef513 Add Rojo config for testing 2017-12-01 00:17:45 -08:00
Lucien Greathouse
63b21b90ff Ripple verbosity flags through the server 2017-12-01 00:17:29 -08:00
Lucien Greathouse
7f3aaf4680 Fix Cargo metadata 2017-11-29 17:41:30 -08:00
30 changed files with 541 additions and 154 deletions

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "modules/Roact"]
path = modules/Roact
url = https://github.com/Roblox/Roact.git
[submodule "modules/Rodux"]
path = modules/Rodux
url = https://github.com/Roblox/Rodux.git
[submodule "modules/RoactRodux"]
path = modules/RoactRodux
url = https://github.com/Roblox/RoactRodux.git
[submodule "modules/TestEZ"]
path = modules/TestEZ
url = https://github.com/Roblox/TestEZ.git

View File

@@ -3,5 +3,22 @@
## Current Master ## Current Master
* *No changes* * *No changes*
## 0.2.3
* Plugin only release
* Tightened `init` file rules to only match script files
* Previously, Rojo would sometimes pick up the wrong file when syncing
## 0.2.2
* Plugin only release
* Fixed broken reconciliation behavior with `init` files
## 0.2.1
* Plugin only release
* Changes default port to 8000
## 0.2.0
* Support for `init.lua` like rbxfs and rbxpacker
* More robust syncing with a new reconciler
## 0.1.0 ## 0.1.0
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs) * Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)

2
Cargo.lock generated
View File

@@ -1,6 +1,6 @@
[root] [root]
name = "rojo" name = "rojo"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"clap 2.28.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.28.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@@ -1,8 +1,10 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.1.0" version = "0.2.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "A tool to create robust Roblox projects" description = "A tool to create robust Roblox projects"
license = "MIT"
repository = "https://github.com/LPGhatguy/rojo"
[[bin]] [[bin]]
name = "rojo" name = "rojo"

124
README.md
View File

@@ -3,53 +3,37 @@
<a href="https://travis-ci.org/LPGhatguy/Rojo"> <a href="https://travis-ci.org/LPGhatguy/Rojo">
<img src="https://api.travis-ci.org/LPGhatguy/Rojo.svg?branch=master" alt="Travis-CI Build Status" /> <img src="https://api.travis-ci.org/LPGhatguy/Rojo.svg?branch=master" alt="Travis-CI Build Status" />
</a> </a>
<a href="#">
<img src="https://img.shields.io/badge/docs-soon-red.svg" alt="Documentation" />
</a>
</div> </div>
<div>&nbsp;</div> <div>&nbsp;</div>
**EARLY DEVELOPMENT, USE WITH CARE** Rojo is a flexible multi-tool designed for creating robust Roblox projects. It's in early development, but is still useful for many projects.
Rojo is a flexible multi-tool designed for creating robust Roblox projects. It's designed for power users who want to use the **best tools available** for building games, libraries, and plugins.
It's designed for power users who want to use the best tools available for building games, libraries, and plugins. ## Features
It has a number of desirable features *right now*: Rojo has a number of desirable features *right now*:
* Work from the filesystem, in your favorite editor * Work on scripts from the filesystem, in your favorite editor
* Version your place, library, or plugin using Git or another VCS * Version your place, library, or plugin using Git or another VCS
Soon, Rojo will be able to: Soon, Rojo will be able to:
* Sync Roblox objects (including models) bi-directionally between the filesystem and Roblox Studio
* Create installation scripts for libraries to be used in standalone places * Create installation scripts for libraries to be used in standalone places
* Similar to [rbxpacker](https://github.com/LPGhatguy/rbxpacker), another one of my projects * Similar to [rbxpacker](https://github.com/LPGhatguy/rbxpacker), another one of my projects
* Add strongly-versioned dependencies to your project * Add strongly-versioned dependencies to your project
## Installation ## Installation
Rojo has two components: Rojo has two components:
* The binary, written in Rust * The command line tool, written in Rust
* The [Roblox Studio plugin](https://www.roblox.com/library/1211549683/Rojo-v0-0-0), written in Lua * The [Roblox Studio plugin](https://www.roblox.com/library/1211549683/Rojo-v0-0-0), written in Lua
To install the binary, there are two options: To install the command line tool, there are two options:
* Cargo, which requires you to have Rust installed * Cargo, if you have Rust installed
* Pre-built binaries from the [the GitHub releases page](https://github.com/LPGhatguy/rojo/releases) * Use `cargo install rojo` -- Rojo will be available with the `rojo` command
* Download a pre-built Windows binary from [the GitHub releases page](https://github.com/LPGhatguy/rojo/releases)
### Cargo (Recommended)
Make sure you have [Rust 1.21 or newer](https://www.rust-lang.org/) installed.
Install Rojo using:
```sh
cargo install rojo
# Installed!
rojo help
```
### Pre-Built (Windows only)
Download the latest binary from [the GitHub releases page](https://github.com/LPGhatguy/rojo/releases). Put it somewhere you can access it from a terminal!
## Usage ## Usage
For more help, use `rojo help`. For more help, use `rojo help`.
@@ -66,8 +50,92 @@ rojo init
Rojo will create an empty project in the directory. Rojo will create an empty project in the directory.
The default project looks like this:
```json
{
"name": "my-new-project",
"servePort": 8000,
"partitions": {}
}
```
### Start Dev Server
To create a server that allows the Rojo Dev Plugin to access your project, use:
```sh
rojo serve
```
The tool will tell you whether it found an existing project. You should then be able to connect and use the project from within Roblox Studio!
### Migrating an Existing Roblox Project ### Migrating an Existing Roblox Project
Coming soon! **Coming soon!**
### Syncing into Roblox
In order to sync code into Roblox, you'll need to add one or more "partitions" to your configuration. A partition tells Rojo how to map directories to Roblox objects.
Each entry in the partitions table has a unique name, a filesystem path, and the full name of the Roblox object to sync into.
For example, if you want to map your `src` directory to an object named `My Cool Game` in `ReplicatedStorage`, you could use this configuration:
```json
{
"name": "rojo",
"servePort": 8000,
"partitions": {
"game": {
"path": "src",
"target": "ReplicatedStorage.My Cool Game"
}
}
}
```
The `path` parameter is relative to the project file.
The `target` starts at `game` and crawls down the tree. If any objects don't exist along the way, they'll be created as `Folder` instances.
Run `rojo serve` in the directory containing this project, then press the "Sync In" or "Toggle Polling" buttons in the Roblox Studio plugin to move code into your game.
### Sync Details
The structure of files and folders on the filesystem are preserved when syncing into game.
Creation of Roblox instances follows a simple set of rules. The first rule that matches the file name is chosen:
| File Name | Instance Type | Notes |
| -------------- | -------------- | ----------------------------------------- |
| `*.server.lua` | `Script` | `Source` will contain the file's contents |
| `*.client.lua` | `LocalScript` | `Source` will contain the file's contents |
| `*.lua` | `ModuleScript` | `Source` will contain the file's contents |
| `*` | `StringValue` | `Value` will contain the file's contents |
Any folders on the filesystem will turn into `Folder` objects unless they contain a file named `init.lua`, `init.server.lua`, or `init.client.lua`. Following the convention of Lua, those objects will instead be whatever the `init` file would turn into.
For example, this file tree:
* my-game
* init.client.lua
* foo.lua
Will turn into this tree in Roblox:
* `my-game` (`LocalScript` with source from `my-game/init.client.lua`)
* `foo` (`ModuleScript` with source from `my-game/foo.lua`)
## Inspiration
There are lots of other tools that sync scripts into Roblox, or otherwise work to improve the development flow outside of Roblox Studio.
Here are a few, if you're looking for alternatives or supplements to Rojo:
* [Studio Bridge by Vocksel](https://github.com/vocksel/studio-bridge)
* [RbxRefresh by Osyris](https://github.com/osyrisrblx/RbxRefresh)
* [RbxSync by evaera](https://github.com/evaera/RbxSync)
* [CodeSync](https://github.com/MemoryPenguin/CodeSync) and [rbx-exteditor](https://github.com/MemoryPenguin/rbx-exteditor) by [MemoryPenguin](https://github.com/MemoryPenguin)
* [rbxmk by Anaminus](https://github.com/anaminus/rbxmk)
I also have a couple tools that Rojo intends to replace:
* [rbxfs](https://github.com/LPGhatguy/rbxfs), which has been deprecated by Rojo
* [rbxpacker](https://github.com/LPGhatguy/rbxpacker), which is still useful
## License ## License
Rojo is available under the terms of the MIT license. See [LICENSE.md](LICENSE.md) for details. Rojo is available under the terms of the MIT license. See [LICENSE.md](LICENSE.md) for details.

1
modules/Roact Submodule

Submodule modules/Roact added at 7cce62b130

1
modules/RoactRodux Submodule

Submodule modules/RoactRodux added at 43c4f347fe

1
modules/Rodux Submodule

Submodule modules/Rodux added at 6c573259ab

1
modules/TestEZ Submodule

Submodule modules/TestEZ added at 9945f562e5

View File

@@ -51,6 +51,6 @@ files["**/*.server.lua"] = {
std = "+plugin", std = "+plugin",
} }
files["**/*-spec.lua"] = { files["**/*.spec.lua"] = {
std = "+testez", std = "+testez",
} }

View File

@@ -1,4 +0,0 @@
{
"rootDirectory": "src",
"rootObject": "ReplicatedStorage.Rojo"
}

View File

@@ -1,3 +1,5 @@
return { return {
pollingRate = 0.3, pollingRate = 0.3,
version = "v0.2.3",
dev = false,
} }

View File

@@ -1,9 +1,17 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local HTTP_DEBUG = false
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local HttpError = require(script.Parent.HttpError) local HttpError = require(script.Parent.HttpError)
local HttpResponse = require(script.Parent.HttpResponse) local HttpResponse = require(script.Parent.HttpResponse)
local function dprint(...)
if HTTP_DEBUG then
print(...)
end
end
local Http = {} local Http = {}
Http.__index = Http Http.__index = Http
@@ -20,6 +28,7 @@ function Http.new(baseUrl)
end end
function Http:get(endpoint) function Http:get(endpoint)
dprint("\nGET", endpoint)
return Promise.new(function(resolve, reject) return Promise.new(function(resolve, reject)
spawn(function() spawn(function()
local ok, result = pcall(function() local ok, result = pcall(function()
@@ -27,6 +36,7 @@ function Http:get(endpoint)
end) end)
if ok then if ok then
dprint("\t", result, "\n")
resolve(HttpResponse.new(result)) resolve(HttpResponse.new(result))
else else
reject(HttpError.fromErrorString(result)) reject(HttpError.fromErrorString(result))
@@ -36,6 +46,8 @@ function Http:get(endpoint)
end end
function Http:post(endpoint, body) function Http:post(endpoint, body)
dprint("\nPOST", endpoint)
dprint(body)
return Promise.new(function(resolve, reject) return Promise.new(function(resolve, reject)
spawn(function() spawn(function()
local ok, result = pcall(function() local ok, result = pcall(function()
@@ -43,6 +55,7 @@ function Http:post(endpoint, body)
end) end)
if ok then if ok then
dprint("\t", result, "\n")
resolve(HttpResponse.new(result)) resolve(HttpResponse.new(result))
else else
reject(HttpError.fromErrorString(result)) reject(HttpError.fromErrorString(result))

View File

@@ -3,26 +3,38 @@ if not plugin then
end end
local Plugin = require(script.Parent.Plugin) local Plugin = require(script.Parent.Plugin)
local Config = require(script.Parent.Config)
local function main() local function main()
local pluginInstance = Plugin.new() local pluginInstance = Plugin.new()
local toolbar = plugin:CreateToolbar("Rojo Plugin 0.1.0") local displayedVersion = Config.dev and "DEV" or Config.version
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "") toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "")
.Click:Connect(function() .Click:Connect(function()
pluginInstance:connect() pluginInstance:connect()
:catch(function(err)
warn(err)
end)
end) end)
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "") toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "")
.Click:Connect(function() .Click:Connect(function()
pluginInstance:syncIn() pluginInstance:syncIn()
:catch(function(err)
warn(err)
end)
end) end)
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "") toolbar:CreateButton("Toggle Polling", "Poll server for changes", "")
.Click:Connect(function() .Click:Connect(function()
spawn(function() spawn(function()
pluginInstance:togglePolling() pluginInstance:togglePolling()
:catch(function(err)
warn(err)
end)
end) end)
end) end)
end end

View File

@@ -2,6 +2,7 @@ local Config = require(script.Parent.Config)
local Http = require(script.Parent.Http) local Http = require(script.Parent.Http)
local Server = require(script.Parent.Server) local Server = require(script.Parent.Server)
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local Reconciler = require(script.Parent.Reconciler)
local function collectMatch(source, pattern) local function collectMatch(source, pattern)
local result = {} local result = {}
@@ -13,91 +14,12 @@ local function collectMatch(source, pattern)
return result return result
end end
local function fileToName(filename)
if filename:find("%.server%.lua$") then
return filename:match("^(.-)%.server%.lua$"), "Script"
elseif filename:find("%.client%.lua$") then
return filename:match("^(.-)%.client%.lua$"), "LocalScript"
elseif filename:find("%.lua") then
return filename:match("^(.-)%.lua$"), "ModuleScript"
else
return filename, "StringValue"
end
end
local function nameToInstance(filename, contents)
local name, className = fileToName(filename)
local instance = Instance.new(className)
instance.Name = name
if className:find("Script$") then
instance.Source = contents
else
instance.Value = contents
end
return instance
end
local function make(item, name)
if item.type == "dir" then
local instance = Instance.new("Folder")
instance.Name = name
for childName, child in pairs(item.children) do
make(child, childName).Parent = instance
end
return instance
elseif item.type == "file" then
return nameToInstance(name, item.contents)
else
error("not implemented")
end
end
local function write(parent, route, item)
local location = parent
for index = 1, #route - 1 do
local piece = route[index]
local newLocation = location:FindFirstChild(piece)
if not newLocation then
newLocation = Instance.new("Folder")
newLocation.Name = piece
newLocation.Parent = location
end
location = newLocation
end
local fileName = route[#route]
local name = fileToName(fileName)
local existing = location:FindFirstChild(name)
local new
if item then
new = make(item, fileName)
end
if existing then
existing:Destroy()
end
if new then
new.Parent = location
end
end
local Plugin = {} local Plugin = {}
Plugin.__index = Plugin Plugin.__index = Plugin
function Plugin.new() function Plugin.new()
local address = "localhost" local address = "localhost"
local port = 8081 local port = Config.dev and 8001 or 8000
local remote = ("http://%s:%d"):format(address, port) local remote = ("http://%s:%d"):format(address, port)
@@ -149,7 +71,7 @@ end
function Plugin:connect() function Plugin:connect()
print("Testing connection...") print("Testing connection...")
self:server() return self:server()
:andThen(function(server) :andThen(function(server)
return server:getInfo() return server:getInfo()
end) end)
@@ -163,8 +85,10 @@ end
function Plugin:togglePolling() function Plugin:togglePolling()
if self._polling then if self._polling then
self:stopPolling() self:stopPolling()
return Promise.resolve(nil)
else else
self:startPolling() return self:startPolling()
end end
end end
@@ -173,12 +97,30 @@ function Plugin:stopPolling()
return return
end end
print("Stopping polling...") print("Stopped polling.")
self._polling = false self._polling = false
self._label.Enabled = false self._label.Enabled = false
end end
function Plugin:_pull(server, project, routes)
local items = server:read(routes):await()
for index = 1, #routes do
local route = routes[index]
local partitionName = route[1]
local partition = project.partitions[partitionName]
local item = items[index]
local fullRoute = collectMatch(partition.target, "[^.]+")
for i = 2, #route do
table.insert(fullRoute, routes[index][i])
end
Reconciler.reconcileRoute(fullRoute, item)
end
end
function Plugin:startPolling() function Plugin:startPolling()
if self._polling then if self._polling then
return return
@@ -204,17 +146,7 @@ function Plugin:startPolling()
table.insert(routes, change.route) table.insert(routes, change.route)
end end
local items = server:read(routes):await() self:_pull(server, project, routes)
for index = 1, #routes do
local partitionName = routes[index][1]
local partition = project.partitions[partitionName]
local data = items[index]
local fullRoute = collectMatch(partition.target, "[^.]+")
write(game, fullRoute, data)
end
wait(Config.pollingRate) wait(Config.pollingRate)
end end
@@ -231,23 +163,13 @@ function Plugin:syncIn()
:andThen(function(server) :andThen(function(server)
local project = server:getInfo():await().project local project = server:getInfo():await().project
local readRoutes = {} local routes = {}
for name in pairs(project.partitions) do for name in pairs(project.partitions) do
table.insert(readRoutes, {name}) table.insert(routes, {name})
end end
local items = server:read(readRoutes):await() self:_pull(server, project, routes)
for index = 1, #readRoutes do
local partitionName = readRoutes[index][1]
local partition = project.partitions[partitionName]
local data = items[index]
local fullRoute = collectMatch(partition.target, "[^.]+")
write(game, fullRoute, data)
end
print("Sync successful!") print("Sync successful!")
end) end)

View File

@@ -2,7 +2,7 @@
An implementation of Promises similar to Promise/A+. An implementation of Promises similar to Promise/A+.
]] ]]
local PROMISE_DEBUG = true local PROMISE_DEBUG = false
-- If promise debugging is on, use a version of pcall that warns on failure. -- If promise debugging is on, use a version of pcall that warns on failure.
-- This is useful for finding errors that happen within Promise itself. -- This is useful for finding errors that happen within Promise itself.
@@ -89,6 +89,9 @@ function Promise.new(callback)
-- Only valid if _status is set to something besides Started -- Only valid if _status is set to something besides Started
_value = nil, _value = nil,
-- If an error occurs with no observers, this will be set.
_unhandledRejection = false,
-- Queues representing functions we should invoke when we update! -- Queues representing functions we should invoke when we update!
_queuedResolve = {}, _queuedResolve = {},
_queuedReject = {}, _queuedReject = {},
@@ -157,6 +160,8 @@ end
The given callbacks are invoked depending on that result. The given callbacks are invoked depending on that result.
]] ]]
function Promise:andThen(successHandler, failureHandler) function Promise:andThen(successHandler, failureHandler)
self._unhandledRejection = false
-- Create a new promise to follow this part of the chain -- Create a new promise to follow this part of the chain
return Promise.new(function(resolve, reject) return Promise.new(function(resolve, reject)
-- Our default callbacks just pass values onto the next promise. -- Our default callbacks just pass values onto the next promise.
@@ -199,6 +204,8 @@ end
This matches the execution model of normal Roblox functions. This matches the execution model of normal Roblox functions.
]] ]]
function Promise:await() function Promise:await()
self._unhandledRejection = false
if self._status == Promise.Status.Started then if self._status == Promise.Status.Started then
local result local result
local bindable = Instance.new("BindableEvent") local bindable = Instance.new("BindableEvent")
@@ -279,11 +286,22 @@ function Promise:_reject(...)
-- synchronously. We'll wait one tick, and if there are still no -- synchronously. We'll wait one tick, and if there are still no
-- observers, then we should put a message in the console. -- observers, then we should put a message in the console.
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format( self._unhandledRejection = true
tostring((...)), local err = tostring((...))
self._source
) spawn(function()
warn(message) -- Someone observed the error, hooray!
if not self._unhandledRejection then
return
end
-- Build a reasonable message
local message = ("Unhandled promise rejection:\n\n%s\n\n%s"):format(
err,
self._source
)
warn(message)
end)
end end
end end

264
plugin/src/Reconciler.lua Normal file
View File

@@ -0,0 +1,264 @@
local Reconciler = {}
--[[
The set of file names that should pass as init files
These files usurp their parents.
]]
local initNames = {
["init.lua"] = true,
["init.server.lua"] = true,
["init.client.lua"] = true,
}
local function isInit(item, itemFileName)
if item and item.type == "dir" then
return
end
return initNames[itemFileName] or false
end
--[[
Determines if the given VFS item has an init file. Yields information about
the file.
]]
local function findInit(item)
if item.type ~= "dir" then
return nil, nil
end
for childFileName, childItem in pairs(item.children) do
if isInit(childItem, childFileName) then
return childItem, childFileName
end
end
return nil, nil
end
--[[
Given a VFS item, returns a Name and ClassName for a corresponding Roblox
instance.
Doesn't take into account init files.
]]
local function itemToName(item, fileName)
if item and item.type == "dir" then
return fileName, "Folder"
elseif item and item.type == "file" or not item then
if fileName:find("%.server%.lua$") then
return fileName:match("^(.-)%.server%.lua$"), "Script"
elseif fileName:find("%.client%.lua$") then
return fileName:match("^(.-)%.client%.lua$"), "LocalScript"
elseif fileName:find("%.lua") then
return fileName:match("^(.-)%.lua$"), "ModuleScript"
else
return fileName, "StringValue"
end
else
error("unknown item type " .. tostring(item.type))
end
end
--[[
Given a VFS item, assigns all relevant values (except Name!) to a Roblox
instance.
]]
local function setValues(rbx, item, fileName)
local _, className = itemToName(item, fileName)
if className:find("Script") then
rbx.Source = item.contents
else
rbx.Value = item.contents
end
end
function Reconciler._reifyShallow(item, fileName)
if item.type == "dir" then
local initItem, initFileName = findInit(item)
if initItem then
local rbx = Reconciler._reify(initItem, initFileName)
rbx.Name = fileName
return rbx
else
local rbx = Instance.new("Folder")
rbx.Name = fileName
return rbx
end
elseif item.type == "file" then
local objectName, className = itemToName(item, fileName)
local rbx = Instance.new(className)
rbx.Name = objectName
setValues(rbx, item, fileName)
return rbx
else
error("unknown item type " .. tostring(item.type))
end
end
--[[
Construct a new Roblox instance tree that corresponds to the given VFS item.
]]
function Reconciler._reify(item, fileName, parent)
local rbx = Reconciler._reifyShallow(item, fileName)
if item.type == "dir" then
for childFileName, childItem in pairs(item.children) do
if not isInit(childItem, childFileName) then
local childRbx = Reconciler._reify(childItem, childFileName)
childRbx.Parent = rbx
end
end
end
rbx.Parent = parent
return rbx
end
function Reconciler.reconcile(rbx, item, fileName, parent)
-- Item was deleted!
if not item then
if isInit(item, fileName) then
if not parent then
return
end
-- Un-usurp parent!
local newParent = Instance.new("Folder")
newParent.Name = parent.Name
for _, child in ipairs(parent:GetChildren()) do
child.Parent = newParent
end
newParent.Parent = parent.Parent
parent:Destroy()
return
else
if rbx then
rbx:Destroy()
end
return
end
end
if item.type == "dir" then
-- Folder was created!
if not rbx then
return Reconciler._reify(item, fileName, parent)
end
local initItem, initFileName = findInit(item)
if initItem then
local _, initClassName = itemToName(initItem, initFileName)
if rbx.ClassName == initClassName then
setValues(rbx, initItem, initFileName)
else
rbx:Destroy()
return Reconciler._reify(item, fileName, parent)
end
else
if rbx.ClassName ~= "Folder" then
rbx:Destroy()
return Reconciler._reify(item, fileName, parent)
end
end
local visitedChildren = {}
for childFileName, childItem in pairs(item.children) do
if not isInit(childItem, childFileName) then
local childName = itemToName(childItem, childFileName)
visitedChildren[childName] = true
Reconciler.reconcile(rbx:FindFirstChild(childName), childItem, childFileName, rbx)
end
end
for _, childRbx in ipairs(rbx:GetChildren()) do
-- Child was deleted!
if not visitedChildren[childRbx.Name] then
childRbx:Destroy()
end
end
return rbx
elseif item.type == "file" then
if isInit(item, fileName) then
-- Usurp our container!
local _, className = itemToName(item, fileName)
if parent.ClassName == className then
rbx = parent
else
rbx = Reconciler._reify(item, fileName, parent.Parent)
rbx.Name = parent.Name
for _, child in ipairs(parent:GetChildren()) do
child.Parent = rbx
end
parent:Destroy()
end
setValues(rbx, item, fileName)
return rbx
else
if not rbx then
return Reconciler._reify(item, fileName, parent)
end
local _, className = itemToName(item, fileName)
if rbx.ClassName ~= className then
rbx:Destroy()
return Reconciler._reify(item, fileName, parent)
end
setValues(rbx, item, fileName)
return rbx
end
else
error("unknown item type " .. tostring(item.type))
end
end
function Reconciler.reconcileRoute(route, item)
local location = game
for i = 1, #route - 1 do
local piece = route[i]
local newLocation = location:FindFirstChild(piece)
if not newLocation then
newLocation = Instance.new("Folder")
newLocation.Name = piece
newLocation.Parent = location
end
location = newLocation
end
local fileName = route[#route]
local name = itemToName(item, fileName)
local rbx = location:FindFirstChild(name)
Reconciler.reconcile(rbx, item, fileName, location)
end
return Reconciler

4
plugin/src/runTests.lua Normal file
View File

@@ -0,0 +1,4 @@
return function()
local TestEZ = require(script.Parent.Parent.TestEZ)
TestEZ.TestBootstrap:run(script.Parent)
end

26
rojo.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "rojo",
"servePort": 8000,
"partitions": {
"plugin": {
"path": "plugin/src",
"target": "ReplicatedStorage.Rojo"
},
"modules/Roact": {
"path": "modules/Roact/lib",
"target": "ReplicatedStorage.Rojo.modules.Roact"
},
"modules/Rodux": {
"path": "modules/Rodux/lib",
"target": "ReplicatedStorage.Rojo.modules.Rodux"
},
"modules/RoactRodux": {
"path": "modules/RoactRodux/lib",
"target": "ReplicatedStorage.Rojo.modules.RoactRodux"
},
"modules/TestEZ": {
"path": "modules/TestEZ/lib",
"target": "ReplicatedStorage.TestEZ"
}
}
}

View File

@@ -128,7 +128,7 @@ fn main() {
} }
let vfs = { let vfs = {
let mut vfs = Vfs::new(); let mut vfs = Vfs::new(config.clone());
for (name, project_partition) in &project.partitions { for (name, project_partition) in &project.partitions {
let path = { let path = {
@@ -158,9 +158,10 @@ fn main() {
{ {
let vfs = vfs.clone(); let vfs = vfs.clone();
let config = config.clone();
thread::spawn(move || { thread::spawn(move || {
VfsWatcher::new(vfs).start(); VfsWatcher::new(config, vfs).start();
}); });
} }

View File

@@ -5,6 +5,8 @@ use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Instant; use std::time::Instant;
use core::Config;
/// Represents a virtual layer over multiple parts of the filesystem. /// Represents a virtual layer over multiple parts of the filesystem.
/// ///
/// Paths in this system are represented as slices of strings, and are always /// Paths in this system are represented as slices of strings, and are always
@@ -21,6 +23,8 @@ pub struct Vfs {
/// A chronologically-sorted list of routes that changed since the Vfs was /// A chronologically-sorted list of routes that changed since the Vfs was
/// created, along with a timestamp denoting when. /// created, along with a timestamp denoting when.
pub change_history: Vec<VfsChange>, pub change_history: Vec<VfsChange>,
config: Config,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -38,11 +42,12 @@ pub enum VfsItem {
} }
impl Vfs { impl Vfs {
pub fn new() -> Vfs { pub fn new(config: Config) -> Vfs {
Vfs { Vfs {
partitions: HashMap::new(), partitions: HashMap::new(),
start_time: Instant::now(), start_time: Instant::now(),
change_history: Vec::new(), change_history: Vec::new(),
config,
} }
} }
@@ -140,6 +145,10 @@ impl Vfs {
} }
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) { pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
if self.config.verbose {
println!("Added change {:?}", route);
}
self.change_history.push(VfsChange { self.change_history.push(VfsChange {
timestamp, timestamp,
route, route,

View File

@@ -6,17 +6,20 @@ use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
use vfs::Vfs; use vfs::Vfs;
use pathext::path_to_route; use pathext::path_to_route;
use core::Config;
pub struct VfsWatcher { pub struct VfsWatcher {
vfs: Arc<Mutex<Vfs>>, vfs: Arc<Mutex<Vfs>>,
watchers: Vec<RecommendedWatcher>, watchers: Vec<RecommendedWatcher>,
config: Config,
} }
impl VfsWatcher { impl VfsWatcher {
pub fn new(vfs: Arc<Mutex<Vfs>>) -> VfsWatcher { pub fn new(config: Config, vfs: Arc<Mutex<Vfs>>) -> VfsWatcher {
VfsWatcher { VfsWatcher {
vfs, vfs,
watchers: Vec::new(), watchers: Vec::new(),
config,
} }
} }
@@ -40,6 +43,7 @@ impl VfsWatcher {
{ {
let vfs = self.vfs.clone(); let vfs = self.vfs.clone();
let config = self.config.clone();
thread::spawn(move || { thread::spawn(move || {
loop { loop {
@@ -47,6 +51,10 @@ impl VfsWatcher {
let mut vfs = vfs.lock().unwrap(); let mut vfs = vfs.lock().unwrap();
let current_time = vfs.current_time(); let current_time = vfs.current_time();
if config.verbose {
println!("FS event {:?}", event);
}
match event { match event {
DebouncedEvent::Write(ref change_path) | DebouncedEvent::Write(ref change_path) |
DebouncedEvent::Create(ref change_path) | DebouncedEvent::Create(ref change_path) |

View File

@@ -1 +0,0 @@
-- meh/init.lua

10
test-project/rojo.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "test-project",
"servePort": 8001,
"partitions": {
"src": {
"path": "src",
"target": "ReplicatedStorage.TestProject"
}
}
}

View File