mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-24 06:35:39 +00:00
Rewrite Project, remove SourceProject (#274)
* Rewrite project file to have relative paths and drop SourceProject * Redo project error types * Tidy up and document Project type * Strip out init command
This commit is contained in:
committed by
GitHub
parent
47c7f63d75
commit
1f7f2b22e7
28
Cargo.lock
generated
28
Cargo.lock
generated
@@ -403,6 +403,11 @@ dependencies = [
|
|||||||
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dtoa"
|
name = "dtoa"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -1634,6 +1639,7 @@ dependencies = [
|
|||||||
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"serde_yaml 0.8.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -1847,6 +1853,25 @@ name = "smallvec"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snax"
|
name = "snax"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2433,6 +2458,7 @@ dependencies = [
|
|||||||
"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
||||||
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||||
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||||
|
"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97"
|
||||||
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
|
"checksum dtoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e"
|
||||||
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
||||||
"checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
"checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
@@ -2586,6 +2612,8 @@ dependencies = [
|
|||||||
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||||
"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
|
"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
|
||||||
"checksum smallvec 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecf3b85f68e8abaa7555aa5abdb1153079387e60b718283d732f03897fcfc86"
|
"checksum smallvec 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ecf3b85f68e8abaa7555aa5abdb1153079387e60b718283d732f03897fcfc86"
|
||||||
|
"checksum snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41207ca11f96a62cd34e6b7fdf73d322b25ae3848eb9d38302169724bb32cf27"
|
||||||
|
"checksum snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5e338c8b0577457c9dda8e794b6ad7231c96e25b1b0dd5842d52249020c1c0"
|
||||||
"checksum snax 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ef82be0baf3ae45701ab992230f1f4889c15984736d87f37558aea3e4e321af"
|
"checksum snax 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7ef82be0baf3ae45701ab992230f1f4889c15984736d87f37558aea3e4e321af"
|
||||||
"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
|
"checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d"
|
||||||
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ ritz = "0.1.0"
|
|||||||
rlua = "0.16.3"
|
rlua = "0.16.3"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
snafu = "0.6.0"
|
||||||
structopt = "0.3"
|
structopt = "0.3"
|
||||||
termcolor = "1.0.5"
|
termcolor = "1.0.5"
|
||||||
uuid = { version = "0.7", features = ["v4", "serde"] }
|
uuid = { version = "0.7", features = ["v4", "serde"] }
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ expression: contents
|
|||||||
<R22>1</R22>
|
<R22>1</R22>
|
||||||
</CoordinateFrame>
|
</CoordinateFrame>
|
||||||
<Ref name="PrimaryPart">null</Ref>
|
<Ref name="PrimaryPart">null</Ref>
|
||||||
<BinaryString name="Tags"><![CDATA[]]></BinaryString>
|
<BinaryString name="Tags">
|
||||||
|
</BinaryString>
|
||||||
</Properties>
|
</Properties>
|
||||||
<Item class="StringValue" referent="2">
|
<Item class="StringValue" referent="2">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">Cool StringValue</string>
|
<string name="Name">Cool StringValue</string>
|
||||||
<BinaryString name="Tags"><![CDATA[]]></BinaryString>
|
<BinaryString name="Tags">
|
||||||
|
</BinaryString>
|
||||||
<string name="Value">Did you know that BaseValue.Changed is different than Instance.Changed?</string>
|
<string name="Value">Did you know that BaseValue.Changed is different than Instance.Changed?</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -6,19 +6,22 @@ expression: contents
|
|||||||
<Item class="Folder" referent="0">
|
<Item class="Folder" referent="0">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">rbxmx_ref</string>
|
<string name="Name">rbxmx_ref</string>
|
||||||
<BinaryString name="Tags"><![CDATA[]]></BinaryString>
|
<BinaryString name="Tags">
|
||||||
|
</BinaryString>
|
||||||
</Properties>
|
</Properties>
|
||||||
<Item class="StringValue" referent="1">
|
<Item class="StringValue" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">Target</string>
|
<string name="Name">Target</string>
|
||||||
<BinaryString name="Tags"><![CDATA[]]></BinaryString>
|
<BinaryString name="Tags">
|
||||||
|
</BinaryString>
|
||||||
<string name="Value">Pointed to by ObjectValue</string>
|
<string name="Value">Pointed to by ObjectValue</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
<Item class="ObjectValue" referent="2">
|
<Item class="ObjectValue" referent="2">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">Pointer</string>
|
<string name="Name">Pointer</string>
|
||||||
<BinaryString name="Tags"><![CDATA[]]></BinaryString>
|
<BinaryString name="Tags">
|
||||||
|
</BinaryString>
|
||||||
<Ref name="Value">1</Ref>
|
<Ref name="Value">1</Ref>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|||||||
@@ -178,15 +178,20 @@ fn update_affected_instances<F: VfsFetcher>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InstigatingSource::ProjectNode(instance_name, project_node) => {
|
InstigatingSource::ProjectNode(project_path, instance_name, project_node) => {
|
||||||
// This instance is the direct subject of a project node. Since
|
// This instance is the direct subject of a project node. Since
|
||||||
// there might be information associated with our instance from
|
// there might be information associated with our instance from
|
||||||
// the project file, we snapshot the entire project node again.
|
// the project file, we snapshot the entire project node again.
|
||||||
|
|
||||||
let snapshot =
|
let snapshot = snapshot_project_node(
|
||||||
snapshot_project_node(&metadata.context, instance_name, project_node, &vfs)
|
&metadata.context,
|
||||||
.expect("snapshot failed")
|
&project_path,
|
||||||
.expect("snapshot did not return an instance");
|
instance_name,
|
||||||
|
project_node,
|
||||||
|
&vfs,
|
||||||
|
)
|
||||||
|
.expect("snapshot failed")
|
||||||
|
.expect("snapshot did not return an instance");
|
||||||
|
|
||||||
let patch_set = compute_patch_set(&snapshot, &tree, id);
|
let patch_set = compute_patch_set(&snapshot, &tree, id);
|
||||||
apply_patch_set(tree, patch_set)
|
apply_patch_set(tree, patch_set)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use failure::Fail;
|
|||||||
use crate::{
|
use crate::{
|
||||||
cli::BuildCommand,
|
cli::BuildCommand,
|
||||||
common_setup,
|
common_setup,
|
||||||
project::ProjectLoadError,
|
project::ProjectError,
|
||||||
vfs::{FsError, RealFetcher, Vfs, WatchMode},
|
vfs::{FsError, RealFetcher, Vfs, WatchMode},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ pub enum BuildError {
|
|||||||
BinaryModelEncodeError(rbx_binary::EncodeError),
|
BinaryModelEncodeError(rbx_binary::EncodeError),
|
||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
#[fail(display = "{}", _0)]
|
||||||
ProjectLoadError(#[fail(cause)] ProjectLoadError),
|
ProjectError(#[fail(cause)] ProjectError),
|
||||||
|
|
||||||
#[fail(display = "{}", _0)]
|
#[fail(display = "{}", _0)]
|
||||||
FsError(#[fail(cause)] FsError),
|
FsError(#[fail(cause)] FsError),
|
||||||
@@ -57,7 +57,7 @@ impl_from!(BuildError {
|
|||||||
io::Error => IoError,
|
io::Error => IoError,
|
||||||
rbx_xml::EncodeError => XmlModelEncodeError,
|
rbx_xml::EncodeError => XmlModelEncodeError,
|
||||||
rbx_binary::EncodeError => BinaryModelEncodeError,
|
rbx_binary::EncodeError => BinaryModelEncodeError,
|
||||||
ProjectLoadError => ProjectLoadError,
|
ProjectError => ProjectError,
|
||||||
FsError => FsError,
|
FsError => FsError,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,17 @@
|
|||||||
use failure::Fail;
|
use failure::Fail;
|
||||||
|
|
||||||
use crate::{
|
use crate::{cli::InitCommand, project::ProjectError};
|
||||||
cli::{InitCommand, InitKind},
|
|
||||||
project::{Project, ProjectInitError},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[fail(display = "Project init error: {}", _0)]
|
#[fail(display = "Project init error: {}", _0)]
|
||||||
ProjectInitError(#[fail(cause)] ProjectInitError),
|
ProjectError(#[fail(cause)] ProjectError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(InitError {
|
impl_from!(InitError {
|
||||||
ProjectInitError => ProjectInitError,
|
ProjectError => ProjectError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn init(options: InitCommand) -> Result<(), InitError> {
|
pub fn init(_options: InitCommand) -> Result<(), InitError> {
|
||||||
let (project_path, project_kind) = match options.kind {
|
unimplemented!("init command");
|
||||||
InitKind::Place => {
|
|
||||||
let path = Project::init_place(&options.path)?;
|
|
||||||
(path, "place")
|
|
||||||
}
|
|
||||||
InitKind::Model => {
|
|
||||||
let path = Project::init_model(&options.path)?;
|
|
||||||
(path, "model")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Created new {} project file at {}",
|
|
||||||
project_kind,
|
|
||||||
project_path.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::ServeCommand,
|
cli::ServeCommand,
|
||||||
project::ProjectLoadError,
|
project::ProjectError,
|
||||||
serve_session::ServeSession,
|
serve_session::ServeSession,
|
||||||
vfs::{RealFetcher, Vfs, WatchMode},
|
vfs::{RealFetcher, Vfs, WatchMode},
|
||||||
web::LiveServer,
|
web::LiveServer,
|
||||||
@@ -19,11 +19,11 @@ const DEFAULT_PORT: u16 = 34872;
|
|||||||
#[derive(Debug, Fail)]
|
#[derive(Debug, Fail)]
|
||||||
pub enum ServeError {
|
pub enum ServeError {
|
||||||
#[fail(display = "Couldn't load project: {}", _0)]
|
#[fail(display = "Couldn't load project: {}", _0)]
|
||||||
ProjectLoad(#[fail(cause)] ProjectLoadError),
|
ProjectError(#[fail(cause)] ProjectError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_from!(ServeError {
|
impl_from!(ServeError {
|
||||||
ProjectLoadError => ProjectLoad,
|
ProjectError => ProjectError,
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
|
pub fn serve(options: ServeCommand) -> Result<(), ServeError> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::path::Path;
|
|||||||
use rbx_dom_weak::RbxInstanceProperties;
|
use rbx_dom_weak::RbxInstanceProperties;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
project::{Project, ProjectLoadError},
|
project::Project,
|
||||||
snapshot::{
|
snapshot::{
|
||||||
apply_patch_set, compute_patch_set, InstanceContext, InstancePropertiesWithMeta, RojoTree,
|
apply_patch_set, compute_patch_set, InstanceContext, InstancePropertiesWithMeta, RojoTree,
|
||||||
},
|
},
|
||||||
@@ -19,11 +19,7 @@ pub fn start<F: VfsFetcher>(
|
|||||||
vfs: &Vfs<F>,
|
vfs: &Vfs<F>,
|
||||||
) -> (Option<Project>, RojoTree) {
|
) -> (Option<Project>, RojoTree) {
|
||||||
log::trace!("Loading project file from {}", fuzzy_project_path.display());
|
log::trace!("Loading project file from {}", fuzzy_project_path.display());
|
||||||
let maybe_project = match Project::load_fuzzy(fuzzy_project_path) {
|
let maybe_project = Project::load_fuzzy(fuzzy_project_path).expect("TODO: Project load failed");
|
||||||
Ok(project) => Some(project),
|
|
||||||
Err(ProjectLoadError::NotFound) => None,
|
|
||||||
Err(other) => panic!("{}", other), // TODO: return error upward
|
|
||||||
};
|
|
||||||
|
|
||||||
log::trace!("Constructing initial tree");
|
log::trace!("Constructing initial tree");
|
||||||
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
let mut tree = RojoTree::new(InstancePropertiesWithMeta {
|
||||||
@@ -37,23 +33,13 @@ pub fn start<F: VfsFetcher>(
|
|||||||
|
|
||||||
let root_id = tree.get_root_id();
|
let root_id = tree.get_root_id();
|
||||||
|
|
||||||
log::trace!("Constructing snapshot context");
|
|
||||||
let snapshot_context = InstanceContext::default();
|
|
||||||
if let Some(project) = &maybe_project {
|
|
||||||
// If the project file defines no plugins, then there's no need to
|
|
||||||
// initialize the snapshot plugin context.
|
|
||||||
if !project.plugins.is_empty() {
|
|
||||||
// TODO: Initialize plugins in instance context
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("Reading project root");
|
log::trace!("Reading project root");
|
||||||
let entry = vfs
|
let entry = vfs
|
||||||
.get(fuzzy_project_path)
|
.get(fuzzy_project_path)
|
||||||
.expect("could not get project path");
|
.expect("could not get project path");
|
||||||
|
|
||||||
log::trace!("Generating snapshot of instances from VFS");
|
log::trace!("Generating snapshot of instances from VFS");
|
||||||
let snapshot = snapshot_from_vfs(&snapshot_context, vfs, &entry)
|
let snapshot = snapshot_from_vfs(&InstanceContext::default(), vfs, &entry)
|
||||||
.expect("snapshot failed")
|
.expect("snapshot failed")
|
||||||
.expect("snapshot did not return an instance");
|
.expect("snapshot did not return an instance");
|
||||||
|
|
||||||
|
|||||||
579
src/project.rs
579
src/project.rs
@@ -1,339 +1,63 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
fmt,
|
fs, io,
|
||||||
fs::{self, File},
|
|
||||||
io,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use failure::Fail;
|
use rbx_dom_weak::UnresolvedRbxValue;
|
||||||
use log::warn;
|
use serde::{Deserialize, Serialize};
|
||||||
use rbx_dom_weak::{RbxValue, UnresolvedRbxValue};
|
use snafu::{ResultExt, Snafu};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
|
|
||||||
static DEFAULT_PLACE: &str = include_str!("../assets/place.project.json");
|
|
||||||
|
|
||||||
pub static PROJECT_FILENAME: &str = "default.project.json";
|
pub static PROJECT_FILENAME: &str = "default.project.json";
|
||||||
|
|
||||||
/// SourceProject is the format that users author projects on-disk. Since we
|
/// Error type returned by any function that handles projects.
|
||||||
/// want to do things like transforming paths to be absolute before handing them
|
#[derive(Debug, Snafu)]
|
||||||
/// off to the rest of Rojo, we use this intermediate struct.
|
pub struct ProjectError(Error);
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
||||||
struct SourceProject {
|
|
||||||
name: String,
|
|
||||||
tree: SourceProjectNode,
|
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[derive(Debug, Snafu)]
|
||||||
serve_port: Option<u16>,
|
enum Error {
|
||||||
|
/// A general IO error occurred.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
Io { source: io::Error, path: PathBuf },
|
||||||
serve_place_ids: Option<HashSet<u64>>,
|
|
||||||
|
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
||||||
#[cfg_attr(not(feature = "user-plugins"), serde(skip_deserializing))]
|
|
||||||
plugins: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceProject {
|
|
||||||
/// Consumes the SourceProject and yields a Project, ready for prime-time.
|
|
||||||
pub fn into_project(self, project_file_location: &Path) -> Project {
|
|
||||||
let tree = self.tree.into_project_node(project_file_location);
|
|
||||||
|
|
||||||
let project_folder = project_file_location.parent().unwrap();
|
|
||||||
let plugins = self
|
|
||||||
.plugins
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| project_folder.join(path))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Project {
|
|
||||||
name: self.name,
|
|
||||||
tree,
|
|
||||||
serve_port: self.serve_port,
|
|
||||||
serve_place_ids: self.serve_place_ids,
|
|
||||||
plugins,
|
|
||||||
file_location: PathBuf::from(project_file_location),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An alternative serializer for `UnresolvedRbxValue` that uses the minimum
|
|
||||||
/// representation of the value.
|
|
||||||
///
|
|
||||||
/// For example, the default Serialize impl might give you:
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// {
|
|
||||||
/// "Type": "Bool",
|
|
||||||
/// "Value": true
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// But in reality, users are expected to write just:
|
|
||||||
///
|
|
||||||
/// ```json
|
|
||||||
/// true
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// This holds true for other values that might be ambiguous or just have more
|
|
||||||
/// complicated representations like enums.
|
|
||||||
fn serialize_unresolved_minimal<S>(
|
|
||||||
unresolved: &UnresolvedRbxValue,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match unresolved {
|
|
||||||
UnresolvedRbxValue::Ambiguous(_) => unresolved.serialize(serializer),
|
|
||||||
UnresolvedRbxValue::Concrete(concrete) => match concrete {
|
|
||||||
RbxValue::Bool { value } => value.serialize(serializer),
|
|
||||||
RbxValue::CFrame { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Color3 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Color3uint8 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Content { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Float32 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Int32 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::String { value } => value.serialize(serializer),
|
|
||||||
RbxValue::UDim { value } => value.serialize(serializer),
|
|
||||||
RbxValue::UDim2 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Vector2 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Vector2int16 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Vector3 { value } => value.serialize(serializer),
|
|
||||||
RbxValue::Vector3int16 { value } => value.serialize(serializer),
|
|
||||||
_ => concrete.serialize(serializer),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper around serialize_unresolved_minimal that handles the HashMap case.
|
|
||||||
fn serialize_unresolved_map<S>(
|
|
||||||
value: &HashMap<String, UnresolvedRbxValue>,
|
|
||||||
serializer: S,
|
|
||||||
) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
use serde::ser::SerializeMap;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Minimal<'a>(
|
|
||||||
#[serde(serialize_with = "serialize_unresolved_minimal")] &'a UnresolvedRbxValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut map = serializer.serialize_map(Some(value.len()))?;
|
|
||||||
for (k, v) in value {
|
|
||||||
map.serialize_key(k)?;
|
|
||||||
map.serialize_value(&Minimal(v))?;
|
|
||||||
}
|
|
||||||
map.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Similar to SourceProject, the structure of nodes in the project tree is
|
|
||||||
/// slightly different on-disk than how we want to handle them in the rest of
|
|
||||||
/// Rojo.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
struct SourceProjectNode {
|
|
||||||
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
|
|
||||||
class_name: Option<String>,
|
|
||||||
|
|
||||||
#[serde(
|
|
||||||
rename = "$properties",
|
|
||||||
default = "HashMap::new",
|
|
||||||
skip_serializing_if = "HashMap::is_empty",
|
|
||||||
serialize_with = "serialize_unresolved_map"
|
|
||||||
)]
|
|
||||||
properties: HashMap<String, UnresolvedRbxValue>,
|
|
||||||
|
|
||||||
#[serde(
|
|
||||||
rename = "$ignoreUnknownInstances",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
ignore_unknown_instances: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(rename = "$path", skip_serializing_if = "Option::is_none")]
|
|
||||||
path: Option<String>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
children: BTreeMap<String, SourceProjectNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceProjectNode {
|
|
||||||
/// Consumes the SourceProjectNode and turns it into a ProjectNode.
|
|
||||||
pub fn into_project_node(self, project_file_location: &Path) -> ProjectNode {
|
|
||||||
let children = self
|
|
||||||
.children
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| {
|
|
||||||
(
|
|
||||||
key.clone(),
|
|
||||||
value.clone().into_project_node(project_file_location),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Make sure that paths are absolute, transforming them by adding the
|
|
||||||
// project folder if they're not already absolute.
|
|
||||||
let path = self.path.as_ref().map(|source_path| {
|
|
||||||
if Path::new(source_path).is_absolute() {
|
|
||||||
PathBuf::from(source_path)
|
|
||||||
} else {
|
|
||||||
let project_folder_location = project_file_location.parent().unwrap();
|
|
||||||
project_folder_location.join(source_path)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ProjectNode {
|
|
||||||
class_name: self.class_name,
|
|
||||||
properties: self.properties,
|
|
||||||
ignore_unknown_instances: self.ignore_unknown_instances,
|
|
||||||
path,
|
|
||||||
children,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum ProjectLoadError {
|
|
||||||
NotFound,
|
|
||||||
|
|
||||||
Io {
|
|
||||||
#[fail(cause)]
|
|
||||||
inner: io::Error,
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
/// An error with JSON parsing occurred.
|
||||||
Json {
|
Json {
|
||||||
#[fail(cause)]
|
source: serde_json::Error,
|
||||||
inner: serde_json::Error,
|
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ProjectLoadError {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
use self::ProjectLoadError::*;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
NotFound => write!(formatter, "Project file not found"),
|
|
||||||
Io { inner, path } => {
|
|
||||||
write!(formatter, "I/O error: {} in path {}", inner, path.display())
|
|
||||||
}
|
|
||||||
Json { inner, path } => write!(
|
|
||||||
formatter,
|
|
||||||
"JSON error: {} in path {}",
|
|
||||||
inner,
|
|
||||||
path.display()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error returned by Project::init_place and Project::init_model
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum ProjectInitError {
|
|
||||||
AlreadyExists(PathBuf),
|
|
||||||
IoError(#[fail(cause)] io::Error),
|
|
||||||
SaveError(#[fail(cause)] ProjectSaveError),
|
|
||||||
JsonError(#[fail(cause)] serde_json::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ProjectInitError {
|
|
||||||
fn fmt(&self, output: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
ProjectInitError::AlreadyExists(path) => {
|
|
||||||
write!(output, "Path {} already exists", path.display())
|
|
||||||
}
|
|
||||||
ProjectInitError::IoError(inner) => write!(output, "IO error: {}", inner),
|
|
||||||
ProjectInitError::SaveError(inner) => write!(output, "{}", inner),
|
|
||||||
ProjectInitError::JsonError(inner) => write!(output, "{}", inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error returned by Project::save
|
|
||||||
#[derive(Debug, Fail)]
|
|
||||||
pub enum ProjectSaveError {
|
|
||||||
#[fail(display = "JSON error: {}", _0)]
|
|
||||||
JsonError(#[fail(cause)] serde_json::Error),
|
|
||||||
|
|
||||||
#[fail(display = "IO error: {}", _0)]
|
|
||||||
IoError(#[fail(cause)] io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
|
||||||
pub struct ProjectNode {
|
|
||||||
pub class_name: Option<String>,
|
|
||||||
pub children: BTreeMap<String, ProjectNode>,
|
|
||||||
pub properties: HashMap<String, UnresolvedRbxValue>,
|
|
||||||
pub ignore_unknown_instances: Option<bool>,
|
|
||||||
|
|
||||||
#[serde(serialize_with = "crate::path_serializer::serialize_option_absolute")]
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProjectNode {
|
|
||||||
fn validate_reserved_names(&self) {
|
|
||||||
for (name, child) in &self.children {
|
|
||||||
if name.starts_with('$') {
|
|
||||||
warn!(
|
|
||||||
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
|
|
||||||
);
|
|
||||||
warn!(
|
|
||||||
"This project uses the key '{}', which should be renamed.",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
child.validate_reserved_names();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_source_node(&self, project_file_location: &Path) -> SourceProjectNode {
|
|
||||||
let children = self
|
|
||||||
.children
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| (key.clone(), value.to_source_node(project_file_location)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// If paths are relative to the project file, transform them to look
|
|
||||||
// Unixy and write relative paths instead.
|
|
||||||
//
|
|
||||||
// This isn't perfect, since it means that paths like .. will stay as
|
|
||||||
// absolute paths and make projects non-portable. Fixing this probably
|
|
||||||
// means keeping the paths relative in the project format and making
|
|
||||||
// everywhere else in Rojo do the resolution locally.
|
|
||||||
let path = self.path.as_ref().map(|path| {
|
|
||||||
let project_folder_location = project_file_location.parent().unwrap();
|
|
||||||
|
|
||||||
match path.strip_prefix(project_folder_location) {
|
|
||||||
Ok(stripped) => stripped.to_str().unwrap().replace("\\", "/"),
|
|
||||||
Err(_) => format!("{}", path.display()),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
SourceProjectNode {
|
|
||||||
class_name: self.class_name.clone(),
|
|
||||||
properties: self.properties.clone(),
|
|
||||||
ignore_unknown_instances: self.ignore_unknown_instances,
|
|
||||||
children,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
|
/// The name of the top-level instance described by the project.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// The tree of instances described by this project. Projects always
|
||||||
|
/// describe at least one instance.
|
||||||
pub tree: ProjectNode,
|
pub tree: ProjectNode,
|
||||||
|
|
||||||
|
/// If specified, sets the default port that `rojo serve` should use when
|
||||||
|
/// using this project for live sync.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub serve_port: Option<u16>,
|
pub serve_port: Option<u16>,
|
||||||
|
|
||||||
|
/// If specified, contains the set of place IDs that this project is
|
||||||
|
/// compatible with when doing live sync.
|
||||||
|
///
|
||||||
|
/// This setting is intended to help prevent syncing a Rojo project into the
|
||||||
|
/// wrong Roblox place.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub serve_place_ids: Option<HashSet<u64>>,
|
pub serve_place_ids: Option<HashSet<u64>>,
|
||||||
pub plugins: Vec<PathBuf>,
|
|
||||||
|
/// The path to the file that this project came from. Relative paths in the
|
||||||
|
/// project should be considered relative to the parent of this field, also
|
||||||
|
/// given by `Project::folder_location`.
|
||||||
|
#[serde(skip)]
|
||||||
pub file_location: PathBuf,
|
pub file_location: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
|
/// Tells whether the given path describes a Rojo project.
|
||||||
pub fn is_project_file(path: &Path) -> bool {
|
pub fn is_project_file(path: &Path) -> bool {
|
||||||
path.file_name()
|
path.file_name()
|
||||||
.and_then(|name| name.to_str())
|
.and_then(|name| name.to_str())
|
||||||
@@ -341,97 +65,6 @@ impl Project {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_place(project_fuzzy_path: &Path) -> Result<PathBuf, ProjectInitError> {
|
|
||||||
let project_path = Project::pick_path_for_init(project_fuzzy_path)?;
|
|
||||||
|
|
||||||
let project_name = if project_fuzzy_path == project_path {
|
|
||||||
project_fuzzy_path
|
|
||||||
.parent()
|
|
||||||
.expect("Path did not have a parent directory")
|
|
||||||
.file_name()
|
|
||||||
.expect("Path did not have a file name")
|
|
||||||
.to_str()
|
|
||||||
.expect("Path had invalid Unicode")
|
|
||||||
} else {
|
|
||||||
project_fuzzy_path
|
|
||||||
.file_name()
|
|
||||||
.expect("Path did not have a file name")
|
|
||||||
.to_str()
|
|
||||||
.expect("Path had invalid Unicode")
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut project = Project::load_from_slice(DEFAULT_PLACE.as_bytes(), &project_path)
|
|
||||||
.map_err(ProjectInitError::JsonError)?;
|
|
||||||
|
|
||||||
project.name = project_name.to_owned();
|
|
||||||
|
|
||||||
project.save().map_err(ProjectInitError::SaveError)?;
|
|
||||||
|
|
||||||
Ok(project_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_model(project_fuzzy_path: &Path) -> Result<PathBuf, ProjectInitError> {
|
|
||||||
let project_path = Project::pick_path_for_init(project_fuzzy_path)?;
|
|
||||||
|
|
||||||
let project_name = if project_fuzzy_path == project_path {
|
|
||||||
project_fuzzy_path
|
|
||||||
.parent()
|
|
||||||
.expect("Path did not have a parent directory")
|
|
||||||
.file_name()
|
|
||||||
.expect("Path did not have a file name")
|
|
||||||
.to_str()
|
|
||||||
.expect("Path had invalid Unicode")
|
|
||||||
} else {
|
|
||||||
project_fuzzy_path
|
|
||||||
.file_name()
|
|
||||||
.expect("Path did not have a file name")
|
|
||||||
.to_str()
|
|
||||||
.expect("Path had invalid Unicode")
|
|
||||||
};
|
|
||||||
|
|
||||||
let project_folder_path = project_path
|
|
||||||
.parent()
|
|
||||||
.expect("Path did not have a parent directory");
|
|
||||||
|
|
||||||
let tree = ProjectNode {
|
|
||||||
path: Some(project_folder_path.join("src")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let project = Project {
|
|
||||||
name: project_name.to_string(),
|
|
||||||
tree,
|
|
||||||
serve_port: None,
|
|
||||||
serve_place_ids: None,
|
|
||||||
plugins: Vec::new(),
|
|
||||||
file_location: project_path.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
project.save().map_err(ProjectInitError::SaveError)?;
|
|
||||||
|
|
||||||
Ok(project_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_path_for_init(project_fuzzy_path: &Path) -> Result<PathBuf, ProjectInitError> {
|
|
||||||
let is_exact = project_fuzzy_path.extension().is_some();
|
|
||||||
|
|
||||||
let project_path = if is_exact {
|
|
||||||
project_fuzzy_path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
project_fuzzy_path.join(PROJECT_FILENAME)
|
|
||||||
};
|
|
||||||
|
|
||||||
match fs::metadata(&project_path) {
|
|
||||||
Err(error) => match error.kind() {
|
|
||||||
io::ErrorKind::NotFound => {}
|
|
||||||
_ => return Err(ProjectInitError::IoError(error)),
|
|
||||||
},
|
|
||||||
Ok(_) => return Err(ProjectInitError::AlreadyExists(project_path)),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(project_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to locate a project represented by the given path.
|
/// Attempt to locate a project represented by the given path.
|
||||||
///
|
///
|
||||||
/// This will find a project if the path refers to a `.project.json` file,
|
/// This will find a project if the path refers to a `.project.json` file,
|
||||||
@@ -464,51 +97,40 @@ impl Project {
|
|||||||
pub fn load_from_slice(
|
pub fn load_from_slice(
|
||||||
contents: &[u8],
|
contents: &[u8],
|
||||||
project_file_location: &Path,
|
project_file_location: &Path,
|
||||||
) -> Result<Project, serde_json::Error> {
|
) -> Result<Self, serde_json::Error> {
|
||||||
let parsed: SourceProject = serde_json::from_slice(&contents)?;
|
let mut project: Self = serde_json::from_slice(&contents)?;
|
||||||
|
project.file_location = project_file_location.to_path_buf();
|
||||||
Ok(parsed.into_project(project_file_location))
|
project.check_compatibility();
|
||||||
|
Ok(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Project, ProjectLoadError> {
|
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Option<Self>, ProjectError> {
|
||||||
if let Some(project_path) = Self::locate(fuzzy_project_location) {
|
if let Some(project_path) = Self::locate(fuzzy_project_location) {
|
||||||
Self::load_exact(&project_path)
|
let project = Self::load_exact(&project_path)?;
|
||||||
|
|
||||||
|
Ok(Some(project))
|
||||||
} else {
|
} else {
|
||||||
Err(ProjectLoadError::NotFound)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_exact(project_file_location: &Path) -> Result<Project, ProjectLoadError> {
|
fn load_exact(project_file_location: &Path) -> Result<Self, ProjectError> {
|
||||||
let contents =
|
let contents = fs::read_to_string(project_file_location).context(Io {
|
||||||
fs::read_to_string(project_file_location).map_err(|error| match error.kind() {
|
path: project_file_location,
|
||||||
io::ErrorKind::NotFound => ProjectLoadError::NotFound,
|
})?;
|
||||||
_ => ProjectLoadError::Io {
|
|
||||||
inner: error,
|
|
||||||
path: project_file_location.to_path_buf(),
|
|
||||||
},
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let parsed: SourceProject =
|
let mut project: Project = serde_json::from_str(&contents).context(Json {
|
||||||
serde_json::from_str(&contents).map_err(|error| ProjectLoadError::Json {
|
path: project_file_location,
|
||||||
inner: error,
|
})?;
|
||||||
path: project_file_location.to_path_buf(),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let project = parsed.into_project(project_file_location);
|
|
||||||
|
|
||||||
|
project.file_location = project_file_location.to_path_buf();
|
||||||
project.check_compatibility();
|
project.check_compatibility();
|
||||||
|
|
||||||
Ok(project)
|
Ok(project)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), ProjectSaveError> {
|
pub fn save(&self) -> Result<(), ProjectError> {
|
||||||
let source_project = self.to_source_project();
|
unimplemented!()
|
||||||
let mut file = File::create(&self.file_location).map_err(ProjectSaveError::IoError)?;
|
|
||||||
|
|
||||||
serde_json::to_writer_pretty(&mut file, &source_project)
|
|
||||||
.map_err(ProjectSaveError::JsonError)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if there are any compatibility issues with this project file and
|
/// Checks if there are any compatibility issues with this project file and
|
||||||
@@ -520,27 +142,80 @@ impl Project {
|
|||||||
pub fn folder_location(&self) -> &Path {
|
pub fn folder_location(&self) -> &Path {
|
||||||
self.file_location.parent().unwrap()
|
self.file_location.parent().unwrap()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn to_source_project(&self) -> SourceProject {
|
/// Describes an instance and its descendants in a project.
|
||||||
// TODO: Use path_serializer instead of transforming paths between
|
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||||
// String and PathBuf?
|
pub struct ProjectNode {
|
||||||
let plugins = self
|
/// If set, defines the ClassName of the described instance.
|
||||||
.plugins
|
///
|
||||||
.iter()
|
/// `$className` MUST be set if `$path` is not set.
|
||||||
.map(|path| {
|
///
|
||||||
path.strip_prefix(self.folder_location())
|
/// `$className` CANNOT be set if `$path` is set and the instance described
|
||||||
.unwrap()
|
/// by that path has a ClassName other than Folder.
|
||||||
.display()
|
#[serde(rename = "$className", skip_serializing_if = "Option::is_none")]
|
||||||
.to_string()
|
pub class_name: Option<String>,
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
SourceProject {
|
/// Contains all of the children of the described instance.
|
||||||
name: self.name.clone(),
|
#[serde(flatten)]
|
||||||
tree: self.tree.to_source_node(&self.file_location),
|
pub children: BTreeMap<String, ProjectNode>,
|
||||||
serve_port: self.serve_port,
|
|
||||||
plugins,
|
/// The properties that will be assigned to the resulting instance.
|
||||||
serve_place_ids: self.serve_place_ids.clone(),
|
///
|
||||||
|
// TODO: Is this legal to set if $path is set?
|
||||||
|
#[serde(
|
||||||
|
rename = "$properties",
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "HashMap::is_empty"
|
||||||
|
)]
|
||||||
|
pub properties: HashMap<String, UnresolvedRbxValue>,
|
||||||
|
|
||||||
|
/// Defines the behavior when Rojo encounters unknown instances in Roblox
|
||||||
|
/// Studio during live sync. `$ignoreUnknownInstances` should be considered
|
||||||
|
/// a large hammer and used with care.
|
||||||
|
///
|
||||||
|
/// If set to `true`, those instances will be left alone. This may cause
|
||||||
|
/// issues when files that turn into instances are removed while Rojo is not
|
||||||
|
/// running.
|
||||||
|
///
|
||||||
|
/// If set to `false`, Rojo will destroy any instances it does not
|
||||||
|
/// recognize.
|
||||||
|
///
|
||||||
|
/// If unset, its default value depends on other settings:
|
||||||
|
/// - If `$path` is not set, defaults to `true`
|
||||||
|
/// - If `$path` is set, defaults to `false`
|
||||||
|
#[serde(
|
||||||
|
rename = "$ignoreUnknownInstances",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub ignore_unknown_instances: Option<bool>,
|
||||||
|
|
||||||
|
/// Defines that this instance should come from the given file path. This
|
||||||
|
/// path can point to any file type supported by Rojo, including Lua files
|
||||||
|
/// (`.lua`), Roblox models (`.rbxm`, `.rbxmx`), and localization table
|
||||||
|
/// spreadsheets (`.csv`).
|
||||||
|
#[serde(
|
||||||
|
rename = "$path",
|
||||||
|
serialize_with = "crate::path_serializer::serialize_option_absolute",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectNode {
|
||||||
|
fn validate_reserved_names(&self) {
|
||||||
|
for (name, child) in &self.children {
|
||||||
|
if name.starts_with('$') {
|
||||||
|
log::warn!(
|
||||||
|
"Keys starting with '$' are reserved by Rojo to ensure forward compatibility."
|
||||||
|
);
|
||||||
|
log::warn!(
|
||||||
|
"This project uses the key '{}', which should be renamed.",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
child.validate_reserved_names();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,16 +110,24 @@ impl Default for InstanceContext {
|
|||||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum InstigatingSource {
|
pub enum InstigatingSource {
|
||||||
Path(#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf),
|
Path(#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf),
|
||||||
ProjectNode(String, ProjectNode),
|
ProjectNode(
|
||||||
|
#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf,
|
||||||
|
String,
|
||||||
|
ProjectNode,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for InstigatingSource {
|
impl fmt::Debug for InstigatingSource {
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()),
|
InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()),
|
||||||
InstigatingSource::ProjectNode(name, node) => {
|
InstigatingSource::ProjectNode(path, name, node) => write!(
|
||||||
write!(formatter, "ProjectNode({}: {:?}", name, node)
|
formatter,
|
||||||
}
|
"ProjectNode({}: {:?}) from path {}",
|
||||||
|
name,
|
||||||
|
node,
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{borrow::Cow, collections::HashMap};
|
use std::{borrow::Cow, collections::HashMap, path::Path};
|
||||||
|
|
||||||
use rbx_reflection::try_resolve_value;
|
use rbx_reflection::try_resolve_value;
|
||||||
|
|
||||||
@@ -47,8 +47,14 @@ impl SnapshotMiddleware for SnapshotProject {
|
|||||||
|
|
||||||
// Snapshotting a project should always return an instance, so this
|
// Snapshotting a project should always return an instance, so this
|
||||||
// unwrap is safe.
|
// unwrap is safe.
|
||||||
let mut snapshot =
|
let mut snapshot = snapshot_project_node(
|
||||||
snapshot_project_node(context, &project.name, &project.tree, vfs)?.unwrap();
|
context,
|
||||||
|
project.folder_location(),
|
||||||
|
&project.name,
|
||||||
|
&project.tree,
|
||||||
|
vfs,
|
||||||
|
)?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Setting the instigating source to the project file path is a little
|
// Setting the instigating source to the project file path is a little
|
||||||
// coarse.
|
// coarse.
|
||||||
@@ -77,6 +83,7 @@ impl SnapshotMiddleware for SnapshotProject {
|
|||||||
|
|
||||||
pub fn snapshot_project_node<F: VfsFetcher>(
|
pub fn snapshot_project_node<F: VfsFetcher>(
|
||||||
context: &InstanceContext,
|
context: &InstanceContext,
|
||||||
|
project_folder: &Path,
|
||||||
instance_name: &str,
|
instance_name: &str,
|
||||||
node: &ProjectNode,
|
node: &ProjectNode,
|
||||||
vfs: &Vfs<F>,
|
vfs: &Vfs<F>,
|
||||||
@@ -91,7 +98,15 @@ pub fn snapshot_project_node<F: VfsFetcher>(
|
|||||||
let mut metadata = InstanceMetadata::default();
|
let mut metadata = InstanceMetadata::default();
|
||||||
|
|
||||||
if let Some(path) = &node.path {
|
if let Some(path) = &node.path {
|
||||||
let entry = vfs.get(path)?;
|
// If the path specified in the project is relative, we assume it's
|
||||||
|
// relative to the folder that the project is in, project_folder.
|
||||||
|
let path = if path.is_relative() {
|
||||||
|
Cow::Owned(project_folder.join(path))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = vfs.get(path.as_path())?;
|
||||||
|
|
||||||
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &entry)? {
|
if let Some(snapshot) = snapshot_from_vfs(context, vfs, &entry)? {
|
||||||
// If a class name was already specified, then it'll override the
|
// If a class name was already specified, then it'll override the
|
||||||
@@ -143,7 +158,9 @@ pub fn snapshot_project_node<F: VfsFetcher>(
|
|||||||
.expect("$className or $path must be specified");
|
.expect("$className or $path must be specified");
|
||||||
|
|
||||||
for (child_name, child_project_node) in &node.children {
|
for (child_name, child_project_node) in &node.children {
|
||||||
if let Some(child) = snapshot_project_node(context, child_name, child_project_node, vfs)? {
|
if let Some(child) =
|
||||||
|
snapshot_project_node(context, project_folder, child_name, child_project_node, vfs)?
|
||||||
|
{
|
||||||
children.push(child);
|
children.push(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +187,7 @@ pub fn snapshot_project_node<F: VfsFetcher>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
metadata.instigating_source = Some(InstigatingSource::ProjectNode(
|
metadata.instigating_source = Some(InstigatingSource::ProjectNode(
|
||||||
|
project_folder.to_path_buf(),
|
||||||
instance_name.to_string(),
|
instance_name.to_string(),
|
||||||
node.clone(),
|
node.clone(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -19,12 +19,9 @@ children:
|
|||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
ProjectNode:
|
ProjectNode:
|
||||||
|
- /foo
|
||||||
- Child
|
- Child
|
||||||
- class_name: Model
|
- $className: Model
|
||||||
children: {}
|
|
||||||
properties: {}
|
|
||||||
ignore_unknown_instances: ~
|
|
||||||
path: ~
|
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context: {}
|
context: {}
|
||||||
name: Child
|
name: Child
|
||||||
|
|||||||
@@ -20,12 +20,9 @@ children:
|
|||||||
ignore_unknown_instances: true
|
ignore_unknown_instances: true
|
||||||
instigating_source:
|
instigating_source:
|
||||||
ProjectNode:
|
ProjectNode:
|
||||||
|
- /foo
|
||||||
- SomeChild
|
- SomeChild
|
||||||
- class_name: Model
|
- $className: Model
|
||||||
children: {}
|
|
||||||
properties: {}
|
|
||||||
ignore_unknown_instances: ~
|
|
||||||
path: ~
|
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context: {}
|
context: {}
|
||||||
name: SomeChild
|
name: SomeChild
|
||||||
|
|||||||
@@ -25,12 +25,9 @@ children:
|
|||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
ProjectNode:
|
ProjectNode:
|
||||||
|
- /foo
|
||||||
- Child
|
- Child
|
||||||
- class_name: ~
|
- $path: file.txt
|
||||||
children: {}
|
|
||||||
properties: {}
|
|
||||||
ignore_unknown_instances: ~
|
|
||||||
path: /foo/file.txt
|
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo/file.txt
|
- /foo/file.txt
|
||||||
- /foo/file.meta.json
|
- /foo/file.meta.json
|
||||||
|
|||||||
@@ -25,12 +25,9 @@ children:
|
|||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
ProjectNode:
|
ProjectNode:
|
||||||
|
- /foo
|
||||||
- Child
|
- Child
|
||||||
- class_name: ~
|
- $path: file.txt
|
||||||
children: {}
|
|
||||||
properties: {}
|
|
||||||
ignore_unknown_instances: ~
|
|
||||||
path: /foo/file.txt
|
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo/file.txt
|
- /foo/file.txt
|
||||||
- /foo/file.meta.json
|
- /foo/file.meta.json
|
||||||
|
|||||||
Reference in New Issue
Block a user