forked from rojo-rbx/rojo
Instead of bailing when children have duplicate filesystem names, syncback now resolves collisions by appending incrementing suffixes (e.g. Foo, Foo1, Foo2). This handles both init-renamed children and any other name collisions. Meta stem derivation is now path-based to correctly handle collision suffixes and dotted names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
663 lines
19 KiB
Rust
663 lines
19 KiB
Rust
use std::{path::Path, str};
|
|
|
|
use anyhow::Context as _;
|
|
use memofs::Vfs;
|
|
use rbx_dom_weak::{
|
|
types::{Enum, Variant},
|
|
ustr, HashMapExt as _, UstrMap,
|
|
};
|
|
|
|
use crate::{
|
|
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
|
|
syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot},
|
|
};
|
|
|
|
use super::{
|
|
dir::{snapshot_dir_no_meta, syncback_dir_no_meta},
|
|
meta_file::{AdjacentMetadata, DirectoryMetadata},
|
|
PathExt as _,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub enum ScriptType {
|
|
Server,
|
|
Client,
|
|
Module,
|
|
Plugin,
|
|
LegacyServer,
|
|
LegacyClient,
|
|
RunContextServer,
|
|
RunContextClient,
|
|
}
|
|
|
|
/// Core routine for turning Lua files into snapshots.
|
|
pub fn snapshot_lua(
|
|
context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
path: &Path,
|
|
name: &str,
|
|
script_type: ScriptType,
|
|
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
|
let run_context_enums = &rbx_reflection_database::get()
|
|
.unwrap()
|
|
.enums
|
|
.get("RunContext")
|
|
.expect("Unable to get RunContext enums!")
|
|
.items;
|
|
|
|
let (class_name, run_context) = match script_type {
|
|
ScriptType::Server => {
|
|
if context.emit_legacy_scripts {
|
|
("Script", run_context_enums.get("Legacy"))
|
|
} else {
|
|
("Script", run_context_enums.get("Server"))
|
|
}
|
|
}
|
|
ScriptType::Client => {
|
|
if context.emit_legacy_scripts {
|
|
("LocalScript", None)
|
|
} else {
|
|
("Script", run_context_enums.get("Client"))
|
|
}
|
|
}
|
|
ScriptType::Module => ("ModuleScript", None),
|
|
ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
|
|
ScriptType::LegacyServer => ("Script", run_context_enums.get("Legacy")),
|
|
ScriptType::LegacyClient => ("LocalScript", None),
|
|
ScriptType::RunContextServer => ("Script", run_context_enums.get("Server")),
|
|
ScriptType::RunContextClient => ("Script", run_context_enums.get("Client")),
|
|
};
|
|
|
|
let contents = vfs.read_to_string_lf_normalized(path)?;
|
|
let contents_str = contents.as_str();
|
|
|
|
let mut properties = UstrMap::with_capacity(2);
|
|
properties.insert(ustr("Source"), contents_str.into());
|
|
|
|
if let Some(run_context) = run_context {
|
|
properties.insert(
|
|
ustr("RunContext"),
|
|
Enum::from_u32(run_context.to_owned()).into(),
|
|
);
|
|
}
|
|
|
|
let mut snapshot = InstanceSnapshot::new()
|
|
.name(name)
|
|
.class_name(class_name)
|
|
.properties(properties)
|
|
.metadata(
|
|
InstanceMetadata::new()
|
|
.instigating_source(path)
|
|
.relevant_paths(vec![vfs.canonicalize(path)?])
|
|
.context(context),
|
|
);
|
|
|
|
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
|
|
|
|
Ok(Some(snapshot))
|
|
}
|
|
|
|
/// Attempts to snapshot an 'init' Lua script contained inside of a folder with
|
|
/// the given name.
|
|
///
|
|
/// Scripts named `init.lua`, `init.server.lua`, or `init.client.lua` usurp
|
|
/// their parents, which acts similarly to `__init__.py` from the Python world.
|
|
pub fn snapshot_lua_init(
|
|
context: &InstanceContext,
|
|
vfs: &Vfs,
|
|
init_path: &Path,
|
|
name: &str,
|
|
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, name)?.unwrap();
|
|
|
|
if dir_snapshot.class_name != "Folder" {
|
|
anyhow::bail!(
|
|
"init.lua, init.server.lua, and init.client.lua can \
|
|
only be used if the instance produced by the containing \
|
|
directory would be a Folder.\n\
|
|
\n\
|
|
The directory {} turned into an instance of class {}.",
|
|
folder_path.display(),
|
|
dir_snapshot.class_name
|
|
);
|
|
}
|
|
|
|
let mut init_snapshot =
|
|
snapshot_lua(context, vfs, init_path, &dir_snapshot.name, script_type)?.unwrap();
|
|
|
|
init_snapshot.children = dir_snapshot.children;
|
|
init_snapshot.metadata = dir_snapshot.metadata;
|
|
// The directory snapshot middleware includes all possible init paths
|
|
// so we don't need to add it here.
|
|
|
|
DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?;
|
|
|
|
Ok(Some(init_snapshot))
|
|
}
|
|
|
|
pub fn syncback_lua<'sync>(
|
|
snapshot: &SyncbackSnapshot<'sync>,
|
|
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
|
let new_inst = snapshot.new_inst();
|
|
|
|
let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
|
|
source.as_bytes().to_vec()
|
|
} else {
|
|
anyhow::bail!("Scripts must have a `Source` property that is a String")
|
|
};
|
|
let mut fs_snapshot = FsSnapshot::new();
|
|
fs_snapshot.add_file(&snapshot.path, contents);
|
|
|
|
let meta = AdjacentMetadata::from_syncback_snapshot(snapshot, snapshot.path.clone())?;
|
|
if let Some(mut meta) = meta {
|
|
// Scripts have relatively few properties that we care about, so shifting
|
|
// is fine.
|
|
meta.properties.shift_remove(&ustr("Source"));
|
|
|
|
if !meta.is_empty() {
|
|
let parent_location = snapshot.path.parent_err()?;
|
|
let file_name = snapshot
|
|
.path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("");
|
|
let meta_stem = file_name
|
|
.strip_suffix(".server.luau")
|
|
.or_else(|| file_name.strip_suffix(".server.lua"))
|
|
.or_else(|| file_name.strip_suffix(".client.luau"))
|
|
.or_else(|| file_name.strip_suffix(".client.lua"))
|
|
.or_else(|| file_name.strip_suffix(".plugin.luau"))
|
|
.or_else(|| file_name.strip_suffix(".plugin.lua"))
|
|
.or_else(|| file_name.strip_suffix(".luau"))
|
|
.or_else(|| file_name.strip_suffix(".lua"))
|
|
.unwrap_or(file_name);
|
|
fs_snapshot.add_file(
|
|
parent_location.join(format!("{meta_stem}.meta.json")),
|
|
serde_json::to_vec_pretty(&meta).context("cannot serialize metadata")?,
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(SyncbackReturn {
|
|
fs_snapshot,
|
|
// Scripts don't have a child!
|
|
children: Vec::new(),
|
|
removed_children: Vec::new(),
|
|
})
|
|
}
|
|
|
|
pub fn syncback_lua_init<'sync>(
|
|
script_type: ScriptType,
|
|
snapshot: &SyncbackSnapshot<'sync>,
|
|
) -> anyhow::Result<SyncbackReturn<'sync>> {
|
|
let new_inst = snapshot.new_inst();
|
|
let path = snapshot.path.join(match script_type {
|
|
ScriptType::Server => "init.server.luau",
|
|
ScriptType::Client => "init.client.luau",
|
|
ScriptType::Module => "init.luau",
|
|
_ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"),
|
|
});
|
|
|
|
let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
|
|
source.as_bytes().to_vec()
|
|
} else {
|
|
anyhow::bail!("Scripts must have a `Source` property that is a String")
|
|
};
|
|
|
|
let mut dir_syncback = syncback_dir_no_meta(snapshot)?;
|
|
dir_syncback.fs_snapshot.add_file(&path, contents);
|
|
|
|
let meta = DirectoryMetadata::from_syncback_snapshot(snapshot, path.clone())?;
|
|
if let Some(mut meta) = meta {
|
|
// Scripts have relatively few properties that we care about, so shifting
|
|
// is fine.
|
|
meta.properties.shift_remove(&ustr("Source"));
|
|
|
|
if !meta.is_empty() {
|
|
dir_syncback.fs_snapshot.add_file(
|
|
snapshot.path.join("init.meta.json"),
|
|
serde_json::to_vec_pretty(&meta)
|
|
.context("could not serialize new init.meta.json")?,
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(dir_syncback)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
use memofs::{InMemoryFs, VfsSnapshot};
|
|
|
|
#[test]
|
|
fn class_module_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/foo.lua"),
|
|
"foo",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_module_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.lua"),
|
|
"foo",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn plugin_module_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.plugin.lua"),
|
|
"foo",
|
|
ScriptType::Plugin,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn class_server_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/foo.server.lua"),
|
|
"foo",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_server_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.server.lua"),
|
|
"foo",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn class_client_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/foo.client.lua"),
|
|
"foo",
|
|
ScriptType::Client,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_client_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.client.lua"),
|
|
"foo",
|
|
ScriptType::Client,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn init_module_from_vfs() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot(
|
|
"/root",
|
|
VfsSnapshot::dir([("init.lua", VfsSnapshot::file("Hello!"))]),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua_init(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/root/init.lua"),
|
|
"root",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn init_module_from_vfs_with_meta() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot(
|
|
"/root",
|
|
VfsSnapshot::dir([
|
|
("init.lua", VfsSnapshot::file("Hello!")),
|
|
(
|
|
"init.meta.json",
|
|
VfsSnapshot::file(r#"{"id": "manually specified"}"#),
|
|
),
|
|
]),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua_init(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/root/init.lua"),
|
|
"root",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn class_module_with_meta() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/foo.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"ignoreUnknownInstances": true
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/foo.lua"),
|
|
"foo",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_module_with_meta() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/foo.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"ignoreUnknownInstances": true
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.lua"),
|
|
"foo",
|
|
ScriptType::Module,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn class_script_with_meta() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/foo.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"ignoreUnknownInstances": true
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/foo.server.lua"),
|
|
"foo",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_script_with_meta() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/foo.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"ignoreUnknownInstances": true
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/foo.server.lua"),
|
|
"foo",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn class_script_disabled() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/bar.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"properties": {
|
|
"Disabled": true
|
|
}
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
|
&vfs,
|
|
Path::new("/bar.server.lua"),
|
|
"bar",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn runcontext_script_disabled() {
|
|
let mut imfs = InMemoryFs::new();
|
|
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
|
.unwrap();
|
|
imfs.load_snapshot(
|
|
"/bar.meta.json",
|
|
VfsSnapshot::file(
|
|
r#"
|
|
{
|
|
"properties": {
|
|
"Disabled": true
|
|
}
|
|
}
|
|
"#,
|
|
),
|
|
)
|
|
.unwrap();
|
|
|
|
let vfs = Vfs::new(imfs);
|
|
|
|
let instance_snapshot = snapshot_lua(
|
|
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
&vfs,
|
|
Path::new("/bar.server.lua"),
|
|
"bar",
|
|
ScriptType::Server,
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
insta::with_settings!({ sort_maps => true }, {
|
|
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
});
|
|
}
|
|
}
|