Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ Making a new release? Simply add the new header with the version and date undern
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
* Fixed a bug where the notification timeout thread would fail to cancel on unmount ([#1211])
* Added a "Forget" option to the sync reminder notification to avoid being reminded for that place in the future ([#1215])
* Improves relative path calculation for sourcemap generation to avoid issues with Windows UNC paths. ([#1217])

[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
[#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

## [7.7.0-rc.1] (November 27th, 2025)

Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ clap = { version = "3.2.25", features = ["derive"] }
profiling = "1.0.15"
yaml-rust2 = "0.10.3"
data-encoding = "2.8.0"
pathdiff = "0.2.3"

blake3 = "1.5.0"
float-cmp = "0.9.0"
Expand All @@ -122,7 +123,7 @@ semver = "1.0.22"
rojo-insta-ext = { path = "crates/rojo-insta-ext" }

criterion = "0.3.6"
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
insta = { version = "1.36.1", features = ["redactions", "yaml", "json"] }
paste = "1.0.14"
pretty_assertions = "1.4.0"
serde_yaml = "0.8.26"
Expand Down
2 changes: 2 additions & 0 deletions crates/memofs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,4 +657,6 @@ mod test {
let err = vfs.canonicalize(&file_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}

// TODO: Add a test to check whether new_default uses StdBackend.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: src/cli/sourcemap.rs
expression: sourcemap_contents
---
{
"name": "default",
"className": "DataModel",
"filePaths": "[...paths omitted...]",
"children": [
{
"name": "ReplicatedStorage",
"className": "ReplicatedStorage",
"children": [
{
"name": "Project",
"className": "ModuleScript",
"filePaths": "[...paths omitted...]",
"children": [
{
"name": "Module",
"className": "Folder",
"children": [
{
"name": "module",
"className": "ModuleScript",
"filePaths": "[...paths omitted...]"
}
]
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
]
}
]
}
]
}
114 changes: 90 additions & 24 deletions src/cli/sourcemap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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::{
Expand All @@ -24,19 +24,20 @@ const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project
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<Cow<'a, Path>>,

#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
children: Vec<SourcemapNode<'a>>,
}

Expand Down Expand Up @@ -70,12 +71,13 @@ pub struct SourcemapCommand {

impl SourcemapCommand {
pub fn run(self) -> anyhow::Result<()> {
let project_path = resolve_path(&self.project);
let project_path = fs_err::canonicalize(resolve_path(&self.project))?;

log::trace!("Constructing in-memory filesystem");
log::trace!("Constructing filesystem with StdBackend");
let vfs = Vfs::new_default();
vfs.set_watch_enabled(self.watch);

log::trace!("Setting up session for sourcemap generation");
let session = ServeSession::new(vfs, project_path)?;
let mut cursor = session.message_queue().cursor();

Expand All @@ -87,14 +89,17 @@ impl SourcemapCommand {

// Pre-build a rayon threadpool with a low number of threads to avoid
// dynamic creation overhead on systems with a high number of cpus.
log::trace!("Setting rayon global threadpool");
rayon::ThreadPoolBuilder::new()
.num_threads(num_cpus::get().min(6))
.build_global()
.unwrap();
.ok();

log::trace!("Writing initial sourcemap");
write_sourcemap(&session, self.output.as_deref(), filter, self.absolute)?;

if self.watch {
log::trace!("Setting up runtime for watch mode");
let rt = Runtime::new().unwrap();

loop {
Expand Down Expand Up @@ -141,26 +146,26 @@ fn patch_set_affects_sourcemap(
!set.removed.is_empty()
// 2. A newly added instance passes the filter
|| set.added.iter().any(|referent| {
let instance = tree
.get_instance(*referent)
.expect("instance did not exist when updating sourcemap");
filter(&instance)
})
let instance = tree
.get_instance(*referent)
.expect("instance did not exist when updating sourcemap");
filter(&instance)
})
// 3. An existing instance has its class name, name,
// or file paths changed, and passes the filter
|| set.updated.iter().any(|updated| {
let changed = updated.changed_class_name.is_some()
|| updated.changed_name.is_some()
|| updated.changed_metadata.is_some();
if changed {
let instance = tree
.get_instance(updated.id)
.expect("instance did not exist when updating sourcemap");
filter(&instance)
} else {
false
}
})
let changed = updated.changed_class_name.is_some()
|| updated.changed_name.is_some()
|| updated.changed_metadata.is_some();
if changed {
let instance = tree
.get_instance(updated.id)
.expect("instance did not exist when updating sourcemap");
filter(&instance)
} else {
false
}
})
})
}

Expand Down Expand Up @@ -208,7 +213,7 @@ fn recurse_create_node<'a>(
} else {
for val in file_paths {
output_file_paths.push(Cow::from(
val.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR),
pathdiff::diff_paths(val, project_dir).expect(PATH_STRIP_FAILED_ERR),
));
}
};
Expand Down Expand Up @@ -250,3 +255,64 @@ fn write_sourcemap(

Ok(())
}

#[cfg(test)]
mod test {
use crate::cli::sourcemap::SourcemapNode;
use crate::cli::SourcemapCommand;
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::<SourcemapNode>(&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::<SourcemapNode>(&raw_sourcemap_contents).unwrap();
insta::assert_json_snapshot!(sourcemap_contents, {
".**.filePaths" => "[...paths omitted...]"
});
}
}
14 changes: 14 additions & 0 deletions test-projects/relative_paths/default.project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "default",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Project": {
"$path": "project/src",
"Module": {
"$path": "module"
}
}
}
}
}
1 change: 1 addition & 0 deletions test-projects/relative_paths/module/module.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return nil
14 changes: 14 additions & 0 deletions test-projects/relative_paths/project/default.project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "default",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"Project": {
"$path": "src/",
"Module": {
"$path": "../module"
}
}
}
}
}
1 change: 1 addition & 0 deletions test-projects/relative_paths/project/src/init.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return nil
Loading