From d3cc11659a14cccbabfd6b2f57fc879d433391c5 Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 30 Jan 2026 08:12:04 -0600 Subject: [PATCH] RunContext refactor + sync updates --- .../script_naming_refactor_424e3277.plan.md | 272 ++++++++ plugin/src/ChangeBatcher/encodeInstance.lua | 117 +++- ...o_end__tests__build__client_in_folder.snap | 3 +- ...end_to_end__tests__build__client_init.snap | 3 +- .../end_to_end__tests__build__issue_546.snap | 4 +- ...d__tests__build__script_meta_disabled.snap | 2 +- ...o_end__tests__build__server_in_folder.snap | 2 +- ...end_to_end__tests__build__server_init.snap | 2 +- ..._end__tests__build__sync_rule_complex.snap | 6 +- .../nested_runcontext/default.project.json | 13 - .../folder1/test.server.luau | 0 .../folder2/default.project.json | 7 - .../folder2/folder3/test.server.luau | 0 ...d_to_end__tests__serve__scripts_all-2.snap | 4 +- ...end_to_end__tests__serve__scripts_all.snap | 4 +- ...__tests__serve__sync_rule_complex_all.snap | 8 +- src/path_encoding.rs | 208 +++++- src/project.rs | 8 - src/serve_session.rs | 5 +- src/snapshot/metadata.rs | 36 +- ..._snapshot__tests__apply__add_property.snap | 3 +- ...s__apply__remove_property_after_patch.snap | 3 +- ...tests__apply__remove_property_initial.snap | 3 +- ...tests__apply__set_name_and_class_name.snap | 3 +- ...__snapshot__tests__compute__add_child.snap | 3 +- src/snapshot_middleware/csv.rs | 4 +- src/snapshot_middleware/lua.rs | 267 ++----- src/snapshot_middleware/meta_file.rs | 5 +- src/snapshot_middleware/mod.rs | 57 +- src/snapshot_middleware/project.rs | 8 +- ...t_middleware__csv__test__csv_from_vfs.snap | 3 +- ...pshot_middleware__csv__test__csv_init.snap | 3 +- ...leware__csv__test__csv_init_with_meta.snap | 3 +- ..._middleware__csv__test__csv_with_meta.snap | 3 +- ...t_middleware__dir__test__empty_folder.snap | 3 +- ...ddleware__dir__test__folder_in_folder.snap | 6 +- ...leware__json__test__instance_from_vfs.snap | 3 +- ...middleware__json__test__with_metadata.snap | 3 +- ...are__json_model__test__model_from_vfs.snap | 6 +- ...on_model__test__model_from_vfs_legacy.snap | 6 +- ...are__lua__test__class_script_disabled.snap | 30 - ...re__lua__test__class_script_with_meta.snap | 28 - ...ddleware__lua__test__client_from_vfs.snap} | 3 +- ...ware__lua__test__init_module_from_vfs.snap | 3 +- ..._test__init_module_from_vfs_with_meta.snap | 3 +- ...ddleware__lua__test__legacy_from_vfs.snap} | 7 +- ...iddleware__lua__test__local_from_vfs.snap} | 7 +- ...ddleware__lua__test__module_from_vfs.snap} | 3 +- ...dleware__lua__test__module_with_meta.snap} | 3 +- ...iddleware__lua__test__plugin_from_vfs.snap | 27 + ...lua__test__runcontext_module_from_vfs.snap | 26 - ...ua__test__runcontext_module_with_meta.snap | 26 - ...ddleware__lua__test__server_disabled.snap} | 3 +- ...ddleware__lua__test__server_from_vfs.snap} | 3 +- ...dleware__lua__test__server_with_meta.snap} | 3 +- ...__meta_file__test__adjacent_read_json.snap | 3 +- ..._meta_file__test__adjacent_read_jsonc.snap | 3 +- ..._meta_file__test__directory_read_json.snap | 3 +- ...meta_file__test__directory_read_jsonc.snap | 3 +- ...eware__project__test__no_name_project.snap | 3 +- ...oject__test__project_from_direct_file.snap | 3 +- ...test__project_path_property_overrides.snap | 3 +- ..._project__test__project_with_children.snap | 6 +- ...t__test__project_with_path_to_project.snap | 3 +- ...ct_with_path_to_project_with_children.snap | 6 +- ...oject__test__project_with_path_to_txt.snap | 3 +- ...est__project_with_resolved_properties.snap | 3 +- ...t__project_with_unresolved_properties.snap | 3 +- ...leware__toml__test__instance_from_vfs.snap | 3 +- ...middleware__toml__test__with_metadata.snap | 3 +- ...dleware__txt__test__instance_from_vfs.snap | 3 +- ..._middleware__txt__test__with_metadata.snap | 3 +- ...leware__yaml__test__instance_from_vfs.snap | 3 +- ...middleware__yaml__test__with_metadata.snap | 3 +- src/snapshot_middleware/util.rs | 5 - src/syncback/file_names.rs | 12 +- src/syncback/mod.rs | 26 +- src/web/api.rs | 660 ++++++++++++++++-- tests/tests/build.rs | 1 - 79 files changed, 1415 insertions(+), 626 deletions(-) create mode 100644 .cursor/plans/script_naming_refactor_424e3277.plan.md delete mode 100644 rojo-test/build-tests/nested_runcontext/default.project.json delete mode 100644 rojo-test/build-tests/nested_runcontext/folder1/test.server.luau delete mode 100644 rojo-test/build-tests/nested_runcontext/folder2/default.project.json delete mode 100644 rojo-test/build-tests/nested_runcontext/folder2/folder3/test.server.luau delete mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap delete mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap => librojo__snapshot_middleware__lua__test__client_from_vfs.snap} (87%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap => librojo__snapshot_middleware__lua__test__legacy_from_vfs.snap} (79%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap => librojo__snapshot_middleware__lua__test__local_from_vfs.snap} (78%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap => librojo__snapshot_middleware__lua__test__module_from_vfs.snap} (86%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__class_module_with_meta.snap => librojo__snapshot_middleware__lua__test__module_with_meta.snap} (86%) create mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_from_vfs.snap delete mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap delete mode 100644 src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap => librojo__snapshot_middleware__lua__test__server_disabled.snap} (88%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap => librojo__snapshot_middleware__lua__test__server_from_vfs.snap} (87%) rename src/snapshot_middleware/snapshots/{librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap => librojo__snapshot_middleware__lua__test__server_with_meta.snap} (87%) diff --git a/.cursor/plans/script_naming_refactor_424e3277.plan.md b/.cursor/plans/script_naming_refactor_424e3277.plan.md new file mode 100644 index 00000000..4ac50ca8 --- /dev/null +++ b/.cursor/plans/script_naming_refactor_424e3277.plan.md @@ -0,0 +1,272 @@ +--- +name: Script Naming Refactor +overview: Remove the `emitLegacyScripts` option entirely and implement a new, deterministic script naming convention where each script type has exactly one file extension. Add period encoding to the Windows encoding system and default that setting to true. +todos: + - id: path-encoding + content: Add %DOT% to CHAR_ENCODINGS, add %% escape for literal %, default encoding to true + status: completed + - id: middleware-enum + content: Add LocalScript, LegacyScript, LocalScriptDir, LegacyScriptDir middleware variants + status: completed + - id: script-type + content: Simplify ScriptType enum and update snapshot_lua logic + status: completed + - id: sync-rules + content: Add .local.lua/.luau and .legacy.lua/.luau sync rules and init paths + status: completed + - id: remove-emit-legacy + content: Remove emitLegacyScripts from Project, InstanceContext, and all usages + status: completed + - id: syncback-runcontext + content: Update get_best_middleware to check RunContext for Script class + status: completed + - id: file-extensions + content: Update extension_for_middleware with new script types + status: completed + - id: meta-suffixes + content: Add .local and .legacy to meta file suffix stripping + status: completed + - id: web-api + content: Update web/api.rs for new script naming conventions + status: completed + - id: update-tests + content: Update snapshot files and test cases + status: completed +isProject: false +--- + +# Script Naming Convention Refactor + +## New File Extension Mapping + + +| Script Type | File Extension | +| -------------------------- | -------------- | +| Script (RunContext.Server) | `.server.luau` | +| Script (RunContext.Client) | `.client.luau` | +| Script (RunContext.Plugin) | `.plugin.luau` | +| Script (RunContext.Legacy) | `.legacy.luau` | +| LocalScript | `.local.luau` | +| ModuleScript | `.luau` | + + +## Key Changes + +### 1. Add Period Encoding, Percent Escaping, and Default to True + +Update `[src/path_encoding.rs](src/path_encoding.rs)`: + +1. Add `(".", "%DOT%")` to `CHAR_ENCODINGS` +2. Add `%%` as escape sequence for literal `%` (encode first, decode last) + +```rust +const CHAR_ENCODINGS: &[(&str, &str)] = &[ + (".", "%DOT%"), // NEW + ("<", "%LT%"), + // ... rest unchanged +]; + +pub fn encode_path_name(name: &str) -> String { + // ... handle leading/trailing spaces ... + + // FIRST: Escape existing % as %% (before adding new % signs) + let mut encoded_middle = middle.replace("%", "%%"); + + // THEN: Encode special characters (which add new % signs) + for (char, encoded) in CHAR_ENCODINGS { + encoded_middle = encoded_middle.replace(char, encoded); + } + // ... +} + +pub fn decode_path_name(name: &str) -> String { + // ... + // FIRST: Decode %NAME% patterns + for (char, encoded) in CHAR_ENCODINGS { + result = result.replace(encoded, char); + } + + // LAST: Unescape %% back to % + result = result.replace("%%", "%"); + // ... +} +``` + +This ensures round-tripping works correctly: + +- `My%DOT%Thing` encodes to `My%%DOT%%Thing`, decodes back to `My%DOT%Thing` +- `My.Thing` encodes to `My%DOT%Thing`, decodes back to `My.Thing` + +Change the default for `encode_windows_invalid_chars` to `true` in `[src/syncback/mod.rs](src/syncback/mod.rs)` `SyncbackRules::encode_windows_invalid_chars()`: + +```rust +pub fn encode_windows_invalid_chars(&self) -> bool { + self.encode_windows_invalid_chars.unwrap_or(true) // Changed from false to true +} +``` + +Also update the `decode_windows_invalid_chars` logic in `[src/serve_session.rs](src/serve_session.rs)` to default to `true`. + +### 2. Add New Middleware Variants + +In `[src/snapshot_middleware/mod.rs](src/snapshot_middleware/mod.rs)`, add new variants: + +- `LocalScript` - for `.local.lua/.luau` files (LocalScript class) +- `LegacyScript` - for `.legacy.lua/.luau` files (Script with RunContext.Legacy) +- `LocalScriptDir` - for `init.local.lua/.luau` directories +- `LegacyScriptDir` - for `init.legacy.lua/.luau` directories + +### 3. Update ScriptType Enum + +In `[src/snapshot_middleware/lua.rs](src/snapshot_middleware/lua.rs)`, simplify to: + +```rust +pub enum ScriptType { + Server, // Script + RunContext.Server + Client, // Script + RunContext.Client + Plugin, // Script + RunContext.Plugin + Legacy, // Script + RunContext.Legacy + Local, // LocalScript + Module, // ModuleScript +} +``` + +Remove `LegacyServer`, `LegacyClient`, `RunContextServer`, `RunContextClient` - these were for the dual-mode system. + +### 4. Update Default Sync Rules + +In `[src/snapshot_middleware/mod.rs](src/snapshot_middleware/mod.rs)` `default_sync_rules()`: + +```rust +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!("*.plugin.lua", PluginScript, ".plugin.lua"), + sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"), + sync_rule!("*.legacy.lua", LegacyScript, ".legacy.lua"), // NEW + sync_rule!("*.legacy.luau", LegacyScript, ".legacy.luau"), // NEW + sync_rule!("*.local.lua", LocalScript, ".local.lua"), // NEW + sync_rule!("*.local.luau", LocalScript, ".local.luau"), // NEW + sync_rule!("*.{lua,luau}", ModuleScript), // Unchanged - catches remaining + // ... rest unchanged +] +``` + +### 5. Update Init Path Handling + +In `[src/snapshot_middleware/mod.rs](src/snapshot_middleware/mod.rs)` `get_dir_middleware()`: + +```rust +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::LocalScriptDir, "init.local.luau"), // NEW + (Middleware::LocalScriptDir, "init.local.lua"), // NEW + (Middleware::LegacyScriptDir, "init.legacy.luau"), // NEW + (Middleware::LegacyScriptDir, "init.legacy.lua"), // NEW + (Middleware::CsvDir, "init.csv"), +] +``` + +### 6. Remove emitLegacyScripts Entirely + +Files to modify: + +- `[src/project.rs](src/project.rs)` - Remove `emit_legacy_scripts` field from `Project` +- `[src/snapshot/metadata.rs](src/snapshot/metadata.rs)` - Remove from `InstanceContext`, remove `with_emit_legacy_scripts()` +- `[src/snapshot_middleware/util.rs](src/snapshot_middleware/util.rs)` - Remove `emit_legacy_scripts_default()` +- `[src/snapshot_middleware/project.rs](src/snapshot_middleware/project.rs)` - Remove `set_emit_legacy_scripts()` call +- `[src/serve_session.rs](src/serve_session.rs)` - Remove emit_legacy_scripts usage + +### 7. Update Syncback Middleware Selection + +In `[src/syncback/mod.rs](src/syncback/mod.rs)` `get_best_middleware()`: + +```rust +middleware = match inst.class.as_str() { + "Script" => { + // Check RunContext to determine which middleware + match inst.properties.get(&ustr("RunContext")) { + Some(Variant::Enum(e)) => match e.to_u32() { + // Legacy = 0, Server = 1, Client = 2, Plugin = 3 + 0 => Middleware::LegacyScript, + 1 => Middleware::ServerScript, + 2 => Middleware::ClientScript, + 3 => Middleware::PluginScript, + _ => Middleware::ServerScript, // fallback + }, + _ => Middleware::LegacyScript, // No RunContext = Legacy behavior + } + } + "LocalScript" => Middleware::LocalScript, + "ModuleScript" => Middleware::ModuleScript, + // ... rest unchanged +} +``` + +### 8. Update File Name Extensions + +In `[src/syncback/file_names.rs](src/syncback/file_names.rs)` `extension_for_middleware()`: + +```rust +Middleware::ServerScript => "server.luau", +Middleware::ClientScript => "client.luau", +Middleware::PluginScript => "plugin.luau", +Middleware::LegacyScript => "legacy.luau", // NEW +Middleware::LocalScript => "local.luau", // NEW +Middleware::ModuleScript => "luau", +``` + +### 9. Update Meta File Suffix Stripping + +In `[src/snapshot_middleware/lua.rs](src/snapshot_middleware/lua.rs)` `syncback_lua()` and `[src/snapshot_middleware/meta_file.rs](src/snapshot_middleware/meta_file.rs)`: + +Add `.local` and `.legacy` to the suffix stripping logic. + +### 10. Update Web API (Two-Way Sync) + +In `[src/web/api.rs](src/web/api.rs)`: + +- Update script handling to use new naming conventions +- Remove all `emit_legacy_scripts` references and conditional logic + +### 11. Update Test Files + +- Delete test projects in `rojo-test/build-tests/nested_runcontext/` that test emitLegacyScripts +- Update all snapshot files (`.snap`) that contain `emit_legacy_scripts: true/false` +- Update test cases in `src/snapshot_middleware/lua.rs` to use the new single behavior + +### 12. Clean Up Comments + +Remove all legacy references and comments about the old dual-mode system throughout the codebase. + +## Files to Modify (Summary) + +Core changes: + +- `src/path_encoding.rs` - Add period encoding (`%DOT%`) and `%%` escape for literal `%` +- `src/syncback/mod.rs` - Default encode_windows_invalid_chars to true, update middleware selection +- `src/serve_session.rs` - Default decode_windows_invalid_chars to true +- `src/snapshot_middleware/mod.rs` - Add middleware variants, update sync rules +- `src/snapshot_middleware/lua.rs` - Simplify ScriptType, update logic +- `src/snapshot/metadata.rs` - Remove emit_legacy_scripts +- `src/project.rs` - Remove emit_legacy_scripts field +- `src/syncback/mod.rs` - Update middleware selection with RunContext check +- `src/syncback/file_names.rs` - Add new extensions +- `src/snapshot_middleware/util.rs` - Remove emit_legacy_scripts_default +- `src/snapshot_middleware/project.rs` - Remove emit_legacy_scripts propagation +- `src/serve_session.rs` - Remove emit_legacy_scripts usage +- `src/web/api.rs` - Update for new conventions +- `src/snapshot_middleware/meta_file.rs` - Add new suffix handling + +Test updates: + +- ~40 snapshot files need `emit_legacy_scripts` line removed and `decode_windows_invalid_chars` changed to `true` +- Test functions need updating to not pass emit_legacy_scripts parameter + diff --git a/plugin/src/ChangeBatcher/encodeInstance.lua b/plugin/src/ChangeBatcher/encodeInstance.lua index b238414c..54f7f8ce 100644 --- a/plugin/src/ChangeBatcher/encodeInstance.lua +++ b/plugin/src/ChangeBatcher/encodeInstance.lua @@ -8,6 +8,12 @@ - All other instances -> .model.json files or directories Children are recursively encoded and included. + + DUPLICATE HANDLING: + - Instances with duplicate-named siblings at ANY level of the path are skipped + - This matches the behavior in ref_properties.rs::is_path_unique + - A path is ambiguous if duplicates exist anywhere from the instance to the root + - A warning is logged when duplicates are encountered ]] local Packages = script.Parent.Parent.Parent.Packages @@ -41,9 +47,65 @@ local DIRECTORY_CLASSES = { AdGui = true, } +-- Finds duplicate names among a list of instances +-- Returns a set of names that appear more than once +local function findDuplicateNames(children: { Instance }): { [string]: boolean } + local nameCounts: { [string]: number } = {} + local duplicates: { [string]: boolean } = {} + + for _, child in children do + local name = child.Name + nameCounts[name] = (nameCounts[name] or 0) + 1 + if nameCounts[name] > 1 then + duplicates[name] = true + end + end + + return duplicates +end + +-- Checks if an instance has duplicate-named siblings +local function hasDuplicateSiblings(instance: Instance): boolean + local parent = instance.Parent + if not parent then + return false + end + + local siblings = parent:GetChildren() + local duplicates = findDuplicateNames(siblings) + return duplicates[instance.Name] == true +end + +-- Checks if the path to an instance is unique by walking up the tree. +-- Returns false if ANY ancestor (including the instance itself) has duplicate siblings. +-- This matches the behavior of is_path_unique in ref_properties.rs +local function isPathUnique(instance: Instance): boolean + local current = instance + + while current do + -- Check if current has duplicate-named siblings + if hasDuplicateSiblings(current) then + return false + end + + -- Move up to parent + current = current.Parent + + -- Stop at game (DataModel) - we've reached the root + if current and current == game then + break + end + end + + return true +end + -- Forward declaration for recursion local encodeInstance +-- Track skipped duplicates for logging +local skippedDuplicateCount = 0 + -- Encode Attributes if present on any instance local function encodeAttributes(instance, properties) -- Try to get Attributes - this works for all instance types @@ -64,7 +126,21 @@ local function encodeAttributes(instance, properties) end end -encodeInstance = function(instance, parentId) +encodeInstance = function(instance, parentId, _skipPathCheck) + -- Check if the entire path to this instance is unique (unless we're in a recursive call) + -- For top-level instances, we check the entire ancestor chain for duplicates + -- For children, the parent already verified the path is unique up to that point, + -- so we only need to check siblings at each level (done in the recursion below) + if not _skipPathCheck and not isPathUnique(instance) then + skippedDuplicateCount += 1 + Log.warn( + "Skipped instance '{}' ({}) - path contains duplicate-named siblings (cannot reliably sync)", + instance:GetFullName(), + instance.ClassName + ) + return nil + end + local properties = {} -- Always try to encode Attributes for any instance type @@ -111,10 +187,25 @@ encodeInstance = function(instance, parentId) end -- For directory classes (Folder, Configuration, etc.), we only encode Attributes (done above) - -- Recursively encode children + -- Recursively encode children, skipping those with duplicate names local children = {} - for _, child in ipairs(instance:GetChildren()) do - local encodedChild = encodeInstance(child, nil) -- parentId will be resolved server-side + local childInstances = instance:GetChildren() + local childDuplicates = findDuplicateNames(childInstances) + + for _, child in ipairs(childInstances) do + -- Skip children with duplicate names + if childDuplicates[child.Name] then + skippedDuplicateCount += 1 + Log.warn( + "Skipped child instance '{}' ({}) - has duplicate-named siblings (cannot reliably sync)", + child:GetFullName(), + child.ClassName + ) + continue + end + + -- Pass true to skip duplicate check since we already checked above + local encodedChild = encodeInstance(child, nil, true) if encodedChild then table.insert(children, encodedChild) end @@ -129,4 +220,20 @@ encodeInstance = function(instance, parentId) } end -return encodeInstance +-- Wrapper function that resets the duplicate count and returns the final count +local function encodeInstanceWithDuplicateTracking(instance, parentId) + skippedDuplicateCount = 0 + local result = encodeInstance(instance, parentId, false) + + -- Log summary if any duplicates were skipped + if skippedDuplicateCount > 0 then + Log.warn( + "Skipped {} location(s) with duplicate-named siblings during pull (cannot reliably sync)", + skippedDuplicateCount + ) + end + + return result, skippedDuplicateCount +end + +return encodeInstanceWithDuplicateTracking diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__client_in_folder.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__client_in_folder.snap index 3953fcb6..b0c562c2 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__client_in_folder.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__client_in_folder.snap @@ -7,9 +7,10 @@ expression: contents client_in_folder - + localScript + 2 -- This is a Lua client script diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__client_init.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__client_init.snap index f3391ebe..d8bfe9ed 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__client_init.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__client_init.snap @@ -3,9 +3,10 @@ source: tests/tests/build.rs expression: contents --- - + client_init + 2 return "From folder/init.client.lua" diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__issue_546.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__issue_546.snap index 3107344a..a4a38afc 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__issue_546.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__issue_546.snap @@ -1,13 +1,13 @@ --- source: tests/tests/build.rs -assertion_line: 98 expression: contents --- - + issue_546 true + 2 print("Hello, world!") diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__script_meta_disabled.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__script_meta_disabled.snap index 28354bd6..153f81e2 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__script_meta_disabled.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__script_meta_disabled.snap @@ -11,7 +11,7 @@ expression: contents hello true - 0 + 1 -- This script should be marked 'Disabled' diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__server_in_folder.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__server_in_folder.snap index c9806d00..6edb857d 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__server_in_folder.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__server_in_folder.snap @@ -10,7 +10,7 @@ expression: contents serverScript - 0 + 1 -- This is a Lua server script diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__server_init.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__server_init.snap index 2bbda073..2f40c2c9 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__server_init.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__server_init.snap @@ -6,7 +6,7 @@ expression: contents server_init - 0 + 1 return "From folder/init.server.luau" diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap index e38050d8..c6988e66 100644 --- a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap +++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap @@ -1,6 +1,5 @@ --- source: tests/tests/build.rs -assertion_line: 104 expression: contents --- @@ -11,13 +10,14 @@ expression: contents bar - 0 + 1 -- Hello, from bar (a Script)! - + baz + 2 -- Hello, from baz (a LocalScript)! diff --git a/rojo-test/build-tests/nested_runcontext/default.project.json b/rojo-test/build-tests/nested_runcontext/default.project.json deleted file mode 100644 index 5327be9a..00000000 --- a/rojo-test/build-tests/nested_runcontext/default.project.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "nested_runcontext", - "emitLegacyScripts": false, - "tree": { - "$className": "Folder", - "folder1": { - "$path": "folder1" - }, - "folder2": { - "$path": "folder2" - } - } -} diff --git a/rojo-test/build-tests/nested_runcontext/folder1/test.server.luau b/rojo-test/build-tests/nested_runcontext/folder1/test.server.luau deleted file mode 100644 index e69de29b..00000000 diff --git a/rojo-test/build-tests/nested_runcontext/folder2/default.project.json b/rojo-test/build-tests/nested_runcontext/folder2/default.project.json deleted file mode 100644 index 67feff44..00000000 --- a/rojo-test/build-tests/nested_runcontext/folder2/default.project.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "nested_runcontext", - "emitLegacyScripts": true, - "tree": { - "$path": "folder3" - } -} diff --git a/rojo-test/build-tests/nested_runcontext/folder2/folder3/test.server.luau b/rojo-test/build-tests/nested_runcontext/folder2/folder3/test.server.luau deleted file mode 100644 index e69de29b..00000000 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all-2.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all-2.snap index 49d8148b..d344efbd 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all-2.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all-2.snap @@ -1,7 +1,6 @@ --- source: tests/tests/serve.rs expression: "read_response.intern_and_redact(&mut redactions, root_id)" - --- instances: id-2: @@ -25,7 +24,7 @@ instances: Parent: id-2 Properties: RunContext: - Enum: 0 + Enum: 1 Source: String: "-- Hello, from bar!" id-4: @@ -41,4 +40,3 @@ instances: String: Updated foo! messageCursor: 1 sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all.snap index 62e5ceef..2671f94b 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__scripts_all.snap @@ -1,7 +1,6 @@ --- source: tests/tests/serve.rs expression: "read_response.intern_and_redact(&mut redactions, root_id)" - --- instances: id-2: @@ -25,7 +24,7 @@ instances: Parent: id-2 Properties: RunContext: - Enum: 0 + Enum: 1 Source: String: "-- Hello, from bar!" id-4: @@ -41,4 +40,3 @@ instances: String: "-- Hello, from foo!" messageCursor: 0 sessionId: id-1 - diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap index 80cd76f6..87cb8b5e 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap @@ -1,6 +1,5 @@ --- source: tests/tests/serve.rs -assertion_line: 284 expression: "read_response.intern_and_redact(&mut redactions, root_id)" --- instances: @@ -28,18 +27,20 @@ instances: Parent: id-2 Properties: RunContext: - Enum: 0 + Enum: 1 Source: String: "-- Hello, from bar (a Script)!" id-4: Children: [] - ClassName: LocalScript + ClassName: Script Id: id-4 Metadata: ignoreUnknownInstances: false Name: baz Parent: id-2 Properties: + RunContext: + Enum: 2 Source: String: "-- Hello, from baz (a LocalScript)!" id-5: @@ -77,4 +78,3 @@ instances: String: "Hello, from qux (a .rojo file that's turned into a StringValue)!" messageCursor: 0 sessionId: id-1 - diff --git a/src/path_encoding.rs b/src/path_encoding.rs index 34908d55..1c8d9871 100644 --- a/src/path_encoding.rs +++ b/src/path_encoding.rs @@ -1,15 +1,20 @@ -//! Encoding and decoding utilities for Windows-invalid filename characters. +//! Encoding and decoding utilities for special filename characters. //! -//! Windows does not allow the following characters in file names: -//! < > : " / \ | ? * +//! This module encodes characters that are problematic in file names: +//! - Windows-invalid characters: < > : " / \ | ? * +//! - Periods (.) to prevent conflicts with file extension parsing +//! - Leading/trailing spaces +//! - Literal percent signs (%) to allow round-tripping //! -//! Windows also does not allow leading/trailing spaces in file names. -//! -//! This module provides functions to encode these characters using a %NAME% -//! format (e.g., `<` becomes `%LT%`) and decode them back. +//! Characters are encoded using a %NAME% format (e.g., `<` becomes `%LT%`, +//! `.` becomes `%DOT%`). Literal `%` is escaped as `%%` (like printf). -/// Mapping of Windows-invalid characters to their encoded representations. +use std::collections::HashMap; +use std::sync::OnceLock; + +/// Mapping of special characters to their encoded representations. const CHAR_ENCODINGS: &[(&str, &str)] = &[ + (".", "%DOT%"), ("<", "%LT%"), (">", "%GT%"), (":", "%COLON%"), @@ -21,13 +26,27 @@ const CHAR_ENCODINGS: &[(&str, &str)] = &[ ("*", "%STAR%"), ]; +/// Returns a map from encoded pattern (e.g., "DOT") to decoded char (e.g., "."). +fn get_decode_map() -> &'static HashMap<&'static str, &'static str> { + static DECODE_MAP: OnceLock> = OnceLock::new(); + DECODE_MAP.get_or_init(|| { + let mut map = HashMap::new(); + for (char, encoded) in CHAR_ENCODINGS { + // Strip the leading and trailing % from the pattern + let pattern = &encoded[1..encoded.len() - 1]; + map.insert(pattern, *char); + } + map + }) +} + const SPACE_ENCODING: &str = "%SPACE%"; -/// Encodes Windows-invalid characters in a file name. +/// Encodes special characters in a file name. /// -/// This replaces characters like `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*` -/// with their encoded representations like `%LT%`, `%GT%`, etc. -/// Leading and trailing spaces are encoded as `%SPACE%`. +/// This replaces characters like `.`, `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*` +/// with their encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc. +/// Literal `%` is escaped as `%%`. Leading and trailing spaces are encoded as `%SPACE%`. pub fn encode_path_name(name: &str) -> String { // Count leading and trailing spaces first (before any modifications) let leading_spaces = name.len() - name.trim_start_matches(' ').len(); @@ -36,8 +55,10 @@ pub fn encode_path_name(name: &str) -> String { // Get the middle part (without leading/trailing spaces) let middle = &name[leading_spaces..name.len() - trailing_spaces]; - // Encode invalid characters in the middle part - let mut encoded_middle = middle.to_string(); + // First: escape % as %% (before adding new % signs from encodings) + let mut encoded_middle = middle.replace('%', "%%"); + + // Then: encode special characters for (char, encoded) in CHAR_ENCODINGS { encoded_middle = encoded_middle.replace(char, encoded); } @@ -49,11 +70,13 @@ pub fn encode_path_name(name: &str) -> String { format!("{}{}{}", encoded_prefix, encoded_middle, encoded_suffix) } -/// Decodes encoded Windows-invalid characters back to their original form. +/// Decodes encoded special characters back to their original form. /// -/// This replaces encoded representations like `%LT%`, `%GT%`, etc. back -/// to their original characters `<`, `>`, etc. -/// `%SPACE%` at the start/end is decoded back to spaces. +/// This replaces encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc. +/// back to their original characters `.`, `<`, `>`, etc. +/// `%%` is decoded to literal `%`. `%SPACE%` at the start/end is decoded back to spaces. +/// +/// Uses a proper left-to-right parser to correctly handle `%%` escaping. pub fn decode_path_name(name: &str) -> String { let mut result = name.to_string(); @@ -71,15 +94,83 @@ pub fn decode_path_name(name: &str) -> String { result = result[..result.len() - SPACE_ENCODING.len()].to_string(); } - // Decode invalid characters in the middle - for (char, encoded) in CHAR_ENCODINGS { - result = result.replace(encoded, char); - } + // Parse and decode the middle part + let decoded_middle = decode_patterns(&result); // Rebuild with actual spaces let prefix = " ".repeat(leading_spaces); let suffix = " ".repeat(trailing_spaces); - format!("{}{}{}", prefix, result, suffix) + format!("{}{}{}", prefix, decoded_middle, suffix) +} + +/// Parses an encoded string left-to-right, properly handling `%%` escapes and `%NAME%` patterns. +fn decode_patterns(input: &str) -> String { + let decode_map = get_decode_map(); + let chars: Vec = input.chars().collect(); + let mut output = String::with_capacity(input.len()); + let mut i = 0; + + while i < chars.len() { + if chars[i] == '%' { + // Check for %% (escaped percent) + if i + 1 < chars.len() && chars[i + 1] == '%' { + output.push('%'); + i += 2; + continue; + } + + // Try to match a %NAME% pattern + if let Some((decoded, consumed)) = try_decode_pattern(&chars, i, decode_map) { + output.push_str(&decoded); + i += consumed; + continue; + } + } + + // Regular character, just copy it + output.push(chars[i]); + i += 1; + } + + output +} + +/// Tries to decode a %NAME% pattern starting at position `start`. +/// Returns Some((decoded_string, chars_consumed)) if successful, None otherwise. +fn try_decode_pattern( + chars: &[char], + start: usize, + decode_map: &HashMap<&str, &str>, +) -> Option<(String, usize)> { + // Must start with % + if chars.get(start) != Some(&'%') { + return None; + } + + // Find the closing % + let mut end = start + 1; + while end < chars.len() && chars[end] != '%' { + // Pattern names are uppercase letters only + if !chars[end].is_ascii_uppercase() { + return None; + } + end += 1; + } + + // Must have found a closing % + if end >= chars.len() || chars[end] != '%' { + return None; + } + + // Extract the pattern name (without the % delimiters) + let pattern_name: String = chars[start + 1..end].iter().collect(); + + // Look up the pattern + if let Some(&decoded) = decode_map.get(pattern_name.as_str()) { + Some((decoded.to_string(), end - start + 1)) + } else { + None + } } #[cfg(test)] @@ -88,15 +179,15 @@ mod tests { #[test] fn test_encode_all_chars() { - let input = r#"<>:"/\|?*"#; - let expected = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%"; + let input = r#".<>:"/\|?*"#; + let expected = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%"; assert_eq!(encode_path_name(input), expected); } #[test] fn test_decode_all_chars() { - let input = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%"; - let expected = r#"<>:"/\|?*"#; + let input = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%"; + let expected = r#".<>:"/\|?*"#; assert_eq!(decode_path_name(input), expected); } @@ -154,4 +245,67 @@ mod tests { let decoded = decode_path_name(&encoded); assert_eq!(decoded, original); } + + #[test] + fn test_encode_period() { + let input = "My.Script"; + let expected = "My%DOT%Script"; + assert_eq!(encode_path_name(input), expected); + } + + #[test] + fn test_decode_period() { + let input = "My%DOT%Script"; + let expected = "My.Script"; + assert_eq!(decode_path_name(input), expected); + } + + #[test] + fn test_roundtrip_with_period() { + let original = "My.Module.Name"; + let encoded = encode_path_name(original); + let decoded = decode_path_name(&encoded); + assert_eq!(decoded, original); + } + + #[test] + fn test_encode_percent() { + let input = "My%Thing"; + let expected = "My%%Thing"; + assert_eq!(encode_path_name(input), expected); + } + + #[test] + fn test_decode_percent() { + let input = "My%%Thing"; + let expected = "My%Thing"; + assert_eq!(decode_path_name(input), expected); + } + + #[test] + fn test_roundtrip_with_percent() { + let original = "My%Thing"; + let encoded = encode_path_name(original); + let decoded = decode_path_name(&encoded); + assert_eq!(decoded, original); + } + + #[test] + fn test_percent_encoding_pattern_roundtrip() { + // This tests that a name containing what looks like an encoding pattern + // round-trips correctly and doesn't get incorrectly decoded + let original = "My%DOT%Thing"; + let encoded = encode_path_name(original); + assert_eq!(encoded, "My%%DOT%%Thing"); + let decoded = decode_path_name(&encoded); + assert_eq!(decoded, original); + } + + #[test] + fn test_combined_period_and_percent() { + let original = "My.%Thing.Other"; + let encoded = encode_path_name(original); + let decoded = decode_path_name(&encoded); + assert_eq!(decoded, original); + } } diff --git a/src/project.rs b/src/project.rs index b968e47c..0e432147 100644 --- a/src/project.rs +++ b/src/project.rs @@ -110,12 +110,6 @@ pub struct Project { #[serde(skip_serializing_if = "Option::is_none")] pub serve_address: Option, - /// Determines if Rojo should emit scripts with the appropriate `RunContext` - /// for `*.client.lua` and `*.server.lua` files in the project instead of - /// using `Script` and `LocalScript` Instances. - #[serde(skip_serializing_if = "Option::is_none")] - pub emit_legacy_scripts: Option, - /// A list of globs, relative to the folder the project file is in, that /// match files that should be excluded if Rojo encounters them. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -573,7 +567,6 @@ mod test { "$path": "src", // Comment after value }, "servePort": 34567, - "emitLegacyScripts": false, // Test glob parsing with comments "globIgnorePaths": [ "**/*.spec.lua", // Ignore test files @@ -601,7 +594,6 @@ mod test { // Verify the parsed values assert_eq!(project.name, Some("TestProject".to_string())); assert_eq!(project.serve_port, Some(34567)); - assert_eq!(project.emit_legacy_scripts, Some(false)); // Verify glob_ignore_paths were parsed correctly assert_eq!(project.glob_ignore_paths.len(), 2); diff --git a/src/serve_session.rs b/src/serve_session.rs index 8623ff10..143187ec 100644 --- a/src/serve_session.rs +++ b/src/serve_session.rs @@ -110,10 +110,9 @@ impl ServeSession { .syncback_rules .as_ref() .map(|rules| rules.encode_windows_invalid_chars()) - .unwrap_or(false); + .unwrap_or(true); - let mut instance_context = - InstanceContext::with_emit_legacy_scripts(root_project.emit_legacy_scripts); + let mut instance_context = InstanceContext::new(); instance_context.set_decode_windows_invalid_chars(decode_windows_invalid_chars); log::trace!("Generating snapshot of instances from VFS"); diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs index 755526ee..9214fdb7 100644 --- a/src/snapshot/metadata.rs +++ b/src/snapshot/metadata.rs @@ -8,11 +8,7 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use crate::{ - glob::Glob, - path_serializer, - project::ProjectNode, - snapshot_middleware::{emit_legacy_scripts_default, Middleware}, - RojoRef, + glob::Glob, path_serializer, project::ProjectNode, snapshot_middleware::Middleware, RojoRef, }; /// Rojo-specific metadata that can be associated with an instance or a snapshot @@ -142,32 +138,26 @@ impl Default for InstanceMetadata { pub struct InstanceContext { #[serde(skip_serializing_if = "Vec::is_empty")] pub path_ignore_rules: Arc>, - pub emit_legacy_scripts: bool, #[serde(skip_serializing_if = "Vec::is_empty")] pub sync_rules: Vec, - /// Whether to decode Windows-invalid characters in file names. - /// Characters like `%LT%`, `%GT%`, `%COLON%`, etc. will be decoded - /// back to `<`, `>`, `:`, etc. in instance names sent to the plugin. - #[serde(default)] + /// Whether to decode special characters in file names. + /// Characters like `%DOT%`, `%LT%`, `%GT%`, `%COLON%`, etc. will be decoded + /// back to `.`, `<`, `>`, `:`, etc. in instance names sent to the plugin. + /// Defaults to true. + #[serde(default = "decode_windows_invalid_chars_default")] pub decode_windows_invalid_chars: bool, } +fn decode_windows_invalid_chars_default() -> bool { + true +} + impl InstanceContext { pub fn new() -> Self { Self { path_ignore_rules: Arc::new(Vec::new()), - emit_legacy_scripts: emit_legacy_scripts_default().unwrap(), sync_rules: Vec::new(), - decode_windows_invalid_chars: false, - } - } - - pub fn with_emit_legacy_scripts(emit_legacy_scripts: Option) -> Self { - Self { - emit_legacy_scripts: emit_legacy_scripts - .or_else(emit_legacy_scripts_default) - .unwrap(), - ..Self::new() + decode_windows_invalid_chars: true, } } @@ -206,10 +196,6 @@ impl InstanceContext { 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> { diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap index 128a9cea..ee1d0ab1 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__add_property.snap @@ -12,8 +12,7 @@ metadata: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap index d494d159..d53e0090 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_after_patch.snap @@ -10,8 +10,7 @@ metadata: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap index 49e1c639..ba8afe2a 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__remove_property_initial.snap @@ -12,8 +12,7 @@ metadata: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap index 6fd952b7..5af96846 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__apply__set_name_and_class_name.snap @@ -10,8 +10,7 @@ metadata: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap index c80e67ad..2f112685 100644 --- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap +++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap @@ -11,8 +11,7 @@ added_instances: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs index 54e51179..c98a4a7c 100644 --- a/src/snapshot_middleware/csv.rs +++ b/src/snapshot_middleware/csv.rs @@ -383,7 +383,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_csv_init( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/root/init.csv"), "root", @@ -421,7 +421,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#, let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_csv_init( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/root/init.csv"), "root", diff --git a/src/snapshot_middleware/lua.rs b/src/snapshot_middleware/lua.rs index 077a9b4b..849d0dc3 100644 --- a/src/snapshot_middleware/lua.rs +++ b/src/snapshot_middleware/lua.rs @@ -21,14 +21,12 @@ use super::{ #[derive(Debug)] pub enum ScriptType { - Server, - Client, - Module, - Plugin, - LegacyServer, - LegacyClient, - RunContextServer, - RunContextClient, + Server, // Script + RunContext.Server + Client, // Script + RunContext.Client + Module, // ModuleScript + Plugin, // Script + RunContext.Plugin + Legacy, // Script + RunContext.Legacy + Local, // LocalScript } /// Core routine for turning Lua files into snapshots. @@ -47,26 +45,12 @@ pub fn snapshot_lua( .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::Server => ("Script", run_context_enums.get("Server")), + ScriptType::Client => ("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")), + ScriptType::Legacy => ("Script", run_context_enums.get("Legacy")), + ScriptType::Local => ("LocalScript", None), }; let contents = vfs.read_to_string_lf_normalized(path)?; @@ -101,8 +85,9 @@ pub fn snapshot_lua( /// 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. +/// Scripts named `init.lua`, `init.server.lua`, `init.client.lua`, `init.local.lua`, +/// `init.legacy.lua`, or `init.plugin.lua` usurp their parents, which acts similarly +/// to `__init__.py` from the Python world. pub fn snapshot_lua_init( context: &InstanceContext, vfs: &Vfs, @@ -115,9 +100,8 @@ pub fn snapshot_lua_init( 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\ + "init scripts 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(), @@ -177,6 +161,8 @@ pub fn syncback_lua<'sync>( .strip_suffix(".server") .or_else(|| file_stem.strip_suffix(".client")) .or_else(|| file_stem.strip_suffix(".plugin")) + .or_else(|| file_stem.strip_suffix(".local")) + .or_else(|| file_stem.strip_suffix(".legacy")) .unwrap_or(file_stem); let meta_name = if meta_name.is_empty() { if snapshot.encode_windows_invalid_chars() { @@ -211,7 +197,9 @@ pub fn syncback_lua_init<'sync>( ScriptType::Server => "init.server.luau", ScriptType::Client => "init.client.luau", ScriptType::Module => "init.luau", - _ => anyhow::bail!("syncback is not yet implemented for {script_type:?}"), + ScriptType::Plugin => "init.plugin.luau", + ScriptType::Legacy => "init.legacy.luau", + ScriptType::Local => "init.local.luau", }); let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) { @@ -248,7 +236,7 @@ mod test { use memofs::{InMemoryFs, VfsSnapshot}; #[test] - fn class_module_from_vfs() { + fn module_from_vfs() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -256,7 +244,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/foo.lua"), "foo", @@ -271,30 +259,7 @@ mod test { } #[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() { + fn plugin_from_vfs() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -302,7 +267,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(false)), + &InstanceContext::new(), &vfs, Path::new("/foo.plugin.lua"), "foo", @@ -317,7 +282,7 @@ mod test { } #[test] - fn class_server_from_vfs() { + fn server_from_vfs() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -325,7 +290,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/foo.server.lua"), "foo", @@ -340,30 +305,7 @@ mod test { } #[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() { + fn client_from_vfs() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -371,7 +313,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/foo.client.lua"), "foo", @@ -386,19 +328,42 @@ mod test { } #[test] - fn runcontext_client_from_vfs() { + fn local_from_vfs() { let mut imfs = InMemoryFs::new(); - imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!")) + imfs.load_snapshot("/foo.local.lua", VfsSnapshot::file("Hello there!")) .unwrap(); let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(false)), + &InstanceContext::new(), &vfs, - Path::new("/foo.client.lua"), + Path::new("/foo.local.lua"), "foo", - ScriptType::Client, + ScriptType::Local, + ) + .unwrap() + .unwrap(); + + insta::with_settings!({ sort_maps => true }, { + insta::assert_yaml_snapshot!(instance_snapshot); + }); + } + + #[test] + fn legacy_from_vfs() { + let mut imfs = InMemoryFs::new(); + imfs.load_snapshot("/foo.legacy.lua", VfsSnapshot::file("Hello there!")) + .unwrap(); + + let vfs = Vfs::new(imfs); + + let instance_snapshot = snapshot_lua( + &InstanceContext::new(), + &vfs, + Path::new("/foo.legacy.lua"), + "foo", + ScriptType::Legacy, ) .unwrap() .unwrap(); @@ -420,7 +385,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua_init( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/root/init.lua"), "root", @@ -452,7 +417,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua_init( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/root/init.lua"), "root", @@ -467,7 +432,7 @@ mod test { } #[test] - fn class_module_with_meta() { + fn module_with_meta() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -486,7 +451,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/foo.lua"), "foo", @@ -501,41 +466,7 @@ mod test { } #[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() { + fn server_with_meta() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -554,7 +485,7 @@ mod test { let vfs = Vfs::new(imfs); let instance_snapshot = snapshot_lua( - &InstanceContext::with_emit_legacy_scripts(Some(true)), + &InstanceContext::new(), &vfs, Path::new("/foo.server.lua"), "foo", @@ -569,41 +500,7 @@ mod test { } #[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() { + fn server_disabled() { let mut imfs = InMemoryFs::new(); imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!")) .unwrap(); @@ -624,43 +521,7 @@ mod test { 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)), + &InstanceContext::new(), &vfs, Path::new("/bar.server.lua"), "bar", diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs index 320c3656..e81212e0 100644 --- a/src/snapshot_middleware/meta_file.rs +++ b/src/snapshot_middleware/meta_file.rs @@ -55,13 +55,14 @@ impl AdjacentMetadata { // Use the file stem from the actual path (which has encoded chars) // rather than the decoded name, so meta files are found correctly // when encodeWindowsInvalidChars is enabled. - // Strip known script type suffixes (.server, .client, .plugin) to get - // the base name for the meta file. + // Strip known script type suffixes to get the base name for the meta file. let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or(""); let base_name = file_stem .strip_suffix(".server") .or_else(|| file_stem.strip_suffix(".client")) .or_else(|| file_stem.strip_suffix(".plugin")) + .or_else(|| file_stem.strip_suffix(".local")) + .or_else(|| file_stem.strip_suffix(".legacy")) .unwrap_or(file_stem); let meta_path_json = path.with_file_name(format!("{base_name}.meta.json")); let meta_path_jsonc = path.with_file_name(format!("{base_name}.meta.jsonc")); diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs index 7aa661ef..0dc7984c 100644 --- a/src/snapshot_middleware/mod.rs +++ b/src/snapshot_middleware/mod.rs @@ -53,10 +53,7 @@ use self::{ yaml::snapshot_yaml, }; -pub use self::{ - lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default, - util::PathExt, -}; +pub use self::{lua::ScriptType, project::snapshot_project_node, util::PathExt}; /// Returns an `InstanceSnapshot` for the provided path. /// This will inspect the path and find the appropriate middleware for it, @@ -98,6 +95,7 @@ pub fn snapshot_from_vfs( // TODO: Is this even necessary anymore? match file_name { "init.server.luau" | "init.server.lua" | "init.client.luau" | "init.client.lua" + | "init.local.luau" | "init.local.lua" | "init.legacy.luau" | "init.legacy.lua" | "init.luau" | "init.lua" | "init.csv" => return Ok(None), _ => {} } @@ -131,6 +129,10 @@ fn get_dir_middleware<'path>( (Middleware::ServerScriptDir, "init.server.lua"), (Middleware::ClientScriptDir, "init.client.luau"), (Middleware::ClientScriptDir, "init.client.lua"), + (Middleware::LocalScriptDir, "init.local.luau"), + (Middleware::LocalScriptDir, "init.local.lua"), + (Middleware::LegacyScriptDir, "init.legacy.luau"), + (Middleware::LegacyScriptDir, "init.legacy.lua"), (Middleware::CsvDir, "init.csv"), ] }); @@ -197,10 +199,8 @@ pub enum Middleware { ClientScript, ModuleScript, PluginScript, - LegacyClientScript, - LegacyServerScript, - RunContextServerScript, - RunContextClientScript, + LocalScript, + LegacyScript, Project, Rbxm, Rbxmx, @@ -218,6 +218,10 @@ pub enum Middleware { #[serde(skip_deserializing)] ModuleScriptDir, #[serde(skip_deserializing)] + LocalScriptDir, + #[serde(skip_deserializing)] + LegacyScriptDir, + #[serde(skip_deserializing)] CsvDir, } @@ -239,18 +243,8 @@ impl Middleware { Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client), Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module), Self::PluginScript => snapshot_lua(context, vfs, path, name, ScriptType::Plugin), - Self::LegacyClientScript => { - snapshot_lua(context, vfs, path, name, ScriptType::LegacyClient) - } - Self::LegacyServerScript => { - snapshot_lua(context, vfs, path, name, ScriptType::LegacyServer) - } - Self::RunContextClientScript => { - snapshot_lua(context, vfs, path, name, ScriptType::RunContextClient) - } - Self::RunContextServerScript => { - snapshot_lua(context, vfs, path, name, ScriptType::RunContextServer) - } + Self::LocalScript => snapshot_lua(context, vfs, path, name, ScriptType::Local), + Self::LegacyScript => snapshot_lua(context, vfs, path, name, ScriptType::Legacy), Self::Project => snapshot_project(context, vfs, path, name), Self::Rbxm => snapshot_rbxm(context, vfs, path, name), Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name), @@ -269,6 +263,10 @@ impl Middleware { Self::ModuleScriptDir => { snapshot_lua_init(context, vfs, path, name, ScriptType::Module) } + Self::LocalScriptDir => snapshot_lua_init(context, vfs, path, name, ScriptType::Local), + Self::LegacyScriptDir => { + snapshot_lua_init(context, vfs, path, name, ScriptType::Legacy) + } Self::CsvDir => snapshot_csv_init(context, vfs, path, name), }; if let Ok(Some(ref mut snapshot)) = output { @@ -299,6 +297,9 @@ impl Middleware { Middleware::ServerScript => syncback_lua(snapshot), Middleware::ClientScript => syncback_lua(snapshot), Middleware::ModuleScript => syncback_lua(snapshot), + Middleware::PluginScript => syncback_lua(snapshot), + Middleware::LocalScript => syncback_lua(snapshot), + Middleware::LegacyScript => syncback_lua(snapshot), Middleware::Rbxm => syncback_rbxm(snapshot), Middleware::Rbxmx => syncback_rbxmx(snapshot), Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"), @@ -309,15 +310,9 @@ impl Middleware { 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::LocalScriptDir => syncback_lua_init(ScriptType::Local, snapshot), + Middleware::LegacyScriptDir => syncback_lua_init(ScriptType::Legacy, 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") - } } } @@ -330,6 +325,8 @@ impl Middleware { | Middleware::ServerScriptDir | Middleware::ClientScriptDir | Middleware::ModuleScriptDir + | Middleware::LocalScriptDir + | Middleware::LegacyScriptDir | Middleware::CsvDir ) } @@ -427,6 +424,10 @@ pub fn default_sync_rules() -> &'static [SyncRule] { sync_rule!("*.client.luau", ClientScript, ".client.luau"), sync_rule!("*.plugin.lua", PluginScript, ".plugin.lua"), sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"), + sync_rule!("*.legacy.lua", LegacyScript, ".legacy.lua"), + sync_rule!("*.legacy.luau", LegacyScript, ".legacy.luau"), + sync_rule!("*.local.lua", LocalScript, ".local.lua"), + sync_rule!("*.local.luau", LocalScript, ".local.luau"), sync_rule!("*.{lua,luau}", ModuleScript), sync_rule!("*.project.json", Project, ".project.json"), sync_rule!("*.project.jsonc", Project, ".project.jsonc"), diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs index 90d743a7..82d9185b 100644 --- a/src/snapshot_middleware/project.rs +++ b/src/snapshot_middleware/project.rs @@ -25,7 +25,7 @@ use crate::{ RojoRef, }; -use super::{emit_legacy_scripts_default, snapshot_from_vfs}; +use super::snapshot_from_vfs; pub fn snapshot_project( context: &InstanceContext, @@ -55,12 +55,6 @@ pub fn snapshot_project( context.add_sync_rules(sync_rules); context.add_path_ignore_rules(rules); - context.set_emit_legacy_scripts( - project - .emit_legacy_scripts - .or_else(emit_legacy_scripts_default) - .unwrap(), - ); match snapshot_project_node(&context, path, project_name, &project.tree, vfs, None)? { Some(found_snapshot) => { diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap index 9364e46f..283d222e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap index 1ed84fa2..12fcb651 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init.snap @@ -20,8 +20,7 @@ metadata: - /root/init.meta.jsonc - /root/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap index 4f8d795b..c8c71fcc 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_init_with_meta.snap @@ -20,8 +20,7 @@ metadata: - /root/init.meta.jsonc - /root/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap index 0d57b0c5..fccb39f4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap index 09b5583f..d0fae6f5 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap @@ -20,8 +20,7 @@ metadata: - /foo/init.meta.jsonc - /foo/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap index 5c2d4e49..7d43c462 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap @@ -20,8 +20,7 @@ metadata: - /foo/init.meta.jsonc - /foo/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ @@ -47,8 +46,7 @@ children: - /foo/Child/init.meta.jsonc - /foo/Child/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: dir schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap index 9cdfe3a3..ec8f79ec 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap index ccc09f10..50aa332e 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__with_metadata.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap index b72fc460..37da1388 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo.model.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ @@ -26,8 +25,7 @@ children: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap index b72fc460..37da1388 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo.model.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ @@ -26,8 +25,7 @@ children: ignore_unknown_instances: false relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap deleted file mode 100644 index 4166ea3a..00000000 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_disabled.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: src/snapshot_middleware/lua.rs -expression: instance_snapshot ---- -snapshot_id: "00000000000000000000000000000000" -metadata: - ignore_unknown_instances: false - instigating_source: - Path: /bar.server.lua - relevant_paths: - - /bar.server.lua - - /bar.meta.json - - /bar.meta.jsonc - - /bar.meta.json5 - context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false - specified_id: ~ - middleware: ~ - schema: ~ -name: bar -class_name: Script -properties: - Disabled: - Bool: true - RunContext: - Enum: 0 - Source: - String: Hello there! -children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap deleted file mode 100644 index 260e7c41..00000000 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_script_with_meta.snap +++ /dev/null @@ -1,28 +0,0 @@ ---- -source: src/snapshot_middleware/lua.rs -expression: instance_snapshot ---- -snapshot_id: "00000000000000000000000000000000" -metadata: - ignore_unknown_instances: true - instigating_source: - Path: /foo.server.lua - relevant_paths: - - /foo.server.lua - - /foo.meta.json - - /foo.meta.jsonc - - /foo.meta.json5 - context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false - specified_id: ~ - middleware: ~ - schema: ~ -name: foo -class_name: Script -properties: - RunContext: - Enum: 0 - Source: - String: Hello there! -children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap similarity index 87% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap index a462139c..f00ca340 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap index 45416fff..48b9c2b6 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs.snap @@ -20,8 +20,7 @@ metadata: - /root/init.meta.jsonc - /root/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap index 736985d4..a08bc42a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__init_module_from_vfs_with_meta.snap @@ -20,8 +20,7 @@ metadata: - /root/init.meta.jsonc - /root/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__legacy_from_vfs.snap similarity index 79% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__legacy_from_vfs.snap index 4743375f..f6cd0dcc 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__legacy_from_vfs.snap @@ -6,15 +6,14 @@ snapshot_id: "00000000000000000000000000000000" metadata: ignore_unknown_instances: false instigating_source: - Path: /foo.server.lua + Path: /foo.legacy.lua relevant_paths: - - /foo.server.lua + - /foo.legacy.lua - /foo.meta.json - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__local_from_vfs.snap similarity index 78% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__local_from_vfs.snap index e7c4aaa4..1744921b 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_client_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__local_from_vfs.snap @@ -6,15 +6,14 @@ snapshot_id: "00000000000000000000000000000000" metadata: ignore_unknown_instances: false instigating_source: - Path: /foo.client.lua + Path: /foo.local.lua relevant_paths: - - /foo.client.lua + - /foo.local.lua - /foo.meta.json - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap similarity index 86% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap index afcea174..9cce5a73 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap similarity index 86% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap index 9c2bd685..081fdba5 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__class_module_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_from_vfs.snap new file mode 100644 index 00000000..4f54e477 --- /dev/null +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__plugin_from_vfs.snap @@ -0,0 +1,27 @@ +--- +source: src/snapshot_middleware/lua.rs +expression: instance_snapshot +--- +snapshot_id: "00000000000000000000000000000000" +metadata: + ignore_unknown_instances: false + instigating_source: + Path: /foo.plugin.lua + relevant_paths: + - /foo.plugin.lua + - /foo.meta.json + - /foo.meta.jsonc + - /foo.meta.json5 + context: + decode_windows_invalid_chars: true + specified_id: ~ + middleware: ~ + schema: ~ +name: foo +class_name: Script +properties: + RunContext: + Enum: 3 + Source: + String: Hello there! +children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap deleted file mode 100644 index a723e363..00000000 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_from_vfs.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: src/snapshot_middleware/lua.rs -expression: instance_snapshot ---- -snapshot_id: "00000000000000000000000000000000" -metadata: - ignore_unknown_instances: false - instigating_source: - Path: /foo.lua - relevant_paths: - - /foo.lua - - /foo.meta.json - - /foo.meta.jsonc - - /foo.meta.json5 - context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false - specified_id: ~ - middleware: ~ - schema: ~ -name: foo -class_name: ModuleScript -properties: - Source: - String: Hello there! -children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap deleted file mode 100644 index 50f5566f..00000000 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_module_with_meta.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: src/snapshot_middleware/lua.rs -expression: instance_snapshot ---- -snapshot_id: "00000000000000000000000000000000" -metadata: - ignore_unknown_instances: true - instigating_source: - Path: /foo.lua - relevant_paths: - - /foo.lua - - /foo.meta.json - - /foo.meta.jsonc - - /foo.meta.json5 - context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false - specified_id: ~ - middleware: ~ - schema: ~ -name: foo -class_name: ModuleScript -properties: - Source: - String: Hello there! -children: [] diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_disabled.snap similarity index 88% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_disabled.snap index 229434cb..075c0f92 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_disabled.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_disabled.snap @@ -13,8 +13,7 @@ metadata: - /bar.meta.jsonc - /bar.meta.json5 context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap similarity index 87% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap index 6c1bbb54..20f64505 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_server_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_with_meta.snap similarity index 87% rename from src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap rename to src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_with_meta.snap index 2cc32295..702e47da 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__runcontext_script_with_meta.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_with_meta.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: false - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap index 1d6d7ed9..854eafeb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_json.snap @@ -10,8 +10,7 @@ metadata: - /foo/bar.meta.jsonc - /foo/bar.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap index 1d6d7ed9..854eafeb 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__adjacent_read_jsonc.snap @@ -10,8 +10,7 @@ metadata: - /foo/bar.meta.jsonc - /foo/bar.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap index d32f1ea7..09c35784 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_json.snap @@ -10,8 +10,7 @@ metadata: - /foo/init.meta.jsonc - /foo/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap index d32f1ea7..09c35784 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__meta_file__test__directory_read_jsonc.snap @@ -10,8 +10,7 @@ metadata: - /foo/init.meta.jsonc - /foo/init.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap index 886e91d8..576ed53d 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__no_name_project.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo/default.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap index df1a0e7a..0abd172a 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo/hello.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap index 8b19153f..0cefd978 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap @@ -11,8 +11,7 @@ metadata: - /foo/other.project.json - /foo/default.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: project schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap index 30baacd2..a999b54c 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ @@ -31,8 +30,7 @@ children: parent_class: Folder relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap index b693973b..ed1b8b73 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap @@ -11,8 +11,7 @@ metadata: - /foo/other.project.json - /foo/default.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: project schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap index 24e08067..9d4a6a30 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap @@ -11,8 +11,7 @@ metadata: - /foo/other.project.json - /foo/default.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: project schema: ~ @@ -32,8 +31,7 @@ children: parent_class: Folder relevant_paths: [] context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap index 6a0fe78e..0c4403af 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap @@ -14,8 +14,7 @@ metadata: - /foo/other.meta.json5 - /foo/default.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: text schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap index a712765d..588c5560 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap index ea0b4016..c90a2906 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap @@ -10,8 +10,7 @@ metadata: relevant_paths: - /foo.project.json context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap index 892e2ab7..6cd888d4 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap index 74ad11a4..ffd1ebea 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__with_metadata.snap @@ -13,8 +13,7 @@ metadata: - foo.meta.jsonc - foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap index be12e2ba..1a96f8c8 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap index 3daf721a..a462fa94 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__with_metadata.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap index 75ac67e0..53f642f6 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__instance_from_vfs.snap @@ -13,8 +13,7 @@ metadata: - /foo.meta.jsonc - /foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: ~ middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap index 0eeb3da0..5d6f6bd0 100644 --- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap +++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__yaml__test__with_metadata.snap @@ -13,8 +13,7 @@ metadata: - foo.meta.jsonc - foo.meta.json5 context: - emit_legacy_scripts: true - decode_windows_invalid_chars: false + decode_windows_invalid_chars: true specified_id: manually specified middleware: ~ schema: ~ diff --git a/src/snapshot_middleware/util.rs b/src/snapshot_middleware/util.rs index c16edcce..145a5628 100644 --- a/src/snapshot_middleware/util.rs +++ b/src/snapshot_middleware/util.rs @@ -48,8 +48,3 @@ where .with_context(|| format!("Path does not have a parent: {}", path.display())) } } - -// TEMP function until rojo 8.0, when it can be replaced with bool::default (aka false) -pub fn emit_legacy_scripts_default() -> Option { - Some(true) -} diff --git a/src/syncback/file_names.rs b/src/syncback/file_names.rs index 2fcf7984..4d1e7351 100644 --- a/src/syncback/file_names.rs +++ b/src/syncback/file_names.rs @@ -91,6 +91,8 @@ pub fn extension_for_middleware(middleware: Middleware) -> &'static str { Middleware::ClientScript => "client.luau", Middleware::ModuleScript => "luau", Middleware::PluginScript => "plugin.luau", + Middleware::LocalScript => "local.luau", + Middleware::LegacyScript => "legacy.luau", Middleware::Project => "project.json5", Middleware::Rbxm => "rbxm", Middleware::Rbxmx => "rbxmx", @@ -98,12 +100,6 @@ pub fn extension_for_middleware(middleware: Middleware) -> &'static str { Middleware::Text => "txt", Middleware::Yaml => "yml", - Middleware::LegacyServerScript - | Middleware::LegacyClientScript - | Middleware::RunContextServerScript - | Middleware::RunContextClientScript => { - todo!("syncback does not work on the middleware {middleware:?} yet") - } // These are manually specified and not `_` to guard against future // middleware additions missing this function. Middleware::Ignore => unimplemented!("syncback does not work on Ignore middleware"), @@ -111,7 +107,9 @@ pub fn extension_for_middleware(middleware: Middleware) -> &'static str { | Middleware::CsvDir | Middleware::ServerScriptDir | Middleware::ClientScriptDir - | Middleware::ModuleScriptDir => { + | Middleware::ModuleScriptDir + | Middleware::LocalScriptDir + | Middleware::LegacyScriptDir => { unimplemented!("directory middleware requires special treatment") } } diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs index 4cb1d68b..7e305fa2 100644 --- a/src/syncback/mod.rs +++ b/src/syncback/mod.rs @@ -325,8 +325,21 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware { "Folder" | "Configuration" | "Tool" | "ScreenGui" | "SurfaceGui" | "BillboardGui" | "AdGui" => Middleware::Dir, "StringValue" => Middleware::Text, - "Script" => Middleware::ServerScript, - "LocalScript" => Middleware::ClientScript, + "Script" => { + // Check RunContext to determine which middleware to use + // RunContext enum values: Legacy = 0, Server = 1, Client = 2, Plugin = 3 + match inst.properties.get(&ustr("RunContext")) { + Some(Variant::Enum(e)) => match e.to_u32() { + 0 => Middleware::LegacyScript, + 1 => Middleware::ServerScript, + 2 => Middleware::ClientScript, + 3 => Middleware::PluginScript, + _ => Middleware::LegacyScript, // Unknown RunContext, default to Legacy + }, + _ => Middleware::LegacyScript, // No RunContext property, default to Legacy + } + } + "LocalScript" => Middleware::LocalScript, "ModuleScript" => Middleware::ModuleScript, "LocalizationTable" => Middleware::Csv, // Default: use JsonModel for everything else (becomes Dir if has children) @@ -339,6 +352,9 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware { Middleware::ServerScript => Middleware::ServerScriptDir, Middleware::ClientScript => Middleware::ClientScriptDir, Middleware::ModuleScript => Middleware::ModuleScriptDir, + Middleware::PluginScript => Middleware::ServerScriptDir, // Plugin scripts with children use server script dir for now + Middleware::LegacyScript => Middleware::LegacyScriptDir, + Middleware::LocalScript => Middleware::LocalScriptDir, Middleware::Csv => Middleware::CsvDir, Middleware::JsonModel | Middleware::Text => Middleware::Dir, _ => middleware, @@ -431,11 +447,11 @@ impl SyncbackRules { Ok(globs) } - /// Returns whether Windows-invalid characters should be encoded in file - /// names during syncback. Defaults to `false`. + /// Returns whether special characters (including periods) should be encoded + /// in file names during syncback. Defaults to `true`. #[inline] pub fn encode_windows_invalid_chars(&self) -> bool { - self.encode_windows_invalid_chars.unwrap_or(false) + self.encode_windows_invalid_chars.unwrap_or(true) } } diff --git a/src/web/api.rs b/src/web/api.rs index bec86a0c..80b8416f 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -257,6 +257,54 @@ impl ApiService { /// Syncback an added instance by creating a file in the filesystem. /// The file watcher will pick up the change and update Rojo's tree. /// + /// Checks if a path in the Rojo tree is unique by verifying no duplicates exist + /// at ANY level of the path (similar to ref_properties::is_path_unique). + /// Returns true if the path is unique, false if duplicates exist at any level. + fn is_tree_path_unique(&self, tree: &crate::snapshot::RojoTree, target_ref: Ref) -> bool { + let mut current_ref = target_ref; + + loop { + let current = match tree.get_instance(current_ref) { + Some(inst) => inst, + None => return false, + }; + + let parent_ref = current.parent(); + if parent_ref.is_none() { + // Reached root - path is unique at all levels + return true; + } + + let parent = match tree.get_instance(parent_ref) { + Some(inst) => inst, + None => return true, + }; + + // Check if any sibling has the same name as current + let current_name = current.name(); + let mut count = 0; + for child_ref in parent.children() { + if let Some(child) = tree.get_instance(*child_ref) { + if child.name() == current_name { + count += 1; + if count > 1 { + // Duplicate found at this level - path is not unique + log::warn!( + "Syncback: Path to '{}' is not unique - duplicate-named siblings at '{}'", + current_name, + parent.name() + ); + return false; + } + } + } + } + + // Move up to parent and check the next level + current_ref = parent_ref; + } + } + /// This follows the same middleware selection logic as the dedicated syncback /// system in `src/syncback/mod.rs` to ensure consistent behavior. fn syncback_added_instance( @@ -280,6 +328,14 @@ impl ApiService { .get_instance(added.parent) .context("Parent instance not found in Rojo tree")?; + // Check if the parent path is unique (no duplicate-named siblings at any level) + if !self.is_tree_path_unique(&tree, added.parent) { + anyhow::bail!( + "Cannot sync instance '{}' - parent path contains duplicate-named siblings", + added.name + ); + } + // Get the parent's filesystem path from its metadata let parent_path = parent_instance .metadata() @@ -302,6 +358,54 @@ impl ApiService { self.syncback_instance_to_path(added, &parent_dir) } + /// Check for duplicate names among children and return the set of unique children + /// that should be synced. Logs warnings for skipped duplicates. + fn filter_duplicate_children<'a>( + &self, + children: &'a [crate::web::interface::AddedInstance], + ) -> Vec<&'a crate::web::interface::AddedInstance> { + use std::collections::HashMap; + + // Count occurrences of each name + let mut name_counts: HashMap<&str, usize> = HashMap::new(); + for child in children { + *name_counts.entry(&child.name).or_insert(0) += 1; + } + + // Find duplicates + let duplicates: std::collections::HashSet<&str> = name_counts + .iter() + .filter(|(_, &count)| count > 1) + .map(|(&name, _)| name) + .collect(); + + // Log skipped duplicates + let mut skipped_count = 0; + for child in children { + if duplicates.contains(child.name.as_str()) { + log::warn!( + "Syncback: Skipped instance '{}' ({}) - has duplicate-named siblings (cannot reliably sync)", + child.name, + child.class_name + ); + skipped_count += 1; + } + } + + if skipped_count > 0 { + log::warn!( + "Syncback: Skipped {} instance(s) with duplicate-named siblings", + skipped_count + ); + } + + // Return only non-duplicate children + children + .iter() + .filter(|child| !duplicates.contains(child.name.as_str())) + .collect() + } + /// Recursively syncback an instance and its children to the filesystem. /// This is the internal implementation that handles the actual file creation. fn syncback_instance_to_path( @@ -320,7 +424,9 @@ impl ApiService { ) })?; - let has_children = !added.children.is_empty(); + // Filter out children with duplicate names (cannot reliably sync) + let unique_children = self.filter_duplicate_children(&added.children); + let has_children = !unique_children.is_empty(); // Determine the appropriate middleware/file format based on class name // This matches the logic in src/syncback/mod.rs::get_best_middleware @@ -345,7 +451,7 @@ impl ApiService { "Syncback: Created ModuleScript directory at {}", dir_path.display() ); - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { @@ -378,7 +484,7 @@ impl ApiService { "Syncback: Created Script directory at {}", dir_path.display() ); - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { @@ -399,7 +505,7 @@ impl ApiService { fs::create_dir_all(&dir_path).with_context(|| { format!("Failed to create directory: {}", dir_path.display()) })?; - let init_path = dir_path.join("init.client.luau"); + let init_path = dir_path.join("init.local.luau"); fs::write(&init_path, source.as_bytes()).with_context(|| { format!("Failed to write file: {}", init_path.display()) })?; @@ -408,11 +514,11 @@ impl ApiService { "Syncback: Created LocalScript directory at {}", dir_path.display() ); - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { - let file_path = parent_dir.join(format!("{}.client.luau", added.name)); + let file_path = parent_dir.join(format!("{}.local.luau", added.name)); fs::write(&file_path, source.as_bytes()).with_context(|| { format!("Failed to write file: {}", file_path.display()) })?; @@ -439,7 +545,8 @@ impl ApiService { }; // Add .gitkeep for empty directories with no metadata (matches dedicated syncback) - if added.children.is_empty() && !has_metadata { + // Uses !has_children which accounts for filtered duplicate children + if !has_children && !has_metadata { fs::write(dir_path.join(".gitkeep"), b"") .with_context(|| "Failed to write .gitkeep")?; } @@ -451,7 +558,7 @@ impl ApiService { ); // Recursively process children - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } @@ -469,7 +576,7 @@ impl ApiService { "Syncback: Created StringValue directory at {}", dir_path.display() ); - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { @@ -508,7 +615,7 @@ impl ApiService { "Syncback: Created LocalizationTable directory at {}", dir_path.display() ); - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { @@ -542,7 +649,7 @@ impl ApiService { ); // Recursively process children - for child in &added.children { + for child in &unique_children { self.syncback_instance_to_path(child, &dir_path)?; } } else { @@ -752,11 +859,9 @@ impl ApiService { /// For `Script` class: /// - RunContext: Client → "client" /// - RunContext: Server → "server" - /// - RunContext: Legacy → "server" (default for legacy mode) + /// - RunContext: Legacy → "legacy" /// - RunContext: Plugin → "plugin" - /// - No RunContext → "server" (default) - /// - /// This ensures proper round-tripping regardless of emitLegacyScripts mode. + /// - No RunContext → "legacy" (default) fn get_script_suffix_for_run_context( &self, added: &crate::web::interface::AddedInstance, @@ -779,16 +884,16 @@ impl ApiService { return match name.as_ref() { "Client" => "client", "Server" => "server", - "Legacy" => "server", + "Legacy" => "legacy", "Plugin" => "plugin", - _ => "server", + _ => "legacy", }; } } } - // Default to server if no RunContext or unrecognized - "server" + // Default to legacy if no RunContext or unrecognized + "legacy" } /// Write an adjacent meta file for scripts without children. @@ -1596,15 +1701,13 @@ mod tests { // Test that script class names result in correct file extensions let test_cases = vec![ ("ModuleScript", "TestModule", "TestModule.luau"), - ("Script", "TestScript", "TestScript.server.luau"), - ("LocalScript", "TestClient", "TestClient.client.luau"), + ("LocalScript", "TestClient", "TestClient.local.luau"), ]; for (class_name, name, expected) in test_cases { let file_name = match class_name { "ModuleScript" => format!("{}.luau", name), - "Script" => format!("{}.server.luau", name), - "LocalScript" => format!("{}.client.luau", name), + "LocalScript" => format!("{}.local.luau", name), _ => format!("{}.model.json", name), }; assert_eq!( @@ -1758,8 +1861,8 @@ mod tests { } #[test] - fn test_run_context_legacy_gives_server_suffix() { - // Script with RunContext: Legacy should produce .server.luau (for legacy mode) + fn test_run_context_legacy_gives_legacy_suffix() { + // Script with RunContext: Legacy should produce .legacy.luau let run_context_enums = rbx_reflection_database::get() .ok() .and_then(|db| db.enums.get("RunContext")) @@ -1792,14 +1895,13 @@ mod tests { // For Script class, the file suffix is determined by RunContext: // - Client → .client.luau // - Server → .server.luau - // - Legacy → .server.luau + // - Legacy → .legacy.luau // - Plugin → .plugin.luau - // This allows proper round-tripping regardless of emitLegacyScripts mode let expected_mappings = vec![ ("Client", "client"), ("Server", "server"), - ("Legacy", "server"), + ("Legacy", "legacy"), ("Plugin", "plugin"), ]; @@ -1807,9 +1909,9 @@ mod tests { let suffix = match run_context { "Client" => "client", "Server" => "server", - "Legacy" => "server", + "Legacy" => "legacy", "Plugin" => "plugin", - _ => "server", + _ => "legacy", }; assert_eq!( suffix, expected_suffix, @@ -1820,14 +1922,14 @@ mod tests { } #[test] - fn test_local_script_always_client() { - // LocalScript class always produces .client.luau regardless of any properties + fn test_local_script_always_local() { + // LocalScript class always produces .local.luau regardless of any properties let class_name = "LocalScript"; - let expected_suffix = "client"; + let expected_suffix = "local"; - // LocalScript doesn't check RunContext - it's always client + // LocalScript doesn't check RunContext - it's always local let suffix = if class_name == "LocalScript" { - "client" + "local" } else { "unknown" }; @@ -2136,8 +2238,6 @@ mod tests { // ========================================================================= mod run_context_comprehensive_tests { use super::*; - use rbx_dom_weak::types::Enum; - use std::collections::HashMap; /// Helper to get RunContext enum value by name fn get_run_context_value(name: &str) -> Option { @@ -2817,7 +2917,7 @@ mod tests { // In scripts-only mode, all instances get ignoreUnknownInstances: true // This prevents accidental deletion of non-script instances let folder = "Folder"; - let is_script = is_script_class(folder); + let _is_script = is_script_class(folder); // Non-scripts should have ignoreUnknownInstances set let should_set_ignore = true; // All instances in scripts-only mode @@ -2868,18 +2968,18 @@ mod tests { expected_extension: ".luau", description: "ModuleScript without RunContext", }, - // LocalScript - always .client.luau + // LocalScript - always .local.luau TestCase { class_name: "LocalScript", run_context: None, - expected_extension: ".client.luau", + expected_extension: ".local.luau", description: "LocalScript", }, // Script with various RunContext values TestCase { class_name: "Script", run_context: None, - expected_extension: ".server.luau", + expected_extension: ".legacy.luau", description: "Script without RunContext (default)", }, TestCase { @@ -2897,7 +2997,7 @@ mod tests { TestCase { class_name: "Script", run_context: Some("Legacy"), - expected_extension: ".server.luau", + expected_extension: ".legacy.luau", description: "Script with RunContext::Legacy", }, TestCase { @@ -2911,16 +3011,16 @@ mod tests { for case in test_cases { let suffix = match case.class_name { "ModuleScript" => String::new(), - "LocalScript" => "client".to_string(), + "LocalScript" => "local".to_string(), "Script" => { // Match on RunContext name to determine suffix match case.run_context { Some("Client") => "client", Some("Server") => "server", - Some("Legacy") => "server", + Some("Legacy") => "legacy", Some("Plugin") => "plugin", - None => "server", - _ => "server", + None => "legacy", + _ => "legacy", } .to_string() } @@ -3003,4 +3103,472 @@ mod tests { } } } + + // ========================================================================= + // Duplicate Path Detection Tests + // ========================================================================= + mod duplicate_path_tests { + // Helper to simulate sibling name checking + fn has_duplicate_sibling_names(names: &[&str], target_name: &str) -> bool { + let count = names.iter().filter(|&&n| n == target_name).count(); + count > 1 + } + + // Simulates the duplicate children filter logic + fn filter_duplicate_names<'a>(names: &[&'a str]) -> Vec<&'a str> { + let mut counts: std::collections::HashMap<&str, usize> = + std::collections::HashMap::new(); + for name in names { + *counts.entry(name).or_insert(0) += 1; + } + + names + .iter() + .filter(|&&name| counts.get(name).copied().unwrap_or(0) <= 1) + .copied() + .collect() + } + + #[test] + fn test_no_duplicates_all_pass() { + let names = vec!["Script1", "Script2", "Script3", "Folder"]; + let filtered = filter_duplicate_names(&names); + assert_eq!(filtered.len(), 4, "All unique names should pass"); + } + + #[test] + fn test_duplicate_siblings_filtered() { + let names = vec!["Script", "Script", "Other"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate 'Script' entries should be filtered" + ); + assert_eq!(filtered[0], "Other", "Only 'Other' should remain"); + } + + #[test] + fn test_multiple_duplicates_filtered() { + let names = vec!["A", "A", "B", "B", "C"]; + let filtered = filter_duplicate_names(&names); + assert_eq!(filtered.len(), 1, "Only unique 'C' should remain"); + assert_eq!(filtered[0], "C"); + } + + #[test] + fn test_all_duplicates_empty_result() { + let names = vec!["Same", "Same", "Same"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 0, + "All duplicates should result in empty list" + ); + } + + #[test] + fn test_sibling_detection_positive() { + let siblings = vec!["Script", "Script", "Other"]; + assert!( + has_duplicate_sibling_names(&siblings, "Script"), + "'Script' should be detected as having duplicate siblings" + ); + } + + #[test] + fn test_sibling_detection_negative() { + let siblings = vec!["Script", "Folder", "Model"]; + assert!( + !has_duplicate_sibling_names(&siblings, "Script"), + "'Script' should NOT be detected as duplicate with unique siblings" + ); + } + + #[test] + fn test_sibling_detection_not_in_list() { + let siblings = vec!["A", "B", "C"]; + assert!( + !has_duplicate_sibling_names(&siblings, "D"), + "'D' not in list should not have duplicates" + ); + } + + #[test] + fn test_path_uniqueness_concept() { + // Test the concept: path is unique only if NO level has duplicates + // + // Tree: + // Root + // ├─ FolderA + // │ └─ Script + // └─ FolderA (duplicate!) + // └─ Script + // + // Path "Root/FolderA/Script" is NOT unique because FolderA is duplicated + + let root_children = vec!["FolderA", "FolderA"]; + let folder_children = vec!["Script"]; + + // Check at root level - duplicates exist + let has_dup_at_root = has_duplicate_sibling_names(&root_children, "FolderA"); + assert!( + has_dup_at_root, + "FolderA should be detected as duplicate at root level" + ); + + // Check at folder level - no duplicates + let has_dup_at_folder = has_duplicate_sibling_names(&folder_children, "Script"); + assert!( + !has_dup_at_folder, + "Script has no duplicate siblings within its parent" + ); + + // But the PATH is still not unique because of the ancestor duplicate + let path_is_unique = !has_dup_at_root; + assert!( + !path_is_unique, + "Path should NOT be unique due to ancestor duplicate" + ); + } + + #[test] + fn test_path_uniqueness_deep_hierarchy() { + // Tree: + // Root (unique) + // └─ Level1 (unique) + // └─ Level2 (has duplicate sibling) + // └─ Target + // + // Path to Target is NOT unique + + let level1_children = vec!["Level2", "Level2", "Other"]; + let level2_children = vec!["Target"]; + + let has_dup_at_level1 = has_duplicate_sibling_names(&level1_children, "Level2"); + assert!(has_dup_at_level1, "Level2 has duplicate at Level1"); + + // Even though Target itself has no duplicates, path is not unique + let has_dup_at_level2 = has_duplicate_sibling_names(&level2_children, "Target"); + assert!(!has_dup_at_level2, "Target has no duplicate at Level2"); + + // Path uniqueness requires ALL levels to be unique + let path_unique = !has_dup_at_level1 && !has_dup_at_level2; + assert!( + !path_unique, + "Path should NOT be unique due to Level2 duplicate" + ); + } + + #[test] + fn test_path_uniqueness_fully_unique() { + // Tree: + // Root + // └─ Folder (unique) + // └─ SubFolder (unique) + // └─ Script (unique) + + let root_children = vec!["Folder"]; + let folder_children = vec!["SubFolder"]; + let sub_children = vec!["Script"]; + + let root_unique = !has_duplicate_sibling_names(&root_children, "Folder"); + let folder_unique = !has_duplicate_sibling_names(&folder_children, "SubFolder"); + let sub_unique = !has_duplicate_sibling_names(&sub_children, "Script"); + + let path_unique = root_unique && folder_unique && sub_unique; + assert!( + path_unique, + "Path should be unique when all levels are unique" + ); + } + + #[test] + fn test_case_sensitive_duplicates() { + // Roblox names are case-sensitive, so "Script" and "script" are different + let names = vec!["Script", "script", "SCRIPT"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 3, + "Case-different names should all be kept (case-sensitive)" + ); + } + + #[test] + fn test_empty_names_handled() { + let names = vec!["", "", "Valid"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Empty string duplicates should be filtered" + ); + assert_eq!(filtered[0], "Valid"); + } + + #[test] + fn test_special_characters_in_names() { + // Names with special characters that might cause filesystem issues + // are handled separately by validate_file_name, but duplicates should still work + let names = vec!["Script (1)", "Script (2)", "Script (1)"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Only 'Script (2)' should remain after filtering duplicates" + ); + assert_eq!(filtered[0], "Script (2)"); + } + + #[test] + fn test_whitespace_names() { + // Names with leading/trailing whitespace are technically different + let names = vec!["Script", " Script", "Script "]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 3, + "Whitespace-different names should all be kept" + ); + } + + // ===================================================================== + // Non-Script Duplicate Tests - Duplicates apply to ALL instance types + // ===================================================================== + + #[test] + fn test_duplicate_folders_filtered() { + // Duplicate Folders should be filtered just like scripts + let names = vec!["MyFolder", "MyFolder", "OtherFolder"]; + let filtered = filter_duplicate_names(&names); + assert_eq!(filtered.len(), 1, "Duplicate 'MyFolder' should be filtered"); + assert_eq!(filtered[0], "OtherFolder"); + } + + #[test] + fn test_duplicate_parts_filtered() { + // Duplicate Parts should be filtered + let names = vec!["Part", "Part", "Baseplate"]; + let filtered = filter_duplicate_names(&names); + assert_eq!(filtered.len(), 1, "Duplicate 'Part' should be filtered"); + assert_eq!(filtered[0], "Baseplate"); + } + + #[test] + fn test_duplicate_models_filtered() { + // Duplicate Models should be filtered + let names = vec!["Car", "Car", "House"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate 'Car' models should be filtered" + ); + } + + #[test] + fn test_duplicate_values_filtered() { + // Duplicate IntValues/StringValues should be filtered + let names = vec!["Score", "Score", "Health"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate 'Score' values should be filtered" + ); + } + + #[test] + fn test_duplicate_gui_elements_filtered() { + // Duplicate GUI elements should be filtered + let names = vec!["MainFrame", "MainFrame", "Sidebar"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate 'MainFrame' GUI should be filtered" + ); + } + + #[test] + fn test_mixed_types_same_name_filtered() { + // Even if types differ, same names are duplicates (filesystem perspective) + // E.g., a Folder named "Data" and a Script named "Data" would conflict + let names = vec!["Data", "Data"]; // Could be Folder + Script + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 0, + "Same-named instances of different types still conflict" + ); + } + + #[test] + fn test_folder_hierarchy_duplicates() { + // Duplicates in folder hierarchies + // Root/ + // ├─ Enemies/ + // │ └─ Enemy (unique) + // └─ Enemies/ (duplicate!) + // └─ Enemy + + let root_children = vec!["Enemies", "Enemies", "Players"]; + let enemies_unique = !has_duplicate_sibling_names(&root_children, "Enemies"); + assert!( + !enemies_unique, + "Duplicate 'Enemies' folders should be detected" + ); + } + + #[test] + fn test_part_ancestor_duplicates() { + // A Part deep in hierarchy with duplicate ancestor + // Workspace/ + // ├─ Zone/ + // │ └─ SpawnPoint (Part) + // └─ Zone/ (duplicate!) + // └─ SpawnPoint (Part) + + let workspace_children = vec!["Zone", "Zone"]; + let zone_unique = !has_duplicate_sibling_names(&workspace_children, "Zone"); + assert!( + !zone_unique, + "Duplicate 'Zone' ancestors make Part paths ambiguous" + ); + } + + #[test] + fn test_remote_event_duplicates() { + // RemoteEvents with duplicate names + let names = vec!["FireBullet", "FireBullet", "TakeDamage"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate RemoteEvents should be filtered" + ); + } + + #[test] + fn test_configuration_duplicates() { + // Configuration instances with duplicates + let names = vec!["Settings", "Settings", "Config"]; + let filtered = filter_duplicate_names(&names); + assert_eq!( + filtered.len(), + 1, + "Duplicate Configurations should be filtered" + ); + } + } + + // ========================================================================= + // Duplicate Path Detection - Integration Scenarios + // ========================================================================= + mod duplicate_path_integration_tests { + + #[test] + fn test_syncback_skips_duplicate_children() { + // When syncing back children, duplicates should be skipped + // This test verifies the filter_duplicate_children logic + + let children_names = vec!["ChildA", "ChildB", "ChildA", "ChildC"]; + let unique: Vec<_> = children_names + .iter() + .filter(|&&name| children_names.iter().filter(|&&n| n == name).count() == 1) + .collect(); + + assert_eq!(unique.len(), 2, "ChildB and ChildC should be kept"); + assert!(unique.contains(&&"ChildB")); + assert!(unique.contains(&&"ChildC")); + } + + #[test] + fn test_recursive_duplicate_handling() { + // Test that duplicates at any level are handled + // + // Parent/ + // ├─ Child1/ + // │ ├─ GrandChild (duplicate) + // │ └─ GrandChild (duplicate) + // └─ Child2/ + // └─ GrandChild (unique within its parent) + + let child1_grandchildren = vec!["GrandChild", "GrandChild"]; + let child2_grandchildren = vec!["GrandChild"]; + + // Child1's grandchildren have duplicates + let child1_filtered: Vec<_> = child1_grandchildren + .iter() + .filter(|&&name| child1_grandchildren.iter().filter(|&&n| n == name).count() == 1) + .collect(); + assert_eq!( + child1_filtered.len(), + 0, + "Child1's duplicate grandchildren filtered" + ); + + // Child2's grandchild is unique + let child2_filtered: Vec<_> = child2_grandchildren + .iter() + .filter(|&&name| child2_grandchildren.iter().filter(|&&n| n == name).count() == 1) + .collect(); + assert_eq!(child2_filtered.len(), 1, "Child2's unique grandchild kept"); + } + + #[test] + fn test_duplicate_log_message_format() { + // Verify the expected log message format for skipped duplicates + let class_name = "ModuleScript"; + let full_name = "game.ReplicatedStorage.DuplicateScript"; + + let expected_msg = format!( + "Skipped instance '{}' ({}) - path contains duplicate-named siblings (cannot reliably sync)", + full_name, class_name + ); + + assert!(expected_msg.contains(full_name)); + assert!(expected_msg.contains(class_name)); + assert!(expected_msg.contains("duplicate-named siblings")); + } + + #[test] + fn test_parent_path_validation() { + // When pulling an instance, we must verify the parent path is unique + // This simulates the is_tree_path_unique check + + // Scenario: Trying to add a script under a folder that has duplicate siblings + let parent_path_levels = vec![ + ("Root", vec!["FolderA", "FolderA"]), // Duplicate at this level! + ]; + + let path_unique = parent_path_levels + .iter() + .all(|(target, siblings)| siblings.iter().filter(|&&s| s == *target).count() == 1); + + assert!( + !path_unique, + "Parent path with duplicates should fail validation" + ); + } + + #[test] + fn test_valid_parent_path() { + // Valid parent path with no duplicates at any level + let parent_path_levels = vec![ + ( + "ReplicatedStorage", + vec!["Workspace", "ReplicatedStorage", "ServerStorage"], + ), + ("Shared", vec!["Shared", "Client", "Server"]), + ]; + + let path_unique = parent_path_levels + .iter() + .all(|(target, siblings)| siblings.iter().filter(|&&s| s == *target).count() == 1); + + assert!(path_unique, "Valid parent path should pass validation"); + } + } } diff --git a/tests/tests/build.rs b/tests/tests/build.rs index 8c533115..d44196ea 100644 --- a/tests/tests/build.rs +++ b/tests/tests/build.rs @@ -44,7 +44,6 @@ gen_build_tests! { json_model_legacy_name, module_in_folder, module_init, - nested_runcontext, optional, project_composed_default, project_composed_file,