forked from rojo-rbx/rojo
Implement Syncback to support converting Roblox files to a Rojo project (#937)
This is a very large commit. Consider checking the linked PR for more information.
This commit is contained in:
@@ -28,24 +28,34 @@ use anyhow::Context;
|
||||
use memofs::{IoResultExt, Vfs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::snapshot::{InstanceContext, InstanceSnapshot, SyncRule};
|
||||
use crate::{glob::Glob, project::DEFAULT_PROJECT_NAMES};
|
||||
use crate::{
|
||||
glob::Glob,
|
||||
project::DEFAULT_PROJECT_NAMES,
|
||||
syncback::{SyncbackReturn, SyncbackSnapshot},
|
||||
};
|
||||
use crate::{
|
||||
snapshot::{InstanceContext, InstanceSnapshot, SyncRule},
|
||||
syncback::validate_file_name,
|
||||
};
|
||||
|
||||
use self::{
|
||||
csv::{snapshot_csv, snapshot_csv_init},
|
||||
dir::snapshot_dir,
|
||||
csv::{snapshot_csv, snapshot_csv_init, syncback_csv, syncback_csv_init},
|
||||
dir::{snapshot_dir, syncback_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,
|
||||
json_model::{snapshot_json_model, syncback_json_model},
|
||||
lua::{snapshot_lua, snapshot_lua_init, syncback_lua, syncback_lua_init},
|
||||
project::{snapshot_project, syncback_project},
|
||||
rbxm::{snapshot_rbxm, syncback_rbxm},
|
||||
rbxmx::{snapshot_rbxmx, syncback_rbxmx},
|
||||
toml::snapshot_toml,
|
||||
txt::snapshot_txt,
|
||||
txt::{snapshot_txt, syncback_txt},
|
||||
yaml::snapshot_yaml,
|
||||
};
|
||||
|
||||
pub use self::{project::snapshot_project_node, util::emit_legacy_scripts_default};
|
||||
pub use self::{
|
||||
lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default,
|
||||
util::PathExt,
|
||||
};
|
||||
|
||||
/// Returns an `InstanceSnapshot` for the provided path.
|
||||
/// This will inspect the path and find the appropriate middleware for it,
|
||||
@@ -63,41 +73,14 @@ pub fn snapshot_from_vfs(
|
||||
};
|
||||
|
||||
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)
|
||||
let (middleware, dir_name, init_path) = get_dir_middleware(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.
|
||||
match middleware {
|
||||
Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
|
||||
_ => middleware.snapshot(context, vfs, &init_path, dir_name),
|
||||
}
|
||||
} else {
|
||||
let file_name = path
|
||||
@@ -116,55 +99,50 @@ pub fn snapshot_from_vfs(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
/// Gets the appropriate middleware for a directory by checking for `init`
|
||||
/// files. This uses an intrinsic priority list and for compatibility,
|
||||
/// that order should be left unchanged.
|
||||
///
|
||||
/// Returns the middleware, the name of the directory, and the path to
|
||||
/// the init location.
|
||||
fn get_dir_middleware<'path>(
|
||||
vfs: &Vfs,
|
||||
dir_path: &'path Path,
|
||||
) -> anyhow::Result<(Middleware, &'path str, PathBuf)> {
|
||||
let dir_name = dir_path
|
||||
.file_name()
|
||||
.expect("Could not extract directory name")
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?;
|
||||
|
||||
static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new();
|
||||
let order = INIT_PATHS.get_or_init(|| {
|
||||
vec![
|
||||
(Middleware::ModuleScriptDir, "init.luau"),
|
||||
(Middleware::ModuleScriptDir, "init.lua"),
|
||||
(Middleware::ServerScriptDir, "init.server.luau"),
|
||||
(Middleware::ServerScriptDir, "init.server.lua"),
|
||||
(Middleware::ClientScriptDir, "init.client.luau"),
|
||||
(Middleware::ClientScriptDir, "init.client.lua"),
|
||||
(Middleware::CsvDir, "init.csv"),
|
||||
]
|
||||
});
|
||||
|
||||
for default_project_name in DEFAULT_PROJECT_NAMES {
|
||||
let project_path = path.join(default_project_name);
|
||||
let project_path = dir_path.join(default_project_name);
|
||||
if vfs.metadata(&project_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(project_path));
|
||||
return Ok((Middleware::Project, dir_name, project_path));
|
||||
}
|
||||
}
|
||||
|
||||
let init_path = path.join("init.luau");
|
||||
if vfs.metadata(&init_path).with_not_found()?.is_some() {
|
||||
return Ok(Some(init_path));
|
||||
for (middleware, name) in order {
|
||||
let test_path = dir_path.join(name);
|
||||
if vfs.metadata(&test_path).with_not_found()?.is_some() {
|
||||
return Ok((*middleware, dir_name, test_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)
|
||||
Ok((Middleware::Dir, dir_name, dir_path.to_path_buf()))
|
||||
}
|
||||
|
||||
/// Gets a snapshot for a path given an InstanceContext and Vfs, taking
|
||||
@@ -194,9 +172,10 @@ fn snapshot_from_path(
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// item into a Roblox Instance. Missing from this list is metadata.
|
||||
/// This is deliberate, as metadata is not a snapshot middleware.
|
||||
///
|
||||
/// Directories cannot be used for sync rules so they're ignored by Serde.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Middleware {
|
||||
@@ -218,6 +197,17 @@ pub enum Middleware {
|
||||
Text,
|
||||
Yaml,
|
||||
Ignore,
|
||||
|
||||
#[serde(skip_deserializing)]
|
||||
Dir,
|
||||
#[serde(skip_deserializing)]
|
||||
ServerScriptDir,
|
||||
#[serde(skip_deserializing)]
|
||||
ClientScriptDir,
|
||||
#[serde(skip_deserializing)]
|
||||
ModuleScriptDir,
|
||||
#[serde(skip_deserializing)]
|
||||
CsvDir,
|
||||
}
|
||||
|
||||
impl Middleware {
|
||||
@@ -230,7 +220,7 @@ impl Middleware {
|
||||
path: &Path,
|
||||
name: &str,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
match self {
|
||||
let mut output = 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),
|
||||
@@ -257,6 +247,120 @@ impl Middleware {
|
||||
Self::Text => snapshot_txt(context, vfs, path, name),
|
||||
Self::Yaml => snapshot_yaml(context, vfs, path, name),
|
||||
Self::Ignore => Ok(None),
|
||||
|
||||
Self::Dir => snapshot_dir(context, vfs, path, name),
|
||||
Self::ServerScriptDir => {
|
||||
snapshot_lua_init(context, vfs, path, name, ScriptType::Server)
|
||||
}
|
||||
Self::ClientScriptDir => {
|
||||
snapshot_lua_init(context, vfs, path, name, ScriptType::Client)
|
||||
}
|
||||
Self::ModuleScriptDir => {
|
||||
snapshot_lua_init(context, vfs, path, name, ScriptType::Module)
|
||||
}
|
||||
Self::CsvDir => snapshot_csv_init(context, vfs, path, name),
|
||||
};
|
||||
if let Ok(Some(ref mut snapshot)) = output {
|
||||
snapshot.metadata.middleware = Some(*self);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Runs the syncback mechanism for the provided middleware given a
|
||||
/// SyncbackSnapshot.
|
||||
pub fn syncback<'sync>(
|
||||
&self,
|
||||
snapshot: &SyncbackSnapshot<'sync>,
|
||||
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
||||
let file_name = snapshot.path.file_name().and_then(|s| s.to_str());
|
||||
if let Some(file_name) = file_name {
|
||||
validate_file_name(file_name).with_context(|| {
|
||||
format!("cannot create a file or directory with name {file_name}")
|
||||
})?;
|
||||
}
|
||||
match self {
|
||||
Middleware::Csv => syncback_csv(snapshot),
|
||||
Middleware::JsonModel => syncback_json_model(snapshot),
|
||||
Middleware::Json => anyhow::bail!("cannot syncback Json middleware"),
|
||||
// Projects are only generated from files that already exist on the
|
||||
// file system, so we don't need to pass a file name.
|
||||
Middleware::Project => syncback_project(snapshot),
|
||||
Middleware::ServerScript => syncback_lua(snapshot),
|
||||
Middleware::ClientScript => syncback_lua(snapshot),
|
||||
Middleware::ModuleScript => syncback_lua(snapshot),
|
||||
Middleware::Rbxm => syncback_rbxm(snapshot),
|
||||
Middleware::Rbxmx => syncback_rbxmx(snapshot),
|
||||
Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"),
|
||||
Middleware::Text => syncback_txt(snapshot),
|
||||
Middleware::Yaml => anyhow::bail!("cannot syncback Yaml middleware"),
|
||||
Middleware::Ignore => anyhow::bail!("cannot syncback Ignore middleware"),
|
||||
Middleware::Dir => syncback_dir(snapshot),
|
||||
Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot),
|
||||
Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot),
|
||||
Middleware::ModuleScriptDir => syncback_lua_init(ScriptType::Module, snapshot),
|
||||
Middleware::CsvDir => syncback_csv_init(snapshot),
|
||||
|
||||
Middleware::PluginScript
|
||||
| Middleware::LegacyServerScript
|
||||
| Middleware::LegacyClientScript
|
||||
| Middleware::RunContextServerScript
|
||||
| Middleware::RunContextClientScript => {
|
||||
anyhow::bail!("syncback is not implemented for {self:?} yet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this particular middleware would become a directory.
|
||||
#[inline]
|
||||
pub fn is_dir(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Middleware::Dir
|
||||
| Middleware::ServerScriptDir
|
||||
| Middleware::ClientScriptDir
|
||||
| Middleware::ModuleScriptDir
|
||||
| Middleware::CsvDir
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether this particular middleware sets its own properties.
|
||||
/// This applies to things like `JsonModel` and `Project`, since they
|
||||
/// set properties without needing a meta.json file.
|
||||
///
|
||||
/// It does not cover middleware like `ServerScript` or `Csv` because they
|
||||
/// need a meta.json file to set properties that aren't their designated
|
||||
/// 'special' properties.
|
||||
#[inline]
|
||||
pub fn handles_own_properties(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Middleware::JsonModel | Middleware::Project | Middleware::Rbxm | Middleware::Rbxmx
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to return a middleware that should be used for the given path.
|
||||
///
|
||||
/// Returns `Err` only if the Vfs cannot read information about the path.
|
||||
pub fn middleware_for_path(
|
||||
vfs: &Vfs,
|
||||
sync_rules: &[SyncRule],
|
||||
path: &Path,
|
||||
) -> anyhow::Result<Option<Self>> {
|
||||
let meta = match vfs.metadata(path).with_not_found()? {
|
||||
Some(meta) => meta,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if meta.is_dir() {
|
||||
let (middleware, _, _) = get_dir_middleware(vfs, path)?;
|
||||
Ok(Some(middleware))
|
||||
} else {
|
||||
for rule in sync_rules.iter().chain(default_sync_rules()) {
|
||||
if rule.matches(path) {
|
||||
return Ok(Some(rule.middleware));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user