Skip to content

Commit

Permalink
Implement sourcemap CLI command (rojo-rbx#530)
Browse files Browse the repository at this point in the history
* Initial implementation of sourcemap CLI command

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <[email protected]>

* Update src/cli/sourcemap.rs

Co-authored-by: JohnnyMorganz <[email protected]>

* Tidy up sourcemap command

* Update CHANGELOG

Co-authored-by: JohnnyMorganz <[email protected]>
Co-authored-by: Lucien Greathouse <[email protected]>
  • Loading branch information
3 people authored Apr 19, 2022
1 parent 9e34f67 commit 0c8be5a
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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)
4 changes: 4 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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.
Expand All @@ -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(),
Expand Down Expand Up @@ -112,6 +115,7 @@ pub enum Subcommand {
Serve(ServeCommand),
Build(BuildCommand),
Upload(UploadCommand),
Sourcemap(SourcemapCommand),
FmtProject(FmtProjectCommand),
Doc(DocCommand),
Plugin(PluginCommand),
Expand Down
139 changes: 139 additions & 0 deletions src/cli/sourcemap.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,

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

/// 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<PathBuf>,

/// 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<SourcemapNode> {
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,
})
}

0 comments on commit 0c8be5a

Please sign in to comment.