Compare commits

...

24 Commits

Author SHA1 Message Date
Lucien Greathouse
69d1accf3f 0.5.0-alpha.3 2019-02-01 17:19:00 -08:00
Lucien Greathouse
785bdb8ecb Implement new project file name, default.project.json (#120)
* Implement new project file name, default.project.json

* Rename all test projects to default.project.json

* Update CHANGELOG

* Fix warning message typo
2019-02-01 17:06:03 -08:00
Lucien Greathouse
78a1947cec Update CHANGELOG 2019-02-01 13:07:15 -08:00
Paul Doyle
0ff59ecb4e Fix issue w/ existing files not being updated in imfs (#119)
* Fix issue w/ existing files not being updated in imfs

* Add a test for updating files
2019-01-31 20:24:42 -08:00
Lucien Greathouse
b58fed16b4 Fix uses using failure::Error 2019-01-30 10:29:38 -08:00
Lucien Greathouse
6719be02c3 Fall back to showing GraphViz source when GraphViz is not installed 2019-01-29 18:10:14 -08:00
Lucien Greathouse
8757834e07 Improve error reporting for IO issues 2019-01-29 17:29:47 -08:00
Lucien Greathouse
aa243d1b8a Add sweet new live sync homepage 2019-01-28 18:30:42 -08:00
Lucien Greathouse
aeb18eb124 Refactor web code to make routing more clear 2019-01-28 18:23:57 -08:00
Lucien Greathouse
6c3e118ee3 Sort inputs in LiveSession 2019-01-28 17:50:47 -08:00
Lucien Greathouse
3c0fe4d684 Reduce number of threads needed for FsWatcher 2019-01-28 17:11:01 -08:00
Lucien Greathouse
12fd9aa1ef Tack on Cargo.lock, missing from previous commit 2019-01-28 16:03:12 -08:00
Lucien Greathouse
821122a33d 0.5.0-alpha.2 2019-01-28 15:45:52 -08:00
Lucien Greathouse
0d9406d991 Update docs links in README 2019-01-28 15:40:54 -08:00
Lucien Greathouse
350eec3bc7 Update docs and generator to be even smarter 2019-01-28 15:39:11 -08:00
Lucien Greathouse
e700b3105a New, less-kludgy doc generator 2019-01-28 15:31:20 -08:00
Lucien Greathouse
dd2a730b4a Update documentation 2019-01-28 15:16:42 -08:00
Lucien Greathouse
c6766bbe77 Fix timeout issue for real this time 2019-01-28 14:55:56 -08:00
Lucien Greathouse
e5d3204b6c Implement .model.json files
Closes #97.
2019-01-28 14:37:35 -08:00
Lucien Greathouse
4767cbd12b Fix composing-models XML to only contain stuff implemented so far 2019-01-28 14:36:17 -08:00
Lucien Greathouse
deb4118c5d Fix long-polling mixup
Fixes #110.
2019-01-28 14:00:22 -08:00
Lucien Greathouse
4516df5aac Fix message in rbx_session 2019-01-28 13:58:24 -08:00
Lucien Greathouse
663df7bdc2 Remove redundant debug assertions in imfs 2019-01-28 11:23:19 -08:00
Lucien Greathouse
e81f0a4a95 Improve IMFS robustness with out-of-order events
Fixes #111.
2019-01-28 11:03:52 -08:00
48 changed files with 921 additions and 287 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
/site /site
/target /target
/server/scratch /scratch-project
**/*.rs.bk **/*.rs.bk
/generate-docs.run /generate-docs.run

View File

@@ -2,7 +2,22 @@
## [Unreleased] ## [Unreleased]
## [0.5.0 Alpha 1](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.1) (January 14, 2019) ## [0.5.0 Alpha 3](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.3) (February 1, 2019)
* Changed default project file name from `roblox-project.json` to `default.project.json` ([#120](https://github.com/LPGhatguy/rojo/pull/120))
* The old file name will still be supported until 0.5.0 is fully released.
* Added warning when loading project files that don't end in `.project.json`
* This new extension enables Rojo to distinguish project files from random JSON files, which is necessary to support nested projects.
* Added new (empty) diagnostic page served from the server
* Added better error messages for when a file is missing that's referenced by a Rojo project
* Added support for visualization endpoints returning GraphViz source when Dot is not available
* Fixed an in-memory filesystem regression introduced recently ([#119](https://github.com/LPGhatguy/rojo/pull/119))
## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
* Added support for `.model.json` files, compatible with 0.4.x
* Fixed in-memory filesystem not handling out-of-order filesystem change events
* Fixed long-polling error caused by a promise mixup ([#110](https://github.com/LPGhatguy/rojo/issues/110))
## [0.5.0 Alpha 1](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.1) (January 25, 2019)
* Changed plugin UI to be way prettier * Changed plugin UI to be way prettier
* Thanks to [Reselim](https://github.com/Reselim) for the design! * Thanks to [Reselim](https://github.com/Reselim) for the design!
* Changed plugin error messages to be a little more useful * Changed plugin error messages to be a little more useful

8
Cargo.lock generated
View File

@@ -345,7 +345,7 @@ dependencies = [
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.5.13" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1248,11 +1248,11 @@ dependencies = [
[[package]] [[package]]
name = "rojo" name = "rojo"
version = "0.5.0-alpha.1" version = "0.5.0-alpha.3"
dependencies = [ dependencies = [
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "csv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1929,7 +1929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86" "checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
"checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" "checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd"
"checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7" "checksum encoding_rs 0.8.14 (registry+https://github.com/rust-lang/crates.io-index)" = "a69d152eaa438a291636c1971b0a370212165ca8a75759eb66818c5ce9b538f7"
"checksum env_logger 0.5.13 (registry+https://github.com/rust-lang/crates.io-index)" = "15b0a4d2e39f8420210be8b27eeda28029729e2fd4291019455016c348240c38" "checksum env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "afb070faf94c85d17d50ca44f6ad076bce18ae92f0037d350947240a36e9d42e"
"checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02" "checksum error-chain 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07e791d3be96241c77c43846b665ef1384606da2cd2a48730abe606a12906e02"
"checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2"
"checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1"

View File

@@ -40,7 +40,7 @@ Soon, Rojo will be able to:
* Sync instances from Roblox Studio to the filesystem * Sync instances from Roblox Studio to the filesystem
* Compile MoonScript and other custom things for your project * Compile MoonScript and other custom things for your project
## [Documentation](https://lpghatguy.github.io/rojo/0.4.x) ## [Documentation](https://lpghatguy.github.io/rojo)
You can also view the documentation by browsing the [docs](https://github.com/LPGhatguy/rojo/tree/master/docs) folder of the repository, but because it uses a number of Markdown extensions, it may not be very readable. You can also view the documentation by browsing the [docs](https://github.com/LPGhatguy/rojo/tree/master/docs) folder of the repository, but because it uses a number of Markdown extensions, it may not be very readable.
## Inspiration and Alternatives ## Inspiration and Alternatives

3
docs/extra.css Normal file
View File

@@ -0,0 +1,3 @@
.md-typeset__table {
width: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,17 @@
digraph "Sync Files" {
graph [
ranksep = "0.7",
nodesep = "0.5",
];
node [
fontname = "monospace",
shape = "record",
];
my_model [label = "MyModel"]
init_server [label = "init.server.lua"]
foo [label = "foo.lua"]
my_model -> init_server
my_model -> foo
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: Sync Files Pages: 1 -->
<svg width="258pt" height="132pt"
viewBox="0.00 0.00 258.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
<title>Sync Files</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-128 254,-128 254,4 -4,4"/>
<!-- my_model -->
<g id="node1" class="node"><title>my_model</title>
<polygon fill="none" stroke="black" points="104,-87.5 104,-123.5 178,-123.5 178,-87.5 104,-87.5"/>
<text text-anchor="middle" x="141" y="-101.8" font-family="monospace" font-size="14.00">MyModel</text>
</g>
<!-- init_server -->
<g id="node2" class="node"><title>init_server</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 140,-36.5 140,-0.5 0,-0.5"/>
<text text-anchor="middle" x="70" y="-14.8" font-family="monospace" font-size="14.00">init.server.lua</text>
</g>
<!-- my_model&#45;&gt;init_server -->
<g id="edge1" class="edge"><title>my_model&#45;&gt;init_server</title>
<path fill="none" stroke="black" d="M126.632,-87.299C116.335,-74.9713 102.308,-58.1787 90.7907,-44.3902"/>
<polygon fill="black" stroke="black" points="93.4435,-42.1065 84.3465,-36.6754 88.0711,-46.594 93.4435,-42.1065"/>
</g>
<!-- foo -->
<g id="node3" class="node"><title>foo</title>
<polygon fill="none" stroke="black" points="176,-0.5 176,-36.5 250,-36.5 250,-0.5 176,-0.5"/>
<text text-anchor="middle" x="213" y="-14.8" font-family="monospace" font-size="14.00">foo.lua</text>
</g>
<!-- my_model&#45;&gt;foo -->
<g id="edge2" class="edge"><title>my_model&#45;&gt;foo</title>
<path fill="none" stroke="black" d="M155.57,-87.299C166.013,-74.9713 180.237,-58.1787 191.917,-44.3902"/>
<polygon fill="black" stroke="black" points="194.659,-46.5681 198.451,-36.6754 189.317,-42.0437 194.659,-46.5681"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,15 @@
digraph "Sync Files" {
graph [
ranksep = "0.7",
nodesep = "0.5",
];
node [
fontname = "monospace",
shape = "record",
];
my_model [label = "MyModel (Script)"]
foo [label = "foo (ModuleScript)"]
my_model -> foo
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: Sync Files Pages: 1 -->
<svg width="173pt" height="132pt"
viewBox="0.00 0.00 173.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
<title>Sync Files</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-128 169,-128 169,4 -4,4"/>
<!-- my_model -->
<g id="node1" class="node"><title>my_model</title>
<polygon fill="none" stroke="black" points="8,-87.5 8,-123.5 157,-123.5 157,-87.5 8,-87.5"/>
<text text-anchor="middle" x="82.5" y="-101.8" font-family="monospace" font-size="14.00">MyModel (Script)</text>
</g>
<!-- foo -->
<g id="node2" class="node"><title>foo</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 165,-36.5 165,-0.5 0,-0.5"/>
<text text-anchor="middle" x="82.5" y="-14.8" font-family="monospace" font-size="14.00">foo (ModuleScript)</text>
</g>
<!-- my_model&#45;&gt;foo -->
<g id="edge1" class="edge"><title>my_model&#45;&gt;foo</title>
<path fill="none" stroke="black" d="M82.5,-87.299C82.5,-75.6626 82.5,-60.0479 82.5,-46.7368"/>
<polygon fill="black" stroke="black" points="86.0001,-46.6754 82.5,-36.6754 79.0001,-46.6755 86.0001,-46.6754"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,17 @@
digraph "Sync Files" {
graph [
ranksep = "0.7",
nodesep = "0.5",
];
node [
fontname = "monospace",
shape = "record",
];
model [label = "My Cool Model (Folder)"]
root_part [label = "RootPart (Part)"]
send_money [label = "SendMoney (RemoteEvent)"]
model -> root_part
model -> send_money
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
-->
<!-- Title: Sync Files Pages: 1 -->
<svg width="390pt" height="132pt"
viewBox="0.00 0.00 390.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
<title>Sync Files</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-128 386,-128 386,4 -4,4"/>
<!-- model -->
<g id="node1" class="node"><title>model</title>
<polygon fill="none" stroke="black" points="75,-87.5 75,-123.5 273,-123.5 273,-87.5 75,-87.5"/>
<text text-anchor="middle" x="174" y="-101.8" font-family="monospace" font-size="14.00">My Cool Model (Folder)</text>
</g>
<!-- root_part -->
<g id="node2" class="node"><title>root_part</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 140,-36.5 140,-0.5 0,-0.5"/>
<text text-anchor="middle" x="70" y="-14.8" font-family="monospace" font-size="14.00">RootPart (Part)</text>
</g>
<!-- model&#45;&gt;root_part -->
<g id="edge1" class="edge"><title>model&#45;&gt;root_part</title>
<path fill="none" stroke="black" d="M152.954,-87.299C137.448,-74.6257 116.168,-57.2335 99.0438,-43.2377"/>
<polygon fill="black" stroke="black" points="100.972,-40.2938 91.0147,-36.6754 96.5426,-45.7138 100.972,-40.2938"/>
</g>
<!-- send_money -->
<g id="node3" class="node"><title>send_money</title>
<polygon fill="none" stroke="black" points="176,-0.5 176,-36.5 382,-36.5 382,-0.5 176,-0.5"/>
<text text-anchor="middle" x="279" y="-14.8" font-family="monospace" font-size="14.00">SendMoney (RemoteEvent)</text>
</g>
<!-- model&#45;&gt;send_money -->
<g id="edge2" class="edge"><title>model&#45;&gt;send_money</title>
<path fill="none" stroke="black" d="M195.248,-87.299C210.904,-74.6257 232.388,-57.2335 249.677,-43.2377"/>
<polygon fill="black" stroke="black" points="252.213,-45.6878 257.783,-36.6754 247.809,-40.2471 252.213,-45.6878"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -2,9 +2,9 @@ This is the documentation home for Rojo.
Available versions of these docs: Available versions of these docs:
* [Latest version (currently 0.5.x)](https://lpghatguy.github.io/rojo)
* [0.5.x](https://lpghatguy.github.io/rojo/0.5.x) * [0.5.x](https://lpghatguy.github.io/rojo/0.5.x)
* [0.4.x](https://lpghatguy.github.io/rojo/0.4.x) * [0.4.x](https://lpghatguy.github.io/rojo/0.4.x)
* [`master` branch](https://lpghatguy.github.io/rojo/master)
**Rojo** is a flexible multi-tool designed for creating robust Roblox projects. **Rojo** is a flexible multi-tool designed for creating robust Roblox projects.

View File

@@ -50,9 +50,6 @@ Metadata begins with a dollar sign (`$`), like `$className`. This is so that chi
All other values are considered children, where the key is the instance's name, and the value is an object, repeating the process. All other values are considered children, where the key is the instance's name, and the value is an object, repeating the process.
## Migrating `.model.json` Files
No upgrade path yet, stay tuned.
## Migrating Unknown Files ## Migrating Unknown Files
If you used Rojo to sync in files as `StringValue` objects, you'll need to make sure those files end with the `txt` extension to preserve this in Rojo 0.5.x. If you used Rojo to sync in files as `StringValue` objects, you'll need to make sure those files end with the `txt` extension to preserve this in Rojo 0.5.x.

View File

@@ -9,6 +9,20 @@ This page aims to describe how Rojo turns files on the filesystem into Roblox ob
| `*.lua` | `ModuleScript` | | `*.lua` | `ModuleScript` |
| `*.csv` | `LocalizationTable` | | `*.csv` | `LocalizationTable` |
| `*.txt` | `StringValue` | | `*.txt` | `StringValue` |
| `*.model.json` | Any |
| `*.rbxm` | Any |
| `*.rbxmx` | Any |
## Limitations
Not all property types can be synced by Rojo in real-time due to limitations of the Roblox Studio plugin API. In these cases, you can usually generate a place file and open it when you start working on a project.
Some common cases you might hit are:
* Binary data (Terrain, CSG, CollectionService tags)
* `MeshPart.MeshId`
* `HttpService.HttpEnabled`
For a list of all property types that Rojo can reason about, both when live-syncing and when building place files, look at [rbx_tree's type coverage documentation](https://github.com/LPGhatguy/rbx-tree/tree/master/rbx_tree#coverage).
## Folders ## Folders
Any directory on the filesystem will turn into a `Folder` instance unless it contains an 'init' script, described below. Any directory on the filesystem will turn into a `Folder` instance unless it contains an 'init' script, described below.
@@ -20,16 +34,68 @@ If a directory contains a file named `init.server.lua`, `init.client.lua`, or `i
For example, these files: For example, these files:
* my-game <div align="center">
* init.client.lua <a href="../images/sync-example-files.svg">
* foo.lua <img src="../images/sync-example-files.svg" alt="Tree of files on disk" />
</a>
</div>
Will turn into these instances in Roblox: Will turn into these instances in Roblox:
![Example of Roblox instances](/images/sync-example.png) <div align="center">
<a href="../images/sync-example-instances.svg">
<img src="../images/sync-example-instances.svg" alt="Tree of instances in Roblox" />
</a>
</div>
## Localization Tables ## Localization Tables
Any CSV files are transformed into `LocalizationTable` instances. Rojo expects these files to follow the same format that Roblox does when importing and exporting localization information. Any CSV files are transformed into `LocalizationTable` instances. Rojo expects these files to follow the same format that Roblox does when importing and exporting localization information.
## Plain Text Files ## Plain Text Files
Plain text files (`.txt`) files are transformed into `StringValue` instances. This is useful for bringing in text data that can be read by scripts at runtime. Plain text files (`.txt`) files are transformed into `StringValue` instances. This is useful for bringing in text data that can be read by scripts at runtime.
## JSON Models
Files ending in `.model.json` can be used to describe simple models. They're designed to be hand-written and are useful for instances like `RemoteEvent`.
A JSON model describing a folder containing a `Part` and a `RemoteEvent` could be described as:
```json
{
"Name": "My Cool Model",
"ClassName": "Folder",
"Children": [
{
"Name": "RootPart",
"ClassName": "Part",
"Properties": {
"Size": {
"Type": "Vector3",
"Value": [4, 4, 4]
}
}
},
{
"Name": "SendMoney",
"ClassName": "RemoteEvent"
}
]
}
```
It would turn into instances in this shape:
<div align="center">
<a href="../images/sync-example-json-model.svg">
<img src="../images/sync-example-json-model.svg" alt="Tree of instances in Roblox" />
</a>
</div>
## Binary and XML Models
Rojo supports both binary (`.rbxm`) and XML (`.rbxmx`) models generated by Roblox Studio or another tool.
Not all property types are supported!
For a rundown of supported types, see:
* [rbxm Type Coverage](https://github.com/LPGhatguy/rbx-tree/tree/master/rbx_binary#coverage)
* [rbxmx Type Coverage](https://github.com/LPGhatguy/rbx-tree/tree/master/rbx_xml#coverage)

View File

@@ -3,23 +3,33 @@
# Kludged documentation generator to support multiple versions. # Kludged documentation generator to support multiple versions.
# Make sure the `site` folder is a checkout of this repository's `gh-pages` # Make sure the `site` folder is a checkout of this repository's `gh-pages`
# branch. # branch.
# To use, copy this file to `generate-docs.run` so that Git will leave it alone,
# then run `generate-docs.run` in the root of the repository.
set -e set -e
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) REMOTE=$(git remote get-url origin)
CHECKOUT="$(mktemp -d)"
OUTPUT="$(pwd)/site"
echo "Building 0.4.x" if [ -d site ]
git checkout v0.4.x then
git pull cd site
mkdocs build --site-dir site/0.4.x git pull
else
git clone "$REMOTE" site
cd site
git checkout gh-pages
fi
git clone "$REMOTE" "$CHECKOUT"
cd "$CHECKOUT"
echo "Building master" echo "Building master"
git checkout master git checkout master
mkdocs build --site-dir site/master mkdocs build --site-dir "$OUTPUT"
echo "Building 0.5.x" echo "Building 0.5.x"
mkdocs build --site-dir site/0.5.x mkdocs build --site-dir "$OUTPUT/0.5.x"
git checkout "$CURRENT_BRANCH" echo "Building 0.4.x"
git checkout v0.4.x
mkdocs build --site-dir "$OUTPUT/0.4.x"

View File

@@ -17,6 +17,9 @@ nav:
- Sync Details: sync-details.md - Sync Details: sync-details.md
- Migrating from 0.4.x to 0.5.x: migrating-to-epiphany.md - Migrating from 0.4.x to 0.5.x: migrating-to-epiphany.md
extra_css:
- extra.css
markdown_extensions: markdown_extensions:
- attr_list - attr_list
- admonition - admonition

View File

@@ -140,14 +140,18 @@ end
function ApiContext:retrieveMessages() function ApiContext:retrieveMessages()
local url = ("%s/api/subscribe/%s"):format(self.baseUrl, self.messageCursor) local url = ("%s/api/subscribe/%s"):format(self.baseUrl, self.messageCursor)
return Http.get(url) local function sendRequest()
:catch(function(err) return Http.get(url)
if err.type == HttpError.Error.Timeout then :catch(function(err)
return self:retrieveMessages() if err.type == HttpError.Error.Timeout then
end return sendRequest()
end
return Promise.reject(err) return Promise.reject(err)
end) end)
end
return sendRequest()
:andThen(rejectFailedRequests) :andThen(rejectFailedRequests)
:andThen(function(response) :andThen(function(response)
local body = response:json() local body = response:json()

View File

@@ -1,6 +1,6 @@
return { return {
codename = "Epiphany", codename = "Epiphany",
version = {0, 5, 0, "-alpha.1"}, version = {0, 5, 0, "-alpha.3"},
expectedServerVersionString = "0.5.0 or newer", expectedServerVersionString = "0.5.0 or newer",
protocolVersion = 2, protocolVersion = 2,
defaultHost = "localhost", defaultHost = "localhost",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "0.5.0-alpha.1" version = "0.5.0-alpha.3"
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" license = "MIT"
@@ -22,7 +22,7 @@ bundle-plugin = []
[dependencies] [dependencies]
clap = "2.27" clap = "2.27"
csv = "1.0" csv = "1.0"
env_logger = "0.5" env_logger = "0.6"
failure = "0.1.3" failure = "0.1.3"
log = "0.4" log = "0.4"
maplit = "1.0.1" maplit = "1.0.1"

54
server/assets/index.html Normal file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<title>Rojo</title>
<style>
* {
margin: 0;
padding: 0;
font: inherit;
}
html {
font-family: sans-serif;
height: 100%;
}
body {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.main {
padding: 1rem;
text-align: center;
margin: 0 auto;
width: 100%;
max-width: 60rem;
background-color: #efefef;
border: 1px solid #666;
border-radius: 4px;
}
.title {
font-size: 2rem;
font-weight: bold;
}
.docs {
font-size: 1.5rem;
font-weight: bold;
}
</style>
</head>
<body>
<div class="main">
<h1 class="title">Rojo Live Sync is up and running!</h1>
<a class="docs" href="https://lpghatguy.github.io/rojo">Rojo Documentation</a>
</div>
</body>
</html>

View File

@@ -20,9 +20,14 @@ fn make_path_absolute(value: &Path) -> PathBuf {
} }
fn main() { fn main() {
env_logger::Builder::from_default_env() {
.default_format_timestamp(false) let log_env = env_logger::Env::default()
.init(); .default_filter_or("warn");
env_logger::Builder::from_env(log_env)
.default_format_timestamp(false)
.init();
}
let app = clap_app!(Rojo => let app = clap_app!(Rojo =>
(version: env!("CARGO_PKG_VERSION")) (version: env!("CARGO_PKG_VERSION"))

View File

@@ -10,7 +10,7 @@ use failure::Fail;
use crate::{ use crate::{
rbx_session::construct_oneoff_tree, rbx_session::construct_oneoff_tree,
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
imfs::Imfs, imfs::{Imfs, FsError},
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -55,14 +55,18 @@ pub enum BuildError {
XmlModelEncodeError(rbx_xml::EncodeError), XmlModelEncodeError(rbx_xml::EncodeError),
#[fail(display = "Binary model file error")] #[fail(display = "Binary model file error")]
BinaryModelEncodeError(rbx_binary::EncodeError) BinaryModelEncodeError(rbx_binary::EncodeError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(BuildError { impl_from!(BuildError {
ProjectLoadFuzzyError => ProjectLoadError, ProjectLoadFuzzyError => ProjectLoadError,
io::Error => IoError, io::Error => IoError,
rbx_xml::EncodeError => XmlModelEncodeError, rbx_xml::EncodeError => XmlModelEncodeError,
rbx_binary::EncodeError => BinaryModelEncodeError rbx_binary::EncodeError => BinaryModelEncodeError,
FsError => FsError,
}); });
pub fn build(options: &BuildOptions) -> Result<(), BuildError> { pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
@@ -75,6 +79,7 @@ pub fn build(options: &BuildOptions) -> Result<(), BuildError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Project::load_fuzzy(&options.fuzzy_project_path)?; let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);

View File

@@ -9,6 +9,7 @@ use failure::Fail;
use crate::{ use crate::{
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
web::Server, web::Server,
imfs::FsError,
live_session::LiveSession, live_session::LiveSession,
}; };
@@ -24,21 +25,26 @@ pub struct ServeOptions {
pub enum ServeError { pub enum ServeError {
#[fail(display = "Project load error: {}", _0)] #[fail(display = "Project load error: {}", _0)]
ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError), ProjectLoadError(#[fail(cause)] ProjectLoadFuzzyError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(ServeError { impl_from!(ServeError {
ProjectLoadFuzzyError => ProjectLoadError, ProjectLoadFuzzyError => ProjectLoadError,
FsError => FsError,
}); });
pub fn serve(options: &ServeOptions) -> Result<(), ServeError> { pub fn serve(options: &ServeOptions) -> Result<(), ServeError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?); let project = Arc::new(Project::load_fuzzy(&options.fuzzy_project_path)?);
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);
let live_session = Arc::new(LiveSession::new(Arc::clone(&project)).unwrap()); let live_session = Arc::new(LiveSession::new(Arc::clone(&project))?);
let server = Server::new(Arc::clone(&live_session)); let server = Server::new(Arc::clone(&live_session));
let port = options.port let port = options.port

View File

@@ -11,7 +11,7 @@ use reqwest::header::{ACCEPT, USER_AGENT, CONTENT_TYPE, COOKIE};
use crate::{ use crate::{
rbx_session::construct_oneoff_tree, rbx_session::construct_oneoff_tree,
project::{Project, ProjectLoadFuzzyError}, project::{Project, ProjectLoadFuzzyError},
imfs::Imfs, imfs::{Imfs, FsError},
}; };
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
@@ -33,6 +33,9 @@ pub enum UploadError {
#[fail(display = "XML model file error")] #[fail(display = "XML model file error")]
XmlModelEncodeError(rbx_xml::EncodeError), XmlModelEncodeError(rbx_xml::EncodeError),
#[fail(display = "{}", _0)]
FsError(#[fail(cause)] FsError),
} }
impl_from!(UploadError { impl_from!(UploadError {
@@ -40,6 +43,7 @@ impl_from!(UploadError {
io::Error => IoError, io::Error => IoError,
reqwest::Error => HttpError, reqwest::Error => HttpError,
rbx_xml::EncodeError => XmlModelEncodeError, rbx_xml::EncodeError => XmlModelEncodeError,
FsError => FsError,
}); });
#[derive(Debug)] #[derive(Debug)]
@@ -56,6 +60,7 @@ pub fn upload(options: &UploadOptions) -> Result<(), UploadError> {
info!("Looking for project at {}", options.fuzzy_project_path.display()); info!("Looking for project at {}", options.fuzzy_project_path.display());
let project = Project::load_fuzzy(&options.fuzzy_project_path)?; let project = Project::load_fuzzy(&options.fuzzy_project_path)?;
project.check_compatibility();
info!("Found project at {}", project.file_location.display()); info!("Found project at {}", project.file_location.display());
info!("Using project {:#?}", project); info!("Using project {:#?}", project);

View File

@@ -1,10 +1,12 @@
use std::{ use std::{
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
time::Duration, time::Duration,
path::Path,
ops::Deref,
thread, thread,
}; };
use log::info; use log::{warn, trace};
use notify::{ use notify::{
self, self,
DebouncedEvent, DebouncedEvent,
@@ -20,97 +22,121 @@ use crate::{
const WATCH_TIMEOUT: Duration = Duration::from_millis(100); const WATCH_TIMEOUT: Duration = Duration::from_millis(100);
fn handle_event(imfs: &Mutex<Imfs>, rbx_session: &Mutex<RbxSession>, event: DebouncedEvent) { /// Watches for changes on the filesystem and links together the in-memory
/// filesystem and in-memory Roblox tree.
pub struct FsWatcher {
watcher: RecommendedWatcher,
}
impl FsWatcher {
/// Start a new FS watcher, watching all of the roots currently attached to
/// the given Imfs.
///
/// `rbx_session` is optional to make testing easier. If it isn't `None`,
/// events will be passed to it after they're given to the Imfs.
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Option<Arc<Mutex<RbxSession>>>) -> FsWatcher {
let (watch_tx, watch_rx) = mpsc::channel();
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
.expect("Could not create filesystem watcher");
{
let imfs = imfs.lock().unwrap();
for root_path in imfs.get_roots() {
watcher.watch(root_path, RecursiveMode::Recursive)
.expect("Could not watch directory");
}
}
{
let imfs = Arc::clone(&imfs);
let rbx_session = rbx_session.as_ref().map(Arc::clone);
thread::spawn(move || {
trace!("Watcher thread started");
while let Ok(event) = watch_rx.recv() {
// handle_fs_event expects an Option<&Mutex<T>>, but we have
// an Option<Arc<Mutex<T>>>, so we coerce with Deref.
let session_ref = rbx_session.as_ref().map(Deref::deref);
handle_fs_event(&imfs, session_ref, event);
}
trace!("Watcher thread stopped");
});
}
FsWatcher {
watcher,
}
}
pub fn stop_watching_path(&mut self, path: &Path) {
match self.watcher.unwatch(path) {
Ok(_) => {},
Err(e) => {
warn!("Could not unwatch path {}: {}", path.display(), e);
},
}
}
}
fn handle_fs_event(imfs: &Mutex<Imfs>, rbx_session: Option<&Mutex<RbxSession>>, event: DebouncedEvent) {
match event { match event {
DebouncedEvent::Create(path) => { DebouncedEvent::Create(path) => {
trace!("Path created: {}", path.display());
{ {
let mut imfs = imfs.lock().unwrap(); let mut imfs = imfs.lock().unwrap();
imfs.path_created(&path).unwrap(); imfs.path_created(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_created(&path); rbx_session.path_created(&path);
} }
}, },
DebouncedEvent::Write(path) => { DebouncedEvent::Write(path) => {
trace!("Path created: {}", path.display());
{ {
let mut imfs = imfs.lock().unwrap(); let mut imfs = imfs.lock().unwrap();
imfs.path_updated(&path).unwrap(); imfs.path_updated(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_updated(&path); rbx_session.path_updated(&path);
} }
}, },
DebouncedEvent::Remove(path) => { DebouncedEvent::Remove(path) => {
trace!("Path removed: {}", path.display());
{ {
let mut imfs = imfs.lock().unwrap(); let mut imfs = imfs.lock().unwrap();
imfs.path_removed(&path).unwrap(); imfs.path_removed(&path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_removed(&path); rbx_session.path_removed(&path);
} }
}, },
DebouncedEvent::Rename(from_path, to_path) => { DebouncedEvent::Rename(from_path, to_path) => {
trace!("Path renamed: {} to {}", from_path.display(), to_path.display());
{ {
let mut imfs = imfs.lock().unwrap(); let mut imfs = imfs.lock().unwrap();
imfs.path_moved(&from_path, &to_path).unwrap(); imfs.path_moved(&from_path, &to_path).unwrap();
} }
{ if let Some(rbx_session) = rbx_session {
let mut rbx_session = rbx_session.lock().unwrap(); let mut rbx_session = rbx_session.lock().unwrap();
rbx_session.path_renamed(&from_path, &to_path); rbx_session.path_renamed(&from_path, &to_path);
} }
}, },
_ => {}, other => {
} trace!("Unhandled FS event: {:?}", other);
} },
/// Watches for changes on the filesystem and links together the in-memory
/// filesystem and in-memory Roblox tree.
pub struct FsWatcher {
#[allow(unused)]
watchers: Vec<RecommendedWatcher>,
}
impl FsWatcher {
pub fn start(imfs: Arc<Mutex<Imfs>>, rbx_session: Arc<Mutex<RbxSession>>) -> FsWatcher {
let mut watchers = Vec::new();
{
let imfs_temp = imfs.lock().unwrap();
for root_path in imfs_temp.get_roots() {
let (watch_tx, watch_rx) = mpsc::channel();
let mut watcher = notify::watcher(watch_tx, WATCH_TIMEOUT)
.expect("Could not create `notify` watcher");
watcher.watch(root_path, RecursiveMode::Recursive)
.expect("Could not watch directory");
watchers.push(watcher);
let imfs = Arc::clone(&imfs);
let rbx_session = Arc::clone(&rbx_session);
let root_path = root_path.clone();
thread::spawn(move || {
info!("Watcher thread ({}) started", root_path.display());
while let Ok(event) = watch_rx.recv() {
handle_event(&imfs, &rbx_session, event);
}
info!("Watcher thread ({}) stopped", root_path.display());
});
}
}
FsWatcher {
watchers,
}
} }
} }

View File

@@ -1,15 +1,41 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::{Path, PathBuf}, path::{self, Path, PathBuf},
fmt,
fs, fs,
io, io,
}; };
use failure::Fail;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
use crate::project::{Project, ProjectNode}; use crate::project::{Project, ProjectNode};
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> io::Result<()> { /// A wrapper around io::Error that also attaches the path associated with the
/// error.
#[derive(Debug, Fail)]
pub struct FsError {
#[fail(cause)]
inner: io::Error,
path: PathBuf,
}
impl FsError {
fn new<P: Into<PathBuf>>(inner: io::Error, path: P) -> FsError {
FsError {
inner,
path: path.into(),
}
}
}
impl fmt::Display for FsError {
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
write!(output, "{}: {}", self.path.display(), self.inner)
}
}
fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> Result<(), FsError> {
match project_node { match project_node {
ProjectNode::Instance(node) => { ProjectNode::Instance(node) => {
for child in node.children.values() { for child in node.children.values() {
@@ -24,9 +50,12 @@ fn add_sync_points(imfs: &mut Imfs, project_node: &ProjectNode) -> io::Result<()
Ok(()) Ok(())
} }
/// The in-memory filesystem keeps a mirror of all files being watcher by Rojo /// The in-memory filesystem keeps a mirror of all files being watched by Rojo
/// in order to deduplicate file changes in the case of bidirectional syncing /// in order to deduplicate file changes in the case of bidirectional syncing
/// from Roblox Studio. /// from Roblox Studio.
///
/// It also enables Rojo to quickly generate React-like snapshots to make
/// reasoning about instances and how they relate to files easier.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Imfs { pub struct Imfs {
items: HashMap<PathBuf, ImfsItem>, items: HashMap<PathBuf, ImfsItem>,
@@ -41,7 +70,7 @@ impl Imfs {
} }
} }
pub fn add_roots_from_project(&mut self, project: &Project) -> io::Result<()> { pub fn add_roots_from_project(&mut self, project: &Project) -> Result<(), FsError> {
add_sync_points(self, &project.tree) add_sync_points(self, &project.tree)
} }
@@ -60,30 +89,42 @@ impl Imfs {
self.items.get(path) self.items.get(path)
} }
pub fn add_root(&mut self, path: &Path) -> io::Result<()> { pub fn add_root(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(!self.is_within_roots(path)); debug_assert!(!self.is_within_roots(path));
self.roots.insert(path.to_path_buf()); self.roots.insert(path.to_path_buf());
self.read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_created(&mut self, path: &Path) -> io::Result<()> { pub fn remove_root(&mut self, path: &Path) {
debug_assert!(path.is_absolute());
if self.roots.get(path).is_some() {
self.remove_item(path);
if let Some(parent_path) = path.parent() {
self.unlink_child(parent_path, path);
}
}
}
pub fn path_created(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
self.read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_updated(&mut self, path: &Path) -> io::Result<()> { pub fn path_updated(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
self.read_from_disk(path) self.descend_and_read_from_disk(path)
} }
pub fn path_removed(&mut self, path: &Path) -> io::Result<()> { pub fn path_removed(&mut self, path: &Path) -> Result<(), FsError> {
debug_assert!(path.is_absolute()); debug_assert!(path.is_absolute());
debug_assert!(self.is_within_roots(path)); debug_assert!(self.is_within_roots(path));
@@ -96,12 +137,7 @@ impl Imfs {
Ok(()) Ok(())
} }
pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> io::Result<()> { pub fn path_moved(&mut self, from_path: &Path, to_path: &Path) -> Result<(), FsError> {
debug_assert!(from_path.is_absolute());
debug_assert!(self.is_within_roots(from_path));
debug_assert!(to_path.is_absolute());
debug_assert!(self.is_within_roots(to_path));
self.path_removed(from_path)?; self.path_removed(from_path)?;
self.path_created(to_path)?; self.path_created(to_path)?;
Ok(()) Ok(())
@@ -132,9 +168,7 @@ impl Imfs {
Some(ImfsItem::Directory(directory)) => { Some(ImfsItem::Directory(directory)) => {
directory.children.remove(child); directory.children.remove(child);
}, },
_ => { _ => {},
panic!("Tried to unlink child of path that wasn't a directory!");
},
} }
} }
@@ -153,11 +187,44 @@ impl Imfs {
} }
} }
fn read_from_disk(&mut self, path: &Path) -> io::Result<()> { fn descend_and_read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
let metadata = fs::metadata(path)?; let root_path = self.get_root_path(path)
.expect("Tried to descent and read for path that wasn't within roots!");
// If this path is a root, we should read the entire thing.
if root_path == path {
self.read_from_disk(path)?;
return Ok(());
}
let relative_path = path.strip_prefix(root_path).unwrap();
let mut current_path = root_path.to_path_buf();
for component in relative_path.components() {
match component {
path::Component::Normal(name) => {
let next_path = current_path.join(name);
if self.items.contains_key(&next_path) {
current_path = next_path;
} else {
break;
}
},
_ => unreachable!(),
}
}
self.read_from_disk(&current_path)
}
fn read_from_disk(&mut self, path: &Path) -> Result<(), FsError> {
let metadata = fs::metadata(path)
.map_err(|e| FsError::new(e, path))?;
if metadata.is_file() { if metadata.is_file() {
let contents = fs::read(path)?; let contents = fs::read(path)
.map_err(|e| FsError::new(e, path))?;
let item = ImfsItem::File(ImfsFile { let item = ImfsItem::File(ImfsFile {
path: path.to_path_buf(), path: path.to_path_buf(),
contents, contents,
@@ -178,8 +245,13 @@ impl Imfs {
self.items.insert(path.to_path_buf(), item); self.items.insert(path.to_path_buf(), item);
for entry in fs::read_dir(path)? { let dir_children = fs::read_dir(path)
let entry = entry?; .map_err(|e| FsError::new(e, path))?;
for entry in dir_children {
let entry = entry
.map_err(|e| FsError::new(e, path))?;
let child_path = entry.path(); let child_path = entry.path();
self.read_from_disk(&child_path)?; self.read_from_disk(&child_path)?;
@@ -195,6 +267,16 @@ impl Imfs {
} }
} }
fn get_root_path<'a>(&'a self, path: &Path) -> Option<&'a Path> {
for root_path in &self.roots {
if path.starts_with(root_path) {
return Some(root_path)
}
}
None
}
fn is_within_roots(&self, path: &Path) -> bool { fn is_within_roots(&self, path: &Path) -> bool {
for root_path in &self.roots { for root_path in &self.roots {
if path.starts_with(root_path) { if path.starts_with(root_path) {

View File

@@ -1,16 +1,15 @@
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
io,
}; };
use crate::{ use crate::{
fs_watcher::FsWatcher,
imfs::{Imfs, FsError},
message_queue::MessageQueue, message_queue::MessageQueue,
project::Project, project::Project,
imfs::Imfs,
session_id::SessionId,
rbx_session::RbxSession, rbx_session::RbxSession,
session_id::SessionId,
snapshot_reconciler::InstanceChanges, snapshot_reconciler::InstanceChanges,
fs_watcher::FsWatcher,
}; };
/// Contains all of the state for a Rojo live-sync session. /// Contains all of the state for a Rojo live-sync session.
@@ -24,7 +23,7 @@ pub struct LiveSession {
} }
impl LiveSession { impl LiveSession {
pub fn new(project: Arc<Project>) -> io::Result<LiveSession> { pub fn new(project: Arc<Project>) -> Result<LiveSession, FsError> {
let imfs = { let imfs = {
let mut imfs = Imfs::new(); let mut imfs = Imfs::new();
imfs.add_roots_from_project(&project)?; imfs.add_roots_from_project(&project)?;
@@ -41,7 +40,7 @@ impl LiveSession {
let fs_watcher = FsWatcher::start( let fs_watcher = FsWatcher::start(
Arc::clone(&imfs), Arc::clone(&imfs),
Arc::clone(&rbx_session), Some(Arc::clone(&rbx_session)),
); );
let session_id = SessionId::new(); let session_id = SessionId::new();

View File

@@ -6,12 +6,14 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use log::warn;
use failure::Fail; use failure::Fail;
use maplit::hashmap; use maplit::hashmap;
use rbx_tree::RbxValue; use rbx_tree::RbxValue;
use serde_derive::{Serialize, Deserialize}; use serde_derive::{Serialize, Deserialize};
pub static PROJECT_FILENAME: &'static str = "roblox-project.json"; pub static PROJECT_FILENAME: &'static str = "default.project.json";
pub static COMPAT_PROJECT_FILENAME: &'static str = "roblox-project.json";
// Methods used for Serde's default value system, which doesn't support using // Methods used for Serde's default value system, which doesn't support using
// value literals directly, only functions that return values. // value literals directly, only functions that return values.
@@ -362,11 +364,17 @@ impl Project {
} else if location_metadata.is_dir() { } else if location_metadata.is_dir() {
let with_file = start_location.join(PROJECT_FILENAME); let with_file = start_location.join(PROJECT_FILENAME);
if let Ok(with_file_metadata) = fs::metadata(&with_file) { if let Ok(file_metadata) = fs::metadata(&with_file) {
if with_file_metadata.is_file() { if file_metadata.is_file() {
return Some(with_file); return Some(with_file);
} else { }
return None; }
let with_compat_file = start_location.join(COMPAT_PROJECT_FILENAME);
if let Ok(file_metadata) = fs::metadata(&with_compat_file) {
if file_metadata.is_file() {
return Some(with_compat_file);
} }
} }
} }
@@ -405,6 +413,25 @@ impl Project {
Ok(()) Ok(())
} }
/// Checks if there are any compatibility issues with this project file and
/// warns the user if there are any.
pub fn check_compatibility(&self) {
let file_name = self.file_location
.file_name().unwrap()
.to_str().expect("Project file path was not valid Unicode!");
if file_name == COMPAT_PROJECT_FILENAME {
warn!("Rojo's default project file name changed in 0.5.0-alpha3.");
warn!("Support for the old project file name will be dropped before 0.5.0 releases.");
warn!("Your project file is named {}", COMPAT_PROJECT_FILENAME);
warn!("Rename your project file to {}", PROJECT_FILENAME);
} else if !file_name.ends_with(".project.json") {
warn!("Starting in Rojo 0.5.0-alpha3, it's recommended to give all project files the");
warn!(".project.json extension. This helps Rojo differentiate project files from");
warn!("other JSON files!");
}
}
fn to_source_project(&self) -> SourceProject { fn to_source_project(&self) -> SourceProject {
SourceProject { SourceProject {
name: self.name.clone(), name: self.name.clone(),

View File

@@ -153,10 +153,14 @@ impl RbxSession {
// If the path doesn't exist or is a directory, we don't care if it // If the path doesn't exist or is a directory, we don't care if it
// updated // updated
match imfs.get(path) { match imfs.get(path) {
Some(ImfsItem::Directory(_)) | None => { Some(ImfsItem::Directory(_)) => {
trace!("Updated path was a directory, ignoring."); trace!("Updated path was a directory, ignoring.");
return; return;
}, },
None => {
trace!("Updated path did not exist in IMFS, ignoring.");
return;
},
Some(ImfsItem::File(_)) => {}, Some(ImfsItem::File(_)) => {},
} }
} }

View File

@@ -6,10 +6,11 @@ use std::{
str, str,
}; };
use serde_derive::{Serialize, Deserialize}; use failure::Fail;
use log::info;
use maplit::hashmap; use maplit::hashmap;
use rbx_tree::{RbxTree, RbxValue, RbxInstanceProperties}; use rbx_tree::{RbxTree, RbxValue, RbxInstanceProperties};
use failure::Fail; use serde_derive::{Serialize, Deserialize};
use crate::{ use crate::{
imfs::{ imfs::{
@@ -53,6 +54,11 @@ pub enum SnapshotError {
path: PathBuf, path: PathBuf,
}, },
JsonModelDecodeError {
inner: serde_json::Error,
path: PathBuf,
},
XmlModelDecodeError { XmlModelDecodeError {
inner: rbx_xml::DecodeError, inner: rbx_xml::DecodeError,
path: PathBuf, path: PathBuf,
@@ -71,6 +77,9 @@ impl fmt::Display for SnapshotError {
SnapshotError::Utf8Error { inner, path } => { SnapshotError::Utf8Error { inner, path } => {
write!(output, "Invalid UTF-8: {} in path {}", inner, path.display()) write!(output, "Invalid UTF-8: {} in path {}", inner, path.display())
}, },
SnapshotError::JsonModelDecodeError { inner, path } => {
write!(output, "Malformed .model.json model: {:?} in path {}", inner, path.display())
},
SnapshotError::XmlModelDecodeError { inner, path } => { SnapshotError::XmlModelDecodeError { inner, path } => {
write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display()) write!(output, "Malformed rbxmx model: {:?} in path {}", inner, path.display())
}, },
@@ -248,7 +257,18 @@ fn snapshot_imfs_file<'source>(
Some("txt") => snapshot_txt_file(file)?, Some("txt") => snapshot_txt_file(file)?,
Some("rbxmx") => snapshot_xml_model_file(file)?, Some("rbxmx") => snapshot_xml_model_file(file)?,
Some("rbxm") => snapshot_binary_model_file(file)?, Some("rbxm") => snapshot_binary_model_file(file)?,
Some(_) | None => return Ok(None), Some("json") => {
let file_stem = file.path
.file_stem().expect("Could not extract file stem")
.to_str().expect("Could not convert path to UTF-8");
if file_stem.ends_with(".model") {
snapshot_json_model_file(file)?
} else {
None
}
},
Some(_) | None => None,
}; };
if let Some(snapshot) = maybe_snapshot.as_mut() { if let Some(snapshot) = maybe_snapshot.as_mut() {
@@ -256,6 +276,8 @@ fn snapshot_imfs_file<'source>(
if let Some(snapshot_name) = instance_name { if let Some(snapshot_name) = instance_name {
snapshot.name = snapshot_name; snapshot.name = snapshot_name;
} }
} else {
info!("File generated no snapshot: {}", file.path.display());
} }
Ok(maybe_snapshot) Ok(maybe_snapshot)
@@ -402,6 +424,57 @@ struct LocalizationEntryJson {
values: HashMap<String, String>, values: HashMap<String, String>,
} }
fn snapshot_json_model_file<'source>(
file: &'source ImfsFile,
) -> SnapshotResult<'source> {
let contents = str::from_utf8(&file.contents)
.map_err(|inner| SnapshotError::Utf8Error {
inner,
path: file.path.to_owned(),
})?;
let json_instance: JsonModelInstance = serde_json::from_str(contents)
.map_err(|inner| SnapshotError::JsonModelDecodeError {
inner,
path: file.path.to_owned(),
})?;
let mut snapshot = json_instance.into_snapshot();
snapshot.metadata.source_path = Some(file.path.to_owned());
Ok(Some(snapshot))
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JsonModelInstance {
name: String,
class_name: String,
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
children: Vec<JsonModelInstance>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
properties: HashMap<String, RbxValue>,
}
impl JsonModelInstance {
fn into_snapshot(mut self) -> RbxSnapshotInstance<'static> {
let children = self.children
.drain(..)
.map(JsonModelInstance::into_snapshot)
.collect();
RbxSnapshotInstance {
name: Cow::Owned(self.name),
class_name: Cow::Owned(self.class_name),
properties: self.properties,
children,
metadata: Default::default(),
}
}
}
fn snapshot_xml_model_file<'source>( fn snapshot_xml_model_file<'source>(
file: &'source ImfsFile, file: &'source ImfsFile,
) -> SnapshotResult<'source> { ) -> SnapshotResult<'source> {

View File

@@ -5,6 +5,7 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use log::warn;
use rbx_tree::RbxId; use rbx_tree::RbxId;
use crate::{ use crate::{
@@ -27,13 +28,21 @@ digraph RojoTree {
"#; "#;
/// Compiles DOT source to SVG by invoking dot on the command line. /// Compiles DOT source to SVG by invoking dot on the command line.
pub fn graphviz_to_svg(source: &str) -> String { pub fn graphviz_to_svg(source: &str) -> Option<String> {
let mut child = Command::new("dot") let command = Command::new("dot")
.arg("-Tsvg") .arg("-Tsvg")
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn() .spawn();
.expect("Failed to spawn GraphViz process -- make sure it's installed in order to use /api/visualize");
let mut child = match command {
Ok(child) => child,
Err(_) => {
warn!("Failed to spawn GraphViz process to visualize current state.");
warn!("If you want pretty graphs, install GraphViz and make sure 'dot' is on your PATH!");
return None;
},
};
{ {
let stdin = child.stdin.as_mut().expect("Failed to open stdin"); let stdin = child.stdin.as_mut().expect("Failed to open stdin");
@@ -41,7 +50,7 @@ pub fn graphviz_to_svg(source: &str) -> String {
} }
let output = child.wait_with_output().expect("Failed to read stdout"); let output = child.wait_with_output().expect("Failed to read stdout");
String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8") Some(String::from_utf8(output.stdout).expect("Failed to parse stdout as UTF-8"))
} }
/// A Display wrapper struct to visualize an RbxSession as SVG. /// A Display wrapper struct to visualize an RbxSession as SVG.

View File

@@ -22,6 +22,8 @@ use crate::{
rbx_session::{MetadataPerInstance}, rbx_session::{MetadataPerInstance},
}; };
static HOME_CONTENT: &str = include_str!("../assets/index.html");
/// Contains the instance metadata relevant to Rojo clients. /// Contains the instance metadata relevant to Rojo clients.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -96,136 +98,31 @@ impl Server {
router!(request, router!(request,
(GET) (/) => { (GET) (/) => {
Response::text("Rojo is up and running!") self.handle_home()
}, },
(GET) (/api/rojo) => { (GET) (/api/rojo) => {
// Get a summary of information about the server. self.handle_api_rojo()
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
Response::json(&ServerInfoResponse {
server_version: self.server_version,
protocol_version: 2,
session_id: self.live_session.session_id,
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
root_instance_id: tree.get_root_id(),
})
}, },
(GET) (/api/subscribe/{ cursor: u32 }) => { (GET) (/api/subscribe/{ cursor: u32 }) => {
// Retrieve any messages past the given cursor index, and if self.handle_api_subscribe(cursor)
// there weren't any, subscribe to receive any new messages.
let message_queue = Arc::clone(&self.live_session.message_queue);
// Did the client miss any messages since the last subscribe?
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
if !new_messages.is_empty() {
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Borrowed(&new_messages),
message_cursor: new_cursor,
})
}
}
let (tx, rx) = mpsc::channel();
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::text("error!").with_status_code(500),
}
message_queue.unsubscribe(sender_id);
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Owned(new_messages),
message_cursor: new_cursor,
})
}
}, },
(GET) (/api/read/{ id_list: String }) => { (GET) (/api/read/{ id_list: String }) => {
let message_queue = Arc::clone(&self.live_session.message_queue);
let requested_ids: Option<Vec<RbxId>> = id_list let requested_ids: Option<Vec<RbxId>> = id_list
.split(',') .split(',')
.map(RbxId::parse_str) .map(RbxId::parse_str)
.collect(); .collect();
let requested_ids = match requested_ids { self.handle_api_read(requested_ids)
Some(id) => id,
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
};
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
let message_cursor = message_queue.get_message_cursor();
let mut instances = HashMap::new();
for &requested_id in &requested_ids {
if let Some(instance) = tree.get_instance(requested_id) {
let metadata = rbx_session.get_instance_metadata(requested_id)
.map(InstanceMetadata::from_session_metadata);
instances.insert(instance.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(instance),
metadata,
});
for descendant in tree.descendants(requested_id) {
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
.map(InstanceMetadata::from_session_metadata);
instances.insert(descendant.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(descendant),
metadata: descendant_meta,
});
}
}
}
Response::json(&ReadResponse {
session_id: self.live_session.session_id,
message_cursor,
instances,
})
}, },
(GET) (/visualize/rbx) => { (GET) (/visualize/rbx) => {
let rbx_session = self.live_session.rbx_session.lock().unwrap(); self.handle_visualize_rbx()
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
Response::svg(graphviz_to_svg(&dot_source))
}, },
(GET) (/visualize/imfs) => { (GET) (/visualize/imfs) => {
let imfs = self.live_session.imfs.lock().unwrap(); self.handle_visualize_imfs()
let dot_source = format!("{}", VisualizeImfs(&imfs));
Response::svg(graphviz_to_svg(&dot_source))
}, },
(GET) (/visualize/path_metadata) => { (GET) (/visualize/path_metadata) => {
let rbx_session = self.live_session.rbx_session.lock().unwrap(); self.handle_visualize_path_metadata()
Response::json(&rbx_session.debug_get_metadata_per_path())
}, },
_ => Response::empty_404() _ => Response::empty_404()
) )
} }
@@ -235,4 +132,131 @@ impl Server {
rouille::start_server(address, move |request| self.handle_request(request)); rouille::start_server(address, move |request| self.handle_request(request));
} }
fn handle_home(&self) -> Response {
Response::html(HOME_CONTENT)
}
/// Get a summary of information about the server
fn handle_api_rojo(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
Response::json(&ServerInfoResponse {
server_version: self.server_version,
protocol_version: 2,
session_id: self.live_session.session_id,
expected_place_ids: self.live_session.project.serve_place_ids.clone(),
root_instance_id: tree.get_root_id(),
})
}
/// Retrieve any messages past the given cursor index, and if
/// there weren't any, subscribe to receive any new messages.
fn handle_api_subscribe(&self, cursor: u32) -> Response {
let message_queue = Arc::clone(&self.live_session.message_queue);
// Did the client miss any messages since the last subscribe?
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
if !new_messages.is_empty() {
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Borrowed(&new_messages),
message_cursor: new_cursor,
})
}
}
let (tx, rx) = mpsc::channel();
let sender_id = message_queue.subscribe(tx);
match rx.recv() {
Ok(_) => (),
Err(_) => return Response::text("error!").with_status_code(500),
}
message_queue.unsubscribe(sender_id);
{
let (new_cursor, new_messages) = message_queue.get_messages_since(cursor);
return Response::json(&SubscribeResponse {
session_id: self.live_session.session_id,
messages: Cow::Owned(new_messages),
message_cursor: new_cursor,
})
}
}
fn handle_api_read(&self, requested_ids: Option<Vec<RbxId>>) -> Response {
let message_queue = Arc::clone(&self.live_session.message_queue);
let requested_ids = match requested_ids {
Some(id) => id,
None => return rouille::Response::text("Malformed ID list").with_status_code(400),
};
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let tree = rbx_session.get_tree();
let message_cursor = message_queue.get_message_cursor();
let mut instances = HashMap::new();
for &requested_id in &requested_ids {
if let Some(instance) = tree.get_instance(requested_id) {
let metadata = rbx_session.get_instance_metadata(requested_id)
.map(InstanceMetadata::from_session_metadata);
instances.insert(instance.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(instance),
metadata,
});
for descendant in tree.descendants(requested_id) {
let descendant_meta = rbx_session.get_instance_metadata(descendant.get_id())
.map(InstanceMetadata::from_session_metadata);
instances.insert(descendant.get_id(), InstanceWithMetadata {
instance: Cow::Borrowed(descendant),
metadata: descendant_meta,
});
}
}
}
Response::json(&ReadResponse {
session_id: self.live_session.session_id,
message_cursor,
instances,
})
}
fn handle_visualize_rbx(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
let dot_source = format!("{}", VisualizeRbxSession(&rbx_session));
match graphviz_to_svg(&dot_source) {
Some(svg) => Response::svg(svg),
None => Response::text(dot_source),
}
}
fn handle_visualize_imfs(&self) -> Response {
let imfs = self.live_session.imfs.lock().unwrap();
let dot_source = format!("{}", VisualizeImfs(&imfs));
match graphviz_to_svg(&dot_source) {
Some(svg) => Response::svg(svg),
None => Response::text(dot_source),
}
}
fn handle_visualize_path_metadata(&self) -> Response {
let rbx_session = self.live_session.rbx_session.lock().unwrap();
Response::json(&rbx_session.debug_get_metadata_per_path())
}
} }

View File

@@ -1,18 +0,0 @@
#!/bin/sh
set -e
if [ ! -d "../test-projects/$1" ]
then
echo "Pick a project that exists!"
exit 1
fi
if [ -d "scratch" ]
then
rm -rf scratch
fi
mkdir -p scratch
cp -r "../test-projects/$1" scratch
cargo run -- serve "scratch/$1"

View File

@@ -1,10 +1,10 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
io,
fs, fs,
path::PathBuf, path::PathBuf,
}; };
use failure::Error;
use tempfile::{TempDir, tempdir}; use tempfile::{TempDir, tempdir};
use librojo::{ use librojo::{
@@ -19,7 +19,7 @@ enum FsEvent {
Moved(PathBuf, PathBuf), Moved(PathBuf, PathBuf),
} }
fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> io::Result<()> { fn send_events(imfs: &mut Imfs, events: &[FsEvent]) -> Result<(), Error> {
for event in events { for event in events {
match event { match event {
FsEvent::Created(path) => imfs.path_created(path)?, FsEvent::Created(path) => imfs.path_created(path)?,
@@ -56,7 +56,7 @@ fn check_expected(real: &Imfs, expected: &ExpectedImfs) {
} }
} }
fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> { fn base_tree() -> Result<(TempDir, Imfs, ExpectedImfs, TestResources), Error> {
let root = tempdir()?; let root = tempdir()?;
let foo_path = root.path().join("foo"); let foo_path = root.path().join("foo");
@@ -125,7 +125,7 @@ fn base_tree() -> io::Result<(TempDir, Imfs, ExpectedImfs, TestResources)> {
} }
#[test] #[test]
fn initial_read() -> io::Result<()> { fn initial_read() -> Result<(), Error> {
let (_root, imfs, expected_imfs, _resources) = base_tree()?; let (_root, imfs, expected_imfs, _resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -134,7 +134,7 @@ fn initial_read() -> io::Result<()> {
} }
#[test] #[test]
fn adding_files() -> io::Result<()> { fn adding_files() -> Result<(), Error> {
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?; let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -178,7 +178,7 @@ fn adding_files() -> io::Result<()> {
} }
#[test] #[test]
fn adding_folder() -> io::Result<()> { fn adding_folder() -> Result<(), Error> {
let (root, imfs, mut expected_imfs, _resources) = base_tree()?; let (root, imfs, mut expected_imfs, _resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -232,6 +232,16 @@ fn adding_folder() -> io::Result<()> {
FsEvent::Created(file1_path.clone()), FsEvent::Created(file1_path.clone()),
FsEvent::Created(file2_path.clone()), FsEvent::Created(file2_path.clone()),
], ],
vec![
FsEvent::Created(file1_path.clone()),
FsEvent::Created(file2_path.clone()),
FsEvent::Created(folder_path.clone()),
],
vec![
FsEvent::Created(file1_path.clone()),
FsEvent::Created(folder_path.clone()),
FsEvent::Created(file2_path.clone()),
],
]; ];
for events in &possible_event_sequences { for events in &possible_event_sequences {
@@ -245,7 +255,36 @@ fn adding_folder() -> io::Result<()> {
} }
#[test] #[test]
fn removing_file() -> io::Result<()> { fn updating_files() -> Result<(), Error> {
let (_root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs);
fs::write(&resources.bar_path, b"bar updated")?;
fs::write(&resources.baz_path, b"baz updated")?;
imfs.path_updated(&resources.bar_path)?;
imfs.path_updated(&resources.baz_path)?;
let bar_updated_item = ImfsItem::File(ImfsFile {
path: resources.bar_path.clone(),
contents: b"bar updated".to_vec(),
});
let baz_updated_item = ImfsItem::File(ImfsFile {
path: resources.baz_path.clone(),
contents: b"baz updated".to_vec(),
});
expected_imfs.items.insert(resources.bar_path.clone(), bar_updated_item);
expected_imfs.items.insert(resources.baz_path.clone(), baz_updated_item);
check_expected(&imfs, &expected_imfs);
Ok(())
}
#[test]
fn removing_file() -> Result<(), Error> {
let (root, mut imfs, mut expected_imfs, resources) = base_tree()?; let (root, mut imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -269,7 +308,7 @@ fn removing_file() -> io::Result<()> {
} }
#[test] #[test]
fn removing_folder() -> io::Result<()> { fn removing_folder() -> Result<(), Error> {
let (root, imfs, mut expected_imfs, resources) = base_tree()?; let (root, imfs, mut expected_imfs, resources) = base_tree()?;
check_expected(&imfs, &expected_imfs); check_expected(&imfs, &expected_imfs);
@@ -294,6 +333,10 @@ fn removing_folder() -> io::Result<()> {
FsEvent::Removed(resources.baz_path.clone()), FsEvent::Removed(resources.baz_path.clone()),
FsEvent::Removed(resources.foo_path.clone()), FsEvent::Removed(resources.foo_path.clone()),
], ],
vec![
FsEvent::Removed(resources.foo_path.clone()),
FsEvent::Removed(resources.baz_path.clone()),
],
]; ];
for events in &possible_event_sequences { for events in &possible_event_sequences {

View File

@@ -21,7 +21,7 @@ lazy_static! {
#[test] #[test]
fn empty() { fn empty() {
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "empty"); assert_eq!(project.name, "empty");
@@ -29,7 +29,7 @@ fn empty() {
#[test] #[test]
fn empty_fuzzy_file() { fn empty_fuzzy_file() {
let project_file_location = TEST_PROJECTS_ROOT.join("empty/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("empty/default.project.json");
let project = Project::load_fuzzy(&project_file_location).unwrap(); let project = Project::load_fuzzy(&project_file_location).unwrap();
assert_eq!(project.name, "empty"); assert_eq!(project.name, "empty");
@@ -45,7 +45,7 @@ fn empty_fuzzy_folder() {
#[test] #[test]
fn single_sync_point() { fn single_sync_point() {
let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("single-sync-point/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
let expected_project = { let expected_project = {
@@ -100,7 +100,7 @@ fn single_sync_point() {
#[test] #[test]
fn test_model() { fn test_model() {
let project_file_location = TEST_PROJECTS_ROOT.join("test-model/roblox-project.json"); let project_file_location = TEST_PROJECTS_ROOT.join("test-model/default.project.json");
let project = Project::load_exact(&project_file_location).unwrap(); let project = Project::load_exact(&project_file_location).unwrap();
assert_eq!(project.name, "test-model"); assert_eq!(project.name, "test-model");

View File

@@ -0,0 +1,14 @@
{
"Name": "All my Remote Events",
"ClassName": "Folder",
"Children": [
{
"Name": "SendMoney",
"ClassName": "RemoteEvent"
},
{
"Name": "SendItems",
"ClassName": "RemoteEvent"
}
]
}

View File

@@ -5,12 +5,10 @@
<Item class="Script" referent="RBX634A9A9988354E4B9D971B2A4DEBD26E"> <Item class="Script" referent="RBX634A9A9988354E4B9D971B2A4DEBD26E">
<Properties> <Properties>
<bool name="Disabled">false</bool> <bool name="Disabled">false</bool>
<Content name="LinkedSource"><null></null></Content>
<string name="Name">Lone Script</string> <string name="Name">Lone Script</string>
<string name="ScriptGuid">{C62CD9FB-FF28-4FD9-9712-AD28A1E92C84}</string> <string name="ScriptGuid">{C62CD9FB-FF28-4FD9-9712-AD28A1E92C84}</string>
<ProtectedString name="Source"><![CDATA[print("Hello world!") <string name="Source"><![CDATA[print("Hello world!")
]]></ProtectedString> ]]></string>
<BinaryString name="Tags"></BinaryString>
</Properties> </Properties>
</Item> </Item>
</roblox> </roblox>

View File

@@ -0,0 +1,6 @@
{
"name": "missing-files",
"tree": {
"$path": "does-not-exist"
}
}

21
test-scratch-project Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Copies a project from 'test-projects' into a folder that can be messed with
# without accidentally checking the results into version control.
set -e
if [ ! -d "test-projects/$1" ]
then
echo "Pick a project that exists!"
exit 1
fi
if [ -d "scratch-project/$1" ]
then
rm -rf "scratch-project/$1"
fi
mkdir -p scratch-project
cp -r "test-projects/$1" scratch-project
cargo run -- serve "scratch-project/$1"