mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Implement incremental syncback mode to preserve existing file structure
- Added an `incremental` flag to the `SyncbackCommand` to allow for preserving existing file structure and middleware formats during syncback operations. - Enhanced the `syncback_loop` function to differentiate between clean mode and incremental mode, ensuring orphaned files are removed only in clean mode. - Updated logging to reflect the mode of operation during syncback. - Introduced new tests to validate the behavior of clean mode versus incremental mode, ensuring proper handling of orphaned files and preservation of existing structures.
This commit is contained in:
parent
0e5e809894
commit
58399bb863
39 changed files with 652 additions and 119 deletions
|
|
@ -2,4 +2,4 @@
|
|||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
|
||||
Removing C:/Users/Joe/AppData/Local/Temp/.tmpGY4Eb8/ignore_trees_removing/src
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ expression: "String::from_utf8_lossy(&output.stdout)"
|
|||
---
|
||||
Writing src/modules/ClientModule.luau
|
||||
Writing src/modules/ServerModule.luau
|
||||
Removing C:/Users/Joe/AppData/Local/Temp/.tmpLKd1Ar/nested_projects_weird/client-only.project.json5
|
||||
Removing C:/Users/Joe/AppData/Local/Temp/.tmpLKd1Ar/nested_projects_weird/server-only.project.json5
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@ source: tests/rojo_test/syncback_util.rs
|
|||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
||||
Writing src/ChildWithDuplicates
|
||||
Writing src/ChildWithoutDuplicates
|
||||
Writing src/ChildWithoutDuplicates/Child
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ expression: "String::from_utf8_lossy(&output.stdout)"
|
|||
---
|
||||
Writing src/Pointer_1.model.json5
|
||||
Writing src/Pointer_2.model.json5
|
||||
Writing src/Target.model.json5
|
||||
Writing src/Target/init.meta.json5
|
||||
Writing src/Target
|
||||
Removing C:/Users/Joe/AppData/Local/Temp/.tmpA4y8vf/ref_properties_duplicate/src/Target.model.json5
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ expression: container.model.json5
|
|||
{
|
||||
attributes: {
|
||||
Rojo_Id: "value_1",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_1",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_1"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
name: "value_1",
|
||||
name: "value_1"
|
||||
},
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Id: "value_1",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_2",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/container/value_2"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
name: "value_2",
|
||||
},
|
||||
name: "value_2"
|
||||
}
|
||||
],
|
||||
className: "Folder",
|
||||
className: "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ expression: src/int_value.model.json5
|
|||
{
|
||||
className: "IntValue",
|
||||
properties: {
|
||||
Value: 1337,
|
||||
},
|
||||
Value: 1337
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ expression: src/Message.model.json5
|
|||
{
|
||||
className: "Message",
|
||||
properties: {
|
||||
Text: "This message should be written to the disk.",
|
||||
},
|
||||
Text: "This message should be written to the disk."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ expression: src/dir_with_meta/init.meta.json5
|
|||
className: "Configuration",
|
||||
properties: {
|
||||
Tags: [
|
||||
"This tag on dir_with_meta",
|
||||
],
|
||||
},
|
||||
"This tag on dir_with_meta"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ expression: src/model_json.model.json5
|
|||
{
|
||||
className: "StringValue",
|
||||
properties: {
|
||||
Value: "This text is model_json",
|
||||
},
|
||||
Value: "This text is model_json"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ expression: src/project_json.project.json5
|
|||
Value: [
|
||||
1337,
|
||||
-1337,
|
||||
1337,
|
||||
],
|
||||
},
|
||||
},
|
||||
1337
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ expression: nested.project.json5
|
|||
BoolValue: {
|
||||
$className: "BoolValue",
|
||||
$properties: {
|
||||
Value: true,
|
||||
},
|
||||
Value: true
|
||||
}
|
||||
},
|
||||
StringValue: {
|
||||
$path: "string_value.txt",
|
||||
},
|
||||
},
|
||||
$path: "string_value.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ expression: property_mismatch.project.json5
|
|||
$className: "BrickColorValue",
|
||||
$properties: {
|
||||
Value: {
|
||||
BrickColor: 345,
|
||||
},
|
||||
},
|
||||
},
|
||||
BrickColor: 345
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/pointer.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/target",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/target"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/target.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Id: "test referent id",
|
||||
Rojo_Id: "test referent id"
|
||||
},
|
||||
className: "Folder",
|
||||
className: "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/pointer.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/target",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/target"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer_2.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target_2",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target_2"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/Target_2.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Id: "identical ID",
|
||||
Rojo_Id: "identical ID"
|
||||
},
|
||||
className: "Folder",
|
||||
className: "Folder"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer1.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target1",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target1"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer2.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target2",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target2"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ expression: src/Pointer3.model.json5
|
|||
---
|
||||
{
|
||||
attributes: {
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target3",
|
||||
Rojo_Ref_Value: "ReplicatedStorage/Target3"
|
||||
},
|
||||
className: "ObjectValue",
|
||||
className: "ObjectValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ expression: default.project.json5
|
|||
project_node: {
|
||||
$className: "BoolValue",
|
||||
$properties: {
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ expression: src/model_json.model.json5
|
|||
{
|
||||
className: "StringValue",
|
||||
properties: {
|
||||
Value: "This should be a .model.json file",
|
||||
},
|
||||
Value: "This should be a .model.json file"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ expression: default.project.json5
|
|||
tree: {
|
||||
$className: "Folder",
|
||||
src: {
|
||||
$path: "src",
|
||||
$path: "src"
|
||||
},
|
||||
trigger_reserialization: {
|
||||
$className: "BoolValue",
|
||||
$properties: {
|
||||
Value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ source: tests/tests/syncback.rs
|
|||
expression: src/adjacent.meta.json5
|
||||
---
|
||||
{
|
||||
$schema: "rojo/adjacent-meta",
|
||||
attributes: {
|
||||
trigger_reserialization: true,
|
||||
},
|
||||
trigger_reserialization: true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ source: tests/tests/syncback.rs
|
|||
expression: src/init/init.meta.json5
|
||||
---
|
||||
{
|
||||
$schema: "rojo/init-meta",
|
||||
attributes: {
|
||||
trigger_reserialization: true,
|
||||
},
|
||||
trigger_reserialization: true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,5 @@ source: tests/tests/syncback.rs
|
|||
expression: src/model.model.json5
|
||||
---
|
||||
{
|
||||
$schema: "rojo/model.json",
|
||||
className: "BoolValue",
|
||||
className: "BoolValue"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ expression: default.project.json5
|
|||
inside_project_file: {
|
||||
$className: "StringValue",
|
||||
$properties: {
|
||||
Value: "imgettingverytiredofwritingthesetests2",
|
||||
},
|
||||
Value: "imgettingverytiredofwritingthesetests2"
|
||||
}
|
||||
},
|
||||
on_file_system: {
|
||||
$attributes: {
|
||||
imgettingverytiredofwritingthesetests: "person299 was ahead of his time",
|
||||
imgettingverytiredofwritingthesetests: "person299 was ahead of his time"
|
||||
},
|
||||
$path: "string_value.txt",
|
||||
},
|
||||
},
|
||||
$path: "string_value.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ expression: default.project.json5
|
|||
{
|
||||
name: "unscriptable_properties",
|
||||
syncbackRules: {
|
||||
syncUnscriptable: true,
|
||||
syncUnscriptable: true
|
||||
},
|
||||
tree: {
|
||||
$className: "BinaryStringValue",
|
||||
$properties: {
|
||||
Value: {
|
||||
BinaryString: "Rojo/is/cool",
|
||||
},
|
||||
},
|
||||
},
|
||||
BinaryString: "Rojo/is/cool"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "clean_mode_orphan_removal",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"ReplicatedStorage": {
|
||||
"$className": "ReplicatedStorage",
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- This script should be KEPT because the input model
|
||||
-- has a ModuleScript with the same name "KeptScript"
|
||||
print("I should be kept")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- This nested script should also be REMOVED
|
||||
-- when its parent folder is removed
|
||||
print("I am also orphaned")
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- This script should be REMOVED in clean mode
|
||||
-- because the input model doesn't have this script
|
||||
print("I am orphaned")
|
||||
|
|
@ -54,6 +54,12 @@ pub struct SyncbackCommand {
|
|||
/// If provided, the prompt for writing to the file system is skipped.
|
||||
#[clap(long, short = 'y')]
|
||||
pub non_interactive: bool,
|
||||
|
||||
/// If provided, syncback will preserve existing file structure and middleware
|
||||
/// formats when possible. Without this flag (default), syncback creates a fresh
|
||||
/// project layout that exactly matches the input file, removing any orphaned files.
|
||||
#[clap(long, short = 'n')]
|
||||
pub incremental: bool,
|
||||
}
|
||||
|
||||
impl SyncbackCommand {
|
||||
|
|
@ -99,12 +105,17 @@ impl SyncbackCommand {
|
|||
}
|
||||
|
||||
let syncback_timer = Instant::now();
|
||||
eprintln!("Beginning syncback...");
|
||||
if self.incremental {
|
||||
eprintln!("Beginning incremental syncback...");
|
||||
} else {
|
||||
eprintln!("Beginning syncback (clean mode)...");
|
||||
}
|
||||
let snapshot = syncback_loop(
|
||||
session_old.vfs(),
|
||||
&mut dom_old,
|
||||
dom_new,
|
||||
session_old.root_project(),
|
||||
self.incremental,
|
||||
)?;
|
||||
log::debug!(
|
||||
"Syncback finished in {:.02}s!",
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ use rbx_dom_weak::{
|
|||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
env,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
|
|
@ -74,6 +74,7 @@ pub fn syncback_loop(
|
|||
old_tree: &mut RojoTree,
|
||||
mut new_tree: WeakDom,
|
||||
project: &Project,
|
||||
incremental: bool,
|
||||
) -> anyhow::Result<FsSnapshot> {
|
||||
let ignore_patterns = project
|
||||
.syncback_rules
|
||||
|
|
@ -192,13 +193,40 @@ pub fn syncback_loop(
|
|||
|
||||
let project_path = project.folder_location();
|
||||
|
||||
// In clean mode, collect all existing paths from old tree for cleanup.
|
||||
// These will be compared against new paths to remove orphaned files.
|
||||
let existing_paths: HashSet<PathBuf> = if !incremental {
|
||||
log::debug!("Clean mode: collecting existing paths for cleanup");
|
||||
let mut paths = HashSet::new();
|
||||
for ref_id in descendants(old_tree.inner(), old_tree.get_root_id()) {
|
||||
if let Some(inst) = old_tree.get_instance(ref_id) {
|
||||
for path in inst.metadata().relevant_paths.iter() {
|
||||
// Skip paths matching ignore patterns
|
||||
if is_valid_path(&ignore_patterns, project_path, path) {
|
||||
// Only include paths that actually exist on disk
|
||||
if path.exists() {
|
||||
paths.insert(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("Collected {} existing paths", paths.len());
|
||||
paths
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
let syncback_data = SyncbackData {
|
||||
vfs,
|
||||
old_tree,
|
||||
new_tree: &new_tree,
|
||||
project,
|
||||
incremental,
|
||||
};
|
||||
|
||||
// Always start with old reference for the Project middleware.
|
||||
// In clean mode, child snapshots will have old=None (handled by SyncbackSnapshot methods).
|
||||
let mut snapshots = vec![SyncbackSnapshot {
|
||||
data: syncback_data,
|
||||
old: Some(old_tree.get_root_id()),
|
||||
|
|
@ -211,19 +239,22 @@ pub fn syncback_loop(
|
|||
|
||||
'syncback: while let Some(snapshot) = snapshots.pop() {
|
||||
let inst_path = snapshot.get_new_inst_path(snapshot.new);
|
||||
// We can quickly check that two subtrees are identical and if they are,
|
||||
// skip reconciling them.
|
||||
if let Some(old_ref) = snapshot.old {
|
||||
match (old_hashes.get(&old_ref), new_hashes.get(&snapshot.new)) {
|
||||
(Some(old), Some(new)) => {
|
||||
if old == new {
|
||||
log::trace!(
|
||||
"Skipping {inst_path} due to it being identically hashed as {old:?}"
|
||||
);
|
||||
continue;
|
||||
// In incremental mode, we can quickly check that two subtrees are identical
|
||||
// and if they are, skip reconciling them. In clean mode, we always process
|
||||
// all instances to ensure fresh structure.
|
||||
if incremental {
|
||||
if let Some(old_ref) = snapshot.old {
|
||||
match (old_hashes.get(&old_ref), new_hashes.get(&snapshot.new)) {
|
||||
(Some(old), Some(new)) => {
|
||||
if old == new {
|
||||
log::trace!(
|
||||
"Skipping {inst_path} due to it being identically hashed as {old:?}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => unreachable!("All Instances in both DOMs should have hashes"),
|
||||
}
|
||||
_ => unreachable!("All Instances in both DOMs should have hashes"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -331,6 +362,91 @@ pub fn syncback_loop(
|
|||
snapshots.extend(syncback.children);
|
||||
}
|
||||
|
||||
// In clean mode, remove any existing paths that weren't overwritten by new structure.
|
||||
// This ensures no orphaned files remain from the old project layout.
|
||||
if !incremental && !existing_paths.is_empty() {
|
||||
log::debug!("Clean mode: checking for orphaned files to remove");
|
||||
|
||||
// Helper to normalize a path by stripping UNC prefix on Windows
|
||||
// This is needed because paths may have different prefixes (\\?\ vs regular)
|
||||
fn normalize_path(p: &Path) -> PathBuf {
|
||||
// Strip Windows UNC prefix if present
|
||||
let path_str = p.to_string_lossy();
|
||||
if path_str.starts_with(r"\\?\") || path_str.starts_with("//?/") {
|
||||
PathBuf::from(&path_str[4..])
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
// Convert added_paths to absolute paths by joining with project base path.
|
||||
// fs_snapshot paths are relative to the project folder.
|
||||
let added_paths: HashSet<PathBuf> = fs_snapshot
|
||||
.added_paths()
|
||||
.into_iter()
|
||||
.map(|p| normalize_path(&project_path.join(p)))
|
||||
.collect();
|
||||
|
||||
// Get the project file path to exclude it from removal
|
||||
let project_file = normalize_path(&project.file_location);
|
||||
|
||||
// First pass: collect normalized paths to remove (deduplicated)
|
||||
let mut paths_to_remove: HashSet<PathBuf> = HashSet::new();
|
||||
for old_path in &existing_paths {
|
||||
let old_path_norm = normalize_path(old_path);
|
||||
|
||||
// Never remove the project file itself
|
||||
if old_path_norm == project_file {
|
||||
log::debug!("Skipping project file: {}", old_path.display());
|
||||
continue;
|
||||
}
|
||||
// Skip if this exact path is being written to
|
||||
if added_paths.contains(&old_path_norm) {
|
||||
continue;
|
||||
}
|
||||
// Skip if a parent of this path is being added
|
||||
// (e.g., the new structure will replace it)
|
||||
if added_paths
|
||||
.iter()
|
||||
.any(|added| old_path_norm.starts_with(added))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Skip if this path is an ancestor of something being written to
|
||||
// (e.g., a directory that will contain new files)
|
||||
if added_paths
|
||||
.iter()
|
||||
.any(|added| added.starts_with(&old_path_norm))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Use normalized path to avoid duplicates from UNC vs regular paths
|
||||
paths_to_remove.insert(old_path_norm);
|
||||
}
|
||||
|
||||
// Second pass: only remove top-level orphaned paths.
|
||||
// Skip any path that is inside another path we're removing,
|
||||
// since remove_dir_all will handle the contents.
|
||||
let paths_vec: Vec<_> = paths_to_remove.iter().collect();
|
||||
for old_path in &paths_vec {
|
||||
// Check if this path is inside another path we're removing
|
||||
let is_nested = paths_vec
|
||||
.iter()
|
||||
.any(|other| *other != *old_path && old_path.starts_with(*other));
|
||||
|
||||
if is_nested {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("Removing orphaned path: {}", old_path.display());
|
||||
if old_path.is_dir() {
|
||||
fs_snapshot.remove_dir(old_path);
|
||||
} else {
|
||||
fs_snapshot.remove_file(old_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(fs_snapshot)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ pub struct SyncbackData<'sync> {
|
|||
pub(super) old_tree: &'sync RojoTree,
|
||||
pub(super) new_tree: &'sync WeakDom,
|
||||
pub(super) project: &'sync Project,
|
||||
/// When true, preserves existing file structure and middleware formats.
|
||||
/// When false (clean mode), all child instances are treated as new.
|
||||
pub(super) incremental: bool,
|
||||
}
|
||||
|
||||
pub struct SyncbackSnapshot<'sync> {
|
||||
|
|
@ -35,11 +38,16 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
/// Constructs a SyncbackSnapshot from the provided refs
|
||||
/// while inheriting this snapshot's path and data. This should be used for
|
||||
/// directories.
|
||||
///
|
||||
/// In clean mode (incremental=false), old_ref is ignored and set to None,
|
||||
/// ensuring fresh filenames and middleware choices for all children.
|
||||
#[inline]
|
||||
pub fn with_joined_path(&self, new_ref: Ref, old_ref: Option<Ref>) -> anyhow::Result<Self> {
|
||||
// In clean mode, ignore old_ref to ensure fresh structure
|
||||
let effective_old_ref = if self.data.incremental { old_ref } else { None };
|
||||
let mut snapshot = Self {
|
||||
data: self.data,
|
||||
old: old_ref,
|
||||
old: effective_old_ref,
|
||||
new: new_ref,
|
||||
path: PathBuf::new(),
|
||||
middleware: None,
|
||||
|
|
@ -62,6 +70,9 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
///
|
||||
/// The actual path of the snapshot is made by getting a file name for the
|
||||
/// snapshot and then appending it to the provided base path.
|
||||
///
|
||||
/// In clean mode (incremental=false), old_ref is ignored and set to None,
|
||||
/// ensuring fresh filenames and middleware choices for all children.
|
||||
#[inline]
|
||||
pub fn with_base_path(
|
||||
&self,
|
||||
|
|
@ -69,9 +80,11 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
new_ref: Ref,
|
||||
old_ref: Option<Ref>,
|
||||
) -> anyhow::Result<Self> {
|
||||
// In clean mode, ignore old_ref to ensure fresh structure
|
||||
let effective_old_ref = if self.data.incremental { old_ref } else { None };
|
||||
let mut snapshot = Self {
|
||||
data: self.data,
|
||||
old: old_ref,
|
||||
old: effective_old_ref,
|
||||
new: new_ref,
|
||||
path: PathBuf::new(),
|
||||
middleware: None,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const INPUT_FILE_PROJECT: &str = "input-project";
|
|||
const INPUT_FILE_PLACE: &str = "input.rbxl";
|
||||
const INPUT_FILE_MODEL: &str = "input.rbxm";
|
||||
|
||||
/// Convenience method to run a `rojo syncback` test.
|
||||
/// Convenience method to run a `rojo syncback` test in clean mode (default).
|
||||
///
|
||||
/// Test projects should be defined in the `syncback-tests` folder; their filename
|
||||
/// should be given as the first parameter.
|
||||
|
|
@ -19,6 +19,18 @@ const INPUT_FILE_MODEL: &str = "input.rbxm";
|
|||
/// The passed in callback is where the actual test body should go. Setup and
|
||||
/// cleanup happens automatically.
|
||||
pub fn run_syncback_test(name: &str, callback: impl FnOnce(&Path)) {
|
||||
run_syncback_test_impl(name, false, callback)
|
||||
}
|
||||
|
||||
/// Convenience method to run a `rojo syncback` test in incremental mode.
|
||||
///
|
||||
/// Use this for tests that depend on preserving existing file structure and
|
||||
/// middleware formats.
|
||||
pub fn run_syncback_test_incremental(name: &str, callback: impl FnOnce(&Path)) {
|
||||
run_syncback_test_impl(name, true, callback)
|
||||
}
|
||||
|
||||
fn run_syncback_test_impl(name: &str, incremental: bool, callback: impl FnOnce(&Path)) {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// let working_dir = get_working_dir_path();
|
||||
|
|
@ -55,21 +67,27 @@ pub fn run_syncback_test(name: &str, callback: impl FnOnce(&Path)) {
|
|||
.expect("Couldn't copy project to temporary directory");
|
||||
};
|
||||
|
||||
let mut args = vec![
|
||||
"--color",
|
||||
"never",
|
||||
"syncback",
|
||||
project_path.to_str().unwrap(),
|
||||
"--input",
|
||||
input_file.to_str().unwrap(),
|
||||
"--non-interactive",
|
||||
"--list",
|
||||
];
|
||||
|
||||
if incremental {
|
||||
args.push("--incremental");
|
||||
}
|
||||
|
||||
let output = Command::new(ROJO_PATH)
|
||||
// I don't really understand why setting the working directory breaks this, but it does.
|
||||
// It's a bit concerning but I'm more interested in writing tests than debugging it right now.
|
||||
// TODO: Figure out why and fix it.
|
||||
// .current_dir(working_dir)
|
||||
.args([
|
||||
"--color",
|
||||
"never",
|
||||
"syncback",
|
||||
project_path.to_str().unwrap(),
|
||||
"--input",
|
||||
input_file.to_str().unwrap(),
|
||||
"--non-interactive",
|
||||
"--list",
|
||||
])
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Couldn't spawn syncback process");
|
||||
|
||||
|
|
|
|||
322
tests/tests/clean_mode.rs
Normal file
322
tests/tests/clean_mode.rs
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
//! Tests for clean mode (default) vs incremental mode behavior.
|
||||
//!
|
||||
//! These tests verify that:
|
||||
//! 1. Clean mode removes orphaned files
|
||||
//! 2. Clean mode doesn't remove files that are being written to
|
||||
//! 3. Clean mode doesn't remove parent directories of files being written
|
||||
//! 4. Incremental mode preserves existing file structure
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::rojo_test::io_util::{copy_recursive, ROJO_PATH};
|
||||
|
||||
const SYNCBACK_TESTS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/rojo-test/syncback-tests");
|
||||
|
||||
/// Helper to run syncback and return success status
|
||||
fn run_syncback(project_path: &Path, input_path: &Path, incremental: bool) -> bool {
|
||||
let mut args = vec![
|
||||
"--color",
|
||||
"never",
|
||||
"syncback",
|
||||
project_path.to_str().unwrap(),
|
||||
"--input",
|
||||
input_path.to_str().unwrap(),
|
||||
"--non-interactive",
|
||||
];
|
||||
|
||||
if incremental {
|
||||
args.push("--incremental");
|
||||
}
|
||||
|
||||
let output = Command::new(ROJO_PATH)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("Couldn't spawn syncback process");
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("Syncback failed!");
|
||||
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
output.status.success()
|
||||
}
|
||||
|
||||
/// Test that clean mode removes orphaned files that don't exist in the input
|
||||
#[test]
|
||||
fn clean_mode_removes_orphaned_files() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// Use the sync_rules test case and add an orphaned file
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input.rbxm");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
// Create an orphaned file that doesn't match anything in the input
|
||||
let orphaned_file = project_path.join("src").join("OrphanedFile.luau");
|
||||
fs_err::write(&orphaned_file, "-- This is orphaned").expect("Couldn't create orphaned file");
|
||||
|
||||
assert!(
|
||||
orphaned_file.exists(),
|
||||
"Orphaned file should exist before syncback"
|
||||
);
|
||||
|
||||
// Run syncback in clean mode (default)
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
// The orphaned file should be removed in clean mode
|
||||
// (the input doesn't have "OrphanedFile" script)
|
||||
assert!(
|
||||
!orphaned_file.exists(),
|
||||
"Orphaned file should be removed in clean mode"
|
||||
);
|
||||
|
||||
// The expected files should exist
|
||||
let module_file = project_path.join("src").join("module.luau");
|
||||
assert!(
|
||||
module_file.exists(),
|
||||
"module.luau should be created by syncback"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that clean mode doesn't remove files that are being written to
|
||||
#[test]
|
||||
fn clean_mode_preserves_written_files() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// Use the child_but_not test case
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("child_but_not")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("child_but_not")
|
||||
.join("input.rbxl");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
// Run syncback in clean mode
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
// The directories being written to should exist
|
||||
let only_one_copy = project_path.join("OnlyOneCopy");
|
||||
let replicated_storage = project_path.join("ReplicatedStorage");
|
||||
|
||||
assert!(
|
||||
only_one_copy.exists(),
|
||||
"OnlyOneCopy directory should exist (it's being written to)"
|
||||
);
|
||||
assert!(
|
||||
replicated_storage.exists(),
|
||||
"ReplicatedStorage directory should exist (it's being written to)"
|
||||
);
|
||||
|
||||
// The files inside should exist
|
||||
assert!(
|
||||
only_one_copy.join("child_of_one.luau").exists(),
|
||||
"child_of_one.luau should exist"
|
||||
);
|
||||
assert!(
|
||||
replicated_storage
|
||||
.join("child_replicated_storage.luau")
|
||||
.exists(),
|
||||
"child_replicated_storage.luau should exist"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that incremental mode preserves existing file structure
|
||||
#[test]
|
||||
fn incremental_mode_preserves_structure() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// Use the sync_rules test case - it has custom extensions
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input.rbxm");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
// Verify the custom-extension files exist before syncback
|
||||
let modulescript = project_path.join("src").join("module.modulescript");
|
||||
let text = project_path.join("src").join("text.text");
|
||||
assert!(
|
||||
modulescript.exists(),
|
||||
"module.modulescript should exist before syncback"
|
||||
);
|
||||
assert!(text.exists(), "text.text should exist before syncback");
|
||||
|
||||
// Run syncback in incremental mode
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, true),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
// In incremental mode, the custom extensions should be preserved
|
||||
assert!(
|
||||
modulescript.exists(),
|
||||
"module.modulescript should be preserved in incremental mode"
|
||||
);
|
||||
assert!(
|
||||
text.exists(),
|
||||
"text.text should be preserved in incremental mode"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that clean mode uses fresh file extensions (not preserving old ones)
|
||||
#[test]
|
||||
fn clean_mode_uses_fresh_extensions() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
// Use the sync_rules test case - it has custom extensions
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input.rbxm");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
// Run syncback in clean mode (default)
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
// In clean mode, the files should use standard extensions
|
||||
let modulescript_old = project_path.join("src").join("module.modulescript");
|
||||
let text_old = project_path.join("src").join("text.text");
|
||||
let module_new = project_path.join("src").join("module.luau");
|
||||
let text_new = project_path.join("src").join("text.txt");
|
||||
|
||||
// Old extensions should NOT exist
|
||||
assert!(
|
||||
!modulescript_old.exists(),
|
||||
"module.modulescript should NOT exist in clean mode"
|
||||
);
|
||||
assert!(
|
||||
!text_old.exists(),
|
||||
"text.text should NOT exist in clean mode"
|
||||
);
|
||||
|
||||
// New standard extensions SHOULD exist
|
||||
assert!(
|
||||
module_new.exists(),
|
||||
"module.luau should exist in clean mode"
|
||||
);
|
||||
assert!(text_new.exists(), "text.txt should exist in clean mode");
|
||||
}
|
||||
|
||||
/// Test that clean mode doesn't remove the project file
|
||||
#[test]
|
||||
fn clean_mode_preserves_project_file() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("ignore_trees_adding")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("ignore_trees_adding")
|
||||
.join("input.rbxl");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
let project_file = project_path.join("default.project.json5");
|
||||
assert!(
|
||||
project_file.exists(),
|
||||
"Project file should exist before syncback"
|
||||
);
|
||||
|
||||
// Run syncback in clean mode
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
// Project file should still exist
|
||||
assert!(
|
||||
project_file.exists(),
|
||||
"Project file should be preserved in clean mode"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that running clean mode twice produces identical results
|
||||
#[test]
|
||||
fn clean_mode_is_idempotent() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("child_but_not")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("child_but_not")
|
||||
.join("input.rbxl");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
// Run syncback twice in clean mode
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"First syncback should succeed"
|
||||
);
|
||||
|
||||
// Capture state after first run
|
||||
let file1 = project_path.join("OnlyOneCopy").join("child_of_one.luau");
|
||||
let content1 = fs_err::read_to_string(&file1).unwrap();
|
||||
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Second syncback should succeed"
|
||||
);
|
||||
|
||||
// File should still exist with same content
|
||||
assert!(
|
||||
file1.exists(),
|
||||
"File should still exist after second syncback"
|
||||
);
|
||||
let content2 = fs_err::read_to_string(&file1).unwrap();
|
||||
assert_eq!(
|
||||
content1, content2,
|
||||
"File content should be identical after second syncback"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
mod build;
|
||||
mod clean_mode;
|
||||
mod serve;
|
||||
mod syncback;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use std::ffi::OsStr;
|
|||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::rojo_test::syncback_util::{run_syncback_test, snapshot_rbxm};
|
||||
use crate::rojo_test::syncback_util::{
|
||||
run_syncback_test, run_syncback_test_incremental, snapshot_rbxm,
|
||||
};
|
||||
|
||||
macro_rules! syncback_tests {
|
||||
($($test_name:ident => $list:expr$(,)?),*) => {$(
|
||||
|
|
@ -28,27 +30,47 @@ macro_rules! syncback_tests {
|
|||
)*};
|
||||
}
|
||||
|
||||
/// Tests that need to run in incremental mode (preserving existing structure)
|
||||
macro_rules! syncback_tests_incremental {
|
||||
($($test_name:ident => $list:expr$(,)?),*) => {$(
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
run_syncback_test_incremental(stringify!($test_name), |path| {
|
||||
for name in $list {
|
||||
let snapshot_name = format!(concat!(stringify!($test_name), "-{}"), name);
|
||||
let new = path.join::<&str>(name);
|
||||
if !new.exists() {
|
||||
panic!("the path stub '{}' does not exist after syncback runs. consider double checking for typos.", name);
|
||||
}
|
||||
if let Some("rbxm") = new.extension().and_then(OsStr::to_str) {
|
||||
let content = fs_err::read(new).unwrap();
|
||||
snapshot_rbxm(&snapshot_name, content, name);
|
||||
} else {
|
||||
let content = fs_err::read_to_string(new).unwrap();
|
||||
assert_snapshot!(snapshot_name, content, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
// Tests that run in clean mode (default)
|
||||
syncback_tests! {
|
||||
// Ensures that there's only one copy written to disk if navigating a
|
||||
// project file might yield two copies
|
||||
child_but_not => ["OnlyOneCopy/child_of_one.luau", "ReplicatedStorage/child_replicated_storage.luau"],
|
||||
// Ensures that syncback works with CSVs
|
||||
csv => ["src/csv_init/init.csv", "src/csv.csv"],
|
||||
// Ensures that if a RojoId is duplicated somewhere in the project, it's
|
||||
// rewritten rather than synced back as a conflict
|
||||
duplicate_rojo_id => ["container.model.json5"],
|
||||
// Ensures that the `ignorePaths` setting works for additions (new files get .json5)
|
||||
ignore_paths_adding => ["src/int_value.model.json5", "src/subfolder/string_value.txt"],
|
||||
// Ensures that the `ignorePaths` setting works for `init` files
|
||||
ignore_paths_init => ["src/non-init.luau", "src/init-file/init.luau"],
|
||||
// Ensures that the `ignorePaths` setting works for removals (new file gets .json5)
|
||||
ignore_paths_removing => ["src/Message.model.json5"],
|
||||
// Ensures that `ignoreTrees` works for additions
|
||||
ignore_trees_adding => [],
|
||||
// Ensures that `ignoreTrees` works for removals
|
||||
ignore_trees_removing => [],
|
||||
// Ensures that all of the JSON middlewares are handled as expected
|
||||
json_middlewares => ["src/dir_with_meta/init.meta.json5", "src/model_json.model.json5", "src/project_json.project.json5"],
|
||||
// Ensures projects that refer to other projects work as expected
|
||||
nested_projects => ["nested.project.json5", "string_value.txt"],
|
||||
// Ensures files that are ignored by nested projects are picked up if
|
||||
|
|
@ -62,28 +84,37 @@ syncback_tests! {
|
|||
project_reserialize => ["attribute_mismatch.luau", "property_mismatch.project.json5"],
|
||||
// Confirms that Instances that cannot serialize as directories serialize as rbxms
|
||||
rbxm_fallback => [],
|
||||
// Ensures that ref properties are linked properly on the file system
|
||||
ref_properties => ["src/pointer.model.json5", "src/target.model.json5"],
|
||||
// Ensures that ref properties are linked when no attributes are manually
|
||||
// set in the DataModel (new files get .json5)
|
||||
ref_properties_blank => ["src/pointer.model.json5", "src/target.txt"],
|
||||
// Ensures that if there is a conflict in RojoRefs, one of them is rewritten
|
||||
ref_properties_conflict => ["src/Pointer_2.model.json5", "src/Target_2.model.json5"],
|
||||
// Ensures that having multiple pointers that are aimed at the same target doesn't trigger ref rewrites.
|
||||
ref_properties_duplicate => [],
|
||||
// Ensures that ref properties that point to nothing after the prune both
|
||||
// do not leave any trace of themselves (new files get .json5)
|
||||
ref_properties_pruned => ["src/Pointer1.model.json5", "src/Pointer2.model.json5", "src/Pointer3.model.json5"],
|
||||
// Ensures that the old middleware is respected during syncback
|
||||
respect_old_middleware => ["default.project.json5", "src/model_json.model.json5", "src/rbxm.rbxm", "src/rbxmx.rbxmx"],
|
||||
// Ensures that the `$schema` field roundtrips with syncback
|
||||
schema_roundtrip => ["default.project.json5", "src/model.model.json5", "src/init/init.meta.json5", "src/adjacent.meta.json5"],
|
||||
// Ensures that StringValues inside project files are written to the
|
||||
// project file, but only if they don't have `$path` set
|
||||
string_value_project => ["default.project.json5"],
|
||||
// Ensures that sync rules are respected. This is really just a test to
|
||||
// ensure it uses the old path when possible, but we want the coverage.
|
||||
sync_rules => ["src/module.modulescript", "src/text.text"],
|
||||
// Ensures that the `syncUnscriptable` setting works
|
||||
unscriptable_properties => ["default.project.json5"],
|
||||
}
|
||||
|
||||
// Tests that run in incremental mode (preserving existing structure)
|
||||
syncback_tests_incremental! {
|
||||
// Ensures that syncback works with CSVs (preserves existing CSV structure)
|
||||
csv => ["src/csv_init/init.csv", "src/csv.csv"],
|
||||
// Ensures that the `ignorePaths` setting works for `init` files
|
||||
ignore_paths_init => ["src/non-init.luau", "src/init-file/init.luau"],
|
||||
// Ensures that all of the JSON middlewares are handled as expected
|
||||
json_middlewares => ["src/dir_with_meta/init.meta.json5", "src/model_json.model.json5", "src/project_json.project.json5"],
|
||||
// Ensures that ref properties are linked properly on the file system
|
||||
ref_properties => ["src/pointer.model.json5", "src/target.model.json5"],
|
||||
// Ensures that if there is a conflict in RojoRefs, one of them is rewritten
|
||||
ref_properties_conflict => ["src/Pointer_2.model.json5", "src/Target_2.model.json5"],
|
||||
// Ensures that the old middleware is respected during syncback (incremental mode only)
|
||||
respect_old_middleware => ["default.project.json5", "src/model_json.model.json5", "src/rbxm.rbxm", "src/rbxmx.rbxmx"],
|
||||
// Ensures that sync rules are respected (incremental mode only - uses old paths when possible)
|
||||
sync_rules => ["src/module.modulescript", "src/text.text"],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue