1use std::{
2 borrow::Cow,
3 io::{BufWriter, Write},
4 mem::forget,
5 path::{self, Path, PathBuf},
6};
7
8use clap::Parser;
9use fs_err::File;
10use memofs::Vfs;
11use rayon::prelude::*;
12use rbx_dom_weak::{types::Ref, Ustr};
13use serde::Serialize;
14use tokio::runtime::Runtime;
15
16use crate::{
17 serve_session::ServeSession,
18 snapshot::{AppliedPatchSet, InstanceWithMeta, RojoTree},
19};
20
21use super::resolve_path;
22
23const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project file!";
24const ABSOLUTE_PATH_FAILED_ERR: &str = "Failed to turn relative path into absolute path!";
25
26#[derive(Serialize)]
28#[serde(rename_all = "camelCase")]
29struct SourcemapNode<'a> {
30 name: &'a str,
31 class_name: Ustr,
32
33 #[serde(
34 skip_serializing_if = "Vec::is_empty",
35 serialize_with = "crate::path_serializer::serialize_vec_absolute"
36 )]
37 file_paths: Vec<Cow<'a, Path>>,
38
39 #[serde(skip_serializing_if = "Vec::is_empty")]
40 children: Vec<SourcemapNode<'a>>,
41}
42
43#[derive(Debug, Parser)]
45pub struct SourcemapCommand {
46 #[clap(default_value = "")]
49 pub project: PathBuf,
50
51 #[clap(long, short)]
56 pub output: Option<PathBuf>,
57
58 #[clap(long)]
60 pub include_non_scripts: bool,
61
62 #[clap(long)]
64 pub watch: bool,
65
66 #[clap(long)]
68 pub absolute: bool,
69}
70
71impl SourcemapCommand {
72 pub fn run(self) -> anyhow::Result<()> {
73 let project_path = resolve_path(&self.project);
74
75 log::trace!("Constructing in-memory filesystem");
76 let vfs = Vfs::new_default();
77 vfs.set_watch_enabled(self.watch);
78
79 let session = ServeSession::new(vfs, project_path)?;
80 let mut cursor = session.message_queue().cursor();
81
82 let filter = if self.include_non_scripts {
83 filter_nothing
84 } else {
85 filter_non_scripts
86 };
87
88 rayon::ThreadPoolBuilder::new()
91 .num_threads(num_cpus::get().min(6))
92 .build_global()
93 .unwrap();
94
95 write_sourcemap(&session, self.output.as_deref(), filter, self.absolute)?;
96
97 if self.watch {
98 let rt = Runtime::new().unwrap();
99
100 loop {
101 let receiver = session.message_queue().subscribe(cursor);
102 let (new_cursor, patch_set) = rt.block_on(receiver).unwrap();
103 cursor = new_cursor;
104
105 if patch_set_affects_sourcemap(&session, &patch_set, filter) {
106 write_sourcemap(&session, self.output.as_deref(), filter, self.absolute)?;
107 }
108 }
109 }
110
111 forget(session);
114
115 Ok(())
116 }
117}
118
119fn filter_nothing(_instance: &InstanceWithMeta) -> bool {
120 true
121}
122
123fn filter_non_scripts(instance: &InstanceWithMeta) -> bool {
124 matches!(
125 instance.class_name().as_str(),
126 "Script" | "LocalScript" | "ModuleScript"
127 )
128}
129
130fn patch_set_affects_sourcemap(
131 session: &ServeSession,
132 patch_set: &[AppliedPatchSet],
133 filter: fn(&InstanceWithMeta) -> bool,
134) -> bool {
135 let tree = session.tree();
136
137 patch_set.par_iter().any(|set| {
139 !set.removed.is_empty()
142 || set.added.iter().any(|referent| {
144 let instance = tree
145 .get_instance(*referent)
146 .expect("instance did not exist when updating sourcemap");
147 filter(&instance)
148 })
149 || set.updated.iter().any(|updated| {
152 let changed = updated.changed_class_name.is_some()
153 || updated.changed_name.is_some()
154 || updated.changed_metadata.is_some();
155 if changed {
156 let instance = tree
157 .get_instance(updated.id)
158 .expect("instance did not exist when updating sourcemap");
159 filter(&instance)
160 } else {
161 false
162 }
163 })
164 })
165}
166
167fn recurse_create_node<'a>(
168 tree: &'a RojoTree,
169 referent: Ref,
170 project_dir: &Path,
171 filter: fn(&InstanceWithMeta) -> bool,
172 use_absolute_paths: bool,
173) -> Option<SourcemapNode<'a>> {
174 let instance = tree.get_instance(referent).expect("instance did not exist");
175
176 let children: Vec<_> = instance
177 .children()
178 .par_iter()
179 .filter_map(|&child_id| {
180 recurse_create_node(tree, child_id, project_dir, filter, use_absolute_paths)
181 })
182 .collect();
183
184 if children.is_empty() && !filter(&instance) {
187 return None;
188 }
189
190 let file_paths = instance
191 .metadata()
192 .relevant_paths
193 .iter()
194 .filter(|path| path.is_file())
196 .map(|path| path.as_path());
197
198 let mut output_file_paths: Vec<Cow<'a, Path>> =
199 Vec::with_capacity(instance.metadata().relevant_paths.len());
200
201 if use_absolute_paths {
202 for val in file_paths {
204 output_file_paths.push(Cow::Owned(
205 path::absolute(val).expect(ABSOLUTE_PATH_FAILED_ERR),
206 ));
207 }
208 } else {
209 for val in file_paths {
210 output_file_paths.push(Cow::from(
211 val.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR),
212 ));
213 }
214 };
215
216 Some(SourcemapNode {
217 name: instance.name(),
218 class_name: instance.class_name(),
219 file_paths: output_file_paths,
220 children,
221 })
222}
223
224fn write_sourcemap(
225 session: &ServeSession,
226 output: Option<&Path>,
227 filter: fn(&InstanceWithMeta) -> bool,
228 use_absolute_paths: bool,
229) -> anyhow::Result<()> {
230 let tree = session.tree();
231
232 let root_node = recurse_create_node(
233 &tree,
234 tree.get_root_id(),
235 session.root_dir(),
236 filter,
237 use_absolute_paths,
238 );
239
240 if let Some(output_path) = output {
241 let mut file = BufWriter::new(File::create(output_path)?);
242 serde_json::to_writer(&mut file, &root_node)?;
243 file.flush()?;
244
245 println!("Created sourcemap at {}", output_path.display());
246 } else {
247 let output = serde_json::to_string(&root_node)?;
248 println!("{}", output);
249 }
250
251 Ok(())
252}