diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f7993cca..c69524807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## Unreleased Changes * Added support for specifying an address to be used by default in the .project.json file ([#447]) * Added support for the new Open Cloud API when uploading. ([#486]) +* Added `sourcemap` command for generating sourcemaps to feed into other tools. ([#530]) [#447]: https://github.com/rojo-rbx/rojo/issues/447 [#486]: https://github.com/rojo-rbx/rojo/issues/486 +[#530]: https://github.com/rojo-rbx/rojo/pull/530 ## [7.0.0] - December 10, 2021 * Fixed Rojo's interactions with properties enabled by FFlags that are not yet enabled. ([#493]) @@ -480,4 +482,4 @@ This is a general maintenance release for the Rojo 0.5.x release series. * More robust syncing with a new reconciler ## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017) -* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs) +* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs) \ No newline at end of file diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cace812ce..2d498b4a3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -6,6 +6,7 @@ mod fmt_project; mod init; mod plugin; mod serve; +mod sourcemap; mod upload; use std::{borrow::Cow, env, path::Path, str::FromStr}; @@ -19,6 +20,7 @@ pub use self::fmt_project::FmtProjectCommand; pub use self::init::{InitCommand, InitKind}; pub use self::plugin::{PluginCommand, PluginSubcommand}; pub use self::serve::ServeCommand; +pub use self::sourcemap::SourcemapCommand; pub use self::upload::UploadCommand; /// Command line options that Rojo accepts, defined using the structopt crate. @@ -40,6 +42,7 @@ impl Options { Subcommand::Serve(subcommand) => subcommand.run(self.global), Subcommand::Build(subcommand) => subcommand.run(), Subcommand::Upload(subcommand) => subcommand.run(), + Subcommand::Sourcemap(subcommand) => subcommand.run(), Subcommand::FmtProject(subcommand) => subcommand.run(), Subcommand::Doc(subcommand) => subcommand.run(), Subcommand::Plugin(subcommand) => subcommand.run(), @@ -112,6 +115,7 @@ pub enum Subcommand { Serve(ServeCommand), Build(BuildCommand), Upload(UploadCommand), + Sourcemap(SourcemapCommand), FmtProject(FmtProjectCommand), Doc(DocCommand), Plugin(PluginCommand), diff --git a/src/cli/sourcemap.rs b/src/cli/sourcemap.rs new file mode 100644 index 000000000..66341dc86 --- /dev/null +++ b/src/cli/sourcemap.rs @@ -0,0 +1,139 @@ +use std::{ + io::{BufWriter, Write}, + path::{Path, PathBuf}, +}; + +use fs_err::File; +use memofs::Vfs; +use rbx_dom_weak::types::Ref; +use serde::Serialize; +use structopt::StructOpt; + +use crate::{ + serve_session::ServeSession, + snapshot::{InstanceWithMeta, RojoTree}, +}; + +use super::resolve_path; + +const PATH_STRIP_FAILED_ERR: &str = "Failed to create relative paths for project file!"; + +/// Representation of a node in the generated sourcemap tree. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct SourcemapNode { + name: String, + class_name: String, + + #[serde(skip_serializing_if = "Vec::is_empty")] + file_paths: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty")] + children: Vec, +} + +/// Generates a sourcemap file from the Rojo project. +#[derive(Debug, StructOpt)] +pub struct SourcemapCommand { + /// Path to the project to use for the sourcemap. Defaults to the current + /// directory. + #[structopt(default_value = "")] + pub project: PathBuf, + + /// Where to output the sourcemap. Omit this to use stdout instead of + /// writing to a file. + /// + /// Should end in .json. + #[structopt(long, short)] + pub output: Option, + + /// If non-script files should be included or not. Defaults to false. + #[structopt(long)] + pub include_non_scripts: bool, +} + +impl SourcemapCommand { + pub fn run(self) -> anyhow::Result<()> { + let project_path = resolve_path(&self.project); + + let mut project_dir = project_path.to_path_buf(); + project_dir.pop(); + + log::trace!("Constructing in-memory filesystem"); + let vfs = Vfs::new_default(); + + let session = ServeSession::new(vfs, &project_path)?; + let tree = session.tree(); + + let filter = if self.include_non_scripts { + filter_nothing + } else { + filter_non_scripts + }; + + let root_node = recurse_create_node(&tree, tree.get_root_id(), &project_dir, filter); + + if let Some(output_path) = self.output { + let mut file = BufWriter::new(File::create(&output_path)?); + serde_json::to_writer(&mut file, &root_node)?; + file.flush()?; + + println!("Created sourcemap at {}", output_path.display()); + } else { + let output = serde_json::to_string(&root_node)?; + println!("{}", output); + } + + Ok(()) + } +} + +fn filter_nothing(_instance: &InstanceWithMeta) -> bool { + true +} + +fn filter_non_scripts(instance: &InstanceWithMeta) -> bool { + match instance.class_name() { + "Script" | "LocalScript" | "ModuleScript" => true, + _ => false, + } +} + +fn recurse_create_node( + tree: &RojoTree, + referent: Ref, + project_dir: &Path, + filter: fn(&InstanceWithMeta) -> bool, +) -> Option { + let instance = tree.get_instance(referent).expect("instance did not exist"); + + let mut children = Vec::new(); + for &child_id in instance.children() { + if let Some(child_node) = recurse_create_node(tree, child_id, &project_dir, filter) { + children.push(child_node); + } + } + + // If this object has no children and doesn't pass the filter, it doesn't + // contain any information we're looking for. + if children.is_empty() && !filter(&instance) { + return None; + } + + let file_paths = instance + .metadata() + .relevant_paths + .iter() + // Not all paths listed as relevant are guaranteed to exist. + .filter(|path| path.is_file()) + .map(|path| path.strip_prefix(project_dir).expect(PATH_STRIP_FAILED_ERR)) + .map(|path| path.to_path_buf()) + .collect(); + + Some(SourcemapNode { + name: instance.name().to_string(), + class_name: instance.class_name().to_string(), + file_paths, + children, + }) +}