mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add Luau-style ref paths migration plan and update related tests
- Introduced a new migration plan for converting `Rojo_Ref_*` attribute paths to Luau require-by-string style, enhancing portability with relative paths. - Updated existing test snapshots to reflect the new path formats, ensuring consistency across various scenarios. - Enhanced the `compute_relative_ref_path` and `resolve_ref_path` functions to support the new path semantics, improving path resolution logic. - Refactored related code in `change_processor`, `syncback`, and `snapshot` modules to accommodate the new reference path handling. These changes improve the robustness and maintainability of reference path management in the project.
This commit is contained in:
parent
d6169c6d84
commit
b8c3520d25
29 changed files with 2147 additions and 179 deletions
|
|
@ -535,7 +535,7 @@ mod tests {
|
|||
meta_content.as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~2".to_string())];
|
||||
let substitutions = vec![("Workspace/Foo".to_string(), "./Foo~2".to_string())];
|
||||
snap.fix_ref_paths(&substitutions);
|
||||
|
||||
let result = std::str::from_utf8(
|
||||
|
|
@ -546,7 +546,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_PrimaryPart": "Workspace/Foo~2""#),
|
||||
result.contains(r#""Rojo_Ref_PrimaryPart": "./Foo~2""#),
|
||||
"Rojo_Ref line should be updated. Got:\n{}",
|
||||
result
|
||||
);
|
||||
|
|
@ -566,7 +566,7 @@ mod tests {
|
|||
b"-- Rojo_Ref_PrimaryPart Workspace/Foo".to_vec(),
|
||||
);
|
||||
|
||||
let substitutions = vec![("Workspace/Foo".to_string(), "Workspace/Foo~2".to_string())];
|
||||
let substitutions = vec![("Workspace/Foo".to_string(), "./Foo~2".to_string())];
|
||||
snap.fix_ref_paths(&substitutions);
|
||||
|
||||
let result = std::str::from_utf8(
|
||||
|
|
@ -581,7 +581,7 @@ mod tests {
|
|||
result
|
||||
);
|
||||
assert!(
|
||||
!result.contains("Workspace/Foo~2"),
|
||||
!result.contains("./Foo~2"),
|
||||
"Non-meta file should not have substitution applied"
|
||||
);
|
||||
}
|
||||
|
|
@ -618,11 +618,11 @@ mod tests {
|
|||
let substitutions = vec![
|
||||
(
|
||||
"__ROJO_REF_00000000000000000000000000000001__".to_string(),
|
||||
"Beams1/001~5".to_string(),
|
||||
"./001~5".to_string(),
|
||||
),
|
||||
(
|
||||
"__ROJO_REF_00000000000000000000000000000002__".to_string(),
|
||||
"Beams1/002~7.model.json5".to_string(),
|
||||
"./002~7.model.json5".to_string(),
|
||||
),
|
||||
];
|
||||
snap.fix_ref_paths(&substitutions);
|
||||
|
|
@ -635,11 +635,11 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_Attachment0": "Beams1/001~5""#),
|
||||
result.contains(r#""Rojo_Ref_Attachment0": "./001~5""#),
|
||||
"Attachment0 should resolve to 001~5. Got:\n{result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_Attachment1": "Beams1/002~7.model.json5""#),
|
||||
result.contains(r#""Rojo_Ref_Attachment1": "./002~7.model.json5""#),
|
||||
"Attachment1 should resolve to 002~7. Got:\n{result}"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -715,15 +715,15 @@ mod tests {
|
|||
let substitutions = vec![
|
||||
(
|
||||
"__ROJO_REF_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa__".to_string(),
|
||||
"Beams1/001~2".to_string(),
|
||||
"./001~2".to_string(),
|
||||
),
|
||||
(
|
||||
"__ROJO_REF_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb__".to_string(),
|
||||
"Beams1/001~5".to_string(),
|
||||
"./001~5".to_string(),
|
||||
),
|
||||
(
|
||||
"__ROJO_REF_cccccccccccccccccccccccccccccccc__".to_string(),
|
||||
"Workspace/Model/Handle.model.json5".to_string(),
|
||||
"@self/Handle.model.json5".to_string(),
|
||||
),
|
||||
];
|
||||
snap.fix_ref_paths(&substitutions);
|
||||
|
|
@ -736,15 +736,15 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_Attachment0": "Beams1/001~2""#),
|
||||
result.contains(r#""Rojo_Ref_Attachment0": "./001~2""#),
|
||||
"Attachment0 wrong. Got:\n{result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_Attachment1": "Beams1/001~5""#),
|
||||
result.contains(r#""Rojo_Ref_Attachment1": "./001~5""#),
|
||||
"Attachment1 wrong. Got:\n{result}"
|
||||
);
|
||||
assert!(
|
||||
result.contains(r#""Rojo_Ref_PrimaryPart": "Workspace/Model/Handle.model.json5""#),
|
||||
result.contains(r#""Rojo_Ref_PrimaryPart": "@self/Handle.model.json5""#),
|
||||
"PrimaryPart wrong. Got:\n{result}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,10 +96,17 @@ pub fn remove_model_name(model_path: &Path) -> anyhow::Result<RemoveNameOutcome>
|
|||
/// For each attribute whose key starts with `Rojo_Ref_` and whose string
|
||||
/// value starts with `old_prefix`, replaces the prefix with `new_prefix`.
|
||||
/// Returns true if any attribute was updated.
|
||||
/// Update `Rojo_Ref_*` attributes in a meta/model file after a rename.
|
||||
///
|
||||
/// For each attribute, resolves the on-disk relative path to absolute using
|
||||
/// `source_abs`, checks if the resolved absolute path is affected by the
|
||||
/// rename (`old_prefix` → `new_prefix`), and if so, recomputes the relative
|
||||
/// path from `source_abs` to the new absolute target.
|
||||
pub fn update_ref_paths_in_file(
|
||||
file_path: &Path,
|
||||
old_prefix: &str,
|
||||
new_prefix: &str,
|
||||
source_abs: &str,
|
||||
) -> anyhow::Result<bool> {
|
||||
use crate::REF_PATH_ATTRIBUTE_PREFIX;
|
||||
|
||||
|
|
@ -118,17 +125,28 @@ pub fn update_ref_paths_in_file(
|
|||
);
|
||||
}
|
||||
|
||||
let old_prefix_slash = format!("{old_prefix}/");
|
||||
let mut updated = false;
|
||||
if let Some(attrs) = val.get_mut("attributes").and_then(|a| a.as_object_mut()) {
|
||||
for (key, value) in attrs.iter_mut() {
|
||||
if key.starts_with(REF_PATH_ATTRIBUTE_PREFIX) {
|
||||
if let Some(path_str) = value.as_str() {
|
||||
if path_str == old_prefix || path_str.starts_with(&format!("{old_prefix}/")) {
|
||||
let new_path = format!("{new_prefix}{}", &path_str[old_prefix.len()..]);
|
||||
*value = serde_json::Value::String(new_path);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
if !key.starts_with(REF_PATH_ATTRIBUTE_PREFIX) {
|
||||
continue;
|
||||
}
|
||||
let Some(path_str) = value.as_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(resolved) =
|
||||
crate::resolve_ref_path_to_absolute(path_str, source_abs)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if resolved == old_prefix || resolved.starts_with(&old_prefix_slash) {
|
||||
let new_abs =
|
||||
format!("{new_prefix}{}", &resolved[old_prefix.len()..]);
|
||||
let new_relative =
|
||||
crate::compute_relative_ref_path(source_abs, &new_abs);
|
||||
*value = serde_json::Value::String(new_relative);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,7 +175,8 @@ pub fn syncback_loop_with_stats(
|
|||
|
||||
log::debug!("Collecting referents for new DOM...");
|
||||
let mut deferred_referents = collect_referents(&new_tree, &pre_prune_paths, None);
|
||||
let placeholder_map = std::mem::take(&mut deferred_referents.placeholder_to_target);
|
||||
let placeholder_map =
|
||||
std::mem::take(&mut deferred_referents.placeholder_to_source_and_target);
|
||||
|
||||
// Remove any properties that are manually blocked from syncback via the
|
||||
// project file.
|
||||
|
|
@ -622,12 +623,17 @@ pub fn syncback_loop_with_stats(
|
|||
|
||||
let final_map = ref_path_map.borrow();
|
||||
let mut substitutions: Vec<(String, String)> = Vec::new();
|
||||
for (placeholder, target_ref) in &placeholder_map {
|
||||
let final_path = final_map
|
||||
for (placeholder, (source_ref, target_ref)) in &placeholder_map {
|
||||
let source_abs = final_map
|
||||
.get(source_ref)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| tentative_fs_path_public(&new_tree, *source_ref));
|
||||
let target_abs = final_map
|
||||
.get(target_ref)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| tentative_fs_path_public(&new_tree, *target_ref));
|
||||
substitutions.push((placeholder.clone(), final_path));
|
||||
let relative = crate::compute_relative_ref_path(&source_abs, &target_abs);
|
||||
substitutions.push((placeholder.clone(), relative));
|
||||
}
|
||||
if !substitutions.is_empty() {
|
||||
log::debug!(
|
||||
|
|
|
|||
|
|
@ -142,11 +142,11 @@ pub struct RefLinks {
|
|||
id_links: HashMap<Ref, Vec<IdRefLink>>,
|
||||
/// Target instances that need a Rojo_Id written (for ID-based system).
|
||||
targets_needing_id: HashSet<Ref>,
|
||||
/// Maps placeholder strings to target Refs. Populated when `final_paths`
|
||||
/// is not available (CLI syncback). After the syncback walk populates the
|
||||
/// `ref_path_map`, these placeholders are substituted with the correct
|
||||
/// dedup-aware filesystem paths via `fix_ref_paths`.
|
||||
pub placeholder_to_target: HashMap<String, Ref>,
|
||||
/// Maps placeholder strings to (source_ref, target_ref) pairs. Populated
|
||||
/// when `final_paths` is not available (CLI syncback pre-walk). The
|
||||
/// placeholder encodes both source and target so that post-processing can
|
||||
/// compute the correct relative path for each (source, target) pair.
|
||||
pub placeholder_to_source_and_target: HashMap<String, (Ref, Ref)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
|
|
@ -195,7 +195,7 @@ pub fn collect_referents(
|
|||
let mut path_links: HashMap<Ref, Vec<PathRefLink>> = HashMap::new();
|
||||
let id_links: HashMap<Ref, Vec<IdRefLink>> = HashMap::new();
|
||||
let targets_needing_id: HashSet<Ref> = HashSet::new();
|
||||
let mut placeholder_to_target: HashMap<String, Ref> = HashMap::new();
|
||||
let mut placeholder_to_source_and_target: HashMap<String, (Ref, Ref)> = HashMap::new();
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(dom.root_ref());
|
||||
|
|
@ -213,35 +213,36 @@ pub fn collect_referents(
|
|||
}
|
||||
|
||||
if dom.get_by_ref(*target_ref).is_some() {
|
||||
// Target exists in DOM -- use path-based system.
|
||||
let target_path = if let Some(fp) = final_paths {
|
||||
// final_paths available (dedup-aware) -- use directly.
|
||||
fp.get(target_ref)
|
||||
let ref_path = if let Some(fp) = final_paths {
|
||||
let source_abs = fp
|
||||
.get(&inst_ref)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| tentative_fs_path(dom, *target_ref))
|
||||
.unwrap_or_else(|| tentative_fs_path(dom, inst_ref));
|
||||
let target_abs = fp
|
||||
.get(target_ref)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| tentative_fs_path(dom, *target_ref));
|
||||
crate::compute_relative_ref_path(&source_abs, &target_abs)
|
||||
} else {
|
||||
// No final_paths yet (pre-walk). Use a unique placeholder
|
||||
// keyed by the target Ref so that duplicate-named siblings
|
||||
// get distinct values. The syncback post-processing step
|
||||
// substitutes these with the correct dedup'd paths.
|
||||
let placeholder = ref_placeholder(*target_ref);
|
||||
placeholder_to_target.insert(placeholder.clone(), *target_ref);
|
||||
let placeholder = ref_placeholder(inst_ref, *target_ref);
|
||||
placeholder_to_source_and_target
|
||||
.insert(placeholder.clone(), (inst_ref, *target_ref));
|
||||
placeholder
|
||||
};
|
||||
path_links.entry(inst_ref).or_default().push(PathRefLink {
|
||||
name: *prop_name,
|
||||
path: target_path,
|
||||
path: ref_path,
|
||||
});
|
||||
} else if let Some(external_path) = pre_prune_paths.get(target_ref) {
|
||||
log::debug!(
|
||||
"Property {}.{} points to pruned instance at '{}', storing as path reference",
|
||||
"Property {}.{} points to pruned instance at '{}', storing as @game/ reference",
|
||||
tentative_fs_path(dom, inst_ref),
|
||||
prop_name,
|
||||
external_path
|
||||
);
|
||||
path_links.entry(inst_ref).or_default().push(PathRefLink {
|
||||
name: *prop_name,
|
||||
path: external_path.clone(),
|
||||
path: format!("@game/{external_path}"),
|
||||
});
|
||||
} else {
|
||||
log::warn!(
|
||||
|
|
@ -257,19 +258,16 @@ pub fn collect_referents(
|
|||
path_links,
|
||||
id_links,
|
||||
targets_needing_id,
|
||||
placeholder_to_target,
|
||||
placeholder_to_source_and_target,
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefix used for ref placeholder strings. Chosen to be unambiguous with
|
||||
/// real filesystem paths (which cannot contain double underscores in this
|
||||
/// position).
|
||||
const REF_PLACEHOLDER_PREFIX: &str = "__ROJO_REF_";
|
||||
const REF_PLACEHOLDER_SUFFIX: &str = "__";
|
||||
|
||||
/// Build a unique placeholder string for a target Ref.
|
||||
fn ref_placeholder(target: Ref) -> String {
|
||||
format!("{REF_PLACEHOLDER_PREFIX}{target}{REF_PLACEHOLDER_SUFFIX}")
|
||||
/// Build a unique placeholder string encoding both source and target Refs.
|
||||
fn ref_placeholder(source: Ref, target: Ref) -> String {
|
||||
format!("{REF_PLACEHOLDER_PREFIX}{source}_TO_{target}{REF_PLACEHOLDER_SUFFIX}")
|
||||
}
|
||||
|
||||
/// Writes reference attributes to instances in the DOM.
|
||||
|
|
@ -453,7 +451,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn collect_referents_uses_unique_placeholders_for_duplicate_names() {
|
||||
fn collect_referents_uses_source_aware_placeholders() {
|
||||
let (dom, _, attachments, beam) = make_beam_attachment_dom();
|
||||
|
||||
let links = collect_referents(&dom, &HashMap::new(), None);
|
||||
|
|
@ -466,16 +464,21 @@ mod tests {
|
|||
path.starts_with(REF_PLACEHOLDER_PREFIX),
|
||||
"path should be a placeholder, got: {path}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
links.placeholder_to_target.get(path).copied(),
|
||||
Some(attachments[2]),
|
||||
"placeholder should map to the specific target Ref"
|
||||
assert!(
|
||||
path.contains("_TO_"),
|
||||
"placeholder should encode both source and target, got: {path}"
|
||||
);
|
||||
|
||||
let (src, tgt) = links
|
||||
.placeholder_to_source_and_target
|
||||
.get(path)
|
||||
.expect("placeholder should map to (source, target)");
|
||||
assert_eq!(*src, beam);
|
||||
assert_eq!(*tgt, attachments[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholders_are_unique_per_target_ref() {
|
||||
fn placeholders_are_unique_per_source_target_pair() {
|
||||
let mut dom2 = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||
let root2 = dom2.root_ref();
|
||||
let c2 = dom2.insert(root2, InstanceBuilder::new("Folder").with_name("Beams1"));
|
||||
|
|
@ -491,14 +494,19 @@ mod tests {
|
|||
|
||||
let links = collect_referents(&dom2, &HashMap::new(), None);
|
||||
|
||||
let placeholders: Vec<&String> = links.placeholder_to_target.keys().collect();
|
||||
let placeholders: Vec<&String> =
|
||||
links.placeholder_to_source_and_target.keys().collect();
|
||||
|
||||
assert!(
|
||||
placeholders.len() >= 2,
|
||||
"should have at least 2 placeholders"
|
||||
);
|
||||
|
||||
let targets: HashSet<&Ref> = links.placeholder_to_target.values().collect();
|
||||
let targets: HashSet<Ref> = links
|
||||
.placeholder_to_source_and_target
|
||||
.values()
|
||||
.map(|(_, t)| *t)
|
||||
.collect();
|
||||
assert!(
|
||||
targets.contains(&att_a),
|
||||
"should have placeholder for att_a"
|
||||
|
|
@ -508,7 +516,6 @@ mod tests {
|
|||
"should have placeholder for att_b"
|
||||
);
|
||||
|
||||
// Crucially: the placeholder strings themselves must be distinct
|
||||
let unique_placeholders: HashSet<&String> = placeholders.iter().copied().collect();
|
||||
assert_eq!(
|
||||
unique_placeholders.len(),
|
||||
|
|
@ -518,20 +525,24 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn collect_referents_with_final_paths_skips_placeholders() {
|
||||
fn collect_referents_with_final_paths_produces_relative_paths() {
|
||||
let (dom, _, attachments, beam) = make_beam_attachment_dom();
|
||||
|
||||
let mut final_paths = HashMap::new();
|
||||
final_paths.insert(attachments[2], "Beams1/001~3".to_string());
|
||||
final_paths.insert(beam, "Beams1/BeamA.model.json5".to_string());
|
||||
final_paths.insert(attachments[2], "Beams1/001~3.model.json5".to_string());
|
||||
|
||||
let links = collect_referents(&dom, &HashMap::new(), Some(&final_paths));
|
||||
|
||||
assert!(
|
||||
links.placeholder_to_target.is_empty(),
|
||||
links.placeholder_to_source_and_target.is_empty(),
|
||||
"should not generate placeholders when final_paths is provided"
|
||||
);
|
||||
|
||||
let beam_links = links.path_links.get(&beam).expect("beam should have refs");
|
||||
assert_eq!(beam_links[0].path, "Beams1/001~3");
|
||||
assert_eq!(
|
||||
beam_links[0].path, "./001~3.model.json5",
|
||||
"should produce relative path (sibling)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue