From 181a003ad2da996ca25eb9be81b5257f9f2b46e6 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Feb 2026 23:31:44 -0600 Subject: [PATCH] Improves sourcemap path handling with pathdiff --- Cargo.lock | 208 +++++++++++++++++- Cargo.toml | 6 +- crates/memofs/Cargo.toml | 2 +- plugin/src/App/init.lua | 6 +- ..._sourcemap__test__maps_absolute_paths.snap | 35 +++ ..._sourcemap__test__maps_relative_paths.snap | 41 ++++ src/cli/sourcemap.rs | 110 +++++++-- src/snapshot/patch_compute.rs | 4 +- .../relative_paths/default.project.json | 14 ++ .../relative_paths/module/module.luau | 1 + .../project/default.project.json | 14 ++ .../relative_paths/project/src/init.luau | 1 + vscode-rojo | 2 +- 13 files changed, 409 insertions(+), 35 deletions(-) create mode 100644 src/cli/snapshots/librojo__cli__sourcemap__test__maps_absolute_paths.snap create mode 100644 src/cli/snapshots/librojo__cli__sourcemap__test__maps_relative_paths.snap create mode 100644 test-projects/relative_paths/default.project.json create mode 100644 test-projects/relative_paths/module/module.luau create mode 100644 test-projects/relative_paths/project/default.project.json create mode 100644 test-projects/relative_paths/project/src/init.luau diff --git a/Cargo.lock b/Cargo.lock index 89dedbf4..6a2dd78f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "num_cpus", "opener", "paste", + "pathdiff", "pretty_assertions", "profiling", "rayon", @@ -1018,6 +1019,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1209,6 +1216,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1239,13 +1259,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1254,7 +1283,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -1492,6 +1521,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "idna" version = "1.1.0" @@ -1520,7 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -1706,10 +1741,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.180" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" [[package]] name = "libredox" @@ -2020,6 +2061,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2177,6 +2224,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2 1.0.106", + "syn 2.0.114", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -2195,7 +2252,7 @@ version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "unicode-xid", + "unicode-xid 0.1.0", ] [[package]] @@ -3191,12 +3248,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3658,6 +3715,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3815,6 +3878,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -3874,6 +3946,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver 1.0.27", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -4219,6 +4325,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2 1.0.106", + "quote 1.0.44", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid 0.2.6", + "wasmparser", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml index 5b6cc68f..3ab7346a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ reqwest = { version = "0.13", default-features = false, features = [ "brotli", "deflate", ] } -tempfile = "3.24.0" +tempfile = "3.25.0" ritz = "0.1.0" roblox_install = "1.0.0" serde = { version = "1.0.228", features = ["derive", "rc"] } @@ -101,6 +101,7 @@ clap = { version = "4", features = ["derive"] } profiling = "1.0.17" yaml-rust2 = "0.11.0" data-encoding = "2.10.0" +pathdiff = "0.2.3" blake3 = "1.8.3" float-cmp = "0.10.0" @@ -125,9 +126,10 @@ semver = "1.0.27" rojo-insta-ext = { path = "crates/rojo-insta-ext" } criterion = "0.8" -insta = { version = "1.46.3", features = ["redactions", "yaml"] } +insta = { version = "1.46.3", features = ["redactions", "yaml", "json"] } paste = "1.0.15" pretty_assertions = "1.4.1" regex = "1" serde_yaml = "0.9" +tempfile = "3" walkdir = "2.5.0" diff --git a/crates/memofs/Cargo.toml b/crates/memofs/Cargo.toml index 5999df6b..324e32a4 100644 --- a/crates/memofs/Cargo.toml +++ b/crates/memofs/Cargo.toml @@ -28,4 +28,4 @@ notify = { version = "8.2", features = ["macos_kqueue"] } notify = "8.2" [dev-dependencies] -tempfile = "3.24.0" +tempfile = "3.25.0" diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 0f8ef36e..43e974e3 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -691,7 +691,11 @@ function App:startSession() -- different one that may have replaced it (e.g., auto-reconnect race). local timeoutSession = serveSession task.delay(30, function() - if self.serveSession ~= nil and self.serveSession == timeoutSession and Settings:get("oneShotSync") then + if + self.serveSession ~= nil + and self.serveSession == timeoutSession + and Settings:get("oneShotSync") + then Log.warn("One-shot sync safety timeout: forcing disconnect") self:endSession() end diff --git a/src/cli/snapshots/librojo__cli__sourcemap__test__maps_absolute_paths.snap b/src/cli/snapshots/librojo__cli__sourcemap__test__maps_absolute_paths.snap new file mode 100644 index 00000000..33e9cf31 --- /dev/null +++ b/src/cli/snapshots/librojo__cli__sourcemap__test__maps_absolute_paths.snap @@ -0,0 +1,35 @@ +--- +source: src/cli/sourcemap.rs +expression: sourcemap_contents +--- +{ + "name": "default", + "className": "DataModel", + "filePaths": "[...1 path omitted...]", + "children": [ + { + "name": "ReplicatedStorage", + "className": "ReplicatedStorage", + "children": [ + { + "name": "Project", + "className": "ModuleScript", + "filePaths": "[...1 path omitted...]", + "children": [ + { + "name": "Module", + "className": "Folder", + "children": [ + { + "name": "module", + "className": "ModuleScript", + "filePaths": "[...1 path omitted...]" + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/cli/snapshots/librojo__cli__sourcemap__test__maps_relative_paths.snap b/src/cli/snapshots/librojo__cli__sourcemap__test__maps_relative_paths.snap new file mode 100644 index 00000000..7a314b8c --- /dev/null +++ b/src/cli/snapshots/librojo__cli__sourcemap__test__maps_relative_paths.snap @@ -0,0 +1,41 @@ +--- +source: src/cli/sourcemap.rs +expression: sourcemap_contents +--- +{ + "name": "default", + "className": "DataModel", + "filePaths": [ + "default.project.json" + ], + "children": [ + { + "name": "ReplicatedStorage", + "className": "ReplicatedStorage", + "children": [ + { + "name": "Project", + "className": "ModuleScript", + "filePaths": [ + "src/init.luau" + ], + "children": [ + { + "name": "Module", + "className": "Folder", + "children": [ + { + "name": "module", + "className": "ModuleScript", + "filePaths": [ + "../module/module.luau" + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/cli/sourcemap.rs b/src/cli/sourcemap.rs index 41ec36b8..1e4acee4 100644 --- a/src/cli/sourcemap.rs +++ b/src/cli/sourcemap.rs @@ -13,7 +13,7 @@ use fs_err::File; use memofs::Vfs; use rayon::prelude::*; use rbx_dom_weak::{types::Ref, Ustr}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tokio::runtime::Runtime; use crate::{ @@ -26,19 +26,20 @@ use super::resolve_path; const ABSOLUTE_PATH_FAILED_ERR: &str = "Failed to turn relative path into absolute path!"; /// Representation of a node in the generated sourcemap tree. -#[derive(Serialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SourcemapNode<'a> { name: &'a str, class_name: Ustr, #[serde( + default, skip_serializing_if = "Vec::is_empty", serialize_with = "crate::path_serializer::serialize_vec_absolute" )] file_paths: Vec>, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] children: Vec>, } @@ -92,7 +93,7 @@ impl SourcemapCommand { rayon::ThreadPoolBuilder::new() .num_threads(num_cpus::get().min(6)) .build_global() - .unwrap(); + .ok(); write_sourcemap( &session, @@ -212,27 +213,21 @@ fn recurse_create_node<'a>( let mut output_file_paths: Vec> = Vec::with_capacity(instance.metadata().relevant_paths.len()); - // Canonicalize project_dir once for consistent comparison - let canonical_project_dir = std::fs::canonicalize(project_dir).ok(); + // Canonicalize project_dir once to normalize Windows \\?\ prefixes + let canonical_project_dir = std::fs::canonicalize(project_dir) + .unwrap_or_else(|_| project_dir.to_path_buf()); for val in file_paths { if use_absolute_paths { let abs_path = path::absolute(val).expect(ABSOLUTE_PATH_FAILED_ERR); output_file_paths.push(Cow::Owned(abs_path)); - } else if let Some(ref proj_dir) = canonical_project_dir { - // Canonicalize file path and strip project dir prefix to get relative path - if let Ok(canonical_file) = std::fs::canonicalize(val) { - if let Ok(relative) = canonical_file.strip_prefix(proj_dir) { - output_file_paths.push(Cow::Owned(relative.to_path_buf())); - continue; - } - } - // Fallback: file is outside project dir - let abs_path = path::absolute(val).expect(ABSOLUTE_PATH_FAILED_ERR); - output_file_paths.push(Cow::Owned(abs_path)); } else { - let abs_path = path::absolute(val).expect(ABSOLUTE_PATH_FAILED_ERR); - output_file_paths.push(Cow::Owned(abs_path)); + let canonical_val = std::fs::canonicalize(val) + .unwrap_or_else(|_| val.to_path_buf()); + output_file_paths.push(Cow::Owned( + pathdiff::diff_paths(&canonical_val, &canonical_project_dir) + .expect("Failed to compute relative path from project dir"), + )); } } @@ -329,3 +324,80 @@ fn write_atomic(target: &Path, data: &[u8]) -> anyhow::Result<()> { Ok(()) } + +#[cfg(test)] +mod test { + use crate::cli::sourcemap::SourcemapNode; + use crate::cli::SourcemapCommand; + use insta::internals::Content; + use std::path::Path; + + #[test] + fn maps_relative_paths() { + let sourcemap_dir = tempfile::tempdir().unwrap(); + let sourcemap_output = sourcemap_dir.path().join("sourcemap.json"); + let project_path = fs_err::canonicalize( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-projects") + .join("relative_paths") + .join("project"), + ) + .unwrap(); + let sourcemap_command = SourcemapCommand { + project: project_path, + output: Some(sourcemap_output.clone()), + include_non_scripts: false, + watch: false, + absolute: false, + }; + assert!(sourcemap_command.run().is_ok()); + + let raw_sourcemap_contents = fs_err::read_to_string(sourcemap_output.as_path()).unwrap(); + let sourcemap_contents = + serde_json::from_str::(&raw_sourcemap_contents).unwrap(); + insta::assert_json_snapshot!(sourcemap_contents); + } + + #[test] + fn maps_absolute_paths() { + let sourcemap_dir = tempfile::tempdir().unwrap(); + let sourcemap_output = sourcemap_dir.path().join("sourcemap.json"); + let project_path = fs_err::canonicalize( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-projects") + .join("relative_paths") + .join("project"), + ) + .unwrap(); + let sourcemap_command = SourcemapCommand { + project: project_path, + output: Some(sourcemap_output.clone()), + include_non_scripts: false, + watch: false, + absolute: true, + }; + assert!(sourcemap_command.run().is_ok()); + + let raw_sourcemap_contents = fs_err::read_to_string(sourcemap_output.as_path()).unwrap(); + let sourcemap_contents = + serde_json::from_str::(&raw_sourcemap_contents).unwrap(); + insta::assert_json_snapshot!(sourcemap_contents, { + ".**.filePaths" => insta::dynamic_redaction(|mut value, _path| { + let mut paths_count = 0; + + match value { + Content::Seq(ref mut vec) => { + for path in vec.iter().map(|i| i.as_str().unwrap()) { + assert!(fs_err::canonicalize(path).is_ok(), "path was not valid"); + assert!(Path::new(path).is_absolute(), "path was not absolute"); + + paths_count += 1; + } + } + _ => panic!("Expected filePaths to be a sequence"), + } + format!("[...{} path{} omitted...]", paths_count, if paths_count != 1 { "s" } else { "" } ) + }) + }); + } +} diff --git a/src/snapshot/patch_compute.rs b/src/snapshot/patch_compute.rs index 30b163f4..bb483e4a 100644 --- a/src/snapshot/patch_compute.rs +++ b/src/snapshot/patch_compute.rs @@ -8,7 +8,9 @@ use rbx_dom_weak::{ ustr, HashMapExt as _, UstrMap, UstrSet, }; -use crate::{variant_eq::variant_eq, RojoRef, REF_PATH_ATTRIBUTE_PREFIX, REF_POINTER_ATTRIBUTE_PREFIX}; +use crate::{ + variant_eq::variant_eq, RojoRef, REF_PATH_ATTRIBUTE_PREFIX, REF_POINTER_ATTRIBUTE_PREFIX, +}; use super::{ patch::{PatchAdd, PatchSet, PatchUpdate}, diff --git a/test-projects/relative_paths/default.project.json b/test-projects/relative_paths/default.project.json new file mode 100644 index 00000000..af85c143 --- /dev/null +++ b/test-projects/relative_paths/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "default", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Project": { + "$path": "project/src", + "Module": { + "$path": "module" + } + } + } + } +} diff --git a/test-projects/relative_paths/module/module.luau b/test-projects/relative_paths/module/module.luau new file mode 100644 index 00000000..15e53c4a --- /dev/null +++ b/test-projects/relative_paths/module/module.luau @@ -0,0 +1 @@ +return nil diff --git a/test-projects/relative_paths/project/default.project.json b/test-projects/relative_paths/project/default.project.json new file mode 100644 index 00000000..a7d8d46c --- /dev/null +++ b/test-projects/relative_paths/project/default.project.json @@ -0,0 +1,14 @@ +{ + "name": "default", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "Project": { + "$path": "src/", + "Module": { + "$path": "../module" + } + } + } + } +} diff --git a/test-projects/relative_paths/project/src/init.luau b/test-projects/relative_paths/project/src/init.luau new file mode 100644 index 00000000..15e53c4a --- /dev/null +++ b/test-projects/relative_paths/project/src/init.luau @@ -0,0 +1 @@ +return nil diff --git a/vscode-rojo b/vscode-rojo index d6c2ff2f..035c12d9 160000 --- a/vscode-rojo +++ b/vscode-rojo @@ -1 +1 @@ -Subproject commit d6c2ff2f71eb5fe8031eefe166194723a9a24b82 +Subproject commit 035c12d9ed499942a1e8b024d61b99675eab232d