mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-04-21 05:06:29 +00:00
Add a new syncRules field project files to allow users to specify middleware to use for files (#813)
This commit is contained in:
@@ -8,7 +8,7 @@ use std::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{glob::Glob, resolution::UnresolvedValue};
|
||||
use crate::{glob::Glob, resolution::UnresolvedValue, snapshot::SyncRule};
|
||||
|
||||
static PROJECT_FILENAME: &str = "default.project.json";
|
||||
|
||||
@@ -84,6 +84,12 @@ pub struct Project {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub glob_ignore_paths: Vec<Glob>,
|
||||
|
||||
/// A list of mappings of globs to syncing rules. If a file matches a glob,
|
||||
/// it will be 'transformed' into an Instance following the rule provided.
|
||||
/// Globs are relative to the folder the project file is in.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub sync_rules: Vec<SyncRule>,
|
||||
|
||||
/// The path to the file that this project came from. Relative paths in the
|
||||
/// project should be considered relative to the parent of this field, also
|
||||
/// given by `Project::folder_location`.
|
||||
|
||||
@@ -4,11 +4,14 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
glob::Glob, path_serializer, project::ProjectNode,
|
||||
snapshot_middleware::emit_legacy_scripts_default,
|
||||
glob::Glob,
|
||||
path_serializer,
|
||||
project::ProjectNode,
|
||||
snapshot_middleware::{emit_legacy_scripts_default, Middleware},
|
||||
};
|
||||
|
||||
/// Rojo-specific metadata that can be associated with an instance or a snapshot
|
||||
@@ -107,6 +110,8 @@ pub struct InstanceContext {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
||||
pub emit_legacy_scripts: bool,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub sync_rules: Vec<SyncRule>,
|
||||
}
|
||||
|
||||
impl InstanceContext {
|
||||
@@ -114,6 +119,7 @@ impl InstanceContext {
|
||||
Self {
|
||||
path_ignore_rules: Arc::new(Vec::new()),
|
||||
emit_legacy_scripts: emit_legacy_scripts_default().unwrap(),
|
||||
sync_rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +150,28 @@ impl InstanceContext {
|
||||
rules.extend(new_rules);
|
||||
}
|
||||
|
||||
/// Extend the list of syncing rules in the context with the given new rules.
|
||||
pub fn add_sync_rules<I>(&mut self, new_rules: I)
|
||||
where
|
||||
I: IntoIterator<Item = SyncRule>,
|
||||
{
|
||||
self.sync_rules.extend(new_rules);
|
||||
}
|
||||
|
||||
/// Clears all sync rules for this InstanceContext
|
||||
pub fn clear_sync_rules(&mut self) {
|
||||
self.sync_rules.clear();
|
||||
}
|
||||
|
||||
pub fn set_emit_legacy_scripts(&mut self, emit_legacy_scripts: bool) {
|
||||
self.emit_legacy_scripts = emit_legacy_scripts;
|
||||
}
|
||||
|
||||
/// Returns the middleware specified by the first sync rule that
|
||||
/// matches the provided path. This does not handle default syncing rules.
|
||||
pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
|
||||
self.sync_rules.iter().find(|&rule| rule.matches(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for InstanceContext {
|
||||
@@ -216,3 +241,64 @@ impl From<&Path> for InstigatingSource {
|
||||
InstigatingSource::Path(path.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an user-specified rule for transforming files
|
||||
/// into Instances using a given middleware.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct SyncRule {
|
||||
/// A pattern used to determine if a file is included in this SyncRule
|
||||
#[serde(rename = "pattern")]
|
||||
pub include: Glob,
|
||||
/// A pattern used to determine if a file is excluded from this SyncRule.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclude: Option<Glob>,
|
||||
/// The middleware specified by the user for this SyncRule
|
||||
#[serde(rename = "use")]
|
||||
pub middleware: Middleware,
|
||||
/// A suffix to trim off of file names, including the file extension.
|
||||
/// If not specified, the file extension is the only thing cut off.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suffix: Option<String>,
|
||||
/// The 'base' of the glob above, allowing it to be used
|
||||
/// relative to a path instead of absolute.
|
||||
#[serde(skip)]
|
||||
pub base_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SyncRule {
|
||||
/// Returns whether the given path matches this rule.
|
||||
pub fn matches(&self, path: &Path) -> bool {
|
||||
match path.strip_prefix(&self.base_path) {
|
||||
Ok(suffix) => {
|
||||
if let Some(pattern) = &self.exclude {
|
||||
if pattern.is_match(suffix) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.include.is_match(suffix)
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> {
|
||||
if let Some(suffix) = &self.suffix {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.with_context(|| format!("file name of {} is invalid", path.display()))?;
|
||||
if file_name.ends_with(suffix) {
|
||||
let end = file_name.len().saturating_sub(suffix.len());
|
||||
Ok(&file_name[..end])
|
||||
} else {
|
||||
Ok(file_name)
|
||||
}
|
||||
} else {
|
||||
// If the user doesn't specify a suffix, we assume they just want
|
||||
// the name of the file (the file_stem)
|
||||
path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.with_context(|| format!("file name of {} is invalid", path.display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,14 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
use super::{
|
||||
dir::{dir_meta, snapshot_dir_no_meta},
|
||||
meta_file::AdjacentMetadata,
|
||||
util::PathExt,
|
||||
};
|
||||
|
||||
pub fn snapshot_csv(
|
||||
_context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".csv")?;
|
||||
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", name));
|
||||
let contents = vfs.read(path)?;
|
||||
|
||||
@@ -74,9 +72,8 @@ pub fn snapshot_csv_init(
|
||||
);
|
||||
}
|
||||
|
||||
let mut init_snapshot = snapshot_csv(context, vfs, init_path)?.unwrap();
|
||||
let mut init_snapshot = snapshot_csv(context, vfs, init_path, &dir_snapshot.name)?.unwrap();
|
||||
|
||||
init_snapshot.name = dir_snapshot.name;
|
||||
init_snapshot.children = dir_snapshot.children;
|
||||
init_snapshot.metadata = dir_snapshot.metadata;
|
||||
|
||||
@@ -185,10 +182,14 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#,
|
||||
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let instance_snapshot =
|
||||
snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = snapshot_csv(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.csv"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
@@ -213,10 +214,14 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#,
|
||||
|
||||
let mut vfs = Vfs::new(imfs);
|
||||
|
||||
let instance_snapshot =
|
||||
snapshot_csv(&InstanceContext::default(), &mut vfs, Path::new("/foo.csv"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = snapshot_csv(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.csv"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
};
|
||||
|
||||
use super::{meta_file::AdjacentMetadata, util::PathExt};
|
||||
use super::meta_file::AdjacentMetadata;
|
||||
|
||||
pub fn snapshot_json(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".json")?;
|
||||
let contents = vfs.read(path)?;
|
||||
|
||||
let value: serde_json::Value = serde_json::from_slice(&contents)
|
||||
@@ -107,6 +107,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.json"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -10,15 +10,12 @@ use crate::{
|
||||
snapshot::{InstanceContext, InstanceSnapshot},
|
||||
};
|
||||
|
||||
use super::util::PathExt;
|
||||
|
||||
pub fn snapshot_json_model(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".model.json")?;
|
||||
|
||||
let contents = vfs.read(path)?;
|
||||
let contents_str = str::from_utf8(&contents)
|
||||
.with_context(|| format!("File was not valid UTF-8: {}", path.display()))?;
|
||||
@@ -158,6 +155,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&vfs,
|
||||
Path::new("/foo.model.json"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -195,6 +193,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&vfs,
|
||||
Path::new("/foo.model.json"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -9,11 +9,10 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
use super::{
|
||||
dir::{dir_meta, snapshot_dir_no_meta},
|
||||
meta_file::AdjacentMetadata,
|
||||
util::match_trailing,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ScriptType {
|
||||
pub enum ScriptType {
|
||||
Server,
|
||||
Client,
|
||||
Module,
|
||||
@@ -24,32 +23,15 @@ pub fn snapshot_lua(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
script_type: ScriptType,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let file_name = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
let run_context_enums = &rbx_reflection_database::get()
|
||||
.enums
|
||||
.get("RunContext")
|
||||
.expect("Unable to get RunContext enums!")
|
||||
.items;
|
||||
|
||||
let (script_type, instance_name) = if let Some(name) = match_trailing(&file_name, ".server.lua")
|
||||
{
|
||||
(ScriptType::Server, name)
|
||||
} else if let Some(name) = match_trailing(&file_name, ".client.lua") {
|
||||
(ScriptType::Client, name)
|
||||
} else if let Some(name) = match_trailing(&file_name, ".lua") {
|
||||
(ScriptType::Module, name)
|
||||
} else if let Some(name) = match_trailing(&file_name, ".server.luau") {
|
||||
(ScriptType::Server, name)
|
||||
} else if let Some(name) = match_trailing(&file_name, ".client.luau") {
|
||||
(ScriptType::Client, name)
|
||||
} else if let Some(name) = match_trailing(&file_name, ".luau") {
|
||||
(ScriptType::Module, name)
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let (class_name, run_context) = match (context.emit_legacy_scripts, script_type) {
|
||||
(false, ScriptType::Server) => ("Script", run_context_enums.get("Server")),
|
||||
(false, ScriptType::Client) => ("Script", run_context_enums.get("Client")),
|
||||
@@ -73,10 +55,10 @@ pub fn snapshot_lua(
|
||||
);
|
||||
}
|
||||
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", instance_name));
|
||||
let meta_path = path.with_file_name(format!("{}.meta.json", name));
|
||||
|
||||
let mut snapshot = InstanceSnapshot::new()
|
||||
.name(instance_name)
|
||||
.name(name)
|
||||
.class_name(class_name)
|
||||
.properties(properties)
|
||||
.metadata(
|
||||
@@ -103,6 +85,7 @@ pub fn snapshot_lua_init(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
init_path: &Path,
|
||||
script_type: ScriptType,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let folder_path = init_path.parent().unwrap();
|
||||
let dir_snapshot = snapshot_dir_no_meta(context, vfs, folder_path)?.unwrap();
|
||||
@@ -119,9 +102,9 @@ pub fn snapshot_lua_init(
|
||||
);
|
||||
}
|
||||
|
||||
let mut init_snapshot = snapshot_lua(context, vfs, init_path)?.unwrap();
|
||||
let mut init_snapshot =
|
||||
snapshot_lua(context, vfs, init_path, &dir_snapshot.name, script_type)?.unwrap();
|
||||
|
||||
init_snapshot.name = dir_snapshot.name;
|
||||
init_snapshot.children = dir_snapshot.children;
|
||||
init_snapshot.metadata = dir_snapshot.metadata;
|
||||
|
||||
@@ -151,6 +134,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.lua"),
|
||||
"foo",
|
||||
ScriptType::Module,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -172,6 +157,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.lua"),
|
||||
"foo",
|
||||
ScriptType::Module,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -193,6 +180,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
"foo",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -214,6 +203,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
"foo",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -235,6 +226,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.client.lua"),
|
||||
"foo",
|
||||
ScriptType::Client,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -256,6 +249,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.client.lua"),
|
||||
"foo",
|
||||
ScriptType::Client,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -283,6 +278,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/root"),
|
||||
"root",
|
||||
ScriptType::Module,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -315,6 +312,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.lua"),
|
||||
"foo",
|
||||
ScriptType::Module,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -347,6 +346,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.lua"),
|
||||
"foo",
|
||||
ScriptType::Module,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -379,6 +380,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
"foo",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -411,6 +414,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/foo.server.lua"),
|
||||
"foo",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -445,6 +450,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
||||
&mut vfs,
|
||||
Path::new("/bar.server.lua"),
|
||||
"bar",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -479,6 +486,8 @@ mod test {
|
||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
||||
&mut vfs,
|
||||
Path::new("/bar.server.lua"),
|
||||
"bar",
|
||||
ScriptType::Server,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -18,30 +18,37 @@ mod toml;
|
||||
mod txt;
|
||||
mod util;
|
||||
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceSnapshot};
|
||||
use crate::glob::Glob;
|
||||
use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule};
|
||||
|
||||
use self::{
|
||||
csv::{snapshot_csv, snapshot_csv_init},
|
||||
dir::snapshot_dir,
|
||||
json::snapshot_json,
|
||||
json_model::snapshot_json_model,
|
||||
lua::{snapshot_lua, snapshot_lua_init},
|
||||
lua::{snapshot_lua, snapshot_lua_init, ScriptType},
|
||||
project::snapshot_project,
|
||||
rbxm::snapshot_rbxm,
|
||||
rbxmx::snapshot_rbxmx,
|
||||
toml::snapshot_toml,
|
||||
txt::snapshot_txt,
|
||||
util::PathExt,
|
||||
};
|
||||
|
||||
pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default};
|
||||
|
||||
/// The main entrypoint to the snapshot function. This function can be pointed
|
||||
/// at any path and will return something if Rojo knows how to deal with it.
|
||||
/// Returns an `InstanceSnapshot` for the provided path.
|
||||
/// This will inspect the path and find the appropriate middleware for it,
|
||||
/// taking user-written rules into account. Then, it will attempt to convert
|
||||
/// the path into an InstanceSnapshot using that middleware.
|
||||
#[profiling::function]
|
||||
pub fn snapshot_from_vfs(
|
||||
context: &InstanceContext,
|
||||
@@ -54,89 +61,234 @@ pub fn snapshot_from_vfs(
|
||||
};
|
||||
|
||||
if meta.is_dir() {
|
||||
let project_path = path.join("default.project.json");
|
||||
if vfs.metadata(&project_path).with_not_found()?.is_some() {
|
||||
return snapshot_project(context, vfs, &project_path);
|
||||
}
|
||||
if let Some(init_path) = get_init_path(vfs, path)? {
|
||||
// TODO: support user-defined init paths
|
||||
for rule in default_sync_rules() {
|
||||
if rule.matches(&init_path) {
|
||||
return match rule.middleware {
|
||||
Middleware::Project => snapshot_project(context, vfs, &init_path),
|
||||
|
||||
let init_path = path.join("init.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
}
|
||||
Middleware::ModuleScript => {
|
||||
snapshot_lua_init(context, vfs, &init_path, ScriptType::Module)
|
||||
}
|
||||
Middleware::ServerScript => {
|
||||
snapshot_lua_init(context, vfs, &init_path, ScriptType::Server)
|
||||
}
|
||||
Middleware::ClientScript => {
|
||||
snapshot_lua_init(context, vfs, &init_path, ScriptType::Client)
|
||||
}
|
||||
|
||||
let init_path = path.join("init.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
}
|
||||
Middleware::Csv => snapshot_csv_init(context, vfs, &init_path),
|
||||
|
||||
let init_path = path.join("init.server.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
_ => snapshot_dir(context, vfs, path),
|
||||
};
|
||||
}
|
||||
}
|
||||
snapshot_dir(context, vfs, path)
|
||||
} else {
|
||||
snapshot_dir(context, vfs, path)
|
||||
}
|
||||
|
||||
let init_path = path.join("init.server.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
}
|
||||
|
||||
let init_path = path.join("init.client.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
}
|
||||
|
||||
let init_path = path.join("init.client.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_lua_init(context, vfs, &init_path);
|
||||
}
|
||||
|
||||
let init_path = path.join("init.csv");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return snapshot_csv_init(context, vfs, &init_path);
|
||||
}
|
||||
|
||||
snapshot_dir(context, vfs, path)
|
||||
} else {
|
||||
let script_name = path
|
||||
.file_name_trim_end(".lua")
|
||||
.or_else(|_| path.file_name_trim_end(".luau"));
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.with_context(|| format!("file name of {} is invalid", path.display()))?;
|
||||
|
||||
let csv_name = path.file_name_trim_end(".csv");
|
||||
|
||||
if let Ok(name) = script_name {
|
||||
match name {
|
||||
// init scripts are handled elsewhere and should not turn into
|
||||
// their own children.
|
||||
"init" | "init.client" | "init.server" => return Ok(None),
|
||||
|
||||
_ => return snapshot_lua(context, vfs, path),
|
||||
}
|
||||
} else if path.file_name_ends_with(".project.json") {
|
||||
return snapshot_project(context, vfs, path);
|
||||
} else if path.file_name_ends_with(".model.json") {
|
||||
return snapshot_json_model(context, vfs, path);
|
||||
} else if path.file_name_ends_with(".meta.json") {
|
||||
// .meta.json files do not turn into their own instances.
|
||||
return Ok(None);
|
||||
} else if path.file_name_ends_with(".json") {
|
||||
return snapshot_json(context, vfs, path);
|
||||
} else if path.file_name_ends_with(".toml") {
|
||||
return snapshot_toml(context, vfs, path);
|
||||
} else if let Ok(name) = csv_name {
|
||||
match name {
|
||||
// init csv are handled elsewhere and should not turn into
|
||||
// their own children.
|
||||
"init" => return Ok(None),
|
||||
|
||||
_ => return snapshot_csv(context, vfs, path),
|
||||
}
|
||||
} else if path.file_name_ends_with(".txt") {
|
||||
return snapshot_txt(context, vfs, path);
|
||||
} else if path.file_name_ends_with(".rbxmx") {
|
||||
return snapshot_rbxmx(context, vfs, path);
|
||||
} else if path.file_name_ends_with(".rbxm") {
|
||||
return snapshot_rbxm(context, vfs, path);
|
||||
// TODO: Is this even necessary anymore?
|
||||
match file_name {
|
||||
"init.server.luau" | "init.server.lua" | "init.client.luau" | "init.client.lua"
|
||||
| "init.luau" | "init.lua" | "init.csv" => return Ok(None),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
snapshot_from_path(context, vfs, path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an `init` path for the given directory.
|
||||
/// This uses an intrinsic priority list and for compatibility,
|
||||
/// it should not be changed.
|
||||
fn get_init_path<P: AsRef<Path>>(vfs: &Vfs, dir: P) -> anyhow::Result<Option<PathBuf>> {
|
||||
let path = dir.as_ref();
|
||||
|
||||
let project_path = path.join("default.project.json");
|
||||
if vfs.metadata(&project_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(project_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.server.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.server.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.client.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.client.lua");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
let init_path = path.join("init.csv");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Gets a snapshot for a path given an InstanceContext and Vfs, taking
|
||||
/// user specified sync rules into account.
|
||||
fn snapshot_from_path(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
if let Some(rule) = context.get_user_sync_rule(path) {
|
||||
return rule
|
||||
.middleware
|
||||
.snapshot(context, vfs, path, rule.file_name_for_path(path)?);
|
||||
} else {
|
||||
for rule in default_sync_rules() {
|
||||
if rule.matches(path) {
|
||||
return rule.middleware.snapshot(
|
||||
context,
|
||||
vfs,
|
||||
path,
|
||||
rule.file_name_for_path(path)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Represents a possible 'transformer' used by Rojo to turn a file system
|
||||
/// item into a Roblox Instance. Missing from this list are directories and
|
||||
/// metadata. This is deliberate, as metadata is not a snapshot middleware
|
||||
/// and directories do not make sense to turn into files.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Middleware {
|
||||
Csv,
|
||||
JsonModel,
|
||||
Json,
|
||||
ServerScript,
|
||||
ClientScript,
|
||||
ModuleScript,
|
||||
Project,
|
||||
Rbxm,
|
||||
Rbxmx,
|
||||
Toml,
|
||||
Text,
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl Middleware {
|
||||
/// Creates a snapshot for the given path from the Middleware with
|
||||
/// the provided name.
|
||||
fn snapshot(
|
||||
&self,
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
match self {
|
||||
Self::Csv => snapshot_csv(context, vfs, path, name),
|
||||
Self::JsonModel => snapshot_json_model(context, vfs, path, name),
|
||||
Self::Json => snapshot_json(context, vfs, path, name),
|
||||
Self::ServerScript => snapshot_lua(context, vfs, path, name, ScriptType::Server),
|
||||
Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
|
||||
Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
|
||||
// At the moment, snapshot_project does not use `name` so we
|
||||
// don't provide it.
|
||||
Self::Project => snapshot_project(context, vfs, path),
|
||||
Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
|
||||
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
|
||||
Self::Toml => snapshot_toml(context, vfs, path, name),
|
||||
Self::Text => snapshot_txt(context, vfs, path, name),
|
||||
Self::Ignore => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper for easily defining a SyncRule. Arguments are passed literally
|
||||
/// to this macro in the order `include`, `middleware`, `suffix`,
|
||||
/// and `exclude`. Both `suffix` and `exclude` are optional.
|
||||
///
|
||||
/// All arguments except `middleware` are expected to be strings.
|
||||
/// The `middleware` parameter is expected to be a variant of `Middleware`,
|
||||
/// not including the enum name itself.
|
||||
macro_rules! sync_rule {
|
||||
($pattern:expr, $middleware:ident) => {
|
||||
SyncRule {
|
||||
middleware: Middleware::$middleware,
|
||||
include: Glob::new($pattern).unwrap(),
|
||||
exclude: None,
|
||||
suffix: None,
|
||||
base_path: PathBuf::new(),
|
||||
}
|
||||
};
|
||||
($pattern:expr, $middleware:ident, $suffix:expr) => {
|
||||
SyncRule {
|
||||
middleware: Middleware::$middleware,
|
||||
include: Glob::new($pattern).unwrap(),
|
||||
exclude: None,
|
||||
suffix: Some($suffix.into()),
|
||||
base_path: PathBuf::new(),
|
||||
}
|
||||
};
|
||||
($pattern:expr, $middleware:ident, $suffix:expr, $exclude:expr) => {
|
||||
SyncRule {
|
||||
middleware: Middleware::$middleware,
|
||||
include: Glob::new($pattern).unwrap(),
|
||||
exclude: Some(Glob::new($exclude).unwrap()),
|
||||
suffix: Some($suffix.into()),
|
||||
base_path: PathBuf::new(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Defines the 'default' syncing rules that Rojo uses.
|
||||
/// These do not broadly overlap, but the order matters for some in the case of
|
||||
/// e.g. JSON models.
|
||||
fn default_sync_rules() -> &'static [SyncRule] {
|
||||
static DEFAULT_SYNC_RULES: OnceLock<Vec<SyncRule>> = OnceLock::new();
|
||||
|
||||
DEFAULT_SYNC_RULES.get_or_init(|| {
|
||||
vec![
|
||||
sync_rule!("*.server.lua", ServerScript, ".server.lua"),
|
||||
sync_rule!("*.server.luau", ServerScript, ".server.luau"),
|
||||
sync_rule!("*.client.lua", ClientScript, ".client.lua"),
|
||||
sync_rule!("*.client.luau", ClientScript, ".client.luau"),
|
||||
sync_rule!("*.{lua,luau}", ModuleScript),
|
||||
// Project middleware doesn't use the file name.
|
||||
sync_rule!("*.project.json", Project),
|
||||
sync_rule!("*.model.json", JsonModel, ".model.json"),
|
||||
sync_rule!("*.json", Json, ".json", "*.meta.json"),
|
||||
sync_rule!("*.toml", Toml),
|
||||
sync_rule!("*.csv", Csv),
|
||||
sync_rule!("*.txt", Text),
|
||||
sync_rule!("*.rbxmx", Rbxmx),
|
||||
sync_rule!("*.rbxm", Rbxm),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::{
|
||||
project::{PathNode, Project, ProjectNode},
|
||||
snapshot::{
|
||||
InstanceContext, InstanceMetadata, InstanceSnapshot, InstigatingSource, PathIgnoreRule,
|
||||
SyncRule,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,12 +24,19 @@ pub fn snapshot_project(
|
||||
.with_context(|| format!("File was not a valid Rojo project: {}", path.display()))?;
|
||||
|
||||
let mut context = context.clone();
|
||||
context.clear_sync_rules();
|
||||
|
||||
let rules = project.glob_ignore_paths.iter().map(|glob| PathIgnoreRule {
|
||||
glob: glob.clone(),
|
||||
base_path: project.folder_location().to_path_buf(),
|
||||
});
|
||||
|
||||
let sync_rules = project.sync_rules.iter().map(|rule| SyncRule {
|
||||
base_path: project.folder_location().to_path_buf(),
|
||||
..rule.clone()
|
||||
});
|
||||
|
||||
context.add_sync_rules(sync_rules);
|
||||
context.add_path_ignore_rules(rules);
|
||||
context.set_emit_legacy_scripts(
|
||||
project
|
||||
|
||||
@@ -5,16 +5,13 @@ use memofs::Vfs;
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::util::PathExt;
|
||||
|
||||
#[profiling::function]
|
||||
pub fn snapshot_rbxm(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".rbxm")?;
|
||||
|
||||
let temp_tree = rbx_binary::from_reader(vfs.read(path)?.as_slice())
|
||||
.with_context(|| format!("Malformed rbxm file: {}", path.display()))?;
|
||||
|
||||
@@ -63,6 +60,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.rbxm"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -5,15 +5,12 @@ use memofs::Vfs;
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::util::PathExt;
|
||||
|
||||
pub fn snapshot_rbxmx(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".rbxmx")?;
|
||||
|
||||
let options = rbx_xml::DecodeOptions::new()
|
||||
.property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown);
|
||||
|
||||
@@ -75,6 +72,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.rbxmx"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -9,14 +9,14 @@ use crate::{
|
||||
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
||||
};
|
||||
|
||||
use super::{meta_file::AdjacentMetadata, util::PathExt};
|
||||
use super::meta_file::AdjacentMetadata;
|
||||
|
||||
pub fn snapshot_toml(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".toml")?;
|
||||
let contents = vfs.read(path)?;
|
||||
|
||||
let value: toml::Value = toml::from_slice(&contents)
|
||||
@@ -114,6 +114,7 @@ mod test {
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.toml"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -6,15 +6,14 @@ use memofs::{IoResultExt, Vfs};
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
|
||||
|
||||
use super::{meta_file::AdjacentMetadata, util::PathExt};
|
||||
use super::meta_file::AdjacentMetadata;
|
||||
|
||||
pub fn snapshot_txt(
|
||||
context: &InstanceContext,
|
||||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
let name = path.file_name_trim_end(".txt")?;
|
||||
|
||||
let contents = vfs.read(path)?;
|
||||
let contents_str = str::from_utf8(&contents)
|
||||
.with_context(|| format!("File was not valid UTF-8: {}", path.display()))?
|
||||
@@ -59,10 +58,14 @@ mod test {
|
||||
|
||||
let mut vfs = Vfs::new(imfs.clone());
|
||||
|
||||
let instance_snapshot =
|
||||
snapshot_txt(&InstanceContext::default(), &mut vfs, Path::new("/foo.txt"))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let instance_snapshot = snapshot_txt(
|
||||
&InstanceContext::default(),
|
||||
&mut vfs,
|
||||
Path::new("/foo.txt"),
|
||||
"foo",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user