1use std::{
2 io::{BufWriter, Write},
3 mem::forget,
4 path::{Path, PathBuf},
5};
6
7use clap::Parser;
8use fs_err::File;
9use memofs::Vfs;
10use rayon::prelude::*;
11use rbx_dom_weak::{types::Ref, Ustr};
12use serde::Serialize;
13use tokio::runtime::Runtime;
14
15use crate::{
16 serve_session::ServeSession,
17 snapshot::{AppliedPatchSet, InstanceWithMeta, RojoTree},
18};
19
20use super::resolve_path;
21
22const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project file!";
23
24#[derive(Serialize)]
26#[serde(rename_all = "camelCase")]
27struct SourcemapNode<'a> {
28 name: &'a str,
29 class_name: Ustr,
30
31 #[serde(skip_serializing_if = "Vec::is_empty")]
32 file_paths: Vec<PathBuf>,
33
34 #[serde(skip_serializing_if = "Vec::is_empty")]
35 children: Vec<SourcemapNode<'a>>,
36}
37
38#[derive(Debug, Parser)]
40pub struct SourcemapCommand {
41 #[clap(default_value = "")]
44 pub project: PathBuf,
45
46 #[clap(long, short)]
51 pub output: Option<PathBuf>,
52
53 #[clap(long)]
55 pub include_non_scripts: bool,
56
57 #[clap(long)]
59 pub watch: bool,
60}
61
62impl SourcemapCommand {
63 pub fn run(self) -> anyhow::Result<()> {
64 let project_path = resolve_path(&self.project);
65
66 log::trace!("Constructing in-memory filesystem");
67 let vfs = Vfs::new_default();
68 vfs.set_watch_enabled(self.watch);
69
70 let session = ServeSession::new(vfs, project_path)?;
71 let mut cursor = session.message_queue().cursor();
72
73 let filter = if self.include_non_scripts {
74 filter_nothing
75 } else {
76 filter_non_scripts
77 };
78
79 rayon::ThreadPoolBuilder::new()
82 .num_threads(num_cpus::get().min(6))
83 .build_global()
84 .unwrap();
85
86 write_sourcemap(&session, self.output.as_deref(), filter)?;
87
88 if self.watch {
89 let rt = Runtime::new().unwrap();
90
91 loop {
92 let receiver = session.message_queue().subscribe(cursor);
93 let (new_cursor, patch_set) = rt.block_on(receiver).unwrap();
94 cursor = new_cursor;
95
96 if patch_set_affects_sourcemap(&session, &patch_set, filter) {
97 write_sourcemap(&session, self.output.as_deref(), filter)?;
98 }
99 }
100 }
101
102 forget(session);
105
106 Ok(())
107 }
108}
109
110fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
111 true
112}
113
114fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
115 matches!(
116 instance.class_name().as_str(),
117 "Script" | "LocalScript" | "ModuleScript"
118 )
119}
120
121fn patch_set_affects_sourcemap(
122 session: &ServeSession,
123 patch_set: &[AppliedPatchSet],
124 filter: fn(&InstanceWithMeta) -> bool,
125) -> bool {
126 let tree = session.tree();
127
128 patch_set.par_iter().any(|set| {
130 !set.removed.is_empty()
133 || set.added.iter().any(|referent| {
135 let instance = tree
136 .get_instance(*referent)
137 .expect("instance did not exist when updating sourcemap");
138 filter(&instance)
139 })
140 || set.updated.iter().any(|updated| {
143 let changed = updated.changed_class_name.is_some()
144 || updated.changed_name.is_some()
145 || updated.changed_metadata.is_some();
146 if changed {
147 let instance = tree
148 .get_instance(updated.id)
149 .expect("instance did not exist when updating sourcemap");
150 filter(&instance)
151 } else {
152 false
153 }
154 })
155 })
156}
157
158fn recurse_create_node<'a>(
159 tree: &'a RojoTree,
160 referent: Ref,
161 project_dir: &Path,
162 filter: fn(&InstanceWithMeta) -> bool,
163) -> Option<SourcemapNode<'a>> {
164 let instance = tree.get_instance(referent).expect("instance did not exist");
165
166 let children: Vec<_> = instance
167 .children()
168 .par_iter()
169 .filter_map(|&child_id| recurse_create_node(tree, child_id, project_dir, filter))
170 .collect();
171
172 if children.is_empty() && !filter(&instance) {
175 return None;
176 }
177
178 let file_paths = instance
179 .metadata()
180 .relevant_paths
181 .iter()
182 .filter(|path| path.is_file())
184 .map(|path| path.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR))
185 .map(|path| path.to_path_buf())
186 .collect();
187
188 Some(SourcemapNode {
189 name: instance.name(),
190 class_name: instance.class_name(),
191 file_paths,
192 children,
193 })
194}
195
196fn write_sourcemap(
197 session: &ServeSession,
198 output: Option<&Path>,
199 filter: fn(&InstanceWithMeta) -> bool,
200) -> anyhow::Result<()> {
201 let tree = session.tree();
202
203 let root_node = recurse_create_node(&tree, tree.get_root_id(), session.root_dir(), filter);
204
205 if let Some(output_path) = output {
206 let mut file = BufWriter::new(File::create(output_path)?);
207 serde_json::to_writer(&mut file, &root_node)?;
208 file.flush()?;
209
210 println!("Created sourcemap at {}", output_path.display());
211 } else {
212 let output = serde_json::to_string(&root_node)?;
213 println!("{}", output);
214 }
215
216 Ok(())
217}