forked from rojo-rbx/rojo
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc497f95e | ||
|
|
52eea667a7 | ||
|
|
c2f7e268ff | ||
|
|
31e5c558ab | ||
|
|
7a7ac9550d | ||
|
|
4d0fdf0dfd | ||
|
|
b448e8007e | ||
|
|
bad0e67266 | ||
|
|
3dee3dd627 | ||
|
|
4772350968 | ||
|
|
eabcc0bd1d | ||
|
|
3a3af6ab10 | ||
|
|
9723622b66 | ||
|
|
3b1d647acb | ||
|
|
6fa925a402 | ||
|
|
c8f837d726 | ||
|
|
4557396564 | ||
|
|
d3d67d47e1 | ||
|
|
42107e0715 | ||
|
|
ed183e0805 | ||
|
|
116be16392 | ||
|
|
2c188738e6 | ||
|
|
ebffba9589 | ||
|
|
ab644c3dfa | ||
|
|
c6cdd8a815 | ||
|
|
d99df59d9b | ||
|
|
c5f8247543 | ||
|
|
72557c9d23 | ||
|
|
1a1b6d923f | ||
|
|
27cf2c8740 | ||
|
|
c08a598d3f | ||
|
|
1318842c36 | ||
|
|
86d7d033d7 | ||
|
|
2df1dfa1cb | ||
|
|
78a1c658d8 | ||
|
|
f52f43fe90 | ||
|
|
58b244b7e9 | ||
|
|
d8bcbee463 | ||
|
|
f00152a9ac | ||
|
|
9720c56765 | ||
|
|
13ce04abb2 | ||
|
|
ab22b55b84 | ||
|
|
73117edbe7 | ||
|
|
d7e2a3542c | ||
|
|
fe240ed577 | ||
|
|
5e98cbe68f | ||
|
|
7a372dc50c | ||
|
|
958b6660be | ||
|
|
e731811911 | ||
|
|
66144cef2f | ||
|
|
68ba3fee6c | ||
|
|
95581dbaa6 | ||
|
|
aaaf3ba0b9 | ||
|
|
b885cae086 | ||
|
|
0f78eb933a | ||
|
|
6ee9a48e20 | ||
|
|
f90c51e923 | ||
|
|
6472a2cbce | ||
|
|
c75cbebbf0 | ||
|
|
12bfcd7b66 | ||
|
|
d365bc076e | ||
|
|
67ac6b7cec | ||
|
|
01325c8c7e | ||
|
|
21e9625c36 | ||
|
|
5bf1f11190 |
@@ -4,12 +4,6 @@ root = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
/site
|
||||
|
||||
15
.gitmodules
vendored
Normal file
15
.gitmodules
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[submodule "plugin/modules/roact"]
|
||||
path = plugin/modules/roact
|
||||
url = https://github.com/Roblox/roact.git
|
||||
[submodule "plugin/modules/rodux"]
|
||||
path = plugin/modules/rodux
|
||||
url = https://github.com/Roblox/rodux.git
|
||||
[submodule "plugin/modules/roact-rodux"]
|
||||
path = plugin/modules/roact-rodux
|
||||
url = https://github.com/Roblox/roact-rodux.git
|
||||
[submodule "plugin/modules/testez"]
|
||||
path = plugin/modules/testez
|
||||
url = https://github.com/Roblox/testez.git
|
||||
[submodule "plugin/modules/lemur"]
|
||||
path = plugin/modules/lemur
|
||||
url = https://github.com/LPGhatguy/lemur.git
|
||||
42
.travis.yml
42
.travis.yml
@@ -1,5 +1,39 @@
|
||||
language: rust
|
||||
matrix:
|
||||
include:
|
||||
- language: python
|
||||
env:
|
||||
- LUA="lua=5.1"
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
before_install:
|
||||
- pip install hererocks
|
||||
- hererocks lua_install -r^ --$LUA
|
||||
- export PATH=$PATH:$PWD/lua_install/bin
|
||||
|
||||
install:
|
||||
- luarocks install luafilesystem
|
||||
- luarocks install busted
|
||||
- luarocks install luacov
|
||||
- luarocks install luacov-coveralls
|
||||
- luarocks install luacheck
|
||||
|
||||
script:
|
||||
- cd plugin
|
||||
- luacheck src
|
||||
- lua -lluacov spec.lua
|
||||
|
||||
after_success:
|
||||
- cd plugin
|
||||
- luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
||||
|
||||
- language: rust
|
||||
rust: stable
|
||||
|
||||
script:
|
||||
- cd server
|
||||
- cargo test --verbose
|
||||
- language: rust
|
||||
rust: beta
|
||||
|
||||
script:
|
||||
- cd server
|
||||
- cargo test --verbose
|
||||
|
||||
40
CHANGES.md
40
CHANGES.md
@@ -1,37 +1,59 @@
|
||||
# Rojo Change Log
|
||||
|
||||
## Current Master
|
||||
* No changes
|
||||
*No changes*
|
||||
|
||||
## 0.3.2
|
||||
## 0.4.3 (April 7, 2018)
|
||||
* Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/LPGhatguy/rojo/pull/58))
|
||||
* Plugin now has much more robust handling and will wipe all state when the server changes.
|
||||
* This should fix issues that would otherwise be solved by restarting Roblox Studio.
|
||||
|
||||
## 0.4.2 (April 4, 2018)
|
||||
* Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`.
|
||||
* The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor.
|
||||
|
||||
## 0.4.1 (April 1, 2018)
|
||||
* Merged plugin repository into main Rojo repository for easier tracking.
|
||||
* Improved `RouteMap` object tracking; this should fix some cases of duplicated instances being synced into the tree.
|
||||
|
||||
## 0.4.0 (March 27, 2018)
|
||||
* Protocol version 1, which shifts more responsibility onto the server
|
||||
* This is a **major breaking** change!
|
||||
* The server now has a content of 'filter plugins', which transform data at various stages in the pipeline
|
||||
* The server now exposes Roblox instance objects instead of file contents, which lines up with how `rojo pack` will work, and paves the way for more robust syncing.
|
||||
* Added `*.model.json` files, which let you embed small Roblox objects into your Rojo tree.
|
||||
* Improved error messages in some cases ([#46](https://github.com/LPGhatguy/rojo/issues/46))
|
||||
|
||||
## 0.3.2 (December 20, 2017)
|
||||
* Fixed `rojo serve` failing to correctly construct an absolute root path when passed as an argument
|
||||
* Fixed intense CPU usage when running `rojo serve`
|
||||
|
||||
## 0.3.1
|
||||
## 0.3.1 (December 14, 2017)
|
||||
* Improved error reporting when invalid JSON is found in a `rojo.json` project
|
||||
* These messages are passed on from Serde
|
||||
|
||||
## 0.3.0
|
||||
## 0.3.0 (December 12, 2017)
|
||||
* Factored out the plugin into a separate repository
|
||||
* Fixed server when using a file as a partition
|
||||
* Previously, trailing slashes were put on the end of a partition even if the read request was an empty string. This broke file reading on Windows when a partition pointed to a file instead of a directory!
|
||||
* Started running automatic tests on Travis CI (#9)
|
||||
|
||||
## 0.2.3
|
||||
## 0.2.3 (December 4, 2017)
|
||||
* 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
|
||||
## 0.2.2 (December 1, 2017)
|
||||
* Plugin only release
|
||||
* Fixed broken reconciliation behavior with `init` files
|
||||
|
||||
## 0.2.1
|
||||
## 0.2.1 (December 1, 2017)
|
||||
* Plugin only release
|
||||
* Changes default port to 8000
|
||||
|
||||
## 0.2.0
|
||||
## 0.2.0 (December 1, 2017)
|
||||
* Support for `init.lua` like rbxfs and rbxpacker
|
||||
* More robust syncing with a new reconciler
|
||||
|
||||
## 0.1.0
|
||||
## 0.1.0 (November 29, 2017)
|
||||
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
||||
49
DESIGN.md
Normal file
49
DESIGN.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Rojo Design - Protocol Version 1
|
||||
This is a super rough draft that I'm trying to use to lay out some of my thoughts.
|
||||
|
||||
## API
|
||||
|
||||
### POST `/read`
|
||||
Accepts a `Vec<Route>` of items to read.
|
||||
|
||||
Returns `Vec<Option<RbxInstance>>`, in the same order as the request.
|
||||
|
||||
### POST `/write`
|
||||
Accepts a `Vec<{ Route, RbxInstance }>` of items to write.
|
||||
|
||||
I imagine that the `Name` attribute of the top-level `RbxInstance` would be ignored in favor of the route name?
|
||||
|
||||
## CLI
|
||||
The `rojo serve` command uses three major components:
|
||||
* A Virtual Filesystem (VFS), which exposes the filesystem as `VfsItem` objects
|
||||
* A VFS watcher, which tracks changes to the filesystem and logs them
|
||||
* An HTTP API, which exposes an interface to the Roblox Studio plugin
|
||||
|
||||
### Transform Plugins
|
||||
Transform plugins (or filter plugins?) can interject in three places:
|
||||
* Transform a `VfsItem` that's being read into an `RbxInstance` in the VFS
|
||||
* Transform an `RbxInstance` that's being written into a `VfsItem` in the VFS
|
||||
* Transform a file change into paths that need to be updated in the VFS watcher
|
||||
|
||||
The plan is to have several built-in plugins that can be rearranged/configured in project settings:
|
||||
|
||||
* Base plugin
|
||||
* Transforms all unhandled files to/from StringValue objects
|
||||
* Script plugin
|
||||
* Transforms `*.lua` files to their appropriate file types
|
||||
* JSON/rbxmx/rbxlx model plugin
|
||||
* External binary plugin
|
||||
* User passes a binary name (like `moonc`) that modifies file contents
|
||||
|
||||
## Roblox Studio Plugin
|
||||
With the protocol version 1 change, the Roblox Studio plugin got a lot simpler. Notably, the plugin doesn't need to be aware of anything about the filesystem's semantics, which is super handy.
|
||||
|
||||
## Bi-directional syncing
|
||||
Quenty laid out a good way to handle bi-directional syncing.
|
||||
|
||||
When receiving a change from the plugin:
|
||||
1. Hash the new contents of the file, store it in a map from routes to hashes
|
||||
2. Write the new file contents to the filesystem
|
||||
3. Later down the line, receive a change event from the filesystem watcher
|
||||
4. When receiving a change, if the item is in the hash map, read it and hash those contents
|
||||
5. If the hash matches the last noted hash, discard the change, else continue as normal
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Lucien Greathouse
|
||||
Copyright (c) 2018 Lucien Greathouse
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
131
README.md
131
README.md
@@ -1,47 +1,59 @@
|
||||
<h1 align="center">Rojo</h1>
|
||||
<div align="center">
|
||||
<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" />
|
||||
</a>
|
||||
<img src="assets/rojo-logo.png" alt="Rojo" height="217" />
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
Rojo is a flexible multi-tool designed for creating robust Roblox projects. It's in early development, but is still useful for many projects.
|
||||
<div align="center">
|
||||
<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" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/badge/latest_version-0.4.3-brightgreen.svg" alt="Current server version" />
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
**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.
|
||||
|
||||
This is the main Rojo repository, containing the binary server component. For the source for the Roblox plugin, [see the rojo-plugin repository](https://github.com/LPGhatguy/rojo-plugin).
|
||||
|
||||
## Features
|
||||
|
||||
Rojo has a number of desirable features *right now*:
|
||||
Rojo lets you:
|
||||
|
||||
* Work on scripts from the filesystem, in your favorite editor
|
||||
* Version your place, library, or plugin using Git or another VCS
|
||||
* Sync JSON-format models from the filesystem into your game
|
||||
|
||||
Soon, Rojo will be able to:
|
||||
Later this year, 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
|
||||
* Similar to [rbxpacker](https://github.com/LPGhatguy/rbxpacker), another one of my projects
|
||||
* Add strongly-versioned dependencies to your project
|
||||
* Sync `rbxmx` models between the filesystem and Roblox Studio
|
||||
* Package projects into `rbxmx` files from the command line
|
||||
|
||||
## Installation
|
||||
Rojo has two components:
|
||||
* The command line tool, written in Rust
|
||||
* The [Roblox Studio plugin](https://www.roblox.com/library/1211549683/Rojo), written in Lua
|
||||
|
||||
To install the command line tool, there are two options:
|
||||
* Cargo, if you have Rust installed
|
||||
* 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)
|
||||
* The server, a binary written in Rust
|
||||
* The plugin, a Roblox Studio plugin written in Lua
|
||||
|
||||
## Usage
|
||||
It's important that the plugin and server are compatible. The plugin will show errors in the Roblox Studio Output window if there is a version mismatch.
|
||||
|
||||
To install the server, either:
|
||||
|
||||
* If you have Rust installed, use `cargo install rojo`
|
||||
* Or, download a pre-built Windows binary from [the GitHub releases page](https://github.com/LPGhatguy/rojo/releases)
|
||||
|
||||
To install the plugin, either:
|
||||
|
||||
* Install the plugin from the [Roblox plugin page](https://www.roblox.com/library/1211549683/Rojo).
|
||||
* This gives you less control over what version you install -- you will always have the latest version.
|
||||
* Or, download the latest release from [the GitHub releases section](https://github.com/LPGhatguy/rojo/releases) and install it into your Roblox plugins folder
|
||||
* You can open this folder by clicking the "Plugins Folder" button from the Plugins toolbar in Roblox Studio
|
||||
|
||||
## Server Usage
|
||||
For more help, use `rojo help`.
|
||||
|
||||
### New Project
|
||||
Just create a new folder and tell Rojo to initialize it!
|
||||
Create a new folder, then use `rojo init` inside that folder to initialize an empty project:
|
||||
|
||||
```sh
|
||||
mkdir my-new-project
|
||||
@@ -50,9 +62,9 @@ cd my-new-project
|
||||
rojo init
|
||||
```
|
||||
|
||||
Rojo will create an empty project in the directory.
|
||||
Rojo will create an empty project file named `rojo.json` in the directory.
|
||||
|
||||
The default project looks like this:
|
||||
The default project file is:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -63,7 +75,7 @@ The default project looks like this:
|
||||
```
|
||||
|
||||
### Start Dev Server
|
||||
To create a server that allows the Rojo Dev Plugin to access your project, use:
|
||||
To start the Rojo dev server, use:
|
||||
|
||||
```sh
|
||||
rojo serve
|
||||
@@ -72,12 +84,14 @@ 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
|
||||
**Coming soon!**
|
||||
[Bi-directional script syncing](https://github.com/LPGhatguy/rojo/issues/5) is on the roadmap for Rojo this year, but isn't implemented.
|
||||
|
||||
In the mean-time, manually migrating scripts is probably the best route forward.
|
||||
|
||||
### 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.
|
||||
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 on your filesystem 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.
|
||||
Each entry in the `partitions` map 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:
|
||||
|
||||
@@ -96,12 +110,14 @@ For example, if you want to map your `src` directory to an object named `My Cool
|
||||
|
||||
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.
|
||||
The `target` parameter is a path to a Roblox object to link the partition to, starting at `game`. If any objects don't exist along the way, Rojo will try to create them.
|
||||
|
||||
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.
|
||||
**Any objects in a partition may be wiped away by Rojo after syncing! If this is not desired, use multiple, smaller partitions.**
|
||||
|
||||
Run `rojo serve` in the directory containing this project, then press the "Sync In" or "Toggle Polling" buttons in the Roblox Studio plugin to sync into your game.
|
||||
|
||||
### Sync Details
|
||||
The structure of files and folders on the filesystem are preserved when syncing into game.
|
||||
The structure of files and diectories 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:
|
||||
|
||||
@@ -110,9 +126,16 @@ Creation of Roblox instances follows a simple set of rules. The first rule that
|
||||
| `*.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 |
|
||||
| `*.model.json` | *Varies* | See the example below |
|
||||
| `*` | `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.
|
||||
Any directories on the filesystem will turn into `Folder` objects.
|
||||
|
||||
Any directory containing one of these files will instead be a `ModuleScript`, `Script`, `LocalScript` containing the directory's contents:
|
||||
|
||||
* `init.lua`
|
||||
* `init.server.lua`
|
||||
* `init.client.lua`
|
||||
|
||||
For example, this file tree:
|
||||
|
||||
@@ -120,15 +143,49 @@ For example, this file tree:
|
||||
* init.client.lua
|
||||
* foo.lua
|
||||
|
||||
Will turn into this tree in Roblox:
|
||||
Will turn into these instances in Roblox:
|
||||
|
||||
* `my-game` (`LocalScript` with source from `my-game/init.client.lua`)
|
||||
* `foo` (`ModuleScript` with source from `my-game/foo.lua`)
|
||||
|
||||
`*.model.json` files are a way to represent simple Roblox instances on the filesystem until `rbxmx` and `rbxlx` support is implemented in Rojo.
|
||||
|
||||
This feature is intended for small instances, like `RemoteEvent` or `*Value` objects.
|
||||
|
||||
JSON Model files are strict, with every property being required. They look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"Name": "hello",
|
||||
"ClassName": "Model",
|
||||
"Children": [
|
||||
{
|
||||
"Name": "Some Part",
|
||||
"ClassName": "Part",
|
||||
"Children": [],
|
||||
"Properties": {}
|
||||
},
|
||||
{
|
||||
"Name": "Some StringValue",
|
||||
"ClassName": "StringValue",
|
||||
"Children": [],
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Properties": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Inspiration
|
||||
There are lots of other tools that sync scripts into Roblox, or otherwise work to improve the development flow outside of Roblox Studio.
|
||||
There are lots of other tools that sync scripts into Roblox or provide other tools for working with Roblox places.
|
||||
|
||||
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)
|
||||
@@ -136,8 +193,14 @@ Here are a few, if you're looking for alternatives or supplements to Rojo:
|
||||
* [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
|
||||
|
||||
## Contributing
|
||||
Pull requests are welcome!
|
||||
|
||||
All pull requests are run against a test suite on Travis CI. That test suite should always pass!
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the MIT license. See [LICENSE.md](LICENSE.md) for details.
|
||||
BIN
assets/rojo-logo.png
Normal file
BIN
assets/rojo-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/rojo-plugin-logo.png
Normal file
BIN
assets/rojo-plugin-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
6
docs/index.md
Normal file
6
docs/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Home
|
||||
This is the documentation home for Rojo.
|
||||
|
||||
**Rojo** is a flexible multi-tool designed for creating robust Roblox projects.
|
||||
|
||||
This documentation is a work in progress, and is incomplete.
|
||||
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
pymdown-extensions
|
||||
20
mkdocs.yml
Normal file
20
mkdocs.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
site_name: Rojo Documentation
|
||||
site_url: https://lpghatguy.github.io/rojo/
|
||||
repo_name: LPGhatguy/rojo
|
||||
repo_url: https://github.com/LPGhatguy/rojo
|
||||
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
primary: 'Red'
|
||||
accent: 'Red'
|
||||
|
||||
pages:
|
||||
- Home: index.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- codehilite:
|
||||
guess_lang: false
|
||||
- toc:
|
||||
permalink: true
|
||||
4
plugin/.editorconfig
Normal file
4
plugin/.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
[*.lua]
|
||||
indent_style = tab
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
1
plugin/.gitignore
vendored
Normal file
1
plugin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/luacov.*
|
||||
56
plugin/.luacheckrc
Normal file
56
plugin/.luacheckrc
Normal file
@@ -0,0 +1,56 @@
|
||||
stds.roblox = {
|
||||
read_globals = {
|
||||
game = {
|
||||
other_fields = true,
|
||||
},
|
||||
|
||||
-- Roblox globals
|
||||
"script",
|
||||
|
||||
-- Extra functions
|
||||
"tick", "warn", "spawn",
|
||||
"wait", "settings", "typeof",
|
||||
|
||||
-- Types
|
||||
"Vector2", "Vector3",
|
||||
"Color3",
|
||||
"UDim", "UDim2",
|
||||
"Rect",
|
||||
"CFrame",
|
||||
"Enum",
|
||||
"Instance",
|
||||
}
|
||||
}
|
||||
|
||||
stds.plugin = {
|
||||
read_globals = {
|
||||
"plugin",
|
||||
}
|
||||
}
|
||||
|
||||
stds.testez = {
|
||||
read_globals = {
|
||||
"describe",
|
||||
"it", "itFOCUS", "itSKIP",
|
||||
"FOCUS", "SKIP", "HACK_NO_XPCALL",
|
||||
"expect",
|
||||
}
|
||||
}
|
||||
|
||||
ignore = {
|
||||
"212", -- unused arguments
|
||||
"421", -- shadowing local variable
|
||||
"422", -- shadowing argument
|
||||
"431", -- shadowing upvalue
|
||||
"432", -- shadowing upvalue argument
|
||||
}
|
||||
|
||||
std = "lua51+roblox"
|
||||
|
||||
files["**/*.server.lua"] = {
|
||||
std = "+plugin",
|
||||
}
|
||||
|
||||
files["**/*.spec.lua"] = {
|
||||
std = "+testez",
|
||||
}
|
||||
8
plugin/.luacov
Normal file
8
plugin/.luacov
Normal file
@@ -0,0 +1,8 @@
|
||||
return {
|
||||
include = {
|
||||
"^src",
|
||||
},
|
||||
exclude = {
|
||||
"%.spec$",
|
||||
},
|
||||
}
|
||||
1
plugin/modules/lemur
Submodule
1
plugin/modules/lemur
Submodule
Submodule plugin/modules/lemur added at 852c71b897
1
plugin/modules/roact
Submodule
1
plugin/modules/roact
Submodule
Submodule plugin/modules/roact added at bbb0663161
1
plugin/modules/roact-rodux
Submodule
1
plugin/modules/roact-rodux
Submodule
Submodule plugin/modules/roact-rodux added at 97fbfee90a
1
plugin/modules/rodux
Submodule
1
plugin/modules/rodux
Submodule
Submodule plugin/modules/rodux added at b8ba486335
1
plugin/modules/testez
Submodule
1
plugin/modules/testez
Submodule
Submodule plugin/modules/testez added at 442b71926d
30
plugin/rojo.json
Normal file
30
plugin/rojo.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "rojo",
|
||||
"servePort": 8000,
|
||||
"partitions": {
|
||||
"plugin": {
|
||||
"path": "src",
|
||||
"target": "ReplicatedStorage.Rojo.plugin"
|
||||
},
|
||||
"modules/roact": {
|
||||
"path": "modules/roact/lib",
|
||||
"target": "ReplicatedStorage.Rojo.modules.Roact"
|
||||
},
|
||||
"modules/rodux": {
|
||||
"path": "modules/rodux/lib",
|
||||
"target": "ReplicatedStorage.Rojo.modules.Rodux"
|
||||
},
|
||||
"modules/roact-rodux": {
|
||||
"path": "modules/roact-rodux/lib",
|
||||
"target": "ReplicatedStorage.Rojo.modules.RoactRodux"
|
||||
},
|
||||
"modules/testez": {
|
||||
"path": "modules/testez/lib",
|
||||
"target": "ReplicatedStorage.TestEZ"
|
||||
},
|
||||
"tests": {
|
||||
"path": "tests",
|
||||
"target": "TestService"
|
||||
}
|
||||
}
|
||||
}
|
||||
69
plugin/spec.lua
Normal file
69
plugin/spec.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
--[[
|
||||
Loads our library and all of its dependencies, then runs tests using TestEZ.
|
||||
]]
|
||||
|
||||
-- If you add any dependencies, add them to this table so they'll be loaded!
|
||||
local LOAD_MODULES = {
|
||||
{"src", "Plugin"},
|
||||
{"modules/testez/lib", "TestEZ"},
|
||||
}
|
||||
|
||||
-- This makes sure we can load Lemur and other libraries that depend on init.lua
|
||||
package.path = package.path .. ";?/init.lua"
|
||||
|
||||
-- If this fails, make sure you've run `lua bin/install-dependencies.lua` first!
|
||||
local lemur = require("modules.lemur")
|
||||
|
||||
--[[
|
||||
Collapses ModuleScripts named 'init' into their parent folders.
|
||||
|
||||
This is the same result as the collapsing mechanism from Rojo.
|
||||
]]
|
||||
local function collapse(root)
|
||||
local init = root:FindFirstChild("init")
|
||||
if init then
|
||||
init.Name = root.Name
|
||||
init.Parent = root.Parent
|
||||
|
||||
for _, child in ipairs(root:GetChildren()) do
|
||||
child.Parent = init
|
||||
end
|
||||
|
||||
root:Destroy()
|
||||
root = init
|
||||
end
|
||||
|
||||
for _, child in ipairs(root:GetChildren()) do
|
||||
if child:IsA("Folder") then
|
||||
collapse(child)
|
||||
end
|
||||
end
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
-- Create a virtual Roblox tree
|
||||
local habitat = lemur.Habitat.new()
|
||||
|
||||
-- We'll put all of our library code and dependencies here
|
||||
local Root = lemur.Instance.new("Folder")
|
||||
Root.Name = "Root"
|
||||
|
||||
-- Load all of the modules specified above
|
||||
for _, module in ipairs(LOAD_MODULES) do
|
||||
local container = lemur.Instance.new("Folder", Root)
|
||||
container.Name = module[2]
|
||||
habitat:loadFromFs(module[1], container)
|
||||
end
|
||||
|
||||
collapse(Root)
|
||||
|
||||
-- Load TestEZ and run our tests
|
||||
local TestEZ = habitat:require(Root.TestEZ)
|
||||
|
||||
local results = TestEZ.TestBootstrap:run(Root.Plugin, TestEZ.Reporters.TextReporter)
|
||||
|
||||
-- Did something go wrong?
|
||||
if results.failureCount > 0 then
|
||||
os.exit(1)
|
||||
end
|
||||
112
plugin/src/Api.lua
Normal file
112
plugin/src/Api.lua
Normal file
@@ -0,0 +1,112 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local Config = require(script.Parent.Config)
|
||||
local Promise = require(script.Parent.Promise)
|
||||
local Version = require(script.Parent.Version)
|
||||
|
||||
local Api = {}
|
||||
Api.__index = Api
|
||||
|
||||
Api.Error = {
|
||||
ServerIdMismatch = "ServerIdMismatch",
|
||||
}
|
||||
|
||||
setmetatable(Api.Error, {
|
||||
__index = function(_, key)
|
||||
error("Invalid API.Error name " .. key, 2)
|
||||
end
|
||||
})
|
||||
|
||||
--[[
|
||||
Api.connect(Http) -> Promise<Api>
|
||||
|
||||
Create a new Api using the given HTTP implementation.
|
||||
|
||||
Attempting to invoke methods on an invalid conext will throw errors!
|
||||
]]
|
||||
function Api.connect(http)
|
||||
local context = {
|
||||
http = http,
|
||||
serverId = nil,
|
||||
currentTime = 0,
|
||||
}
|
||||
|
||||
setmetatable(context, Api)
|
||||
|
||||
return context:_start()
|
||||
end
|
||||
|
||||
function Api:_start()
|
||||
return self.http:get("/")
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
if response.protocolVersion ~= Config.protocolVersion then
|
||||
local message = (
|
||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
||||
"\nYour server is version %s, with protocol version %s." ..
|
||||
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
||||
):format(
|
||||
Version.display(Config.version), Config.protocolVersion,
|
||||
Config.expectedApiVersionString,
|
||||
response.serverVersion, response.protocolVersion
|
||||
)
|
||||
|
||||
return Promise.reject(message)
|
||||
end
|
||||
|
||||
self.serverId = response.serverId
|
||||
self.currentTime = response.currentTime
|
||||
|
||||
return self
|
||||
end)
|
||||
end
|
||||
|
||||
function Api:getInfo()
|
||||
return self.http:get("/")
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
if response.serverId ~= self.serverId then
|
||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
||||
end
|
||||
|
||||
return response
|
||||
end)
|
||||
end
|
||||
|
||||
function Api:read(paths)
|
||||
local body = HttpService:JSONEncode(paths)
|
||||
|
||||
return self.http:post("/read", body)
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
if response.serverId ~= self.serverId then
|
||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
||||
end
|
||||
|
||||
return response.items
|
||||
end)
|
||||
end
|
||||
|
||||
function Api:getChanges()
|
||||
local url = ("/changes/%f"):format(self.currentTime)
|
||||
|
||||
return self.http:get(url)
|
||||
:andThen(function(response)
|
||||
response = response:json()
|
||||
|
||||
if response.serverId ~= self.serverId then
|
||||
return Promise.reject(Api.Error.ServerIdMismatch)
|
||||
end
|
||||
|
||||
self.currentTime = response.currentTime
|
||||
|
||||
return response.changes
|
||||
end)
|
||||
end
|
||||
|
||||
return Api
|
||||
7
plugin/src/Config.lua
Normal file
7
plugin/src/Config.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
return {
|
||||
pollingRate = 0.2,
|
||||
version = {0, 4, 3},
|
||||
expectedServerVersionString = "0.4.x",
|
||||
protocolVersion = 1,
|
||||
dev = false,
|
||||
}
|
||||
7
plugin/src/Config.spec.lua
Normal file
7
plugin/src/Config.spec.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
return function()
|
||||
local Config = require(script.Parent.Config)
|
||||
|
||||
it("should have 'dev' disabled", function()
|
||||
expect(Config.dev).to.equal(false)
|
||||
end)
|
||||
end
|
||||
67
plugin/src/Http.lua
Normal file
67
plugin/src/Http.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local HTTP_DEBUG = false
|
||||
|
||||
local Promise = require(script.Parent.Promise)
|
||||
local HttpError = require(script.Parent.HttpError)
|
||||
local HttpResponse = require(script.Parent.HttpResponse)
|
||||
|
||||
local function dprint(...)
|
||||
if HTTP_DEBUG then
|
||||
print(...)
|
||||
end
|
||||
end
|
||||
|
||||
local Http = {}
|
||||
Http.__index = Http
|
||||
|
||||
function Http.new(baseUrl)
|
||||
assert(type(baseUrl) == "string", "Http.new needs a baseUrl!")
|
||||
|
||||
local http = {
|
||||
baseUrl = baseUrl
|
||||
}
|
||||
|
||||
setmetatable(http, Http)
|
||||
|
||||
return http
|
||||
end
|
||||
|
||||
function Http:get(endpoint)
|
||||
dprint("\nGET", endpoint)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
local ok, result = pcall(function()
|
||||
return HttpService:GetAsync(self.baseUrl .. endpoint, true)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
dprint("\t", result, "\n")
|
||||
resolve(HttpResponse.new(result))
|
||||
else
|
||||
reject(HttpError.fromErrorString(result))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function Http:post(endpoint, body)
|
||||
dprint("\nPOST", endpoint)
|
||||
dprint(body)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
local ok, result = pcall(function()
|
||||
return HttpService:PostAsync(self.baseUrl .. endpoint, body)
|
||||
end)
|
||||
|
||||
if ok then
|
||||
dprint("\t", result, "\n")
|
||||
resolve(HttpResponse.new(result))
|
||||
else
|
||||
reject(HttpError.fromErrorString(result))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return Http
|
||||
60
plugin/src/HttpError.lua
Normal file
60
plugin/src/HttpError.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
local HttpError = {}
|
||||
HttpError.__index = HttpError
|
||||
|
||||
HttpError.Error = {
|
||||
HttpNotEnabled = {
|
||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||
},
|
||||
ConnectFailed = {
|
||||
message = "Rojo plugin couldn't connect to the Rojo server.\n" ..
|
||||
"Make sure the server is running -- use 'Rojo serve' to run it!",
|
||||
},
|
||||
Unknown = {
|
||||
message = "Rojo encountered an unknown error: {{message}}",
|
||||
},
|
||||
}
|
||||
|
||||
function HttpError.new(type, extraMessage)
|
||||
extraMessage = extraMessage or ""
|
||||
local message = type.message:gsub("{{message}}", extraMessage)
|
||||
|
||||
local err = {
|
||||
type = type,
|
||||
message = message,
|
||||
}
|
||||
|
||||
setmetatable(err, HttpError)
|
||||
|
||||
return err
|
||||
end
|
||||
|
||||
function HttpError:__tostring()
|
||||
return self.message
|
||||
end
|
||||
|
||||
--[[
|
||||
This method shouldn't have to exist. Ugh.
|
||||
]]
|
||||
function HttpError.fromErrorString(err)
|
||||
err = err:lower()
|
||||
|
||||
if err:find("^http requests are not enabled") then
|
||||
return HttpError.new(HttpError.Error.HttpNotEnabled)
|
||||
end
|
||||
|
||||
if err:find("^curl error") then
|
||||
return HttpError.new(HttpError.Error.ConnectFailed)
|
||||
end
|
||||
|
||||
return HttpError.new(HttpError.Error.Unknown, err)
|
||||
end
|
||||
|
||||
function HttpError:report()
|
||||
warn(self.message)
|
||||
if self.type == HttpError.Error.HttpNotEnabled then
|
||||
game:GetService("Selection"):Set{game:GetService("HttpService")}
|
||||
end
|
||||
end
|
||||
|
||||
return HttpError
|
||||
20
plugin/src/HttpResponse.lua
Normal file
20
plugin/src/HttpResponse.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
local HttpService = game:GetService("HttpService")
|
||||
|
||||
local HttpResponse = {}
|
||||
HttpResponse.__index = HttpResponse
|
||||
|
||||
function HttpResponse.new(body)
|
||||
local response = {
|
||||
body = body,
|
||||
}
|
||||
|
||||
setmetatable(response, HttpResponse)
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
function HttpResponse:json()
|
||||
return HttpService:JSONDecode(self.body)
|
||||
end
|
||||
|
||||
return HttpResponse
|
||||
79
plugin/src/Main.server.lua
Normal file
79
plugin/src/Main.server.lua
Normal file
@@ -0,0 +1,79 @@
|
||||
if not plugin then
|
||||
return
|
||||
end
|
||||
|
||||
local Plugin = require(script.Parent.Plugin)
|
||||
local Config = require(script.Parent.Config)
|
||||
local Version = require(script.Parent.Version)
|
||||
|
||||
--[[
|
||||
Check if the user is using a newer version of Rojo than last time. If they
|
||||
are, show them a reminder to make sure they check their server version.
|
||||
]]
|
||||
local function checkUpgrade()
|
||||
-- When developing Rojo, there's no use in doing version checks
|
||||
if Config.dev then
|
||||
return
|
||||
end
|
||||
|
||||
local lastVersion = plugin:GetSetting("LastRojoVersion")
|
||||
|
||||
if lastVersion then
|
||||
local wasUpgraded = Version.compare(Config.version, lastVersion) == 1
|
||||
|
||||
if wasUpgraded then
|
||||
local message = (
|
||||
"\nRojo detected an upgrade from version %s to version %s." ..
|
||||
"\nMake sure you have also upgraded your server!" ..
|
||||
"\n\nRojo version %s is intended for use with server version %s.\n"
|
||||
):format(
|
||||
Version.display(lastVersion), Version.display(Config.version),
|
||||
Version.display(Config.version), Config.expectedServerVersionString
|
||||
)
|
||||
|
||||
print(message)
|
||||
end
|
||||
end
|
||||
|
||||
plugin:SetSetting("LastRojoVersion", Config.version)
|
||||
end
|
||||
|
||||
local function main()
|
||||
local pluginInstance = Plugin.new()
|
||||
|
||||
local displayedVersion = Config.dev and "DEV" or Version.display(Config.version)
|
||||
|
||||
local toolbar = plugin:CreateToolbar("Rojo Plugin " .. displayedVersion)
|
||||
|
||||
toolbar:CreateButton("Test Connection", "Connect to Rojo Server", "")
|
||||
.Click:Connect(function()
|
||||
checkUpgrade()
|
||||
|
||||
pluginInstance:connect()
|
||||
:catch(function(err)
|
||||
warn(err)
|
||||
end)
|
||||
end)
|
||||
|
||||
toolbar:CreateButton("Sync In", "Sync into Roblox Studio", "")
|
||||
.Click:Connect(function()
|
||||
checkUpgrade()
|
||||
|
||||
pluginInstance:syncIn()
|
||||
:catch(function(err)
|
||||
warn(err)
|
||||
end)
|
||||
end)
|
||||
|
||||
toolbar:CreateButton("Toggle Polling", "Poll server for changes", "")
|
||||
.Click:Connect(function()
|
||||
checkUpgrade()
|
||||
|
||||
pluginInstance:togglePolling()
|
||||
:catch(function(err)
|
||||
warn(err)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
main()
|
||||
253
plugin/src/Plugin.lua
Normal file
253
plugin/src/Plugin.lua
Normal file
@@ -0,0 +1,253 @@
|
||||
local Config = require(script.Parent.Config)
|
||||
local Http = require(script.Parent.Http)
|
||||
local Api = require(script.Parent.Api)
|
||||
local Promise = require(script.Parent.Promise)
|
||||
local Reconciler = require(script.Parent.Reconciler)
|
||||
|
||||
local function collectMatch(source, pattern)
|
||||
local result = {}
|
||||
|
||||
for match in source:gmatch(pattern) do
|
||||
table.insert(result, match)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local Plugin = {}
|
||||
Plugin.__index = Plugin
|
||||
|
||||
function Plugin.new()
|
||||
local address = "localhost"
|
||||
local port = Config.dev and 8001 or 8000
|
||||
|
||||
local remote = ("http://%s:%d"):format(address, port)
|
||||
|
||||
local self = {
|
||||
_http = Http.new(remote),
|
||||
_reconciler = Reconciler.new(),
|
||||
_api = nil,
|
||||
_polling = false,
|
||||
}
|
||||
|
||||
setmetatable(self, Plugin)
|
||||
|
||||
do
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
screenGui.Name = "Rojo UI"
|
||||
screenGui.Parent = game.CoreGui
|
||||
screenGui.DisplayOrder = -1
|
||||
screenGui.Enabled = false
|
||||
|
||||
local label = Instance.new("TextLabel")
|
||||
label.Font = Enum.Font.SourceSans
|
||||
label.TextSize = 20
|
||||
label.Text = "Rojo polling..."
|
||||
label.BackgroundColor3 = Color3.fromRGB(31, 31, 31)
|
||||
label.BackgroundTransparency = 0.5
|
||||
label.BorderSizePixel = 0
|
||||
label.TextColor3 = Color3.new(1, 1, 1)
|
||||
label.Size = UDim2.new(0, 120, 0, 28)
|
||||
label.Position = UDim2.new(0, 0, 0, 0)
|
||||
label.Parent = screenGui
|
||||
|
||||
self._label = screenGui
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Clears all state and issues a notice to the user that the plugin has
|
||||
restarted.
|
||||
]]
|
||||
function Plugin:restart()
|
||||
warn("The server has changed since the last request, reloading plugin...")
|
||||
|
||||
self._reconciler:clear()
|
||||
self._api = nil
|
||||
self._polling = false
|
||||
end
|
||||
|
||||
function Plugin:api()
|
||||
if not self._api then
|
||||
self._api = Api.connect(self._http)
|
||||
:catch(function(err)
|
||||
self._api = nil
|
||||
return Promise.reject(err)
|
||||
end)
|
||||
end
|
||||
|
||||
return self._api
|
||||
end
|
||||
|
||||
function Plugin:connect()
|
||||
print("Testing connection...")
|
||||
|
||||
return self:api()
|
||||
:andThen(function(api)
|
||||
local ok, info = api:getInfo():await()
|
||||
|
||||
if not ok then
|
||||
return Promise.reject(info)
|
||||
end
|
||||
|
||||
print("Server found!")
|
||||
print("Protocol version:", info.protocolVersion)
|
||||
print("Server version:", info.serverVersion)
|
||||
end)
|
||||
:catch(function(err)
|
||||
if err == Api.Error.ServerIdMismatch then
|
||||
self:restart()
|
||||
return self:connect()
|
||||
else
|
||||
return Promise.reject(err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Plugin:togglePolling()
|
||||
if self._polling then
|
||||
return self:stopPolling()
|
||||
else
|
||||
return self:startPolling()
|
||||
end
|
||||
end
|
||||
|
||||
function Plugin:stopPolling()
|
||||
if not self._polling then
|
||||
return Promise.resolve(false)
|
||||
end
|
||||
|
||||
print("Stopped polling.")
|
||||
|
||||
self._polling = false
|
||||
self._label.Enabled = false
|
||||
|
||||
return Promise.resolve(true)
|
||||
end
|
||||
|
||||
function Plugin:_pull(api, project, routes)
|
||||
return api:read(routes)
|
||||
:andThen(function(items)
|
||||
for index = 1, #routes do
|
||||
local itemRoute = routes[index]
|
||||
local partitionName = itemRoute[1]
|
||||
local partition = project.partitions[partitionName]
|
||||
local item = items[index]
|
||||
|
||||
local partitionRoute = collectMatch(partition.target, "[^.]+")
|
||||
|
||||
-- If the item route's length was 1, we need to rename the instance to
|
||||
-- line up with the partition's root object name.
|
||||
--
|
||||
-- This is a HACK!
|
||||
if #itemRoute == 1 then
|
||||
if item then
|
||||
local objectName = partition.target:match("[^.]+$")
|
||||
item.Name = objectName
|
||||
end
|
||||
end
|
||||
|
||||
local fullRoute = {}
|
||||
for _, piece in ipairs(partitionRoute) do
|
||||
table.insert(fullRoute, piece)
|
||||
end
|
||||
|
||||
for i = 2, #itemRoute do
|
||||
table.insert(fullRoute, itemRoute[i])
|
||||
end
|
||||
|
||||
self._reconciler:reconcileRoute(fullRoute, item, itemRoute)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Plugin:startPolling()
|
||||
if self._polling then
|
||||
return
|
||||
end
|
||||
|
||||
print("Starting to poll...")
|
||||
|
||||
self._polling = true
|
||||
self._label.Enabled = true
|
||||
|
||||
return self:api()
|
||||
:andThen(function(api)
|
||||
local syncOk, result = self:syncIn():await()
|
||||
|
||||
if not syncOk then
|
||||
return Promise.reject(result)
|
||||
end
|
||||
|
||||
local infoOk, info = api:getInfo():await()
|
||||
|
||||
if not infoOk then
|
||||
return Promise.reject(info)
|
||||
end
|
||||
|
||||
while self._polling do
|
||||
local changes = api:getChanges():await()
|
||||
|
||||
if #changes > 0 then
|
||||
local routes = {}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
table.insert(routes, change.route)
|
||||
end
|
||||
|
||||
local pullOk, pullResult = self:_pull(api, info.project, routes):await()
|
||||
|
||||
if not pullOk then
|
||||
return Promise.reject(pullResult)
|
||||
end
|
||||
end
|
||||
|
||||
wait(Config.pollingRate)
|
||||
end
|
||||
end)
|
||||
:catch(function(err)
|
||||
self:stopPolling()
|
||||
|
||||
if err == Api.Error.ServerIdMismatch then
|
||||
self:restart()
|
||||
return self:startPolling()
|
||||
else
|
||||
return Promise.reject(err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Plugin:syncIn()
|
||||
print("Syncing from server...")
|
||||
|
||||
return self:api()
|
||||
:andThen(function(api)
|
||||
local ok, info = api:getInfo():await()
|
||||
|
||||
if not ok then
|
||||
return Promise.reject(info)
|
||||
end
|
||||
|
||||
local routes = {}
|
||||
|
||||
for name in pairs(info.project.partitions) do
|
||||
table.insert(routes, {name})
|
||||
end
|
||||
|
||||
self:_pull(api, info.project, routes)
|
||||
|
||||
print("Sync successful!")
|
||||
end)
|
||||
:catch(function(err)
|
||||
if err == Api.Error.ServerIdMismatch then
|
||||
self:restart()
|
||||
return self:syncIn()
|
||||
else
|
||||
return Promise.reject(err)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return Plugin
|
||||
307
plugin/src/Promise.lua
Normal file
307
plugin/src/Promise.lua
Normal file
@@ -0,0 +1,307 @@
|
||||
--[[
|
||||
An implementation of Promises similar to Promise/A+.
|
||||
]]
|
||||
|
||||
local PROMISE_DEBUG = false
|
||||
|
||||
-- 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.
|
||||
local wpcall
|
||||
if PROMISE_DEBUG then
|
||||
wpcall = function(f, ...)
|
||||
local result = { pcall(f, ...) }
|
||||
|
||||
if not result[1] then
|
||||
warn(result[2])
|
||||
end
|
||||
|
||||
return unpack(result)
|
||||
end
|
||||
else
|
||||
wpcall = pcall
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a function that invokes a callback with correct error handling and
|
||||
resolution mechanisms.
|
||||
]]
|
||||
local function createAdvancer(callback, resolve, reject)
|
||||
return function(...)
|
||||
local result = { wpcall(callback, ...) }
|
||||
local ok = table.remove(result, 1)
|
||||
|
||||
if ok then
|
||||
resolve(unpack(result))
|
||||
else
|
||||
reject(unpack(result))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function isEmpty(t)
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
local Promise = {}
|
||||
Promise.__index = Promise
|
||||
|
||||
Promise.Status = {
|
||||
Started = "Started",
|
||||
Resolved = "Resolved",
|
||||
Rejected = "Rejected",
|
||||
}
|
||||
|
||||
--[[
|
||||
Constructs a new Promise with the given initializing callback.
|
||||
|
||||
This is generally only called when directly wrapping a non-promise API into
|
||||
a promise-based version.
|
||||
|
||||
The callback will receive 'resolve' and 'reject' methods, used to start
|
||||
invoking the promise chain.
|
||||
|
||||
For example:
|
||||
|
||||
local function get(url)
|
||||
return Promise.new(function(resolve, reject)
|
||||
spawn(function()
|
||||
resolve(HttpService:GetAsync(url))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
get("https://google.com")
|
||||
:andThen(function(stuff)
|
||||
print("Got some stuff!", stuff)
|
||||
end)
|
||||
]]
|
||||
function Promise.new(callback)
|
||||
local promise = {
|
||||
-- Used to locate where a promise was created
|
||||
_source = debug.traceback(),
|
||||
|
||||
-- A tag to identify us as a promise
|
||||
_type = "Promise",
|
||||
|
||||
_status = Promise.Status.Started,
|
||||
|
||||
-- A table containing a list of all results, whether success or failure.
|
||||
-- Only valid if _status is set to something besides Started
|
||||
_value = nil,
|
||||
|
||||
-- If an error occurs with no observers, this will be set.
|
||||
_unhandledRejection = false,
|
||||
|
||||
-- Queues representing functions we should invoke when we update!
|
||||
_queuedResolve = {},
|
||||
_queuedReject = {},
|
||||
}
|
||||
|
||||
setmetatable(promise, Promise)
|
||||
|
||||
local function resolve(...)
|
||||
promise:_resolve(...)
|
||||
end
|
||||
|
||||
local function reject(...)
|
||||
promise:_reject(...)
|
||||
end
|
||||
|
||||
local ok, err = wpcall(callback, resolve, reject)
|
||||
|
||||
if not ok and promise._status == Promise.Status.Started then
|
||||
reject(err)
|
||||
end
|
||||
|
||||
return promise
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately resolved value.
|
||||
]]
|
||||
function Promise.resolve(value)
|
||||
return Promise.new(function(resolve)
|
||||
resolve(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a promise that represents the immediately rejected value.
|
||||
]]
|
||||
function Promise.reject(value)
|
||||
return Promise.new(function(_, reject)
|
||||
reject(value)
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns a new promise that:
|
||||
* is resolved when all input promises resolve
|
||||
* is rejected if ANY input promises reject
|
||||
]]
|
||||
function Promise.all(...)
|
||||
error("unimplemented", 2)
|
||||
end
|
||||
|
||||
--[[
|
||||
Is the given object a Promise instance?
|
||||
]]
|
||||
function Promise.is(object)
|
||||
if type(object) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
return object._type == "Promise"
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new promise that receives the result of this promise.
|
||||
|
||||
The given callbacks are invoked depending on that result.
|
||||
]]
|
||||
function Promise:andThen(successHandler, failureHandler)
|
||||
self._unhandledRejection = false
|
||||
|
||||
-- Create a new promise to follow this part of the chain
|
||||
return Promise.new(function(resolve, reject)
|
||||
-- Our default callbacks just pass values onto the next promise.
|
||||
-- This lets success and failure cascade correctly!
|
||||
|
||||
local successCallback = resolve
|
||||
if successHandler then
|
||||
successCallback = createAdvancer(successHandler, resolve, reject)
|
||||
end
|
||||
|
||||
local failureCallback = reject
|
||||
if failureHandler then
|
||||
failureCallback = createAdvancer(failureHandler, resolve, reject)
|
||||
end
|
||||
|
||||
if self._status == Promise.Status.Started then
|
||||
-- If we haven't resolved yet, put ourselves into the queue
|
||||
table.insert(self._queuedResolve, successCallback)
|
||||
table.insert(self._queuedReject, failureCallback)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
-- This promise has already resolved! Trigger success immediately.
|
||||
successCallback(unpack(self._value))
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
-- This promise died a terrible death! Trigger failure immediately.
|
||||
failureCallback(unpack(self._value))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--[[
|
||||
Used to catch any errors that may have occurred in the promise.
|
||||
]]
|
||||
function Promise:catch(failureCallback)
|
||||
return self:andThen(nil, failureCallback)
|
||||
end
|
||||
|
||||
--[[
|
||||
Yield until the promise is completed.
|
||||
|
||||
This matches the execution model of normal Roblox functions.
|
||||
]]
|
||||
function Promise:await()
|
||||
self._unhandledRejection = false
|
||||
|
||||
if self._status == Promise.Status.Started then
|
||||
local result
|
||||
local bindable = Instance.new("BindableEvent")
|
||||
|
||||
self:andThen(function(...)
|
||||
result = {...}
|
||||
bindable:Fire(true)
|
||||
end, function(...)
|
||||
result = {...}
|
||||
bindable:Fire(false)
|
||||
end)
|
||||
|
||||
local ok = bindable.Event:Wait()
|
||||
bindable:Destroy()
|
||||
|
||||
return ok, unpack(result)
|
||||
elseif self._status == Promise.Status.Resolved then
|
||||
return true, unpack(self._value)
|
||||
elseif self._status == Promise.Status.Rejected then
|
||||
return false, unpack(self._value)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_resolve(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
-- If the resolved value was a Promise, we chain onto it!
|
||||
if Promise.is((...)) then
|
||||
-- Without this warning, arguments sometimes mysteriously disappear
|
||||
if select("#", ...) > 1 then
|
||||
local message = (
|
||||
"When returning a Promise from andThen, extra arguments are " ..
|
||||
"discarded! See:\n\n%s"
|
||||
):format(
|
||||
self._source
|
||||
)
|
||||
warn(message)
|
||||
end
|
||||
|
||||
(...):andThen(function(...)
|
||||
self:_resolve(...)
|
||||
end, function(...)
|
||||
self:_reject(...)
|
||||
end)
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Resolved
|
||||
self._value = {...}
|
||||
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedResolve) do
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
function Promise:_reject(...)
|
||||
if self._status ~= Promise.Status.Started then
|
||||
return
|
||||
end
|
||||
|
||||
self._status = Promise.Status.Rejected
|
||||
self._value = {...}
|
||||
|
||||
-- If there are any rejection handlers, call those!
|
||||
if not isEmpty(self._queuedReject) then
|
||||
-- We assume that these callbacks will not throw errors.
|
||||
for _, callback in ipairs(self._queuedReject) do
|
||||
callback(...)
|
||||
end
|
||||
else
|
||||
-- At this point, no one was able to observe the error.
|
||||
-- An error handler might still be attached if the error occurred
|
||||
-- synchronously. We'll wait one tick, and if there are still no
|
||||
-- observers, then we should put a message in the console.
|
||||
|
||||
self._unhandledRejection = true
|
||||
local err = tostring((...))
|
||||
|
||||
spawn(function()
|
||||
-- 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
|
||||
|
||||
return Promise
|
||||
262
plugin/src/Promise.spec.lua
Normal file
262
plugin/src/Promise.spec.lua
Normal file
@@ -0,0 +1,262 @@
|
||||
return function()
|
||||
local Promise = require(script.Parent.Promise)
|
||||
|
||||
describe("Promise.new", function()
|
||||
it("should instantiate with a callback", function()
|
||||
local promise = Promise.new(function() end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should invoke the given callback with resolve and reject", function()
|
||||
local callCount = 0
|
||||
local resolveArg
|
||||
local rejectArg
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
resolveArg = resolve
|
||||
rejectArg = reject
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(resolveArg).to.be.a("function")
|
||||
expect(rejectArg).to.be.a("function")
|
||||
expect(promise._status).to.equal(Promise.Status.Started)
|
||||
end)
|
||||
|
||||
it("should resolve promises on resolve()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve)
|
||||
callCount = callCount + 1
|
||||
resolve()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
end)
|
||||
|
||||
it("should reject promises on reject()", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function(resolve, reject)
|
||||
callCount = callCount + 1
|
||||
reject()
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
end)
|
||||
|
||||
it("should reject on error in callback", function()
|
||||
local callCount = 0
|
||||
|
||||
local promise = Promise.new(function()
|
||||
callCount = callCount + 1
|
||||
error("hahah")
|
||||
end)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(callCount).to.equal(1)
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]:find("hahah")).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.resolve", function()
|
||||
it("should immediately resolve with a value", function()
|
||||
local promise = Promise.resolve(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
end)
|
||||
|
||||
it("should chain onto passed promises", function()
|
||||
local promise = Promise.resolve(Promise.new(function(_, reject)
|
||||
reject(7)
|
||||
end))
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(7)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise.reject", function()
|
||||
it("should immediately reject with a value", function()
|
||||
local promise = Promise.reject(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
end)
|
||||
|
||||
it("should pass a promise as-is as an error", function()
|
||||
local innerPromise = Promise.new(function(resolve)
|
||||
resolve(6)
|
||||
end)
|
||||
|
||||
local promise = Promise.reject(innerPromise)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(innerPromise)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Promise:andThen", function()
|
||||
it("should chain onto resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local promise = Promise.resolve(5)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end, function()
|
||||
badCallCount = badCallCount + 1
|
||||
end)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto rejected promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local promise = Promise.reject(5)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
badCallCount = badCallCount + 1
|
||||
end, function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(5)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(5)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto asynchronously resolved promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local startResolution
|
||||
local promise = Promise.new(function(resolve)
|
||||
startResolution = resolve
|
||||
end)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end, function()
|
||||
badCallCount = badCallCount + 1
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
startResolution(6)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Resolved)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should chain onto asynchronously rejected promises", function()
|
||||
local args
|
||||
local argsLength
|
||||
local callCount = 0
|
||||
local badCallCount = 0
|
||||
|
||||
local startResolution
|
||||
local promise = Promise.new(function(_, reject)
|
||||
startResolution = reject
|
||||
end)
|
||||
|
||||
local chained = promise
|
||||
:andThen(function()
|
||||
badCallCount = badCallCount + 1
|
||||
end, function(...)
|
||||
args = {...}
|
||||
argsLength = select("#", ...)
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
startResolution(6)
|
||||
|
||||
expect(badCallCount).to.equal(0)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(argsLength).to.equal(1)
|
||||
expect(args[1]).to.equal(6)
|
||||
|
||||
expect(promise).to.be.ok()
|
||||
expect(promise._status).to.equal(Promise.Status.Rejected)
|
||||
expect(promise._value[1]).to.equal(6)
|
||||
|
||||
expect(chained).to.be.ok()
|
||||
expect(chained).never.to.equal(promise)
|
||||
expect(chained._status).to.equal(Promise.Status.Resolved)
|
||||
expect(#chained._value).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
214
plugin/src/Reconciler.lua
Normal file
214
plugin/src/Reconciler.lua
Normal file
@@ -0,0 +1,214 @@
|
||||
local RouteMap = require(script.Parent.RouteMap)
|
||||
|
||||
local function classEqual(rbx, className)
|
||||
if className == "*" then
|
||||
return true
|
||||
end
|
||||
|
||||
return rbx.ClassName == className
|
||||
end
|
||||
|
||||
local function reparent(rbx, parent)
|
||||
if rbx then
|
||||
-- It's possible that 'rbx' is a service or some other object that we
|
||||
-- can't change the parent of. That's the only reason why Parent would
|
||||
-- fail except for rbx being previously destroyed!
|
||||
pcall(function()
|
||||
rbx.Parent = parent
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Attempts to match up Roblox instances and object specifiers for
|
||||
reconciliation.
|
||||
|
||||
An object is considered a match if they have the same Name and ClassName.
|
||||
|
||||
primaryChildren and secondaryChildren can each be either a list of Roblox
|
||||
instances or object specifiers. Since they share a common shape, switching
|
||||
the two around isn't problematic!
|
||||
|
||||
visited is expected to be an empty table initially. It will be filled with
|
||||
the set of children that have been visited so far.
|
||||
]]
|
||||
local function findNextChildPair(primaryChildren, secondaryChildren, visited)
|
||||
for _, primaryChild in ipairs(primaryChildren) do
|
||||
if not visited[primaryChild] then
|
||||
visited[primaryChild] = true
|
||||
|
||||
for _, secondaryChild in ipairs(secondaryChildren) do
|
||||
if primaryChild.ClassName == secondaryChild.ClassName and primaryChild.Name == secondaryChild.Name then
|
||||
visited[secondaryChild] = true
|
||||
|
||||
return primaryChild, secondaryChild
|
||||
end
|
||||
end
|
||||
|
||||
return primaryChild, nil
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local Reconciler = {}
|
||||
Reconciler.__index = Reconciler
|
||||
|
||||
function Reconciler.new()
|
||||
local reconciler = {
|
||||
_routeMap = RouteMap.new(),
|
||||
}
|
||||
|
||||
setmetatable(reconciler, Reconciler)
|
||||
|
||||
return reconciler
|
||||
end
|
||||
|
||||
--[[
|
||||
A semi-smart algorithm that attempts to apply the given item's children to
|
||||
an existing Roblox object.
|
||||
]]
|
||||
function Reconciler:_reconcileChildren(rbx, item)
|
||||
local visited = {}
|
||||
local rbxChildren = rbx:GetChildren()
|
||||
|
||||
-- Reconcile any children that were added or updated
|
||||
while true do
|
||||
local itemChild, rbxChild = findNextChildPair(item.Children, rbxChildren, visited)
|
||||
|
||||
if not itemChild then
|
||||
break
|
||||
end
|
||||
|
||||
reparent(self:reconcile(rbxChild, itemChild), rbx)
|
||||
end
|
||||
|
||||
-- Reconcile any children that were deleted
|
||||
while true do
|
||||
local rbxChild, itemChild = findNextChildPair(rbxChildren, item.Children, visited)
|
||||
|
||||
if not rbxChild then
|
||||
break
|
||||
end
|
||||
|
||||
reparent(self:reconcile(rbxChild, itemChild), rbx)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Construct a new Roblox object from the given item.
|
||||
]]
|
||||
function Reconciler:_reify(item)
|
||||
local className = item.ClassName
|
||||
|
||||
-- "*" represents a match of any class. It reifies as a folder!
|
||||
if className == "*" then
|
||||
className = "Folder"
|
||||
end
|
||||
|
||||
local rbx = Instance.new(className)
|
||||
rbx.Name = item.Name
|
||||
|
||||
for key, property in pairs(item.Properties) do
|
||||
-- TODO: Check for compound types, like Vector3!
|
||||
rbx[key] = property.Value
|
||||
end
|
||||
|
||||
self:_reconcileChildren(rbx, item)
|
||||
|
||||
if item.Route then
|
||||
self._routeMap:insert(item.Route, rbx)
|
||||
end
|
||||
|
||||
return rbx
|
||||
end
|
||||
|
||||
--[[
|
||||
Clears any state that the Reconciler has, effectively restarting it.
|
||||
]]
|
||||
function Reconciler:clear()
|
||||
self._routeMap:clear()
|
||||
end
|
||||
|
||||
--[[
|
||||
Apply the changes represented by the given item to a Roblox object that's a
|
||||
child of the given instance.
|
||||
]]
|
||||
function Reconciler:reconcile(rbx, item)
|
||||
-- Item was deleted
|
||||
if not item then
|
||||
if rbx then
|
||||
rbx:Destroy()
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Item was created!
|
||||
if not rbx then
|
||||
return self:_reify(item)
|
||||
end
|
||||
|
||||
-- Item changed type!
|
||||
if not classEqual(rbx, item.ClassName) then
|
||||
rbx:Destroy()
|
||||
|
||||
rbx = self:_reify(item)
|
||||
end
|
||||
|
||||
-- Apply all properties, Roblox will de-duplicate changes
|
||||
for key, property in pairs(item.Properties) do
|
||||
-- TODO: Transform property value based on property.Type
|
||||
-- Right now, we assume that 'value' is primitive!
|
||||
|
||||
rbx[key] = property.Value
|
||||
end
|
||||
|
||||
-- Use a dumb algorithm for reconciling children
|
||||
self:_reconcileChildren(rbx, item)
|
||||
|
||||
if item.Route then
|
||||
self._routeMap:insert(item.Route, rbx)
|
||||
end
|
||||
|
||||
return rbx
|
||||
end
|
||||
|
||||
function Reconciler:reconcileRoute(route, item, itemRoute)
|
||||
local parent
|
||||
local rbx = game
|
||||
|
||||
for i = 1, #route do
|
||||
local piece = route[i]
|
||||
|
||||
local child = rbx:FindFirstChild(piece)
|
||||
|
||||
-- We should get services instead of making folders here.
|
||||
if rbx == game and not child then
|
||||
local _
|
||||
_, child = pcall(game.GetService, game, piece)
|
||||
end
|
||||
|
||||
-- We don't want to create a folder if we're reaching our target item!
|
||||
if not child and i ~= #route then
|
||||
child = Instance.new("Folder")
|
||||
child.Parent = rbx
|
||||
child.Name = piece
|
||||
end
|
||||
|
||||
parent = rbx
|
||||
rbx = child
|
||||
end
|
||||
|
||||
-- Let's check the route map!
|
||||
if not rbx then
|
||||
rbx = self._routeMap:get(itemRoute)
|
||||
end
|
||||
|
||||
rbx = self:reconcile(rbx, item)
|
||||
|
||||
reparent(rbx, parent)
|
||||
end
|
||||
|
||||
return Reconciler
|
||||
103
plugin/src/RouteMap.lua
Normal file
103
plugin/src/RouteMap.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
--[[
|
||||
A map from Route objects (given by the server) to Roblox instances (created
|
||||
by the plugin).
|
||||
]]
|
||||
|
||||
local function hashRoute(route)
|
||||
return table.concat(route, "/")
|
||||
end
|
||||
|
||||
local RouteMap = {}
|
||||
RouteMap.__index = RouteMap
|
||||
|
||||
function RouteMap.new()
|
||||
local self = {
|
||||
_map = {},
|
||||
_reverseMap = {},
|
||||
_connectionsByRbx = {},
|
||||
}
|
||||
|
||||
setmetatable(self, RouteMap)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function RouteMap:insert(route, rbx)
|
||||
local hashed = hashRoute(route)
|
||||
|
||||
self._map[hashed] = rbx
|
||||
self._reverseMap[rbx] = hashed
|
||||
self._connectionsByRbx[rbx] = rbx.AncestryChanged:Connect(function(_, parent)
|
||||
if parent == nil then
|
||||
self:removeByRbx(rbx)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function RouteMap:get(route)
|
||||
return self._map[hashRoute(route)]
|
||||
end
|
||||
|
||||
function RouteMap:removeByRoute(route)
|
||||
local hashedRoute = hashRoute(route)
|
||||
local rbx = self._map[hashedRoute]
|
||||
|
||||
if rbx then
|
||||
self._map[hashedRoute] = nil
|
||||
self._reverseMap[rbx] = nil
|
||||
self._connectionsByRbx[rbx] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function RouteMap:removeByRbx(rbx)
|
||||
local hashedRoute = self._reverseMap[rbx]
|
||||
|
||||
if hashedRoute then
|
||||
self._map[hashedRoute] = nil
|
||||
self._reverseMap[rbx] = nil
|
||||
self._connectionsByRbx[rbx] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function RouteMap:removeRbxDescendants(parentRbx)
|
||||
for rbx in pairs(self._reverseMap) do
|
||||
if rbx:IsDescendantOf(parentRbx) then
|
||||
self:removeByRbx(rbx)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RouteMap:clear()
|
||||
self._map = {}
|
||||
self._reverseMap = {}
|
||||
|
||||
for _, connection in pairs(self._connectionsByRbx) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self._connectionsByRbx = {}
|
||||
end
|
||||
|
||||
function RouteMap:visualize()
|
||||
-- Log all of our keys so that the visualization has a stable order.
|
||||
local keys = {}
|
||||
|
||||
for key in pairs(self._map) do
|
||||
table.insert(keys, key)
|
||||
end
|
||||
|
||||
table.sort(keys)
|
||||
|
||||
local buffer = {}
|
||||
for _, key in ipairs(keys) do
|
||||
local visualized = ("- %s: %s"):format(
|
||||
key,
|
||||
self._map[key]:GetFullName()
|
||||
)
|
||||
table.insert(buffer, visualized)
|
||||
end
|
||||
|
||||
return table.concat(buffer, "\n")
|
||||
end
|
||||
|
||||
return RouteMap
|
||||
40
plugin/src/Version.lua
Normal file
40
plugin/src/Version.lua
Normal file
@@ -0,0 +1,40 @@
|
||||
local function compare(a, b)
|
||||
if a > b then
|
||||
return 1
|
||||
elseif a < b then
|
||||
return -1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
local Version = {}
|
||||
|
||||
--[[
|
||||
Compares two versions of the form {major, minor, revision}.
|
||||
|
||||
If a is newer than b, 1.
|
||||
If a is older than b, -1.
|
||||
If a and b are the same, 0.
|
||||
]]
|
||||
function Version.compare(a, b)
|
||||
local major = compare(a[1], b[1])
|
||||
local minor = compare(a[2] or 0, b[2] or 0)
|
||||
local revision = compare(a[3] or 0, b[3] or 0)
|
||||
|
||||
if major ~= 0 then
|
||||
return major
|
||||
end
|
||||
|
||||
if minor ~= 0 then
|
||||
return minor
|
||||
end
|
||||
|
||||
return revision
|
||||
end
|
||||
|
||||
function Version.display(version)
|
||||
return table.concat(version, ".")
|
||||
end
|
||||
|
||||
return Version
|
||||
28
plugin/src/Version.spec.lua
Normal file
28
plugin/src/Version.spec.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
return function()
|
||||
local Version = require(script.Parent.Version)
|
||||
|
||||
it("should compare equal versions", function()
|
||||
expect(Version.compare({1, 2, 3}, {1, 2, 3})).to.equal(0)
|
||||
expect(Version.compare({0, 4, 0}, {0, 4})).to.equal(0)
|
||||
expect(Version.compare({0, 0, 123}, {0, 0, 123})).to.equal(0)
|
||||
expect(Version.compare({26}, {26})).to.equal(0)
|
||||
expect(Version.compare({26, 42}, {26, 42})).to.equal(0)
|
||||
expect(Version.compare({1, 0, 0}, {1})).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should compare newer, older versions", function()
|
||||
expect(Version.compare({1}, {0})).to.equal(1)
|
||||
expect(Version.compare({1, 1}, {1, 0})).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should compare different major versions", function()
|
||||
expect(Version.compare({1, 3, 2}, {2, 2, 1})).to.equal(-1)
|
||||
expect(Version.compare({1, 2}, {2, 1})).to.equal(-1)
|
||||
expect(Version.compare({1}, {2})).to.equal(-1)
|
||||
end)
|
||||
|
||||
it("should compare different minor versions", function()
|
||||
expect(Version.compare({1, 2, 3}, {1, 3, 2})).to.equal(-1)
|
||||
expect(Version.compare({50, 1}, {50, 2})).to.equal(-1)
|
||||
end)
|
||||
end
|
||||
4
plugin/src/runTests.lua
Normal file
4
plugin/src/runTests.lua
Normal file
@@ -0,0 +1,4 @@
|
||||
return function()
|
||||
local TestEZ = require(script.Parent.Parent.TestEZ)
|
||||
TestEZ.TestBootstrap:run(script.Parent)
|
||||
end
|
||||
2
plugin/tests/runTests.server.lua
Normal file
2
plugin/tests/runTests.server.lua
Normal file
@@ -0,0 +1,2 @@
|
||||
local TestEZ = require(game.ReplicatedStorage.TestEZ)
|
||||
TestEZ.TestBootstrap:run(game.ReplicatedStorage.Rojo.plugin)
|
||||
5
server/.editorconfig
Normal file
5
server/.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
467
Cargo.lock → server/Cargo.lock
generated
467
Cargo.lock → server/Cargo.lock
generated
@@ -7,9 +7,20 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.10.2"
|
||||
name = "aho-corasick"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
@@ -18,13 +29,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.3"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -48,7 +58,7 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -57,7 +67,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"brotli-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -72,7 +82,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.3"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -85,8 +95,8 @@ name = "chrono"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -96,13 +106,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.29.0"
|
||||
version = "2.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -175,18 +185,18 @@ name = "env_logger"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -194,7 +204,7 @@ name = "flate2"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"miniz-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -205,7 +215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -213,24 +223,22 @@ name = "fsevent-sys"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.2.1"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon-sys"
|
||||
version = "0.2.0"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gcc"
|
||||
@@ -252,12 +260,12 @@ name = "inotify"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.3.4"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -269,15 +277,31 @@ dependencies = [
|
||||
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.34"
|
||||
version = "0.2.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
@@ -289,7 +313,15 @@ name = "memchr"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -297,7 +329,7 @@ name = "mime"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -316,8 +348,8 @@ name = "miniz-sys"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -326,13 +358,13 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"nix 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -342,7 +374,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
@@ -354,24 +386,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"buf_redux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime_guess 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.31"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -380,7 +410,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -389,47 +419,47 @@ version = "4.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"filetime 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fsevent 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.35"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.34"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.41"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -460,7 +490,7 @@ version = "0.7.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -472,22 +502,44 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.3.15"
|
||||
name = "proc-macro2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.3.18"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.32"
|
||||
version = "0.1.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -495,7 +547,7 @@ name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -510,22 +562,52 @@ dependencies = [
|
||||
"utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "rojo"
|
||||
version = "0.3.2"
|
||||
name = "regex-syntax"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rojo"
|
||||
version = "0.4.3"
|
||||
dependencies = [
|
||||
"clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rouille 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -535,16 +617,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"filetime 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"flate2 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"multipart 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tiny_http 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tiny_http 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -554,11 +636,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -566,42 +647,43 @@ name = "serde"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"num 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.24"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.24"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.18.0"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.8"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -621,33 +703,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.11.11"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synom"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.5"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -664,8 +739,8 @@ name = "termion"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -683,7 +758,7 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -695,30 +770,42 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.38"
|
||||
name = "thread_local"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-util"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.4"
|
||||
@@ -739,9 +826,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.0.4"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unreachable"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "0.2.38"
|
||||
@@ -754,7 +849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "1.6.0"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -767,12 +862,17 @@ name = "utf8-ranges"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
@@ -781,12 +881,18 @@ name = "vec_map"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.0.1"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"same-file 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -794,11 +900,30 @@ name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "ws2_32-sys"
|
||||
version = "0.2.1"
|
||||
@@ -810,9 +935,10 @@ dependencies = [
|
||||
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
||||
"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455"
|
||||
"checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
"checksum ascii 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae7d751998c189c1d4468cf0a39bb2eae052a9c58d50ebb3b9591ee3813ad50"
|
||||
"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860"
|
||||
"checksum atty 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "af80143d6f7608d746df1520709e5d141c96f240b0e62b0aa41bdfb53374d9d4"
|
||||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf"
|
||||
@@ -820,11 +946,11 @@ dependencies = [
|
||||
"checksum brotli2 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ea9d0bbab1235017a09226b079ed733bca4bf9ecb6b6102bd01aac79ea082dca"
|
||||
"checksum buf_redux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b115bd9935c68b58f80ff867e1c46942c4aed79e78bcc8c2bc22d50f52bb9099"
|
||||
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27"
|
||||
"checksum cc 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b13a57efd6b30ecd6598ebdb302cca617930b5470647570468a65d12ef9719"
|
||||
"checksum cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2b4911e4bdcb4100c7680e7e854ff38e23f1b34d4d9e079efae3da2801341ffc"
|
||||
"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de"
|
||||
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
|
||||
"checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
|
||||
"checksum clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "110d43e343eb29f4f51c1db31beb879d546db27998577e5715270a54bcf41d3f"
|
||||
"checksum clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0f16b89cbb9ee36d87483dc939fe9f1e13c05898d56d7b230a0d4dff033a536"
|
||||
"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
|
||||
"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
|
||||
"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
|
||||
@@ -834,77 +960,92 @@ dependencies = [
|
||||
"checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
|
||||
"checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
|
||||
"checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
|
||||
"checksum filetime 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "aa75ec8f7927063335a9583e7fa87b0110bb888cf766dc01b54c0ff70d760c8e"
|
||||
"checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f"
|
||||
"checksum flate2 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)" = "e6234dd4468ae5d1e2dbb06fe2b058696fdc50a339c68a393aefbf00bc81e423"
|
||||
"checksum fsevent 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "c4bbbf71584aeed076100b5665ac14e3d85eeb31fdbb45fbd41ef9a682b5ec05"
|
||||
"checksum fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a772d36c338d07a032d5375a36f15f9a7043bf0cb8ce7cee658e037c6032874"
|
||||
"checksum fuchsia-zircon 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6c0581a4e363262e52b87f59ee2afe3415361c6ec35e665924eb08afe8ff159"
|
||||
"checksum fuchsia-zircon-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43f3795b4bae048dc6123a6b972cadde2e676f9ded08aef6bb77f5f157684a82"
|
||||
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
"checksum gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)" = "5e33ec290da0d127825013597dbdfc28bee4964690c7ce1166cbc2a7bd08b1bb"
|
||||
"checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d"
|
||||
"checksum inotify 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887fcc180136e77a85e6a6128579a719027b1bab9b1c38ea4444244fe262c20c"
|
||||
"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
|
||||
"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "36fbc8a8929c632868295d0178dd8f63fc423fd7537ad0738372bd010b3ac9b0"
|
||||
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
|
||||
"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
|
||||
"checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b"
|
||||
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
|
||||
"checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376"
|
||||
"checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20"
|
||||
"checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d"
|
||||
"checksum mime 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ec0c2f4d901bf1d4a2192a40b4b570ae3b19c51243e549defc1de741940aa787"
|
||||
"checksum mime_guess 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "422acd80644209a8c8c66a20514840d8c092eb1eab2898ca7c548cc1d64c8998"
|
||||
"checksum miniz-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "609ce024854aeb19a0ef7567d348aaa5a746b32fb72e336df7fcc16869d7e2b4"
|
||||
"checksum mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a637d1ca14eacae06296a008fa7ad955347e34efcb5891cfd8ba05491a37907e"
|
||||
"checksum miow 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3e690c5df6b2f60acd45d56378981e827ff8295562fc8d34f573deb267a59cd1"
|
||||
"checksum multipart 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b68c9a0c757bd65893af529f7af6e7a71442e57ca6d9db1fa69b79e2f05f6b49"
|
||||
"checksum net2 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "3a80f842784ef6c9a958b68b7516bc7e35883c614004dd94959a4dca1b716c09"
|
||||
"checksum net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)" = "9044faf1413a1057267be51b5afba8eb1090bd2231c693664aa1db716fe1eae0"
|
||||
"checksum nix 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bfb3ddedaa14746434a02041940495bf11325c22f6d36125d3bdd56090d50a79"
|
||||
"checksum notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5c3812da3098f210a0bb440f9c008471a031aa4c1de07a264fdd75456c95a4eb"
|
||||
"checksum num 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "cc4083e14b542ea3eb9b5f33ff48bd373a92d78687e74f4cc0a30caeb754f0ca"
|
||||
"checksum num-integer 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "d1452e8b06e448a07f0e6ebb0bb1d92b8890eea63288c0b627331d53514d0fba"
|
||||
"checksum num-iter 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)" = "7485fcc84f85b4ecd0ea527b14189281cf27d60e583ae65ebc9c088b13dffe01"
|
||||
"checksum num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "cacfcab5eb48250ee7d0c7896b51a2c5eec99c1feea5f32025635f5ae4b00070"
|
||||
"checksum num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e"
|
||||
"checksum num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f8d26da319fb45674985c78f1d1caf99aa4941f785d384a2ae36d0740bc3e2fe"
|
||||
"checksum num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "4b226df12c5a59b63569dd57fafb926d91b385dfce33d8074a412411b689d593"
|
||||
"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364"
|
||||
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
|
||||
"checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc"
|
||||
"checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f"
|
||||
"checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03"
|
||||
"checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2"
|
||||
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
|
||||
"checksum rand 0.3.18 (registry+https://github.com/rust-lang/crates.io-index)" = "6475140dfd8655aeb72e1fd4b7a1cc1c202be65d71669476e392fe62532b9edd"
|
||||
"checksum redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ab105df655884ede59d45b7070c8a65002d921461ee813a024558ca16030eea0"
|
||||
"checksum proc-macro2 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "388d7ea47318c5ccdeb9ba6312cee7d3f65dd2804be8580a170fce410d50b786"
|
||||
"checksum quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0ff51282f28dc1b53fd154298feaa2e77c5ea0dba68e1fd8b03b72fbe13d2a"
|
||||
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
|
||||
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
|
||||
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
|
||||
"checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb"
|
||||
"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
|
||||
"checksum regex-syntax 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b2550876c31dc914696a6c2e01cbce8afba79a93c8ae979d2fe051c0230b3756"
|
||||
"checksum remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24"
|
||||
"checksum rouille 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d6415f261a8775bef50e9fcfb14ed73209ce637f753f9d1c8c6122559e559001"
|
||||
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
"checksum same-file 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "70a18720d745fb9ca6a041b37cb36d0b21066006b6cff8b5b360142d4b81fb60"
|
||||
"checksum same-file 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cfb6eded0b06a0b512c8ddbcf04089138c9b4362c2f696f3c3d76039d68f3637"
|
||||
"checksum serde 0.6.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97b18e9e53de541f11e497357d6c5eaeb39f0cb9c8734e274abe4935f6991fa"
|
||||
"checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1"
|
||||
"checksum serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "02c92ea07b6e49b959c1481804ebc9bfd92d3c459f1274c9a9546829e42a66ce"
|
||||
"checksum serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75c6aac7b99801a16db5b40b7bf0d7e4ba16e76fbf231e32a4677f271cac0603"
|
||||
"checksum serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7cf5b0b5b4bd22eeecb7e01ac2e1225c7ef5e4272b79ee28a8392a8c8489c839"
|
||||
"checksum serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "d3bcee660dcde8f52c3765dd9ca5ee36b4bf35470a738eb0bd5a8752b0389645"
|
||||
"checksum serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "f1711ab8b208541fa8de00425f6a577d90f27bb60724d2bb5fd911314af9668f"
|
||||
"checksum serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89b340a48245bc03ddba31d0ff1709c118df90edc6adabaca4aac77aea181cce"
|
||||
"checksum serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5c508584d9913df116b91505eec55610a2f5b16e9ed793c46e4d0152872b3e74"
|
||||
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
|
||||
"checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537"
|
||||
"checksum slab 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d807fd58c4181bbabed77cb3b891ba9748241a552bcc5be698faaebefc54f46e"
|
||||
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
|
||||
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
|
||||
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
|
||||
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
|
||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||
"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59"
|
||||
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
"checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693"
|
||||
"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
|
||||
"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
|
||||
"checksum time 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d788d3aa77bc0ef3e9621256885555368b47bd495c13dd2e7413c89f845520"
|
||||
"checksum tiny_http 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "016f040cfc9b5be610de3619eaaa57017fa0b0b678187327bde75fc146e2a41f"
|
||||
"checksum thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963"
|
||||
"checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098"
|
||||
"checksum tiny_http 0.5.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2f4d55c9a213880d1f0c89ded183f209c6e45b912ca6c7df6f93c163773572e1"
|
||||
"checksum ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd2be2d6639d0f8fe6cdda291ad456e23629558d466e2789d2c3e9892bda285d"
|
||||
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
|
||||
"checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f"
|
||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||
"checksum url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)" = "cbaa8377a162d88e7d15db0cf110c8523453edcbc5bc66d2b6fffccffa34a068"
|
||||
"checksum url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa35e768d4daf1d85733418a49fb42e10d7f633e394fccab4ab7aba897053fe2"
|
||||
"checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7"
|
||||
"checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f"
|
||||
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
|
||||
"checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f"
|
||||
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
|
||||
"checksum walkdir 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40b6d201f4f8998a837196b6de9c73e35af14c992cbb92c4ab641d2c2dce52de"
|
||||
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
"checksum walkdir 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "63636bd0eb3d00ccb8b9036381b526efac53caf112b7783b730ab3f8e44da369"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "0.3.2"
|
||||
version = "0.4.3"
|
||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||
description = "A tool to create robust Roblox projects"
|
||||
license = "MIT"
|
||||
@@ -18,3 +18,5 @@ serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
notify = "4.0.0"
|
||||
rand = "0.3"
|
||||
regex = "0.2"
|
||||
lazy_static = "1.0"
|
||||
97
server/src/bin.rs
Normal file
97
server/src/bin.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
#[macro_use] extern crate serde_derive;
|
||||
#[macro_use] extern crate rouille;
|
||||
#[macro_use] extern crate clap;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate notify;
|
||||
extern crate rand;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate regex;
|
||||
|
||||
pub mod web;
|
||||
pub mod core;
|
||||
pub mod project;
|
||||
pub mod pathext;
|
||||
pub mod vfs;
|
||||
pub mod rbx;
|
||||
pub mod plugin;
|
||||
pub mod plugins;
|
||||
pub mod commands;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
use pathext::canonicalish;
|
||||
|
||||
fn main() {
|
||||
let matches = clap_app!(rojo =>
|
||||
(version: env!("CARGO_PKG_VERSION"))
|
||||
(author: env!("CARGO_PKG_AUTHORS"))
|
||||
(about: env!("CARGO_PKG_DESCRIPTION"))
|
||||
|
||||
(@subcommand init =>
|
||||
(about: "Creates a new Rojo project")
|
||||
(@arg PATH: "Path to the place to create the project. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@subcommand serve =>
|
||||
(about: "Serves the project's files for use with the Rojo Studio plugin.")
|
||||
(@arg PROJECT: "Path to the project to serve. Defaults to the current directory.")
|
||||
(@arg port: --port +takes_value "The port to listen on. Defaults to 8000.")
|
||||
)
|
||||
|
||||
(@subcommand pack =>
|
||||
(about: "Packs the project into a GUI installer bundle. NOT YET IMPLEMENTED!")
|
||||
(@arg PROJECT: "Path to the project to pack. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@arg verbose: --verbose "Enable extended logging.")
|
||||
).get_matches();
|
||||
|
||||
let verbose = match matches.occurrences_of("verbose") {
|
||||
0 => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
match matches.subcommand() {
|
||||
("init", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||
let full_path = canonicalish(project_path);
|
||||
|
||||
commands::init(&full_path);
|
||||
},
|
||||
("serve", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
|
||||
let project_path = match sub_matches.value_of("PROJECT") {
|
||||
Some(v) => canonicalish(PathBuf::from(v)),
|
||||
None => std::env::current_dir().unwrap(),
|
||||
};
|
||||
|
||||
let port = {
|
||||
match sub_matches.value_of("port") {
|
||||
Some(source) => match source.parse::<u64>() {
|
||||
Ok(value) => Some(value),
|
||||
Err(_) => {
|
||||
eprintln!("Invalid port '{}'", source);
|
||||
process::exit(1);
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
|
||||
commands::serve(&project_path, verbose, port);
|
||||
},
|
||||
("pack", _) => {
|
||||
eprintln!("'rojo pack' is not yet implemented!");
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Please specify a subcommand!");
|
||||
eprintln!("Try 'rojo help' for information.");
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
16
server/src/commands/init.rs
Normal file
16
server/src/commands/init.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use project::Project;
|
||||
|
||||
pub fn init(project_path: &PathBuf) {
|
||||
match Project::init(project_path) {
|
||||
Ok(_) => {
|
||||
println!("Created new empty project at {}", project_path.display());
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new project.\n{}", e);
|
||||
process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
5
server/src/commands/mod.rs
Normal file
5
server/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod serve;
|
||||
mod init;
|
||||
|
||||
pub use self::serve::*;
|
||||
pub use self::init::*;
|
||||
99
server/src/commands/serve.rs
Normal file
99
server/src/commands/serve.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use rand;
|
||||
|
||||
use project::{Project, ProjectLoadError};
|
||||
use plugin::{PluginChain};
|
||||
use plugins::{DefaultPlugin, JsonModelPlugin, ScriptPlugin};
|
||||
use vfs::{VfsSession, VfsWatcher};
|
||||
use web;
|
||||
|
||||
pub fn serve(project_path: &PathBuf, verbose: bool, port: Option<u64>) {
|
||||
let server_id = rand::random::<u64>();
|
||||
|
||||
if verbose {
|
||||
println!("Attempting to locate project at {}...", project_path.display());
|
||||
}
|
||||
|
||||
let project = match Project::load(project_path) {
|
||||
Ok(v) => {
|
||||
println!("Using project from {}", project_path.display());
|
||||
v
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
ProjectLoadError::InvalidJson(serde_err) => {
|
||||
eprintln!(
|
||||
"Found invalid JSON!\nProject in: {}\nError: {}",
|
||||
project_path.display(),
|
||||
serde_err,
|
||||
);
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||
eprintln!("Found project file, but failed to read it!");
|
||||
eprintln!(
|
||||
"Check the permissions of the project file at\n{}",
|
||||
project_path.display(),
|
||||
);
|
||||
|
||||
process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
// Any other error is fine; use the default project.
|
||||
println!("Found no project file, using default project...");
|
||||
Project::default()
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let web_config = web::WebConfig {
|
||||
verbose,
|
||||
port: port.unwrap_or(project.serve_port),
|
||||
server_id,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref PLUGIN_CHAIN: PluginChain = PluginChain::new(vec![
|
||||
Box::new(ScriptPlugin::new()),
|
||||
Box::new(JsonModelPlugin::new()),
|
||||
Box::new(DefaultPlugin::new()),
|
||||
]);
|
||||
}
|
||||
|
||||
let vfs = {
|
||||
let mut vfs = VfsSession::new(&PLUGIN_CHAIN);
|
||||
|
||||
for (name, project_partition) in &project.partitions {
|
||||
let path = {
|
||||
let given_path = Path::new(&project_partition.path);
|
||||
|
||||
if given_path.is_absolute() {
|
||||
given_path.to_path_buf()
|
||||
} else {
|
||||
project_path.join(given_path)
|
||||
}
|
||||
};
|
||||
|
||||
vfs.insert_partition(name, path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
println!("Server listening on port {}", web_config.port);
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
web::start(web_config, project.clone(), &PLUGIN_CHAIN, vfs.clone());
|
||||
}
|
||||
1
server/src/core.rs
Normal file
1
server/src/core.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub type Route = Vec<String>;
|
||||
82
server/src/plugin.rs
Normal file
82
server/src/plugin.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
use core::Route;
|
||||
|
||||
pub enum TransformFileResult {
|
||||
Value(Option<RbxInstance>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum RbxChangeResult {
|
||||
Write(Option<VfsItem>),
|
||||
Pass,
|
||||
|
||||
// TODO: Error case
|
||||
}
|
||||
|
||||
pub enum FileChangeResult {
|
||||
MarkChanged(Option<Vec<Route>>),
|
||||
Pass,
|
||||
}
|
||||
|
||||
pub trait Plugin {
|
||||
/// Invoked when a file is read from the filesystem and needs to be turned
|
||||
/// into a Roblox instance.
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult;
|
||||
|
||||
/// Invoked when a Roblox Instance change is reported by the Roblox Studio
|
||||
/// plugin and needs to be turned into a file to save.
|
||||
fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> RbxChangeResult;
|
||||
|
||||
/// Invoked when a file changes on the filesystem. The result defines what
|
||||
/// routes are marked as needing to be refreshed.
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult;
|
||||
}
|
||||
|
||||
/// A set of plugins that are composed in order.
|
||||
pub struct PluginChain {
|
||||
plugins: Vec<Box<Plugin + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl PluginChain {
|
||||
pub fn new(plugins: Vec<Box<Plugin + Send + Sync>>) -> PluginChain {
|
||||
PluginChain {
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_file(&self, vfs_item: &VfsItem) -> Option<RbxInstance> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.transform_file(self, vfs_item) {
|
||||
TransformFileResult::Value(rbx_item) => return rbx_item,
|
||||
TransformFileResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn handle_rbx_change(&self, route: &Route, rbx_item: &RbxInstance) -> Option<VfsItem> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.handle_rbx_change(route, rbx_item) {
|
||||
RbxChangeResult::Write(vfs_item) => return vfs_item,
|
||||
RbxChangeResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn handle_file_change(&self, route: &Route) -> Option<Vec<Route>> {
|
||||
for plugin in &self.plugins {
|
||||
match plugin.handle_file_change(route) {
|
||||
FileChangeResult::MarkChanged(changes) => return changes,
|
||||
FileChangeResult::Pass => {},
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
67
server/src/plugins/default_plugin.rs
Normal file
67
server/src/plugins/default_plugin.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// A plugin with simple transforms:
|
||||
/// * Directories become Folder instances
|
||||
/// * Files become StringValue objects with 'Value' as their contents
|
||||
pub struct DefaultPlugin;
|
||||
|
||||
impl DefaultPlugin {
|
||||
pub fn new() -> DefaultPlugin {
|
||||
DefaultPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DefaultPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Value".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "StringValue".to_string(),
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let mut rbx_children = Vec::new();
|
||||
|
||||
for (_, child_item) in children {
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(rbx_item) => {
|
||||
rbx_children.push(rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: vfs_item.name().clone(),
|
||||
class_name: "*".to_string(),
|
||||
children: rbx_children,
|
||||
properties: HashMap::new(),
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
FileChangeResult::MarkChanged(Some(vec![route.clone()]))
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
55
server/src/plugins/json_model_plugin.rs
Normal file
55
server/src/plugins/json_model_plugin.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use regex::Regex;
|
||||
use serde_json;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::RbxInstance;
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref JSON_MODEL_PATTERN: Regex = Regex::new(r"^(.*?)\.model\.json$").unwrap();
|
||||
}
|
||||
|
||||
pub struct JsonModelPlugin;
|
||||
|
||||
impl JsonModelPlugin {
|
||||
pub fn new() -> JsonModelPlugin {
|
||||
JsonModelPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for JsonModelPlugin {
|
||||
fn transform_file(&self, _plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let rbx_name = match JSON_MODEL_PATTERN.captures(vfs_item.name()) {
|
||||
Some(captures) => captures.get(1).unwrap().as_str().to_string(),
|
||||
None => return TransformFileResult::Pass,
|
||||
};
|
||||
|
||||
let mut rbx_item: RbxInstance = match serde_json::from_str(contents) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("Unable to parse JSON Model File named {}: {}", vfs_item.name(), e);
|
||||
|
||||
return TransformFileResult::Pass; // This should be an error in the future!
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.route = Some(vfs_item.route().to_vec());
|
||||
rbx_item.name = rbx_name;
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
&VfsItem::Dir { .. } => TransformFileResult::Pass,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, _route: &Route) -> FileChangeResult {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
7
server/src/plugins/mod.rs
Normal file
7
server/src/plugins/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod default_plugin;
|
||||
mod script_plugin;
|
||||
mod json_model_plugin;
|
||||
|
||||
pub use self::default_plugin::*;
|
||||
pub use self::script_plugin::*;
|
||||
pub use self::json_model_plugin::*;
|
||||
124
server/src/plugins/script_plugin.rs
Normal file
124
server/src/plugins/script_plugin.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use core::Route;
|
||||
use plugin::{Plugin, PluginChain, TransformFileResult, RbxChangeResult, FileChangeResult};
|
||||
use rbx::{RbxInstance, RbxValue};
|
||||
use vfs::VfsItem;
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVER_PATTERN: Regex = Regex::new(r"^(.*?)\.server\.lua$").unwrap();
|
||||
static ref CLIENT_PATTERN: Regex = Regex::new(r"^(.*?)\.client\.lua$").unwrap();
|
||||
static ref MODULE_PATTERN: Regex = Regex::new(r"^(.*?)\.lua$").unwrap();
|
||||
}
|
||||
|
||||
static SERVER_INIT: &'static str = "init.server.lua";
|
||||
static CLIENT_INIT: &'static str = "init.client.lua";
|
||||
static MODULE_INIT: &'static str = "init.lua";
|
||||
|
||||
pub struct ScriptPlugin;
|
||||
|
||||
impl ScriptPlugin {
|
||||
pub fn new() -> ScriptPlugin {
|
||||
ScriptPlugin
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for ScriptPlugin {
|
||||
fn transform_file(&self, plugins: &PluginChain, vfs_item: &VfsItem) -> TransformFileResult {
|
||||
match vfs_item {
|
||||
&VfsItem::File { ref contents, .. } => {
|
||||
let name = vfs_item.name();
|
||||
|
||||
let (class_name, rbx_name) = {
|
||||
if let Some(captures) = SERVER_PATTERN.captures(name) {
|
||||
("Script".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = CLIENT_PATTERN.captures(name) {
|
||||
("LocalScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else if let Some(captures) = MODULE_PATTERN.captures(name) {
|
||||
("ModuleScript".to_string(), captures.get(1).unwrap().as_str().to_string())
|
||||
} else {
|
||||
return TransformFileResult::Pass;
|
||||
}
|
||||
};
|
||||
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert("Source".to_string(), RbxValue::String {
|
||||
value: contents.clone(),
|
||||
});
|
||||
|
||||
TransformFileResult::Value(Some(RbxInstance {
|
||||
name: rbx_name,
|
||||
class_name: class_name,
|
||||
children: Vec::new(),
|
||||
properties,
|
||||
route: Some(vfs_item.route().to_vec()),
|
||||
}))
|
||||
},
|
||||
&VfsItem::Dir { ref children, .. } => {
|
||||
let init_item = {
|
||||
let maybe_item = children.get(SERVER_INIT)
|
||||
.or(children.get(CLIENT_INIT))
|
||||
.or(children.get(MODULE_INIT));
|
||||
|
||||
match maybe_item {
|
||||
Some(v) => v,
|
||||
None => return TransformFileResult::Pass,
|
||||
}
|
||||
};
|
||||
|
||||
let mut rbx_item = match self.transform_file(plugins, init_item) {
|
||||
TransformFileResult::Value(Some(item)) => item,
|
||||
_ => {
|
||||
eprintln!("Inconsistency detected in ScriptPlugin!");
|
||||
return TransformFileResult::Pass;
|
||||
},
|
||||
};
|
||||
|
||||
rbx_item.name.clear();
|
||||
rbx_item.name.push_str(vfs_item.name());
|
||||
|
||||
for (child_name, child_item) in children {
|
||||
if child_name == init_item.name() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match plugins.transform_file(child_item) {
|
||||
Some(child_rbx_item) => {
|
||||
rbx_item.children.push(child_rbx_item);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
TransformFileResult::Value(Some(rbx_item))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_file_change(&self, route: &Route) -> FileChangeResult {
|
||||
let leaf = match route.last() {
|
||||
Some(v) => v,
|
||||
None => return FileChangeResult::Pass,
|
||||
};
|
||||
|
||||
let is_init = leaf == SERVER_INIT
|
||||
|| leaf == CLIENT_INIT
|
||||
|| leaf == MODULE_INIT;
|
||||
|
||||
if is_init {
|
||||
let mut changed = route.clone();
|
||||
changed.pop();
|
||||
|
||||
FileChangeResult::MarkChanged(Some(vec![changed]))
|
||||
} else {
|
||||
FileChangeResult::Pass
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_rbx_change(&self, _route: &Route, _rbx_item: &RbxInstance) -> RbxChangeResult {
|
||||
RbxChangeResult::Pass
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,19 @@ impl fmt::Display for ProjectInitError {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProjectPartition {
|
||||
/// A slash-separated path to a file or folder, relative to the project's
|
||||
/// directory.
|
||||
pub path: String,
|
||||
|
||||
/// A dot-separated route to a Roblox instance, relative to game.
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
/// Represents a project configured by a user for use with Rojo. Holds anything
|
||||
/// that can be configured with `rojo.json`.
|
||||
///
|
||||
/// In the future, this object will hold dependency information and other handy
|
||||
/// configurables
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Project {
|
||||
@@ -57,6 +66,7 @@ pub struct Project {
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Creates a new empty Project object with the given name.
|
||||
pub fn new<T: Into<String>>(name: T) -> Project {
|
||||
Project {
|
||||
name: name.into(),
|
||||
@@ -64,10 +74,12 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a new project inside the given folder path.
|
||||
pub fn init<T: AsRef<Path>>(location: T) -> Result<Project, ProjectInitError> {
|
||||
let location = location.as_ref();
|
||||
let package_path = location.join(PROJECT_FILENAME);
|
||||
|
||||
// We abort if the project file already exists.
|
||||
match fs::metadata(&package_path) {
|
||||
Ok(_) => return Err(ProjectInitError::AlreadyExists),
|
||||
Err(_) => {},
|
||||
@@ -78,11 +90,14 @@ impl Project {
|
||||
Err(_) => return Err(ProjectInitError::FailedToCreate),
|
||||
};
|
||||
|
||||
// Try to give the project a meaningful name.
|
||||
// If we can't, we'll just fall back to a default.
|
||||
let name = match location.file_name() {
|
||||
Some(v) => v.to_string_lossy().into_owned(),
|
||||
None => "new-project".to_string(),
|
||||
};
|
||||
|
||||
// Configure the project with all of the values we know so far.
|
||||
let project = Project::new(name);
|
||||
let serialized = serde_json::to_string_pretty(&project).unwrap();
|
||||
|
||||
@@ -94,6 +109,8 @@ impl Project {
|
||||
Ok(project)
|
||||
}
|
||||
|
||||
/// Attempts to load a project from the file named PROJECT_FILENAME from the
|
||||
/// given folder.
|
||||
pub fn load<T: AsRef<Path>>(location: T) -> Result<Project, ProjectLoadError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
@@ -120,6 +137,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the given project file to the given folder with the appropriate name.
|
||||
pub fn save<T: AsRef<Path>>(&self, location: T) -> Result<(), ProjectSaveError> {
|
||||
let package_path = location.as_ref().join(Path::new(PROJECT_FILENAME));
|
||||
|
||||
@@ -139,7 +157,7 @@ impl Project {
|
||||
impl Default for Project {
|
||||
fn default() -> Project {
|
||||
Project {
|
||||
name: "some-project".to_string(),
|
||||
name: "new-project".to_string(),
|
||||
serve_port: 8000,
|
||||
partitions: HashMap::new(),
|
||||
}
|
||||
34
server/src/rbx.rs
Normal file
34
server/src/rbx.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents data about a Roblox instance
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct RbxInstance {
|
||||
pub name: String,
|
||||
pub class_name: String,
|
||||
pub children: Vec<RbxInstance>,
|
||||
pub properties: HashMap<String, RbxValue>,
|
||||
|
||||
/// The route that this instance was generated from, if there was one.
|
||||
pub route: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Any kind value that can be used by Roblox
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase", tag = "Type")]
|
||||
pub enum RbxValue {
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
String {
|
||||
value: String,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Bool {
|
||||
value: bool,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
Number {
|
||||
value: f64,
|
||||
},
|
||||
|
||||
// TODO: Compound types like Vector3
|
||||
}
|
||||
7
server/src/vfs/mod.rs
Normal file
7
server/src/vfs/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod vfs_session;
|
||||
mod vfs_item;
|
||||
mod vfs_watcher;
|
||||
|
||||
pub use self::vfs_session::*;
|
||||
pub use self::vfs_item::*;
|
||||
pub use self::vfs_watcher::*;
|
||||
31
server/src/vfs/vfs_item.rs
Normal file
31
server/src/vfs/vfs_item.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A VfsItem represents either a file or directory as it came from the filesystem.
|
||||
///
|
||||
/// The interface here is intentionally simplified to make it easier to traverse
|
||||
/// files that have been read into memory.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum VfsItem {
|
||||
File {
|
||||
route: Vec<String>,
|
||||
contents: String,
|
||||
},
|
||||
Dir {
|
||||
route: Vec<String>,
|
||||
children: HashMap<String, VfsItem>,
|
||||
},
|
||||
}
|
||||
|
||||
impl VfsItem {
|
||||
pub fn name(&self) -> &String {
|
||||
self.route().last().unwrap()
|
||||
}
|
||||
|
||||
pub fn route(&self) -> &[String] {
|
||||
match self {
|
||||
&VfsItem::File { ref route, .. } => route,
|
||||
&VfsItem::Dir { ref route, .. } => route,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use core::Config;
|
||||
use plugin::PluginChain;
|
||||
use vfs::VfsItem;
|
||||
|
||||
/// Represents a virtual layer over multiple parts of the filesystem.
|
||||
///
|
||||
/// Paths in this system are represented as slices of strings, and are always
|
||||
/// relative to a partition, which is an absolute path into the real filesystem.
|
||||
pub struct Vfs {
|
||||
pub struct VfsSession {
|
||||
/// Contains all of the partitions mounted by the Vfs.
|
||||
///
|
||||
/// These must be absolute paths!
|
||||
pub partitions: HashMap<String, PathBuf>,
|
||||
|
||||
/// When the Vfs was initialized; used for change tracking.
|
||||
pub start_time: Instant,
|
||||
partitions: HashMap<String, PathBuf>,
|
||||
|
||||
/// A chronologically-sorted list of routes that changed since the Vfs was
|
||||
/// created, along with a timestamp denoting when.
|
||||
pub change_history: Vec<VfsChange>,
|
||||
change_history: Vec<VfsChange>,
|
||||
|
||||
config: Config,
|
||||
/// When the Vfs was initialized; used for change tracking.
|
||||
start_time: Instant,
|
||||
|
||||
plugin_chain: &'static PluginChain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -34,26 +34,31 @@ pub struct VfsChange {
|
||||
route: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type")]
|
||||
pub enum VfsItem {
|
||||
File { contents: String },
|
||||
Dir { children: HashMap<String, VfsItem> },
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
pub fn new(config: Config) -> Vfs {
|
||||
Vfs {
|
||||
impl VfsSession {
|
||||
pub fn new(plugin_chain: &'static PluginChain) -> VfsSession {
|
||||
VfsSession {
|
||||
partitions: HashMap::new(),
|
||||
start_time: Instant::now(),
|
||||
change_history: Vec::new(),
|
||||
config,
|
||||
plugin_chain,
|
||||
}
|
||||
}
|
||||
|
||||
fn route_to_path<R: Borrow<str>>(&self, route: &[R]) -> Option<PathBuf> {
|
||||
pub fn get_partitions(&self) -> &HashMap<String, PathBuf> {
|
||||
&self.partitions
|
||||
}
|
||||
|
||||
pub fn insert_partition<P: Into<PathBuf>>(&mut self, name: &str, path: P) {
|
||||
let path = path.into();
|
||||
|
||||
assert!(path.is_absolute());
|
||||
|
||||
self.partitions.insert(name.to_string(), path.into());
|
||||
}
|
||||
|
||||
fn route_to_path(&self, route: &[String]) -> Option<PathBuf> {
|
||||
let (partition_name, rest) = match route.split_first() {
|
||||
Some((first, rest)) => (first.borrow(), rest),
|
||||
Some((first, rest)) => (first, rest),
|
||||
None => return None,
|
||||
};
|
||||
|
||||
@@ -77,7 +82,8 @@ impl Vfs {
|
||||
Some(full_path)
|
||||
}
|
||||
|
||||
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<VfsItem, ()> {
|
||||
fn read_dir<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let reader = match fs::read_dir(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
@@ -92,11 +98,13 @@ impl Vfs {
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
match self.read_path(&path) {
|
||||
Ok(child_item) => {
|
||||
let name = path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
|
||||
let mut child_route = route.iter().cloned().collect::<Vec<_>>();
|
||||
child_route.push(name.clone());
|
||||
|
||||
match self.read_path(&child_route, &path) {
|
||||
Ok(child_item) => {
|
||||
children.insert(name, child_item);
|
||||
},
|
||||
Err(_) => {},
|
||||
@@ -104,11 +112,13 @@ impl Vfs {
|
||||
}
|
||||
|
||||
Ok(VfsItem::Dir {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<VfsItem, ()> {
|
||||
fn read_file<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
let mut file = match File::open(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return Err(()),
|
||||
@@ -122,11 +132,12 @@ impl Vfs {
|
||||
}
|
||||
|
||||
Ok(VfsItem::File {
|
||||
route: route.iter().cloned().collect::<Vec<_>>(),
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_path<P: AsRef<Path>>(&self, path: P) -> Result<VfsItem, ()> {
|
||||
fn read_path<P: AsRef<Path>>(&self, route: &[String], path: P) -> Result<VfsItem, ()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let metadata = match fs::metadata(path) {
|
||||
@@ -135,31 +146,38 @@ impl Vfs {
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
self.read_dir(path)
|
||||
self.read_dir(route, path)
|
||||
} else if metadata.is_file() {
|
||||
self.read_file(path)
|
||||
self.read_file(route, path)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current time, used for logging timestamps for file changes.
|
||||
pub fn current_time(&self) -> f64 {
|
||||
let elapsed = self.start_time.elapsed();
|
||||
|
||||
elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9
|
||||
}
|
||||
|
||||
/// Register a new change to the filesystem at the given timestamp and VFS
|
||||
/// route.
|
||||
pub fn add_change(&mut self, timestamp: f64, route: Vec<String>) {
|
||||
if self.config.verbose {
|
||||
println!("Added change {:?}", route);
|
||||
}
|
||||
|
||||
match self.plugin_chain.handle_file_change(&route) {
|
||||
Some(routes) => {
|
||||
for route in routes {
|
||||
self.change_history.push(VfsChange {
|
||||
timestamp,
|
||||
route,
|
||||
});
|
||||
}
|
||||
},
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect a list of changes that occured since the given timestamp.
|
||||
pub fn changes_since(&self, timestamp: f64) -> &[VfsChange] {
|
||||
let mut marker: Option<usize> = None;
|
||||
|
||||
@@ -178,18 +196,19 @@ impl Vfs {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R: Borrow<str>>(&self, route: &[R]) -> Result<VfsItem, ()> {
|
||||
/// Read an item from the filesystem using the given VFS route.
|
||||
pub fn read(&self, route: &[String]) -> Result<VfsItem, ()> {
|
||||
match self.route_to_path(route) {
|
||||
Some(path) => self.read_path(&path),
|
||||
Some(path) => self.read_path(route, &path),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<R: Borrow<str>>(&self, _route: &[R], _item: VfsItem) -> Result<(), ()> {
|
||||
pub fn write(&self, _route: &[String], _item: VfsItem) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn delete<R: Borrow<str>>(&self, _route: &[R]) -> Result<(), ()> {
|
||||
pub fn delete(&self, _route: &[String]) -> Result<(), ()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
108
server/src/vfs/vfs_watcher.rs
Normal file
108
server/src/vfs/vfs_watcher.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use pathext::path_to_route;
|
||||
use vfs::VfsSession;
|
||||
|
||||
/// An object that registers watchers on the real filesystem and relays those
|
||||
/// changes to the virtual filesystem layer.
|
||||
pub struct VfsWatcher {
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
}
|
||||
|
||||
impl VfsWatcher {
|
||||
pub fn new(vfs: Arc<Mutex<VfsSession>>) -> VfsWatcher {
|
||||
VfsWatcher {
|
||||
vfs,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_watcher(
|
||||
vfs: Arc<Mutex<VfsSession>>,
|
||||
rx: mpsc::Receiver<DebouncedEvent>,
|
||||
partition_name: String,
|
||||
root_path: PathBuf,
|
||||
) {
|
||||
loop {
|
||||
let event = rx.recv().unwrap();
|
||||
|
||||
let mut vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
match event {
|
||||
DebouncedEvent::Write(ref change_path) |
|
||||
DebouncedEvent::Create(ref change_path) |
|
||||
DebouncedEvent::Remove(ref change_path) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, change_path) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", change_path.display());
|
||||
}
|
||||
},
|
||||
DebouncedEvent::Rename(ref from_change, ref to_change) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, from_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", from_change.display());
|
||||
}
|
||||
|
||||
if let Some(mut route) = path_to_route(&root_path, to_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
eprintln!("Failed to get route from {}", to_change.display());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(self) {
|
||||
let mut watchers = Vec::new();
|
||||
|
||||
// Create an extra scope so that `vfs` gets dropped and unlocked
|
||||
{
|
||||
let vfs = self.vfs.lock().unwrap();
|
||||
|
||||
for (ref partition_name, ref root_path) in vfs.get_partitions() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))
|
||||
.expect("Unable to create watcher! This is a bug in Rojo.");
|
||||
|
||||
match watcher.watch(&root_path, RecursiveMode::Recursive) {
|
||||
Ok(_) => (),
|
||||
Err(_) => {
|
||||
panic!("Unable to watch partition {}, with path {}! Make sure that it's a file or directory.", partition_name, root_path.display());
|
||||
},
|
||||
}
|
||||
|
||||
watchers.push(watcher);
|
||||
|
||||
{
|
||||
let partition_name = partition_name.to_string();
|
||||
let root_path = root_path.to_path_buf();
|
||||
let vfs = self.vfs.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
Self::start_watcher(vfs, rx, partition_name, root_path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
}
|
||||
}
|
||||
225
server/src/web.rs
Normal file
225
server/src/web.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use std::io::Read;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
use project::Project;
|
||||
use vfs::{VfsSession, VfsChange};
|
||||
use rbx::RbxInstance;
|
||||
use plugin::PluginChain;
|
||||
|
||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1024; // 25 MiB
|
||||
|
||||
/// The set of configuration the web server needs to start.
|
||||
pub struct WebConfig {
|
||||
pub port: u64,
|
||||
pub verbose: bool,
|
||||
pub server_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ServerInfo<'a> {
|
||||
server_version: &'static str,
|
||||
protocol_version: u64,
|
||||
server_id: &'a str,
|
||||
project: &'a Project,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ReadResult<'a> {
|
||||
items: Vec<Option<RbxInstance>>,
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangesResult<'a> {
|
||||
changes: &'a [VfsChange],
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WriteSpecifier {
|
||||
route: String,
|
||||
item: RbxInstance,
|
||||
}
|
||||
|
||||
fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
||||
let data = serde_json::to_string(&value).unwrap();
|
||||
rouille::Response::from_data("application/json", data)
|
||||
}
|
||||
|
||||
/// Pulls text that may be JSON out of a Rouille Request object.
|
||||
///
|
||||
/// Doesn't do any actual parsing -- all this method does is verify the content
|
||||
/// type of the request and read the request's body.
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
// Bail out if the request body isn't marked as JSON
|
||||
match request.header("Content-Type") {
|
||||
Some(header) => if !header.starts_with("application/json") {
|
||||
return None;
|
||||
},
|
||||
None => return None,
|
||||
}
|
||||
|
||||
let body = match request.data() {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Allocate a buffer and read up to MAX_BODY_SIZE+1 bytes into it.
|
||||
let mut out = Vec::new();
|
||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64).read_to_end(&mut out) {
|
||||
Ok(_) => {},
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
// If the body was too big (MAX_BODY_SIZE+1), we abort instead of trying to
|
||||
// process it.
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = match String::from_utf8(out) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// Reads the body out of a Rouille Request and attempts to turn it into JSON.
|
||||
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let body = match read_json_text(&request) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let parsed = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
// TODO: Change return type to some sort of Result
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
/// Start the Rojo web server and park our current thread.
|
||||
pub fn start(config: WebConfig, project: Project, plugin_chain: &'static PluginChain, vfs: Arc<Mutex<VfsSession>>) {
|
||||
let address = format!("localhost:{}", config.port);
|
||||
|
||||
let server_id = config.server_id.to_string();
|
||||
|
||||
rouille::start_server(address, move |request| {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
// Get a summary of information about the server.
|
||||
|
||||
let current_time = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
vfs.current_time()
|
||||
};
|
||||
|
||||
json(ServerInfo {
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
protocol_version: 1,
|
||||
server_id: &server_id,
|
||||
project: &project,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(GET) (/changes/{ last_time: f64 }) => {
|
||||
// Get the list of changes since the given time.
|
||||
|
||||
let vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
let changes = vfs.changes_since(last_time);
|
||||
|
||||
json(ChangesResult {
|
||||
changes,
|
||||
server_id: &server_id,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/read) => {
|
||||
// Read some instances from the server according to a JSON
|
||||
// format body.
|
||||
|
||||
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
// Read the files off of the filesystem that the client
|
||||
// requested.
|
||||
let (items, current_time) = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for route in &read_request {
|
||||
match vfs.read(&route) {
|
||||
Ok(v) => items.push(Some(v)),
|
||||
Err(_) => items.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
(items, current_time)
|
||||
};
|
||||
|
||||
// Transform all of our VfsItem objects into Roblox instances
|
||||
// the client can use.
|
||||
let rbx_items = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
match *item {
|
||||
Some(ref item) => plugin_chain.transform_file(item),
|
||||
None => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if config.verbose {
|
||||
println!("Got read request: {:?}", read_request);
|
||||
println!("Responding with:\n\t{:?}", rbx_items);
|
||||
}
|
||||
|
||||
json(ReadResult {
|
||||
server_id: &server_id,
|
||||
items: rbx_items,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/write) => {
|
||||
// Not yet implemented.
|
||||
|
||||
let _write_request: Vec<WriteSpecifier> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
rouille::Response::empty_404()
|
||||
},
|
||||
|
||||
_ => rouille::Response::empty_404()
|
||||
)
|
||||
});
|
||||
}
|
||||
210
src/bin.rs
210
src/bin.rs
@@ -1,210 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate rouille;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
extern crate notify;
|
||||
extern crate rand;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
pub mod web;
|
||||
pub mod core;
|
||||
pub mod project;
|
||||
pub mod pathext;
|
||||
pub mod vfs;
|
||||
pub mod vfs_watch;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use core::Config;
|
||||
use pathext::canonicalish;
|
||||
use project::{Project, ProjectLoadError};
|
||||
use vfs::Vfs;
|
||||
use vfs_watch::VfsWatcher;
|
||||
|
||||
fn main() {
|
||||
let matches = clap_app!(rojo =>
|
||||
(version: env!("CARGO_PKG_VERSION"))
|
||||
(author: env!("CARGO_PKG_AUTHORS"))
|
||||
(about: env!("CARGO_PKG_DESCRIPTION"))
|
||||
|
||||
(@subcommand init =>
|
||||
(about: "Creates a new Rojo project")
|
||||
(@arg PATH: "Path to the place to create the project. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@subcommand serve =>
|
||||
(about: "Serves the project's files for use with the Rojo Studio plugin.")
|
||||
(@arg PROJECT: "Path to the project to serve. Defaults to the current directory.")
|
||||
(@arg port: --port +takes_value "The port to listen on. Defaults to 8000.")
|
||||
)
|
||||
|
||||
(@subcommand pack =>
|
||||
(about: "Packs the project into a GUI installer bundle. NOT YET IMPLEMENTED!")
|
||||
(@arg PROJECT: "Path to the project to pack. Defaults to the current directory.")
|
||||
)
|
||||
|
||||
(@arg verbose: --verbose "Enable extended logging.")
|
||||
).get_matches();
|
||||
|
||||
let verbose = match matches.occurrences_of("verbose") {
|
||||
0 => false,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
let server_id = rand::random::<u64>();
|
||||
|
||||
if verbose {
|
||||
println!("Server ID: {}", server_id);
|
||||
}
|
||||
|
||||
match matches.subcommand() {
|
||||
("init", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
let project_path = Path::new(sub_matches.value_of("PATH").unwrap_or("."));
|
||||
let full_path = canonicalish(project_path);
|
||||
|
||||
match Project::init(&full_path) {
|
||||
Ok(_) => {
|
||||
println!("Created new empty project at {}", full_path.display());
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create new project.\n{}", e);
|
||||
std::process::exit(1);
|
||||
},
|
||||
}
|
||||
},
|
||||
("serve", sub_matches) => {
|
||||
let sub_matches = sub_matches.unwrap();
|
||||
|
||||
let project_path = match sub_matches.value_of("PROJECT") {
|
||||
Some(v) => canonicalish(PathBuf::from(v)),
|
||||
None => std::env::current_dir().unwrap(),
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("Attempting to locate project at {}", project_path.display());
|
||||
}
|
||||
|
||||
let project = match Project::load(&project_path) {
|
||||
Ok(v) => {
|
||||
println!("Using project from {}", project_path.display());
|
||||
v
|
||||
},
|
||||
Err(err) => {
|
||||
match err {
|
||||
ProjectLoadError::InvalidJson(serde_err) => {
|
||||
eprintln!(
|
||||
"Found invalid JSON!\nProject in: {}\nError: {}",
|
||||
project_path.display(),
|
||||
serde_err,
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
},
|
||||
ProjectLoadError::FailedToOpen | ProjectLoadError::FailedToRead => {
|
||||
eprintln!("Found project file, but failed to read it!");
|
||||
eprintln!(
|
||||
"Check the permissions of the project file at\n{}",
|
||||
project_path.display(),
|
||||
);
|
||||
|
||||
std::process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
// Any other error is fine; use the default project.
|
||||
},
|
||||
}
|
||||
|
||||
println!("Found no project file, using default project...");
|
||||
Project::default()
|
||||
},
|
||||
};
|
||||
|
||||
let port = {
|
||||
match sub_matches.value_of("port") {
|
||||
Some(source) => match source.parse::<u64>() {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid port '{}'", source);
|
||||
std::process::exit(1);
|
||||
},
|
||||
},
|
||||
None => project.serve_port,
|
||||
}
|
||||
};
|
||||
|
||||
let config = Config {
|
||||
port,
|
||||
verbose,
|
||||
server_id,
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("Loading VFS...");
|
||||
}
|
||||
|
||||
let vfs = {
|
||||
let mut vfs = Vfs::new(config.clone());
|
||||
|
||||
for (name, project_partition) in &project.partitions {
|
||||
let path = {
|
||||
let given_path = Path::new(&project_partition.path);
|
||||
|
||||
if given_path.is_absolute() {
|
||||
given_path.to_path_buf()
|
||||
} else {
|
||||
project_path.join(given_path)
|
||||
}
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!(
|
||||
"Partition '{}': {} @ {}",
|
||||
name,
|
||||
project_partition.target,
|
||||
project_partition.path
|
||||
);
|
||||
}
|
||||
|
||||
vfs.partitions.insert(name.clone(), path);
|
||||
}
|
||||
|
||||
Arc::new(Mutex::new(vfs))
|
||||
};
|
||||
|
||||
{
|
||||
let vfs = vfs.clone();
|
||||
let config = config.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
VfsWatcher::new(config, vfs).start();
|
||||
});
|
||||
}
|
||||
|
||||
web::start(config.clone(), project.clone(), vfs.clone());
|
||||
|
||||
println!("Server listening on port {}", port);
|
||||
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
},
|
||||
("pack", _) => {
|
||||
eprintln!("'rojo pack' is not yet implemented!");
|
||||
std::process::exit(1);
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Please specify a subcommand!");
|
||||
eprintln!("Try 'rojo help' for information.");
|
||||
std::process::exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub port: u64,
|
||||
pub verbose: bool,
|
||||
pub server_id: u64,
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use core::Config;
|
||||
use pathext::path_to_route;
|
||||
use vfs::Vfs;
|
||||
|
||||
pub struct VfsWatcher {
|
||||
vfs: Arc<Mutex<Vfs>>,
|
||||
watchers: Vec<RecommendedWatcher>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl VfsWatcher {
|
||||
pub fn new(config: Config, vfs: Arc<Mutex<Vfs>>) -> VfsWatcher {
|
||||
VfsWatcher {
|
||||
vfs,
|
||||
watchers: Vec::new(),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(mut self) {
|
||||
{
|
||||
let outer_vfs = self.vfs.lock().unwrap();
|
||||
|
||||
for (partition_name, root_path) in &outer_vfs.partitions {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let partition_name = partition_name.clone();
|
||||
let root_path = root_path.clone();
|
||||
|
||||
let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))
|
||||
.expect("Unable to create watcher!");
|
||||
|
||||
watcher
|
||||
.watch(&root_path, RecursiveMode::Recursive)
|
||||
.expect("Unable to watch path!");
|
||||
|
||||
self.watchers.push(watcher);
|
||||
|
||||
{
|
||||
let vfs = self.vfs.clone();
|
||||
let config = self.config.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let event = rx.recv().unwrap();
|
||||
let mut vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
if config.verbose {
|
||||
println!("FS event {:?}", event);
|
||||
}
|
||||
|
||||
match event {
|
||||
DebouncedEvent::Write(ref change_path) |
|
||||
DebouncedEvent::Create(ref change_path) |
|
||||
DebouncedEvent::Remove(ref change_path) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, change_path) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
println!("Failed to get route from {}", change_path.display());
|
||||
}
|
||||
},
|
||||
DebouncedEvent::Rename(ref from_change, ref to_change) => {
|
||||
if let Some(mut route) = path_to_route(&root_path, from_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
println!("Failed to get route from {}", from_change.display());
|
||||
}
|
||||
|
||||
if let Some(mut route) = path_to_route(&root_path, to_change) {
|
||||
route.insert(0, partition_name.clone());
|
||||
|
||||
vfs.add_change(current_time, route);
|
||||
} else {
|
||||
println!("Failed to get route from {}", to_change.display());
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
}
|
||||
}
|
||||
170
src/web.rs
170
src/web.rs
@@ -1,170 +0,0 @@
|
||||
use std::io::Read;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use rouille;
|
||||
use serde;
|
||||
use serde_json;
|
||||
|
||||
use core::Config;
|
||||
use project::Project;
|
||||
use vfs::{Vfs, VfsChange, VfsItem};
|
||||
|
||||
static MAX_BODY_SIZE: usize = 25 * 1024 * 1025; // 25 MiB
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ServerInfo<'a> {
|
||||
server_version: &'static str,
|
||||
protocol_version: u64,
|
||||
server_id: &'a str,
|
||||
project: &'a Project,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ReadResult<'a> {
|
||||
items: Vec<Option<VfsItem>>,
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ChangesResult<'a> {
|
||||
changes: &'a [VfsChange],
|
||||
server_id: &'a str,
|
||||
current_time: f64,
|
||||
}
|
||||
|
||||
fn json<T: serde::Serialize>(value: T) -> rouille::Response {
|
||||
let data = serde_json::to_string(&value).unwrap();
|
||||
rouille::Response::from_data("application/json", data)
|
||||
}
|
||||
|
||||
fn read_json_text(request: &rouille::Request) -> Option<String> {
|
||||
match request.header("Content-Type") {
|
||||
Some(header) => if !header.starts_with("application/json") {
|
||||
return None;
|
||||
},
|
||||
None => return None,
|
||||
}
|
||||
|
||||
let body = match request.data() {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let mut out = Vec::new();
|
||||
match body.take(MAX_BODY_SIZE.saturating_add(1) as u64)
|
||||
.read_to_end(&mut out)
|
||||
{
|
||||
Ok(_) => {},
|
||||
Err(_) => return None,
|
||||
}
|
||||
|
||||
if out.len() > MAX_BODY_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parsed = match String::from_utf8(out) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
fn read_json<T>(request: &rouille::Request) -> Option<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let body = match read_json_text(&request) {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let parsed = match serde_json::from_str(&body) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
pub fn start(config: Config, project: Project, vfs: Arc<Mutex<Vfs>>) {
|
||||
let address = format!("localhost:{}", config.port);
|
||||
|
||||
let server_id = config.server_id.to_string();
|
||||
|
||||
thread::spawn(move || {
|
||||
rouille::start_server(address, move |request| {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
let current_time = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
vfs.current_time()
|
||||
};
|
||||
|
||||
json(ServerInfo {
|
||||
server_version: env!("CARGO_PKG_VERSION"),
|
||||
protocol_version: 0,
|
||||
server_id: &server_id,
|
||||
project: &project,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(GET) (/changes/{ last_time: f64 }) => {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
let current_time = vfs.current_time();
|
||||
let changes = vfs.changes_since(last_time);
|
||||
|
||||
json(ChangesResult {
|
||||
changes,
|
||||
server_id: &server_id,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/read) => {
|
||||
let read_request: Vec<Vec<String>> = match read_json(&request) {
|
||||
Some(v) => v,
|
||||
None => return rouille::Response::empty_400(),
|
||||
};
|
||||
|
||||
let (items, current_time) = {
|
||||
let vfs = vfs.lock().unwrap();
|
||||
|
||||
let current_time = vfs.current_time();
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for route in &read_request {
|
||||
match vfs.read(&route) {
|
||||
Ok(v) => items.push(Some(v)),
|
||||
Err(_) => items.push(None),
|
||||
}
|
||||
}
|
||||
|
||||
(items, current_time)
|
||||
};
|
||||
|
||||
json(ReadResult {
|
||||
server_id: &server_id,
|
||||
items,
|
||||
current_time,
|
||||
})
|
||||
},
|
||||
|
||||
(POST) (/write) => {
|
||||
rouille::Response::empty_404()
|
||||
},
|
||||
|
||||
_ => rouille::Response::empty_404()
|
||||
)
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"partitions": {
|
||||
"src": {
|
||||
"path": "src",
|
||||
"target": "ReplicatedStorage.TestProject"
|
||||
"target": "ReplicatedFirst"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
test-project/src/hello.model.json
Normal file
24
test-project/src/hello.model.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"Name": "hello",
|
||||
"ClassName": "Model",
|
||||
"Children": [
|
||||
{
|
||||
"Name": "Some Part",
|
||||
"ClassName": "Part",
|
||||
"Children": [],
|
||||
"Properties": {}
|
||||
},
|
||||
{
|
||||
"Name": "Some StringValue",
|
||||
"ClassName": "StringValue",
|
||||
"Children": [],
|
||||
"Properties": {
|
||||
"Value": {
|
||||
"Type": "String",
|
||||
"Value": "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Properties": {}
|
||||
}
|
||||
Reference in New Issue
Block a user