mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Merge 23fdad2e62 into 988efb45b1
This commit is contained in:
commit
11edc6ec65
18 changed files with 712 additions and 36 deletions
|
|
@ -31,6 +31,7 @@ Making a new release? Simply add the new header with the version and date undern
|
|||
|
||||
## Unreleased
|
||||
|
||||
* Added `--base` flag to `rojo build` and `rojo upload` for merging a Rojo project into an existing Roblox file ([#1226])
|
||||
* `inf` and `nan` values in properties are now synced ([#1176])
|
||||
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
||||
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
||||
|
|
@ -48,6 +49,7 @@ Making a new release? Simply add the new header with the version and date undern
|
|||
[#1211]: https://github.com/rojo-rbx/rojo/pull/1211
|
||||
[#1215]: https://github.com/rojo-rbx/rojo/pull/1215
|
||||
[#1217]: https://github.com/rojo-rbx/rojo/pull/1217
|
||||
[#1226]: https://github.com/rojo-rbx/rojo/pull/1226
|
||||
|
||||
## [7.7.0-rc.1] (November 27th, 2025)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 112
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="ReplicatedStorage" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">ReplicatedStorage</string>
|
||||
</Properties>
|
||||
<Item class="ModuleScript" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">Hello</string>
|
||||
<string name="Source"><![CDATA[-- hello from rojo
|
||||
]]></string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
<Item class="Workspace" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
</Properties>
|
||||
<Item class="Part" referent="3">
|
||||
<Properties>
|
||||
<string name="Name">PreservedPart</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 112
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">BaseFolder</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="Folder" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">base_merge_model</string>
|
||||
</Properties>
|
||||
<Item class="ModuleScript" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">Child</string>
|
||||
<string name="Source"><![CDATA[-- model child
|
||||
]]></string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 112
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="ReplicatedStorage" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">ReplicatedStorage</string>
|
||||
</Properties>
|
||||
<Item class="ModuleScript" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">ExistingScript</string>
|
||||
<string name="Source">-- existing</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
<Item class="ModuleScript" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">Hello</string>
|
||||
<string name="Source"><![CDATA[-- hello from rojo
|
||||
]]></string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
<Item class="Workspace" referent="3">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
</Properties>
|
||||
<Item class="Part" referent="4">
|
||||
<Properties>
|
||||
<string name="Name">BasePart</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
23
rojo-test/build-tests/base_merge_delete/base.rbxlx
Normal file
23
rojo-test/build-tests/base_merge_delete/base.rbxlx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<roblox version="4">
|
||||
<Item class="ReplicatedStorage" referent="RBX1">
|
||||
<Properties>
|
||||
<string name="Name">ReplicatedStorage</string>
|
||||
</Properties>
|
||||
<Item class="ModuleScript" referent="RBX2">
|
||||
<Properties>
|
||||
<string name="Name">ShouldBeDeleted</string>
|
||||
<ProtectedString name="Source"><![CDATA[-- this should be removed]]></ProtectedString>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
<Item class="Workspace" referent="RBX3">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
</Properties>
|
||||
<Item class="Part" referent="RBX4">
|
||||
<Properties>
|
||||
<string name="Name">PreservedPart</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "base_merge_delete",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
rojo-test/build-tests/base_merge_delete/src/Hello.luau
Normal file
1
rojo-test/build-tests/base_merge_delete/src/Hello.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- hello from rojo
|
||||
7
rojo-test/build-tests/base_merge_model/base.rbxmx
Normal file
7
rojo-test/build-tests/base_merge_model/base.rbxmx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<roblox version="4">
|
||||
<Item class="Folder" referent="RBX1">
|
||||
<Properties>
|
||||
<string name="Name">BaseFolder</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</roblox>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "base_merge_model",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
1
rojo-test/build-tests/base_merge_model/src/Child.luau
Normal file
1
rojo-test/build-tests/base_merge_model/src/Child.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- model child
|
||||
23
rojo-test/build-tests/base_merge_place/base.rbxlx
Normal file
23
rojo-test/build-tests/base_merge_place/base.rbxlx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<roblox version="4">
|
||||
<Item class="ReplicatedStorage" referent="RBX1">
|
||||
<Properties>
|
||||
<string name="Name">ReplicatedStorage</string>
|
||||
</Properties>
|
||||
<Item class="ModuleScript" referent="RBX2">
|
||||
<Properties>
|
||||
<string name="Name">ExistingScript</string>
|
||||
<ProtectedString name="Source"><![CDATA[-- existing]]></ProtectedString>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
<Item class="Workspace" referent="RBX3">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
</Properties>
|
||||
<Item class="Part" referent="RBX4">
|
||||
<Properties>
|
||||
<string name="Name">BasePart</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
11
rojo-test/build-tests/base_merge_place/default.project.json
Normal file
11
rojo-test/build-tests/base_merge_place/default.project.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "base_merge_place",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"Hello": {
|
||||
"$path": "src/Hello.luau"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
rojo-test/build-tests/base_merge_place/src/Hello.luau
Normal file
1
rojo-test/build-tests/base_merge_place/src/Hello.luau
Normal file
|
|
@ -0,0 +1 @@
|
|||
-- hello from rojo
|
||||
|
|
@ -11,7 +11,7 @@ use memofs::Vfs;
|
|||
use roblox_install::RobloxStudio;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::serve_session::ServeSession;
|
||||
use crate::{merge_base, serve_session::ServeSession};
|
||||
|
||||
use super::resolve_path;
|
||||
|
||||
|
|
@ -39,6 +39,13 @@ pub struct BuildCommand {
|
|||
#[clap(long, short, conflicts_with = "output")]
|
||||
pub plugin: Option<PathBuf>,
|
||||
|
||||
/// Path to a base Roblox file (.rbxl, .rbxlx, .rbxm, .rbxmx) to merge
|
||||
/// the project into. The Rojo project tree is merged into this file so
|
||||
/// that the output contains both the base content and the project's
|
||||
/// scripts/configuration.
|
||||
#[clap(long)]
|
||||
pub base: Option<PathBuf>,
|
||||
|
||||
/// Whether to automatically rebuild when any input files change.
|
||||
#[clap(long)]
|
||||
pub watch: bool,
|
||||
|
|
@ -84,7 +91,7 @@ impl BuildCommand {
|
|||
let session = ServeSession::new(vfs, project_path)?;
|
||||
let mut cursor = session.message_queue().cursor();
|
||||
|
||||
write_model(&session, &output_path, output_kind)?;
|
||||
write_model(&session, &output_path, output_kind, self.base.as_deref())?;
|
||||
|
||||
if self.watch {
|
||||
let rt = Runtime::new().unwrap();
|
||||
|
|
@ -94,7 +101,7 @@ impl BuildCommand {
|
|||
let (new_cursor, _patch_set) = rt.block_on(receiver).unwrap();
|
||||
cursor = new_cursor;
|
||||
|
||||
write_model(&session, &output_path, output_kind)?;
|
||||
write_model(&session, &output_path, output_kind, self.base.as_deref())?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,39 +162,70 @@ fn write_model(
|
|||
session: &ServeSession,
|
||||
output: &Path,
|
||||
output_kind: OutputKind,
|
||||
base_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
println!("Building project '{}'", session.project_name());
|
||||
|
||||
let tree = session.tree();
|
||||
let root_id = tree.get_root_id();
|
||||
|
||||
log::trace!("Opening output file for write");
|
||||
let mut file = BufWriter::new(File::create(output)?);
|
||||
|
||||
match output_kind {
|
||||
OutputKind::Rbxm => {
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
|
||||
if let Some(base_path) = base_path {
|
||||
let base_path = super::resolve_path(base_path);
|
||||
|
||||
log::trace!("Reading base file: {}", base_path.display());
|
||||
let base_dom = merge_base::read_base_dom(&base_path)?;
|
||||
|
||||
log::trace!("Merging Rojo project into base file");
|
||||
let merged_dom = merge_base::merge_rojo_into_base(base_dom, &tree)?;
|
||||
|
||||
// The merged DOM's root is always the WeakDom synthetic root
|
||||
// (DataModel). For both place and model output, we encode its
|
||||
// children — the services/instances — not the root itself.
|
||||
let encode_ids = merged_dom.root().children().to_vec();
|
||||
|
||||
match output_kind {
|
||||
OutputKind::Rbxl | OutputKind::Rbxm => {
|
||||
rbx_binary::to_writer(&mut file, &merged_dom, &encode_ids)?;
|
||||
}
|
||||
OutputKind::Rbxlx | OutputKind::Rbxmx => {
|
||||
rbx_xml::to_writer(&mut file, &merged_dom, &encode_ids, xml_encode_config())?;
|
||||
}
|
||||
}
|
||||
OutputKind::Rbxl => {
|
||||
let root_instance = tree.get_instance(root_id).unwrap();
|
||||
let top_level_ids = root_instance.children();
|
||||
} else {
|
||||
let root_id = tree.get_root_id();
|
||||
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
|
||||
}
|
||||
OutputKind::Rbxmx => {
|
||||
// Model files include the root instance of the tree and all its
|
||||
// descendants.
|
||||
match output_kind {
|
||||
OutputKind::Rbxm => {
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), &[root_id])?;
|
||||
}
|
||||
OutputKind::Rbxl => {
|
||||
let root_instance = tree.get_instance(root_id).unwrap();
|
||||
let top_level_ids = root_instance.children();
|
||||
|
||||
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
|
||||
}
|
||||
OutputKind::Rbxlx => {
|
||||
// Place files don't contain an entry for the DataModel, but our
|
||||
// WeakDom representation does.
|
||||
rbx_binary::to_writer(&mut file, tree.inner(), top_level_ids)?;
|
||||
}
|
||||
OutputKind::Rbxmx => {
|
||||
// Model files include the root instance of the tree and all its
|
||||
// descendants.
|
||||
|
||||
let root_instance = tree.get_instance(root_id).unwrap();
|
||||
let top_level_ids = root_instance.children();
|
||||
rbx_xml::to_writer(&mut file, tree.inner(), &[root_id], xml_encode_config())?;
|
||||
}
|
||||
OutputKind::Rbxlx => {
|
||||
// Place files don't contain an entry for the DataModel, but our
|
||||
// WeakDom representation does.
|
||||
|
||||
rbx_xml::to_writer(&mut file, tree.inner(), top_level_ids, xml_encode_config())?;
|
||||
let root_instance = tree.get_instance(root_id).unwrap();
|
||||
let top_level_ids = root_instance.children();
|
||||
|
||||
rbx_xml::to_writer(
|
||||
&mut file,
|
||||
tree.inner(),
|
||||
top_level_ids,
|
||||
xml_encode_config(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::{mem::forget, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use clap::Parser;
|
||||
|
|
@ -8,7 +8,7 @@ use reqwest::{
|
|||
StatusCode,
|
||||
};
|
||||
|
||||
use crate::{auth_cookie::get_auth_cookie, serve_session::ServeSession};
|
||||
use crate::{auth_cookie::get_auth_cookie, merge_base, serve_session::ServeSession};
|
||||
|
||||
use super::resolve_path;
|
||||
|
||||
|
|
@ -19,6 +19,13 @@ pub struct UploadCommand {
|
|||
#[clap(default_value = "")]
|
||||
pub project: PathBuf,
|
||||
|
||||
/// Path to a base Roblox file (.rbxl, .rbxlx, .rbxm, .rbxmx) to merge
|
||||
/// with the project before uploading. When provided, the Rojo project
|
||||
/// tree is merged into this file so that the upload contains both the
|
||||
/// base content and the project's scripts.
|
||||
#[clap(long)]
|
||||
pub base: Option<PathBuf>,
|
||||
|
||||
/// Authenication cookie to use. If not specified, Rojo will attempt to find one from the system automatically.
|
||||
#[clap(long)]
|
||||
pub cookie: Option<String>,
|
||||
|
|
@ -44,19 +51,39 @@ impl UploadCommand {
|
|||
|
||||
let session = ServeSession::new(vfs, project_path)?;
|
||||
|
||||
let tree = session.tree();
|
||||
let inner_tree = tree.inner();
|
||||
let root = inner_tree.root();
|
||||
|
||||
let encode_ids = match root.class.as_str() {
|
||||
"DataModel" => root.children().to_vec(),
|
||||
_ => vec![root.referent()],
|
||||
};
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
log::trace!("Encoding binary model");
|
||||
rbx_binary::to_writer(&mut buffer, tree.inner(), &encode_ids)?;
|
||||
if let Some(base_path) = &self.base {
|
||||
let base_path = resolve_path(base_path);
|
||||
|
||||
log::trace!("Reading base file: {}", base_path.display());
|
||||
let base_dom = merge_base::read_base_dom(&base_path)?;
|
||||
|
||||
log::trace!("Merging Rojo project into base file");
|
||||
let tree = session.tree();
|
||||
let merged_dom = merge_base::merge_rojo_into_base(base_dom, &tree)?;
|
||||
|
||||
let encode_ids = merged_dom.root().children().to_vec();
|
||||
|
||||
log::trace!("Encoding merged binary model");
|
||||
rbx_binary::to_writer(&mut buffer, &merged_dom, &encode_ids)?;
|
||||
} else {
|
||||
let tree = session.tree();
|
||||
let inner_tree = tree.inner();
|
||||
let root = inner_tree.root();
|
||||
|
||||
let encode_ids = match root.class.as_str() {
|
||||
"DataModel" => root.children().to_vec(),
|
||||
_ => vec![root.referent()],
|
||||
};
|
||||
|
||||
log::trace!("Encoding binary model");
|
||||
rbx_binary::to_writer(&mut buffer, inner_tree, &encode_ids)?;
|
||||
}
|
||||
|
||||
// Avoid dropping ServeSession: it's potentially very expensive to
|
||||
// drop and we're about to exit anyway.
|
||||
forget(session);
|
||||
|
||||
match (self.cookie, self.api_key, self.universe_id) {
|
||||
(cookie, None, universe) => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod change_processor;
|
|||
mod glob;
|
||||
mod json;
|
||||
mod lua_ast;
|
||||
mod merge_base;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
mod path_serializer;
|
||||
|
|
|
|||
371
src/merge_base.rs
Normal file
371
src/merge_base.rs
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
//! Shared logic for merging a Rojo project tree into a base Roblox file.
|
||||
//!
|
||||
//! Used by both `rojo build --base` and `rojo upload --base`.
|
||||
|
||||
use std::{io::BufReader, path::Path};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use fs_err::File;
|
||||
use rbx_dom_weak::{types::Ref, Instance, WeakDom};
|
||||
|
||||
use crate::snapshot::RojoTree;
|
||||
|
||||
/// A pair of (base instance, Rojo instance) whose children still need merging.
|
||||
struct MergePair {
|
||||
base_ref: Ref,
|
||||
rojo_ref: Ref,
|
||||
}
|
||||
|
||||
/// Reads a base Roblox file (.rbxl, .rbxlx, .rbxm, .rbxmx) into a WeakDom.
|
||||
pub fn read_base_dom(path: &Path) -> anyhow::Result<WeakDom> {
|
||||
let extension = path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.context("Base file must have a .rbxl, .rbxlx, .rbxm, or .rbxmx extension")?;
|
||||
|
||||
let content = BufReader::new(
|
||||
File::open(path)
|
||||
.with_context(|| format!("Could not open base file at {}", path.display()))?,
|
||||
);
|
||||
|
||||
match extension {
|
||||
"rbxl" | "rbxm" => rbx_binary::from_reader(content)
|
||||
.with_context(|| format!("Could not deserialize binary file at {}", path.display())),
|
||||
"rbxlx" | "rbxmx" => {
|
||||
let config = rbx_xml::DecodeOptions::new()
|
||||
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
|
||||
rbx_xml::from_reader(content, config)
|
||||
.with_context(|| format!("Could not deserialize XML file at {}", path.display()))
|
||||
}
|
||||
_ => bail!(
|
||||
"Base file must be .rbxl, .rbxlx, .rbxm, or .rbxmx, got .{}",
|
||||
extension
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges the Rojo project tree into a base WeakDom.
|
||||
///
|
||||
/// For place projects (root class is "DataModel"), iteratively merges Rojo's
|
||||
/// children into matching base children by name and className. For model
|
||||
/// projects, clones the entire Rojo tree as a child of the base root.
|
||||
///
|
||||
/// The `ignore_unknown_instances` metadata on each Rojo instance controls
|
||||
/// whether unmatched base children are preserved or deleted:
|
||||
/// - `true` (default for project nodes without `$path`): preserve
|
||||
/// - `false` (default when `$path` is set): delete
|
||||
pub fn merge_rojo_into_base(mut base: WeakDom, rojo: &RojoTree) -> anyhow::Result<WeakDom> {
|
||||
let rojo_root_id = rojo.get_root_id();
|
||||
let rojo_inner = rojo.inner();
|
||||
let rojo_root = rojo_inner
|
||||
.get_by_ref(rojo_root_id)
|
||||
.expect("Rojo tree should have a root");
|
||||
let base_root_ref = base.root_ref();
|
||||
|
||||
// For model projects (root is not DataModel), clone entire Rojo tree
|
||||
// as a child of the base root.
|
||||
if rojo_root.class.as_str() != "DataModel" {
|
||||
let cloned_ref = rojo_inner.clone_into_external(rojo_root_id, &mut base);
|
||||
base.transfer_within(cloned_ref, base_root_ref);
|
||||
return Ok(base);
|
||||
}
|
||||
|
||||
// For place projects: iteratively merge children using a stack.
|
||||
let mut stack = vec![MergePair {
|
||||
base_ref: base_root_ref,
|
||||
rojo_ref: rojo_root_id,
|
||||
}];
|
||||
|
||||
while let Some(pair) = stack.pop() {
|
||||
let rojo_children: Vec<Ref> = rojo_inner
|
||||
.get_by_ref(pair.rojo_ref)
|
||||
.expect("Rojo instance should exist")
|
||||
.children()
|
||||
.to_vec();
|
||||
|
||||
let base_children: Vec<Ref> = base
|
||||
.get_by_ref(pair.base_ref)
|
||||
.expect("Base instance should exist")
|
||||
.children()
|
||||
.to_vec();
|
||||
|
||||
let mut base_matched = vec![false; base_children.len()];
|
||||
|
||||
for &rojo_child_ref in &rojo_children {
|
||||
let rojo_child = rojo_inner
|
||||
.get_by_ref(rojo_child_ref)
|
||||
.expect("Rojo child should exist");
|
||||
|
||||
// Find the first unmatched base child with the same name+class.
|
||||
let base_match = base_children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(idx, base_child_ref)| {
|
||||
if base_matched[*idx] {
|
||||
return false;
|
||||
}
|
||||
let base_child = base
|
||||
.get_by_ref(**base_child_ref)
|
||||
.expect("Base child should exist");
|
||||
base_child.name == rojo_child.name
|
||||
&& base_child.class.as_str() == rojo_child.class.as_str()
|
||||
});
|
||||
|
||||
match base_match {
|
||||
Some((idx, &base_child_ref)) => {
|
||||
base_matched[idx] = true;
|
||||
update_properties(&mut base, base_child_ref, rojo_child);
|
||||
stack.push(MergePair {
|
||||
base_ref: base_child_ref,
|
||||
rojo_ref: rojo_child_ref,
|
||||
});
|
||||
}
|
||||
None => {
|
||||
let cloned_ref = rojo_inner.clone_into_external(rojo_child_ref, &mut base);
|
||||
base.transfer_within(cloned_ref, pair.base_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete unmatched base children if ignore_unknown_instances is false.
|
||||
let ignore_unknown = rojo
|
||||
.get_metadata(pair.rojo_ref)
|
||||
.map(|m| m.ignore_unknown_instances)
|
||||
.unwrap_or(true);
|
||||
|
||||
if !ignore_unknown {
|
||||
for (idx, &base_child_ref) in base_children.iter().enumerate() {
|
||||
if !base_matched[idx] {
|
||||
base.destroy(base_child_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
/// Overlays Rojo properties onto a base instance. Properties that exist only
|
||||
/// in the base instance are preserved; properties from Rojo overwrite any
|
||||
/// existing base values.
|
||||
fn update_properties(base: &mut WeakDom, base_ref: Ref, rojo_instance: &Instance) {
|
||||
let base_instance = base
|
||||
.get_by_ref_mut(base_ref)
|
||||
.expect("Base instance should exist");
|
||||
|
||||
base_instance.name.clone_from(&rojo_instance.name);
|
||||
|
||||
for (prop_name, prop_value) in &rojo_instance.properties {
|
||||
base_instance
|
||||
.properties
|
||||
.insert(*prop_name, prop_value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::snapshot::{InstanceMetadata, InstanceSnapshot, RojoTree};
|
||||
use rbx_dom_weak::{types::Variant, ustr, InstanceBuilder};
|
||||
|
||||
#[test]
|
||||
fn merge_adds_new_children() {
|
||||
let mut base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("ReplicatedStorage").with_name("ReplicatedStorage"),
|
||||
);
|
||||
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("Game")
|
||||
.class_name("DataModel")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("ReplicatedStorage")
|
||||
.class_name("ReplicatedStorage")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("NewScript")
|
||||
.class_name("ModuleScript")])]),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let root_children = merged.root().children();
|
||||
assert_eq!(root_children.len(), 1);
|
||||
|
||||
let rs = merged.get_by_ref(root_children[0]).unwrap();
|
||||
assert_eq!(rs.name, "ReplicatedStorage");
|
||||
assert_eq!(rs.children().len(), 1);
|
||||
|
||||
let child = merged.get_by_ref(rs.children()[0]).unwrap();
|
||||
assert_eq!(child.name, "NewScript");
|
||||
assert_eq!(child.class.as_str(), "ModuleScript");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_deletes_unknown_when_not_ignored() {
|
||||
let mut base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let rs_ref = base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("ReplicatedStorage").with_name("ReplicatedStorage"),
|
||||
);
|
||||
base.insert(
|
||||
rs_ref,
|
||||
InstanceBuilder::new("ModuleScript").with_name("OldScript"),
|
||||
);
|
||||
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("Game")
|
||||
.class_name("DataModel")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("ReplicatedStorage")
|
||||
.class_name("ReplicatedStorage")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(false))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("NewScript")
|
||||
.class_name("ModuleScript")])]),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let root_children = merged.root().children();
|
||||
let rs = merged.get_by_ref(root_children[0]).unwrap();
|
||||
assert_eq!(rs.children().len(), 1, "OldScript should have been deleted");
|
||||
|
||||
let child = merged.get_by_ref(rs.children()[0]).unwrap();
|
||||
assert_eq!(child.name, "NewScript");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_preserves_unknown_when_ignored() {
|
||||
let mut base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let rs_ref = base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("ReplicatedStorage").with_name("ReplicatedStorage"),
|
||||
);
|
||||
base.insert(
|
||||
rs_ref,
|
||||
InstanceBuilder::new("ModuleScript").with_name("OldScript"),
|
||||
);
|
||||
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("Game")
|
||||
.class_name("DataModel")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("ReplicatedStorage")
|
||||
.class_name("ReplicatedStorage")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("NewScript")
|
||||
.class_name("ModuleScript")])]),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let root_children = merged.root().children();
|
||||
let rs = merged.get_by_ref(root_children[0]).unwrap();
|
||||
assert_eq!(
|
||||
rs.children().len(),
|
||||
2,
|
||||
"Both OldScript and NewScript should be present"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_preserves_unmanaged_services() {
|
||||
let mut base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("ReplicatedStorage").with_name("ReplicatedStorage"),
|
||||
);
|
||||
base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("Workspace").with_name("Workspace"),
|
||||
);
|
||||
|
||||
// Rojo project only manages ReplicatedStorage, not Workspace
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("Game")
|
||||
.class_name("DataModel")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("ReplicatedStorage")
|
||||
.class_name("ReplicatedStorage")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))]),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let root_children = merged.root().children();
|
||||
assert_eq!(
|
||||
root_children.len(),
|
||||
2,
|
||||
"Both ReplicatedStorage and Workspace should be present"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_overlays_properties() {
|
||||
let mut base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
base.insert(
|
||||
base.root_ref(),
|
||||
InstanceBuilder::new("ModuleScript")
|
||||
.with_name("Script")
|
||||
.with_property("Source", Variant::String("-- base source".into()))
|
||||
.with_property("Disabled", Variant::Bool(true)),
|
||||
);
|
||||
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("Game")
|
||||
.class_name("DataModel")
|
||||
.metadata(InstanceMetadata::new().ignore_unknown_instances(true))
|
||||
.children(vec![InstanceSnapshot::new()
|
||||
.name("Script")
|
||||
.class_name("ModuleScript")
|
||||
.property(ustr("Source"), Variant::String("-- rojo source".into()))]),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let script = merged.get_by_ref(merged.root().children()[0]).unwrap();
|
||||
|
||||
// Rojo property overwrites base
|
||||
assert_eq!(
|
||||
script.properties.get(&ustr("Source")),
|
||||
Some(&Variant::String("-- rojo source".into()))
|
||||
);
|
||||
// Base-only property is preserved
|
||||
assert_eq!(
|
||||
script.properties.get(&ustr("Disabled")),
|
||||
Some(&Variant::Bool(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_model_clones_into_base() {
|
||||
let base = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
|
||||
let rojo = RojoTree::new(
|
||||
InstanceSnapshot::new()
|
||||
.name("MyModel")
|
||||
.class_name("Folder"),
|
||||
);
|
||||
|
||||
let merged = merge_rojo_into_base(base, &rojo).unwrap();
|
||||
|
||||
let root_children = merged.root().children();
|
||||
assert_eq!(root_children.len(), 1);
|
||||
|
||||
let model = merged.get_by_ref(root_children[0]).unwrap();
|
||||
assert_eq!(model.name, "MyModel");
|
||||
assert_eq!(model.class.as_str(), "Folder");
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,27 @@ gen_build_tests! {
|
|||
no_name_top_level_project,
|
||||
}
|
||||
|
||||
macro_rules! gen_build_base_tests {
|
||||
( $($test_name: ident, $base_file: expr, $output_ext: expr,)* ) => {
|
||||
$(
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<build_ $test_name>]() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
run_build_base_test(stringify!($test_name), $base_file, $output_ext);
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
gen_build_base_tests! {
|
||||
base_merge_place, "base.rbxlx", "rbxlx",
|
||||
base_merge_delete, "base.rbxlx", "rbxlx",
|
||||
base_merge_model, "base.rbxmx", "rbxmx",
|
||||
}
|
||||
|
||||
fn run_build_test(test_name: &str) {
|
||||
let working_dir = get_working_dir_path();
|
||||
|
||||
|
|
@ -107,3 +128,49 @@ fn run_build_test(test_name: &str) {
|
|||
assert_snapshot!(test_name, contents);
|
||||
});
|
||||
}
|
||||
|
||||
fn run_build_base_test(test_name: &str, base_file: &str, output_ext: &str) {
|
||||
let working_dir = get_working_dir_path();
|
||||
|
||||
let input_path = Path::new(BUILD_TESTS_PATH).join(test_name);
|
||||
let base_path = input_path.join(base_file);
|
||||
|
||||
let output_dir = tempdir().expect("couldn't create temporary directory");
|
||||
let output_path = output_dir
|
||||
.path()
|
||||
.join(format!("{}.{}", test_name, output_ext));
|
||||
|
||||
let output = Command::new(ROJO_PATH)
|
||||
.args([
|
||||
"build",
|
||||
input_path.to_str().unwrap(),
|
||||
"-o",
|
||||
output_path.to_str().unwrap(),
|
||||
"--base",
|
||||
base_path.to_str().unwrap(),
|
||||
])
|
||||
.env("RUST_LOG", "error")
|
||||
.current_dir(working_dir)
|
||||
.output()
|
||||
.expect("Couldn't start Rojo");
|
||||
|
||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
||||
eprint!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
|
||||
assert!(output.status.success(), "Rojo did not exit successfully");
|
||||
|
||||
let contents = fs::read_to_string(&output_path).expect("Couldn't read output file");
|
||||
|
||||
let mut settings = insta::Settings::new();
|
||||
|
||||
let snapshot_path = Path::new(BUILD_TESTS_PATH)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("build-test-snapshots");
|
||||
|
||||
settings.set_snapshot_path(snapshot_path);
|
||||
|
||||
settings.bind(|| {
|
||||
assert_snapshot!(test_name, contents);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue