//! Defines the semantics that Rojo uses to turn entries on the filesystem into //! Roblox instances using the instance snapshot subsystem. //! //! These modules define how files turn into instances. #![allow(dead_code)] mod csv; mod dir; mod json; mod json_model; mod lua; mod meta_file; mod project; mod rbxm; mod rbxmx; mod toml; mod txt; mod util; use std::{ path::{Path, PathBuf}, sync::OnceLock, }; use anyhow::Context; use memofs::{IoResultExt, Vfs}; use serde::{Deserialize, Serialize}; 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, ScriptType}, project::snapshot_project, rbxm::snapshot_rbxm, rbxmx::snapshot_rbxmx, toml::snapshot_toml, txt::snapshot_txt, }; pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default}; /// 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, vfs: &Vfs, path: &Path, ) -> anyhow::Result> { let meta = match vfs.metadata(path).with_not_found()? { Some(meta) => meta, None => return Ok(None), }; if meta.is_dir() { if let Some(init_path) = get_init_path(vfs, path)? { // TODO: support user-defined init paths // If and when we do, make sure to go support it in // `Project::set_file_name`, as right now it special-cases // `default.project.json` as an `init` path. for rule in default_sync_rules() { if rule.matches(&init_path) { return match rule.middleware { Middleware::Project => { let name = init_path .parent() .and_then(Path::file_name) .and_then(|s| s.to_str()).expect("default.project.json should be inside a folder with a unicode name"); snapshot_project(context, vfs, &init_path, name) } 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) } Middleware::Csv => snapshot_csv_init(context, vfs, &init_path), _ => snapshot_dir(context, vfs, path), }; } } snapshot_dir(context, vfs, path) } else { snapshot_dir(context, vfs, path) } } else { let file_name = path .file_name() .and_then(|n| n.to_str()) .with_context(|| format!("file name of {} is invalid", path.display()))?; // 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), _ => {} } 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>(vfs: &Vfs, dir: P) -> anyhow::Result> { 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> { 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> { 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), Self::Project => snapshot_project(context, vfs, path, name), 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. pub fn default_sync_rules() -> &'static [SyncRule] { static DEFAULT_SYNC_RULES: OnceLock> = 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), sync_rule!("*.project.json", Project, ".project.json"), 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), ] }) }